github.com/cayleygraph/cayley@v0.7.7/graph/sql/cockroach/cockroach.go (about)

     1  package cockroach
     2  
     3  import (
     4  	"bytes"
     5  	"database/sql"
     6  	"fmt"
     7  	"strings"
     8  
     9  	"github.com/cayleygraph/cayley/clog"
    10  	"github.com/cayleygraph/cayley/graph"
    11  	graphlog "github.com/cayleygraph/cayley/graph/log"
    12  	csql "github.com/cayleygraph/cayley/graph/sql"
    13  	"github.com/jackc/pgx"
    14  	_ "github.com/jackc/pgx/stdlib" // registers "pgx" driver
    15  )
    16  
    17  const Type = "cockroach"
    18  
    19  func init() {
    20  	csql.Register(Type, csql.Registration{
    21  		Driver:      "pgx",
    22  		HashType:    `BYTEA`,
    23  		BytesType:   `BYTEA`,
    24  		HorizonType: `BIGSERIAL`,
    25  		TimeType:    `timestamp with time zone`,
    26  		NodesTableExtra: `
    27  	FAMILY fhash (hash),
    28  	FAMILY frefs (refs),
    29  	FAMILY fvalue (value, value_string, datatype, language, iri, bnode,
    30  		value_int, value_bool, value_float, value_time)
    31  `,
    32  		QueryDialect: csql.QueryDialect{
    33  			RegexpOp: "~",
    34  			FieldQuote: func(name string) string {
    35  				return pgx.Identifier{name}.Sanitize()
    36  			},
    37  			Placeholder: func(n int) string {
    38  				return fmt.Sprintf("$%d", n)
    39  			},
    40  		},
    41  		NoForeignKeys: true,
    42  		Error:         convError,
    43  		//Estimated: func(table string) string{
    44  		//	return "SELECT reltuples::BIGINT AS estimate FROM pg_class WHERE relname='"+table+"';"
    45  		//},
    46  		RunTx:               runTxCockroach,
    47  		TxRetry:             retryTxCockroach,
    48  		NoSchemaChangesInTx: true,
    49  	})
    50  }
    51  
    52  // AmbiguousCommitError represents an error that left a transaction in an
    53  // ambiguous state: unclear if it committed or not.
    54  type AmbiguousCommitError struct {
    55  	error
    56  }
    57  
    58  // retryTxCockroach runs the transaction and will retry in case of a retryable error.
    59  // https://www.cockroachlabs.com/docs/transactions.html#client-side-transaction-retries
    60  func retryTxCockroach(tx *sql.Tx, stmts func() error) error {
    61  	// Specify that we intend to retry this txn in case of CockroachDB retryable
    62  	// errors.
    63  	if _, err := tx.Exec("SAVEPOINT cockroach_restart"); err != nil {
    64  		return err
    65  	}
    66  
    67  	for {
    68  		released := false
    69  
    70  		err := stmts()
    71  
    72  		if err == nil {
    73  			// RELEASE acts like COMMIT in CockroachDB. We use it since it gives us an
    74  			// opportunity to react to retryable errors, whereas tx.Commit() doesn't.
    75  			released = true
    76  			if _, err = tx.Exec("RELEASE SAVEPOINT cockroach_restart"); err == nil {
    77  				return nil
    78  			}
    79  		}
    80  		// We got an error; let's see if it's a retryable one and, if so, restart. We look
    81  		// for either the standard PG errcode SerializationFailureError:40001 or the Cockroach extension
    82  		// errcode RetriableError:CR000. The Cockroach extension has been removed server-side, but support
    83  		// for it has been left here for now to maintain backwards compatibility.
    84  		pgErr, ok := err.(pgx.PgError)
    85  		if retryable := ok && (pgErr.Code == "CR000" || pgErr.Code == "40001"); !retryable {
    86  			if released {
    87  				err = &AmbiguousCommitError{err}
    88  			}
    89  			return err
    90  		}
    91  		if _, err = tx.Exec("ROLLBACK TO SAVEPOINT cockroach_restart"); err != nil {
    92  			return err
    93  		}
    94  	}
    95  }
    96  
    97  func convError(err error) error {
    98  	e, ok := err.(pgx.PgError)
    99  	if !ok {
   100  		return err
   101  	}
   102  	switch e.Code {
   103  	case "42P07":
   104  		return graph.ErrDatabaseExists
   105  	}
   106  	return err
   107  }
   108  
   109  func convInsertError(err error) error {
   110  	if err == nil {
   111  		return err
   112  	}
   113  	if pe, ok := err.(pgx.PgError); ok {
   114  		if pe.Code == "23505" {
   115  			// TODO: reference to delta
   116  			return &graph.DeltaError{Err: graph.ErrQuadExists}
   117  		}
   118  	}
   119  	return err
   120  }
   121  
   122  // runTxCockroach performs the node and quad updates in the provided transaction.
   123  // This is based on ../postgres/postgres.go:RunTx, but focuses on doing fewer insert statements,
   124  // since those are comparatively expensive for CockroachDB.
   125  func runTxCockroach(tx *sql.Tx, nodes []graphlog.NodeUpdate, quads []graphlog.QuadUpdate, opts graph.IgnoreOpts) error {
   126  	// First, compile the sets of nodes, split by csql.ValueType.
   127  	// Each of those will require a separate INSERT statement.
   128  	type nodeEntry struct {
   129  		refInc int
   130  		values []interface{} // usually two, but sometimes three elements (includes hash)
   131  	}
   132  	nodeEntries := make(map[csql.ValueType][]nodeEntry)
   133  	for _, n := range nodes {
   134  		if n.RefInc < 0 {
   135  			panic("unexpected node update")
   136  		}
   137  		nodeType, values, err := csql.NodeValues(csql.NodeHash{n.Hash}, n.Val)
   138  		if err != nil {
   139  			return err
   140  		}
   141  		nodeEntries[nodeType] = append(nodeEntries[nodeType], nodeEntry{
   142  			refInc: n.RefInc,
   143  			values: values,
   144  		})
   145  	}
   146  
   147  	// Next, build and execute the INSERT statements for each type.
   148  	for nodeType, entries := range nodeEntries {
   149  		var query bytes.Buffer
   150  		var allValues []interface{}
   151  		valCols := nodeType.Columns()
   152  		fmt.Fprintf(&query, "INSERT INTO nodes (refs, hash, %s) VALUES ", strings.Join(valCols, ", "))
   153  		ph := 1 // next placeholder counter
   154  		for i, entry := range entries {
   155  			if i > 0 {
   156  				fmt.Fprint(&query, ", ")
   157  			}
   158  			fmt.Fprint(&query, "(")
   159  			// sanity check
   160  			if len(entry.values) != 1+len(valCols) { // +1 for hash, which is in values
   161  				panic(fmt.Sprintf("internal error: %d entry values vs. %d value columns", len(entry.values), len(valCols)))
   162  			}
   163  			for j := 0; j < 1+len(entry.values); j++ { // +1 for refs
   164  				if j > 0 {
   165  					fmt.Fprint(&query, ", ")
   166  				}
   167  				fmt.Fprintf(&query, "$%d", ph)
   168  				ph++
   169  			}
   170  			fmt.Fprint(&query, ")")
   171  			allValues = append(allValues, entry.refInc)
   172  			allValues = append(allValues, entry.values...)
   173  		}
   174  		fmt.Fprint(&query, " ON CONFLICT (hash) DO UPDATE SET refs = nodes.refs + EXCLUDED.refs RETURNING NOTHING;")
   175  		_, err := tx.Exec(query.String(), allValues...)
   176  		err = convInsertError(err)
   177  		if err != nil {
   178  			clog.Errorf("couldn't exec node INSERT statement [%s]: %v", query.String(), err)
   179  			return err
   180  		}
   181  	}
   182  
   183  	// Now do the same thing with quads.
   184  	// It is simpler because there's only one composite type to insert,
   185  	// so only one INSERT statement is required.
   186  	if len(quads) == 0 {
   187  		return nil
   188  	}
   189  
   190  	var query bytes.Buffer
   191  	var allValues []interface{}
   192  	fmt.Fprintf(&query, "INSERT INTO quads (subject_hash, predicate_hash, object_hash, label_hash, ts) VALUES ")
   193  	for i, d := range quads {
   194  		if d.Del {
   195  			panic("unexpected quad delete")
   196  		}
   197  		if i > 0 {
   198  			fmt.Fprint(&query, ", ")
   199  		}
   200  		fmt.Fprintf(&query, "($%d, $%d, $%d, $%d, now())", 4*i+1, 4*i+2, 4*i+3, 4*i+4)
   201  		allValues = append(allValues,
   202  			csql.NodeHash{d.Quad.Subject}.SQLValue(),
   203  			csql.NodeHash{d.Quad.Predicate}.SQLValue(),
   204  			csql.NodeHash{d.Quad.Object}.SQLValue(),
   205  			csql.NodeHash{d.Quad.Label}.SQLValue())
   206  	}
   207  	if opts.IgnoreDup {
   208  		fmt.Fprint(&query, " ON CONFLICT (subject_hash, predicate_hash, object_hash) DO NOTHING")
   209  		// Only use RETURNING NOTHING when we're ignoring duplicates;
   210  		// otherwise the error returned on duplicates will be different.
   211  		fmt.Fprint(&query, " RETURNING NOTHING")
   212  	}
   213  	fmt.Fprint(&query, ";")
   214  	_, err := tx.Exec(query.String(), allValues...)
   215  	err = convInsertError(err)
   216  	if err != nil {
   217  		if _, ok := err.(*graph.DeltaError); !ok {
   218  			clog.Errorf("couldn't exec quad INSERT statement [%s]: %v", query.String(), err)
   219  		}
   220  		return err
   221  	}
   222  	return nil
   223  }