| 2157 | /* ── Edge CRUD ──────────────────────────────────────────────────── */ |
| 2158 | |
| 2159 | int64_t cbm_store_insert_edge(cbm_store_t *s, const cbm_edge_t *e) { |
| 2160 | /* Conflict target includes local_name_gen (#768) so IMPORTS edges with |
| 2161 | * different local_name coexist while re-inserting the same import still |
| 2162 | * upserts. Must match the table's UNIQUE constraint in init_schema. */ |
| 2163 | sqlite3_stmt *stmt = |
| 2164 | prepare_cached(s, &s->stmt_insert_edge, |
| 2165 | "INSERT INTO edges (project, source_id, target_id, type, properties) " |
| 2166 | "VALUES (?1, ?2, ?3, ?4, ?5) " |
| 2167 | "ON CONFLICT(source_id, target_id, type, local_name_gen) DO UPDATE SET " |
| 2168 | "properties = json_patch(properties, ?5) " |
| 2169 | "RETURNING id;"); |
| 2170 | if (!stmt) { |
| 2171 | return CBM_STORE_ERR; |
| 2172 | } |
| 2173 | |
| 2174 | bind_text(stmt, SKIP_ONE, safe_str(e->project)); |
| 2175 | sqlite3_bind_int64(stmt, PAIR_LEN, e->source_id); |
| 2176 | sqlite3_bind_int64(stmt, CBM_SZ_3, e->target_id); |
| 2177 | bind_text(stmt, ST_COL_4, safe_str(e->type)); |
| 2178 | bind_text(stmt, ST_COL_5, safe_props(e->properties_json)); |
| 2179 | |
| 2180 | int rc = sqlite3_step(stmt); |
| 2181 | if (rc == SQLITE_ROW) { |
| 2182 | int64_t id = sqlite3_column_int64(stmt, 0); |
| 2183 | sqlite3_reset(stmt); /* unblock COMMIT — RETURNING leaves stmt active */ |
| 2184 | return id; |
| 2185 | } |
| 2186 | sqlite3_reset(stmt); |
| 2187 | store_set_error_sqlite(s, "insert_edge"); |
| 2188 | return CBM_STORE_ERR; |
| 2189 | } |
| 2190 | |
| 2191 | /* Scan an edge from current row of stmt. */ |
| 2192 | static void scan_edge(sqlite3_stmt *stmt, cbm_edge_t *e) { |