| 1678 | /* ── Node CRUD ──────────────────────────────────────────────────── */ |
| 1679 | |
| 1680 | int64_t cbm_store_upsert_node(cbm_store_t *s, const cbm_node_t *n) { |
| 1681 | sqlite3_stmt *stmt = |
| 1682 | prepare_cached(s, &s->stmt_upsert_node, |
| 1683 | "INSERT INTO nodes (project, label, name, qualified_name, file_path, " |
| 1684 | "start_line, end_line, properties) " |
| 1685 | "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) " |
| 1686 | "ON CONFLICT(project, qualified_name) DO UPDATE SET " |
| 1687 | "label=?2, name=?3, file_path=?5, start_line=?6, end_line=?7, properties=?8 " |
| 1688 | "RETURNING id;"); |
| 1689 | if (!stmt) { |
| 1690 | return CBM_STORE_ERR; |
| 1691 | } |
| 1692 | |
| 1693 | bind_text(stmt, SKIP_ONE, safe_str(n->project)); |
| 1694 | bind_text(stmt, ST_COL_2, safe_str(n->label)); |
| 1695 | bind_text(stmt, ST_COL_3, safe_str(n->name)); |
| 1696 | bind_text(stmt, ST_COL_4, safe_str(n->qualified_name)); |
| 1697 | bind_text(stmt, ST_COL_5, safe_str(n->file_path)); |
| 1698 | sqlite3_bind_int(stmt, ST_COL_6, n->start_line); |
| 1699 | sqlite3_bind_int(stmt, ST_COL_7, n->end_line); |
| 1700 | bind_text(stmt, ST_COL_8, safe_props(n->properties_json)); |
| 1701 | |
| 1702 | int rc = sqlite3_step(stmt); |
| 1703 | if (rc == SQLITE_ROW) { |
| 1704 | int64_t id = sqlite3_column_int64(stmt, 0); |
| 1705 | sqlite3_reset(stmt); /* unblock COMMIT — RETURNING leaves stmt active */ |
| 1706 | return id; |
| 1707 | } |
| 1708 | sqlite3_reset(stmt); |
| 1709 | store_set_error_sqlite(s, "upsert_node"); |
| 1710 | return CBM_STORE_ERR; |
| 1711 | } |
| 1712 | |
| 1713 | /* Scan a node from current row of stmt. Heap-allocates strings. */ |
| 1714 | static void scan_node(sqlite3_stmt *stmt, cbm_node_t *n) { |