| 2039 | /* ── Edge CRUD ──────────────────────────────────────────────────── */ |
| 2040 | |
| 2041 | int64_t cbm_store_insert_edge(cbm_store_t *s, const cbm_edge_t *e) { |
| 2042 | /* Conflict target includes local_name_gen (#768) so IMPORTS edges with |
| 2043 | * different local_name coexist while re-inserting the same import still |
| 2044 | * upserts. Must match the table's UNIQUE constraint in init_schema. */ |
| 2045 | sqlite3_stmt *stmt = |
| 2046 | prepare_cached(s, &s->stmt_insert_edge, |
| 2047 | "INSERT INTO edges (project, source_id, target_id, type, properties) " |
| 2048 | "VALUES (?1, ?2, ?3, ?4, ?5) " |
| 2049 | "ON CONFLICT(source_id, target_id, type, local_name_gen) DO UPDATE SET " |
| 2050 | "properties = json_patch(properties, ?5) " |
| 2051 | "RETURNING id;"); |
| 2052 | if (!stmt) { |
| 2053 | return CBM_STORE_ERR; |
| 2054 | } |
| 2055 | |
| 2056 | bind_text(stmt, SKIP_ONE, safe_str(e->project)); |
| 2057 | sqlite3_bind_int64(stmt, PAIR_LEN, e->source_id); |
| 2058 | sqlite3_bind_int64(stmt, CBM_SZ_3, e->target_id); |
| 2059 | bind_text(stmt, ST_COL_4, safe_str(e->type)); |
| 2060 | bind_text(stmt, ST_COL_5, safe_props(e->properties_json)); |
| 2061 | |
| 2062 | int rc = sqlite3_step(stmt); |
| 2063 | if (rc == SQLITE_ROW) { |
| 2064 | int64_t id = sqlite3_column_int64(stmt, 0); |
| 2065 | sqlite3_reset(stmt); /* unblock COMMIT — RETURNING leaves stmt active */ |
| 2066 | return id; |
| 2067 | } |
| 2068 | sqlite3_reset(stmt); |
| 2069 | store_set_error_sqlite(s, "insert_edge"); |
| 2070 | return CBM_STORE_ERR; |
| 2071 | } |
| 2072 | |
| 2073 | /* Scan an edge from current row of stmt. */ |
| 2074 | static void scan_edge(sqlite3_stmt *stmt, cbm_edge_t *e) { |