| 1781 | /* ── Node CRUD ──────────────────────────────────────────────────── */ |
| 1782 | |
| 1783 | int64_t cbm_store_upsert_node(cbm_store_t *s, const cbm_node_t *n) { |
| 1784 | sqlite3_stmt *stmt = |
| 1785 | prepare_cached(s, &s->stmt_upsert_node, |
| 1786 | "INSERT INTO nodes (project, label, name, qualified_name, file_path, " |
| 1787 | "start_line, end_line, properties) " |
| 1788 | "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) " |
| 1789 | "ON CONFLICT(project, qualified_name) DO UPDATE SET " |
| 1790 | "label=?2, name=?3, file_path=?5, start_line=?6, end_line=?7, properties=?8 " |
| 1791 | "RETURNING id;"); |
| 1792 | if (!stmt) { |
| 1793 | return CBM_STORE_ERR; |
| 1794 | } |
| 1795 | |
| 1796 | bind_text(stmt, SKIP_ONE, safe_str(n->project)); |
| 1797 | bind_text(stmt, ST_COL_2, safe_str(n->label)); |
| 1798 | bind_text(stmt, ST_COL_3, safe_str(n->name)); |
| 1799 | bind_text(stmt, ST_COL_4, safe_str(n->qualified_name)); |
| 1800 | bind_text(stmt, ST_COL_5, safe_str(n->file_path)); |
| 1801 | sqlite3_bind_int(stmt, ST_COL_6, n->start_line); |
| 1802 | sqlite3_bind_int(stmt, ST_COL_7, n->end_line); |
| 1803 | bind_text(stmt, ST_COL_8, safe_props(n->properties_json)); |
| 1804 | |
| 1805 | int rc = sqlite3_step(stmt); |
| 1806 | if (rc == SQLITE_ROW) { |
| 1807 | int64_t id = sqlite3_column_int64(stmt, 0); |
| 1808 | sqlite3_reset(stmt); /* unblock COMMIT — RETURNING leaves stmt active */ |
| 1809 | return id; |
| 1810 | } |
| 1811 | sqlite3_reset(stmt); |
| 1812 | store_set_error_sqlite(s, "upsert_node"); |
| 1813 | return CBM_STORE_ERR; |
| 1814 | } |
| 1815 | |
| 1816 | /* Scan a node from current row of stmt. Heap-allocates strings. */ |
| 1817 | static void scan_node(sqlite3_stmt *stmt, cbm_node_t *n) { |