#include "entity_ops.h" #include "../../../../cpp/vendor/sqlite3/sqlite3.h" #include #include #include #include #include namespace ge { // ---------------------------------------------------------------------------- // FNV1a-64 — debe coincidir con graph_sources.cpp // ---------------------------------------------------------------------------- static uint64_t fnv1a64(const char* s) { uint64_t h = 1469598103934665603ULL; for (; s && *s; ++s) { h ^= (uint8_t)*s; h *= 1099511628211ULL; } return h; } // ---------------------------------------------------------------------------- // Heuristicas — sin para evitar peso. Inspeccion lineal del string. // ---------------------------------------------------------------------------- static bool is_email(const char* s) { if (!s || !*s) return false; const char* at = std::strchr(s, '@'); if (!at || at == s) return false; if (std::strchr(at + 1, '@')) return false; // dos @ const char* dot = std::strchr(at + 1, '.'); if (!dot || dot[1] == 0) return false; if (std::strchr(s, ' ')) return false; return true; } static bool is_ipv4(const char* s) { if (!s || !*s) return false; int parts = 0, digits = 0, n = 0; for (const char* p = s; ; ++p) { if (*p >= '0' && *p <= '9') { n = n * 10 + (*p - '0'); if (++digits > 3 || n > 255) return false; } else if (*p == '.' || *p == 0) { if (digits == 0) return false; ++parts; digits = 0; n = 0; if (*p == 0) break; } else { return false; } } return parts == 4; } static bool is_url(const char* s) { if (!s) return false; return std::strncmp(s, "http://", 7) == 0 || std::strncmp(s, "https://", 8) == 0; } static bool is_domain(const char* s) { if (!s || !*s) return false; if (std::strchr(s, ' ') || std::strchr(s, '@') || std::strchr(s, '/')) return false; const char* dot = std::strchr(s, '.'); if (!dot || dot == s || dot[1] == 0) return false; // El TLD debe ser al menos 2 caracteres alfabeticos int tld = 0; for (const char* p = std::strrchr(s, '.') + 1; *p; ++p) { if (!std::isalpha((unsigned char)*p)) return false; ++tld; } return tld >= 2; } static bool is_phone(const char* s) { if (!s || !*s) return false; int digits = 0; for (const char* p = s; *p; ++p) { if (*p >= '0' && *p <= '9') ++digits; else if (*p == '+' || *p == ' ' || *p == '-' || *p == '(' || *p == ')' || *p == '.') {} else return false; } return digits >= 7 && digits <= 15; } DetectedType detect_type(const char* text) { if (!text || !*text) return DT_TEXT; if (is_email(text)) return DT_EMAIL; if (is_ipv4(text)) return DT_IP_ADDRESS; if (is_url(text)) return DT_URL; if (is_phone(text)) return DT_PHONE; // antes que domain (numeros con puntos) if (is_domain(text)) return DT_DOMAIN; return DT_TEXT; } const char* detected_type_name(DetectedType dt) { switch (dt) { case DT_EMAIL: return "email"; case DT_IP_ADDRESS: return "ip_address"; case DT_URL: return "url"; case DT_DOMAIN: return "domain"; case DT_PHONE: return "phone"; case DT_TEXT: default: return "text"; } } // ---------------------------------------------------------------------------- // SQLite helpers // ---------------------------------------------------------------------------- static bool exec_one(sqlite3* db, const char* sql, const char** params, int n_params) { sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) != SQLITE_OK) return false; for (int i = 0; i < n_params; ++i) { sqlite3_bind_text(st, i + 1, params[i] ? params[i] : "", -1, SQLITE_TRANSIENT); } int rc = sqlite3_step(st); sqlite3_finalize(st); return rc == SQLITE_DONE; } static std::string now_iso() { using namespace std::chrono; auto t = system_clock::to_time_t(system_clock::now()); char buf[32]; std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", std::gmtime(&t)); return std::string(buf) + "Z"; } static long long now_ms() { using namespace std::chrono; return duration_cast(system_clock::now().time_since_epoch()).count(); } // ---------------------------------------------------------------------------- // CRUD // ---------------------------------------------------------------------------- bool entity_insert(const char* db_path, const char* name, const char* type_ref, char* out_id, size_t out_id_n) { if (!db_path || !name || !*name || !out_id || out_id_n < 32) return false; std::string tref; if (type_ref && *type_ref) { tref = type_ref; } else { tref = detected_type_name(detect_type(name)); } std::snprintf(out_id, out_id_n, "%s_%lld", tref.c_str(), now_ms()); std::string ts = now_iso(); sqlite3* db = nullptr; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) { if (db) sqlite3_close(db); return false; } const char* sql = "INSERT INTO entities (id, name, type_ref, source, created_at, updated_at) " "VALUES (?, ?, ?, 'manual', ?, ?)"; const char* params[5] = { out_id, name, tref.c_str(), ts.c_str(), ts.c_str() }; bool ok = exec_one(db, sql, params, 5); sqlite3_close(db); return ok; } bool entity_delete(const char* db_path, const char* id) { if (!db_path || !id) return false; sqlite3* db = nullptr; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) { if (db) sqlite3_close(db); return false; } const char* p_rel[2] = { id, id }; const char* p_ent[1] = { id }; exec_one(db, "DELETE FROM relations WHERE from_entity = ? OR to_entity = ?", p_rel, 2); bool ok = exec_one(db, "DELETE FROM entities WHERE id = ?", p_ent, 1); sqlite3_close(db); return ok; } bool entity_update_type(const char* db_path, const char* id, const char* new_type) { if (!db_path || !id || !new_type) return false; sqlite3* db = nullptr; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) { if (db) sqlite3_close(db); return false; } std::string ts = now_iso(); const char* p[3] = { new_type, ts.c_str(), id }; bool ok = exec_one(db, "UPDATE entities SET type_ref = ?, updated_at = ? WHERE id = ?", p, 3); sqlite3_close(db); return ok; } bool entity_duplicate(const char* db_path, const char* id, char* out_id, size_t out_id_n) { if (!db_path || !id || !out_id || out_id_n < 32) return false; sqlite3* db = nullptr; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) { if (db) sqlite3_close(db); return false; } sqlite3_stmt* st = nullptr; const char* sel = "SELECT name, type_ref, description, domain, tags, source, metadata, notes " "FROM entities WHERE id = ?"; if (sqlite3_prepare_v2(db, sel, -1, &st, nullptr) != SQLITE_OK) { sqlite3_close(db); return false; } sqlite3_bind_text(st, 1, id, -1, SQLITE_TRANSIENT); if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); sqlite3_close(db); return false; } auto col = [&](int i) { const unsigned char* p = sqlite3_column_text(st, i); return std::string(p ? (const char*)p : ""); }; std::string name = col(0) + " (copia)"; std::string tref = col(1); std::string desc = col(2); std::string dom = col(3); std::string tags = col(4); std::string src = col(5); std::string meta = col(6); std::string notes = col(7); sqlite3_finalize(st); std::snprintf(out_id, out_id_n, "%s_%lld", tref.c_str(), now_ms()); std::string ts = now_iso(); const char* ins = "INSERT INTO entities (id, name, type_ref, description, domain, tags, source, " "metadata, notes, created_at, updated_at) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; sqlite3_stmt* ist = nullptr; if (sqlite3_prepare_v2(db, ins, -1, &ist, nullptr) != SQLITE_OK) { sqlite3_close(db); return false; } sqlite3_bind_text(ist, 1, out_id, -1, SQLITE_TRANSIENT); sqlite3_bind_text(ist, 2, name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(ist, 3, tref.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(ist, 4, desc.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(ist, 5, dom.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(ist, 6, tags.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(ist, 7, src.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(ist, 8, meta.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(ist, 9, notes.c_str(),-1, SQLITE_TRANSIENT); sqlite3_bind_text(ist, 10, ts.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(ist, 11, ts.c_str(), -1, SQLITE_TRANSIENT); bool ok = sqlite3_step(ist) == SQLITE_DONE; sqlite3_finalize(ist); sqlite3_close(db); return ok; } bool relation_insert(const char* db_path, const char* from_id, const char* to_id, const char* name) { if (!db_path || !from_id || !to_id) return false; const char* rel = (name && *name) ? name : k_default_relation_name; sqlite3* db = nullptr; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) { if (db) sqlite3_close(db); return false; } char id[64]; std::snprintf(id, sizeof(id), "rel_%lld", now_ms()); std::string ts = now_iso(); const char* sql = "INSERT INTO relations (id, name, from_entity, to_entity, created_at, updated_at) " "VALUES (?, ?, ?, ?, ?, ?)"; const char* params[6] = { id, rel, from_id, to_id, ts.c_str(), ts.c_str() }; bool ok = exec_one(db, sql, params, 6); sqlite3_close(db); return ok; } // ---------------------------------------------------------------------------- // Notes (markdown) // ---------------------------------------------------------------------------- bool entity_get_notes(const char* db_path, const char* id, std::string* out) { if (!db_path || !id || !out) return false; out->clear(); sqlite3* db = nullptr; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) { if (db) sqlite3_close(db); return false; } sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(db, "SELECT notes FROM entities WHERE id = ?", -1, &st, nullptr) != SQLITE_OK) { sqlite3_close(db); return false; } sqlite3_bind_text(st, 1, id, -1, SQLITE_TRANSIENT); bool ok = false; if (sqlite3_step(st) == SQLITE_ROW) { const unsigned char* p = sqlite3_column_text(st, 0); if (p) *out = (const char*)p; ok = true; } sqlite3_finalize(st); sqlite3_close(db); return ok; } bool entity_set_notes(const char* db_path, const char* id, const char* notes) { if (!db_path || !id) return false; sqlite3* db = nullptr; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) { if (db) sqlite3_close(db); return false; } std::string ts = now_iso(); const char* p[3] = { notes ? notes : "", ts.c_str(), id }; bool ok = exec_one(db, "UPDATE entities SET notes = ?, updated_at = ? WHERE id = ?", p, 3); sqlite3_close(db); return ok; } // ---------------------------------------------------------------------------- // JSON helpers — parser + writer minimal para objetos planos y arrays de // strings. NO es un parser JSON general (no soporta objetos anidados, ni // arrays anidados, ni escapes Unicode \uXXXX). Suficiente para tags JSON // y metadata flat del Inspector. // ---------------------------------------------------------------------------- namespace { void skip_ws(const char*& p, const char* end) { while (p < end) { char c = *p; if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { ++p; continue; } break; } } // Parse un string JSON: comillas + contenido con escapes basicos. Devuelve // el contenido decodificado en `out`. Avanza `p`. Retorna false si malformado. bool parse_json_string(const char*& p, const char* end, std::string* out) { if (p >= end || *p != '"') return false; ++p; out->clear(); while (p < end) { char c = *p++; if (c == '"') return true; if (c == '\\' && p < end) { char e = *p++; switch (e) { case 'n': out->push_back('\n'); break; case 't': out->push_back('\t'); break; case 'r': out->push_back('\r'); break; case '"': out->push_back('"'); break; case '\\': out->push_back('\\'); break; case '/': out->push_back('/'); break; case 'b': out->push_back('\b'); break; case 'f': out->push_back('\f'); break; case 'u': // Skip 4 hex chars; emit '?' (no soportamos Unicode escapes). for (int i = 0; i < 4 && p < end; ++i) ++p; out->push_back('?'); break; default: out->push_back(e); break; } } else { out->push_back(c); } } return false; } // Parse un literal: numero, true, false, null. Avanza `p`. Devuelve raw // (lo que aparece literalmente en el JSON, sin comillas). bool parse_json_literal(const char*& p, const char* end, std::string* out) { out->clear(); while (p < end) { char c = *p; if (c == ',' || c == '}' || c == ']' || c == ' ' || c == '\t' || c == '\n' || c == '\r') break; out->push_back(c); ++p; } return !out->empty(); } void escape_json_string(const std::string& s, std::string* out) { out->push_back('"'); for (char c : s) { switch (c) { case '"': *out += "\\\""; break; case '\\': *out += "\\\\"; break; case '\n': *out += "\\n"; break; case '\r': *out += "\\r"; break; case '\t': *out += "\\t"; break; default: if ((unsigned char)c < 0x20) { char buf[8]; std::snprintf(buf, sizeof(buf), "\\u%04x", (unsigned)(unsigned char)c); *out += buf; } else { out->push_back(c); } } } out->push_back('"'); } // Parsea `["a","b",...]` (array plano de strings). Devuelve true si OK. // Tolerante: si no es un array, devuelve false con `out` vacio. bool parse_string_array(const std::string& src, std::vector* out) { out->clear(); if (src.empty()) return true; const char* p = src.c_str(); const char* end = p + src.size(); skip_ws(p, end); if (p >= end || *p != '[') return false; ++p; skip_ws(p, end); if (p < end && *p == ']') return true; // [] while (p < end) { skip_ws(p, end); std::string s; if (*p == '"') { if (!parse_json_string(p, end, &s)) return false; } else { if (!parse_json_literal(p, end, &s)) return false; } out->push_back(s); skip_ws(p, end); if (p >= end) return false; if (*p == ',') { ++p; continue; } if (*p == ']') { ++p; return true; } return false; } return false; } // Parsea `{"k":"v","k2":42,"k3":true}` (objeto plano). Devuelve los pares // en orden, con `is_string=true` si el valor venia entre comillas. bool parse_flat_object(const std::string& src, std::vector* out) { out->clear(); if (src.empty()) return true; const char* p = src.c_str(); const char* end = p + src.size(); skip_ws(p, end); if (p >= end || *p != '{') return false; ++p; skip_ws(p, end); if (p < end && *p == '}') return true; // {} while (p < end) { skip_ws(p, end); std::string key; if (*p != '"' || !parse_json_string(p, end, &key)) return false; skip_ws(p, end); if (p >= end || *p != ':') return false; ++p; skip_ws(p, end); if (p >= end) return false; MetadataField f; f.key = std::move(key); if (*p == '"') { f.is_string = true; if (!parse_json_string(p, end, &f.value_str)) return false; } else if (*p == '{' || *p == '[') { // Objeto/array anidado: consumir como literal hasta el cierre balanceado. int depth = 0; while (p < end) { char c = *p++; f.value_str.push_back(c); if (c == '{' || c == '[') ++depth; else if (c == '}' || c == ']') { --depth; if (depth == 0) break; } } f.is_string = false; } else { f.is_string = false; if (!parse_json_literal(p, end, &f.value_str)) return false; } out->push_back(std::move(f)); skip_ws(p, end); if (p >= end) return false; if (*p == ',') { ++p; continue; } if (*p == '}') { ++p; return true; } return false; } return false; } void build_string_array(const std::vector& xs, std::string* out) { out->push_back('['); for (size_t i = 0; i < xs.size(); ++i) { if (i) out->push_back(','); escape_json_string(xs[i], out); } out->push_back(']'); } void build_flat_object(const std::vector& fs, std::string* out) { out->push_back('{'); bool first = true; for (const auto& f : fs) { if (f.key.empty()) continue; // Saltar fields con valor vacio para no ensuciar el JSON con // claves "" / null. La excepcion es bool/string vacio explicito — // pero en v1 las claves vacias se omiten. if (f.is_string && f.value_str.empty()) continue; if (!f.is_string && f.value_str.empty()) continue; if (!first) out->push_back(','); first = false; escape_json_string(f.key, out); out->push_back(':'); if (f.is_string) { escape_json_string(f.value_str, out); } else { // Validar literal minimamente: si no es number/bool/null/array/object, // re-emitir como string para no producir JSON invalido. const std::string& v = f.value_str; char first_c = v[0]; bool ok_literal = (first_c == '-' || (first_c >= '0' && first_c <= '9')) || v == "true" || v == "false" || v == "null" || first_c == '{' || first_c == '['; if (ok_literal) { *out += v; } else { escape_json_string(v, out); } } } out->push_back('}'); } } // namespace bool entity_load_full(const char* db_path, const char* id, EntityRecord* out) { if (!db_path || !id || !out) return false; *out = EntityRecord{}; out->id = id; sqlite3* db = nullptr; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) { if (db) sqlite3_close(db); return false; } sqlite3_stmt* st = nullptr; const char* sql = "SELECT name, type_ref, description, status, tags, metadata " "FROM entities WHERE id = ?"; if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) != SQLITE_OK) { sqlite3_close(db); return false; } sqlite3_bind_text(st, 1, id, -1, SQLITE_TRANSIENT); bool ok = false; if (sqlite3_step(st) == SQLITE_ROW) { auto col = [&](int i) { const unsigned char* p = sqlite3_column_text(st, i); return std::string(p ? (const char*)p : ""); }; out->name = col(0); out->type_ref = col(1); out->description = col(2); out->status = col(3); std::string tags_json = col(4); std::string meta_json = col(5); parse_string_array(tags_json, &out->tags); parse_flat_object(meta_json, &out->metadata); ok = true; } sqlite3_finalize(st); sqlite3_close(db); return ok; } bool entity_update(const char* db_path, const EntityRecord& rec) { if (!db_path || rec.id.empty()) return false; sqlite3* db = nullptr; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) { if (db) sqlite3_close(db); return false; } std::string tags_json; std::string meta_json; build_string_array(rec.tags, &tags_json); build_flat_object(rec.metadata, &meta_json); std::string ts = now_iso(); std::string status = rec.status.empty() ? std::string("active") : rec.status; const char* sql = "UPDATE entities SET name = ?, type_ref = ?, description = ?, " "status = ?, tags = ?, metadata = ?, updated_at = ? WHERE id = ?"; sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) != SQLITE_OK) { sqlite3_close(db); return false; } sqlite3_bind_text(st, 1, rec.name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 2, rec.type_ref.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 3, rec.description.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 4, status.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 5, tags_json.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 6, meta_json.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 7, ts.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 8, rec.id.c_str(), -1, SQLITE_TRANSIENT); bool ok = sqlite3_step(st) == SQLITE_DONE; sqlite3_finalize(st); sqlite3_close(db); return ok; } bool entity_list_distinct_tags(const char* db_path, std::vector* out) { if (!db_path || !out) return false; out->clear(); sqlite3* db = nullptr; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) { if (db) sqlite3_close(db); return false; } // json_each esta disponible (SQLITE_ENABLE_JSON1 — ver CMakeLists del // amalgamation). Solo strings como tag (kind = 'text'). const char* sql = "SELECT DISTINCT json_each.value FROM entities, json_each(entities.tags) " "WHERE json_each.type = 'text' ORDER BY json_each.value"; sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) != SQLITE_OK) { sqlite3_close(db); return false; } while (sqlite3_step(st) == SQLITE_ROW) { const unsigned char* p = sqlite3_column_text(st, 0); if (p && *p) out->emplace_back((const char*)p); } sqlite3_finalize(st); sqlite3_close(db); return true; } // ---------------------------------------------------------------------------- // FTS5 search (issue 0009) // ---------------------------------------------------------------------------- namespace { // Sanitiza la query del usuario para FTS5: drop de chars que rompen el // parser, tokeniza por whitespace y envuelve cada token en comillas dobles // con prefijo `*`. Asi `"john doe"` -> `"john"* "doe"*`. La presencia de // varios tokens equivale a AND implicito en FTS5. std::string fts5_sanitize(const char* q) { std::string out; if (!q) return out; const char* p = q; while (*p) { while (*p && std::isspace((unsigned char)*p)) ++p; if (!*p) break; std::string tok; while (*p && !std::isspace((unsigned char)*p)) { char c = *p++; // Drop chars con significado en FTS5. if (c == '"' || c == '*' || c == '(' || c == ')' || c == ':' || c == '^' || c == '\'' || c == '!' || c == '+') continue; tok += c; } if (tok.empty()) continue; if (!out.empty()) out += ' '; out += '"'; out += tok; out += "\"*"; } return out; } } // namespace bool entity_search_fts(const char* db_path, const char* query, int limit, std::vector* out) { if (!db_path || !out) return false; out->clear(); if (limit < 1) limit = 1; if (limit > 200) limit = 200; std::string match = fts5_sanitize(query); if (match.empty()) return true; sqlite3* db = nullptr; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) { if (db) sqlite3_close(db); return false; } // entities_fts.id y entities.id colisionan en JOIN — calificamos siempre. const char* sql = "SELECT e.id, e.name, e.type_ref, bm25(entities_fts) AS r " "FROM entities_fts JOIN entities AS e ON e.rowid = entities_fts.rowid " "WHERE entities_fts MATCH ?1 ORDER BY r ASC LIMIT ?2"; sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) != SQLITE_OK) { sqlite3_close(db); return false; } sqlite3_bind_text(st, 1, match.c_str(), (int)match.size(), SQLITE_TRANSIENT); sqlite3_bind_int (st, 2, limit); while (sqlite3_step(st) == SQLITE_ROW) { EntityHit h; const unsigned char* id = sqlite3_column_text(st, 0); const unsigned char* name = sqlite3_column_text(st, 1); const unsigned char* tref = sqlite3_column_text(st, 2); h.id = id ? (const char*)id : ""; h.name = name ? (const char*)name : ""; h.type_ref = tref ? (const char*)tref : ""; h.rank = sqlite3_column_double(st, 3); out->push_back(std::move(h)); } sqlite3_finalize(st); sqlite3_close(db); return true; } bool entity_list_by_tags(const char* db_path, const std::vector& tags, std::vector* out_ids) { if (!db_path || !out_ids) return false; out_ids->clear(); if (tags.empty()) return true; sqlite3* db = nullptr; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) { if (db) sqlite3_close(db); return false; } // Una clausula EXISTS por tag, todas en AND. std::string sql = "SELECT id FROM entities WHERE "; for (size_t i = 0; i < tags.size(); ++i) { if (i) sql += " AND "; sql += "EXISTS(SELECT 1 FROM json_each(entities.tags) WHERE json_each.value = ?)"; } sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(db, sql.c_str(), -1, &st, nullptr) != SQLITE_OK) { sqlite3_close(db); return false; } for (size_t i = 0; i < tags.size(); ++i) { sqlite3_bind_text(st, (int)(i + 1), tags[i].c_str(), (int)tags[i].size(), SQLITE_TRANSIENT); } while (sqlite3_step(st) == SQLITE_ROW) { const unsigned char* p = sqlite3_column_text(st, 0); if (p && *p) out_ids->emplace_back((const char*)p); } sqlite3_finalize(st); sqlite3_close(db); return true; } bool entity_list_rows(const char* db_path, std::vector* out) { if (!db_path || !out) return false; out->clear(); sqlite3* db = nullptr; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) { if (db) sqlite3_close(db); return false; } const char* sql = "SELECT id, COALESCE(name,''), COALESCE(type_ref,''), " " COALESCE(status,''), COALESCE(updated_at,'') " "FROM entities ORDER BY type_ref, name"; sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) != SQLITE_OK) { sqlite3_close(db); return false; } while (sqlite3_step(st) == SQLITE_ROW) { EntityRowSnapshot r; const unsigned char* a0 = sqlite3_column_text(st, 0); const unsigned char* a1 = sqlite3_column_text(st, 1); const unsigned char* a2 = sqlite3_column_text(st, 2); const unsigned char* a3 = sqlite3_column_text(st, 3); const unsigned char* a4 = sqlite3_column_text(st, 4); r.id = a0 ? (const char*)a0 : ""; r.name = a1 ? (const char*)a1 : ""; r.type_ref = a2 ? (const char*)a2 : ""; r.status = a3 ? (const char*)a3 : ""; r.updated_at = a4 ? (const char*)a4 : ""; out->push_back(std::move(r)); } sqlite3_finalize(st); sqlite3_close(db); return true; } // ---------------------------------------------------------------------------- // Index user_data -> sql id // ---------------------------------------------------------------------------- bool entity_index_build(const char* db_path, EntityIndex* idx) { if (!db_path || !idx) return false; idx->by_hash.clear(); sqlite3* db = nullptr; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) { if (db) sqlite3_close(db); return false; } sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(db, "SELECT id FROM entities", -1, &st, nullptr) != SQLITE_OK) { sqlite3_close(db); return false; } while (sqlite3_step(st) == SQLITE_ROW) { const unsigned char* p = sqlite3_column_text(st, 0); if (!p) continue; const char* id = (const char*)p; idx->by_hash.emplace(fnv1a64(id), std::string(id)); } sqlite3_finalize(st); sqlite3_close(db); return true; } const char* entity_index_lookup(const EntityIndex& idx, uint64_t user_data) { auto it = idx.by_hash.find(user_data); return (it == idx.by_hash.end()) ? nullptr : it->second.c_str(); } } // namespace ge