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 }