#include "tableview.h" #include "duckdb.h" #include "../../../../cpp/vendor/sqlite3/sqlite3.h" #include #include #include #include namespace ge { namespace { // Escape simple para SQL identifiers — solo permite [A-Za-z0-9_]. Usado para // nombres de tabla / columnas que vienen de metadata. Si encuentra un char // invalido, lo reemplaza por '_'. NO sustituye al binding de parametros. std::string sanitize_ident(const char* s) { std::string out; if (!s) return out; for (const char* p = s; *p; ++p) { char c = *p; if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') out += c; else out += '_'; } return out; } // Escape para literales de string en SQL: dobla las comillas simples. std::string sql_escape(const char* s) { std::string out; if (!s) return out; for (const char* p = s; *p; ++p) { out += *p; if (*p == '\'') out += '\''; } return out; } // Helper RAII alrededor de duckdb_database/connection. struct DuckHandle { duckdb_database db = nullptr; duckdb_connection cn = nullptr; bool open(const char* path) { if (duckdb_open(path, &db) == DuckDBError) return false; if (duckdb_connect(db, &cn) == DuckDBError) return false; return true; } ~DuckHandle() { if (cn) duckdb_disconnect(&cn); if (db) duckdb_close(&db); } }; bool duck_query_silent(duckdb_connection cn, const char* sql) { duckdb_result r; duckdb_state st = duckdb_query(cn, sql, &r); duckdb_destroy_result(&r); return st == DuckDBSuccess; } } // namespace bool tableview_smoke_test(const char* duckdb_path) { DuckHandle h; if (!h.open(duckdb_path)) return false; duckdb_result r; if (duckdb_query(h.cn, "SELECT 42 AS x", &r) == DuckDBError) { duckdb_destroy_result(&r); return false; } bool ok = duckdb_row_count(&r) == 1 && duckdb_value_int64(&r, 0, 0) == 42; duckdb_destroy_result(&r); return ok; } bool tableview_count(const char* duckdb_path, const char* duck_table, const char* sql_filter, int64_t* out) { if (!duckdb_path || !duck_table || !out) return false; *out = 0; DuckHandle h; if (!h.open(duckdb_path)) return false; std::string tname = sanitize_ident(duck_table); if (tname.empty()) return false; std::string sql = "SELECT COUNT(*) FROM " + tname; if (sql_filter && *sql_filter) { sql += " WHERE "; sql += sql_filter; } duckdb_result r; if (duckdb_query(h.cn, sql.c_str(), &r) == DuckDBError) { std::fprintf(stderr, "[tableview_count] %s\n", duckdb_result_error(&r) ? duckdb_result_error(&r) : "?"); duckdb_destroy_result(&r); return false; } if (duckdb_row_count(&r) > 0) { *out = duckdb_value_int64(&r, 0, 0); } duckdb_destroy_result(&r); return true; } bool tableview_page(const char* duckdb_path, const char* duck_table, const char* id_column, const std::vector& columns, const char* sql_filter, const char* ops_db, const char* row_type, int64_t offset, int64_t limit, std::vector* out) { if (!out) return false; out->clear(); if (!duckdb_path || !duck_table || !id_column) return false; if (limit < 1) limit = 1; if (limit > 5000) limit = 5000; DuckHandle h; if (!h.open(duckdb_path)) return false; std::string idc = sanitize_ident(id_column); std::string tn = sanitize_ident(duck_table); if (idc.empty() || tn.empty()) return false; // Si tenemos ops_db y row_type, hacemos LEFT JOIN para detectar promovidas // a traves de json_extract sobre entities.metadata.source.row_id. bool join_ops = (ops_db && *ops_db && row_type && *row_type); if (join_ops) { // ATTACH del SQLite. Las attaches viven por conexion; idempotente // detectando si ya existe seria mas robusto pero este path se llama // por cada page() — abrimos conexion fresca cada vez asi que no. std::string attach = "ATTACH '" + sql_escape(ops_db) + "' AS ops (TYPE SQLITE)"; if (!duck_query_silent(h.cn, attach.c_str())) { // sin fallar — sin promovidas, solo perdemos el flag. join_ops = false; } } // SELECT: id_column + columns... + (CASE WHEN e.id NULL THEN '' ELSE e.id END). std::string sel = "SELECT t." + idc; for (const auto& c : columns) { std::string cc = sanitize_ident(c.c_str()); if (!cc.empty()) sel += ", t." + cc; } if (join_ops) { sel += ", COALESCE(e.id, '')"; } else { sel += ", ''"; } sel += " FROM " + tn + " AS t"; if (join_ops) { sel += " LEFT JOIN ops.entities AS e ON "; sel += "json_extract_string(e.metadata, '$.source.row_id') = CAST(t." + idc + " AS VARCHAR)"; sel += " AND e.type_ref = '" + sql_escape(row_type) + "'"; } if (sql_filter && *sql_filter) { sel += " WHERE "; sel += sql_filter; } sel += " ORDER BY t." + idc + " ASC"; sel += " LIMIT " + std::to_string(limit); sel += " OFFSET " + std::to_string(offset); duckdb_result r; if (duckdb_query(h.cn, sel.c_str(), &r) == DuckDBError) { std::fprintf(stderr, "[tableview_page] %s\n", duckdb_result_error(&r) ? duckdb_result_error(&r) : "?"); duckdb_destroy_result(&r); return false; } idx_t rows = duckdb_row_count(&r); idx_t cols = duckdb_column_count(&r); out->reserve((size_t)rows); for (idx_t row = 0; row < rows; ++row) { TablePageRow tr; // col 0 = id if (!duckdb_value_is_null(&r, 0, row)) { char* v = duckdb_value_varchar(&r, 0, row); tr.id = v ? v : ""; if (v) duckdb_free(v); } // cols 1..N-2 = columns[] idx_t expected_cols = (idx_t)columns.size(); tr.values.reserve(expected_cols); for (idx_t i = 0; i < expected_cols; ++i) { idx_t c = 1 + i; if (c >= cols) { tr.values.emplace_back(""); continue; } if (duckdb_value_is_null(&r, c, row)) { tr.values.emplace_back(""); } else { char* v = duckdb_value_varchar(&r, c, row); tr.values.emplace_back(v ? v : ""); if (v) duckdb_free(v); } } // ultima col = promoted_entity_id idx_t prom_col = cols > 0 ? cols - 1 : 0; if (cols > 0 && !duckdb_value_is_null(&r, prom_col, row)) { char* v = duckdb_value_varchar(&r, prom_col, row); tr.promoted_entity_id = v ? v : ""; if (v) duckdb_free(v); } out->push_back(std::move(tr)); } duckdb_destroy_result(&r); return true; } bool tableview_create(const char* ops_db, const char* name, const char* duckdb_path, const char* duck_table, const char* row_type, char* out_id, std::size_t out_id_n) { if (!ops_db || !duckdb_path || !duck_table) return false; if (!name || !*name) name = "Table"; auto now_ms = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); char id[80]; std::snprintf(id, sizeof(id), "table_%lld", (long long)now_ms); if (out_id && out_id_n > 0) { std::snprintf(out_id, out_id_n, "%s", id); } sqlite3* db = nullptr; if (sqlite3_open_v2(ops_db, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) { if (db) sqlite3_close(db); return false; } std::string meta = "{"; meta += "\"duckdb_path\":\""; meta += sql_escape(duckdb_path); meta += "\","; meta += "\"table_name\":\""; meta += sql_escape(duck_table); meta += "\","; meta += "\"row_type\":\""; meta += sql_escape(row_type ? row_type : ""); meta += "\","; meta += "\"id_column\":\"id\","; meta += "\"label_column\":\"name\","; meta += "\"columns\":[],"; meta += "\"filter_sql\":\"\","; meta += "\"expanded\":false"; meta += "}"; const char* ins = "INSERT INTO entities(id, name, type_ref, status, tags, source, metadata, " " created_at, updated_at) " "VALUES (?, ?, 'Table', 'active', '[]', 'manual', ?, " " strftime('%Y-%m-%dT%H:%M:%fZ','now'), " " strftime('%Y-%m-%dT%H:%M:%fZ','now'))"; sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(db, ins, -1, &st, nullptr) != SQLITE_OK) { sqlite3_close(db); return false; } sqlite3_bind_text(st, 1, id, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 3, meta.c_str(), -1, SQLITE_TRANSIENT); bool ok = sqlite3_step(st) == SQLITE_DONE; sqlite3_finalize(st); sqlite3_close(db); return ok; } // ---------------------------------------------------------------------------- // Path resolution + counts cache // ---------------------------------------------------------------------------- namespace { uint64_t fnv1a64(const char* s) { uint64_t h = 1469598103934665603ULL; for (; s && *s; ++s) { h ^= (uint8_t)*s; h *= 1099511628211ULL; } return h; } std::string dirname_of(const char* path) { if (!path) return ""; std::string s = path; auto pos = s.find_last_of("/\\"); if (pos == std::string::npos) return "."; return s.substr(0, pos); } bool is_absolute(const char* p) { if (!p || !*p) return false; if (p[0] == '/') return true; if (std::strlen(p) >= 2 && p[1] == ':' && ((p[0] >= 'A' && p[0] <= 'Z') || (p[0] >= 'a' && p[0] <= 'z'))) return true; return false; } } // namespace std::string tableview_resolve_path(const char* ops_db, const char* maybe_rel) { if (!maybe_rel) return ""; if (is_absolute(maybe_rel)) return maybe_rel; std::string base = dirname_of(ops_db); if (base.empty()) base = "."; return base + "/" + maybe_rel; } bool tableview_refresh_counts(const char* ops_db, std::unordered_map* out) { if (!ops_db || !out) return false; out->clear(); sqlite3* db = nullptr; if (sqlite3_open_v2(ops_db, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) { if (db) sqlite3_close(db); return false; } const char* sql = "SELECT id, " " json_extract(metadata, '$.duckdb_path'), " " json_extract(metadata, '$.table_name'), " " json_extract(metadata, '$.filter_sql') " "FROM entities WHERE type_ref = 'Table'"; 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* id_p = sqlite3_column_text(st, 0); const unsigned char* path_p = sqlite3_column_text(st, 1); const unsigned char* tab_p = sqlite3_column_text(st, 2); const unsigned char* flt_p = sqlite3_column_text(st, 3); if (!id_p || !path_p || !tab_p) continue; std::string abs = tableview_resolve_path(ops_db, (const char*)path_p); int64_t total = 0; if (!tableview_count(abs.c_str(), (const char*)tab_p, flt_p ? (const char*)flt_p : nullptr, &total)) { std::fprintf(stderr, "[tableview_refresh_counts] count failed for id=%s\n", id_p); continue; } out->emplace(fnv1a64((const char*)id_p), total); } sqlite3_finalize(st); sqlite3_close(db); return true; } } // namespace ge