feat(0035a): tipo Group + columna group_id en entities

Plumbing para issue 0035 — agrupacion de resultados de enrichers
cuando exceden umbral. Sin cambios visibles para el usuario todavia.

- Migracion idempotente: ALTER TABLE entities ADD COLUMN group_id si
  no existe (detectado via PRAGMA table_info). Se ejecuta al abrir
  el proyecto en switch_to_project y en el bootstrap inicial.
- Tipo Group en examples/types.yaml (template) y en el types.yaml
  del proyecto default activo en Windows.
- shape=square (regla en types_registry.cpp extendida a Group),
  color=#94A3B8, icon=ti-stack-2.
- Fields: name (req), count (int), enricher (string), batch_id (string).

Refs: issues/0035a-group-type-and-schema.md
This commit is contained in:
2026-05-03 14:23:23 +02:00
parent b0706b71c0
commit fc4f0824da
11 changed files with 559 additions and 8 deletions
+71
View File
@@ -55,6 +55,7 @@ CREATE TABLE IF NOT EXISTS entities (
source TEXT NOT NULL DEFAULT 'graph_explorer',
metadata TEXT NOT NULL DEFAULT '{}',
notes TEXT NOT NULL DEFAULT '',
group_id TEXT,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
);
@@ -300,6 +301,76 @@ static bool bootstrap_operations_db(const std::string& path, std::string* error_
return true;
}
// ----------------------------------------------------------------------------
// Migraciones idempotentes de operations.db existente
// ----------------------------------------------------------------------------
//
// Cada migracion es un check + ALTER TABLE / CREATE ... condicional. Se
// ejecuta una vez al abrir un proyecto (project_migrate_schema). Detecta el
// estado actual via PRAGMA table_info y ejecuta solo lo que falta. No-op si
// la BD ya esta al dia.
//
// Issue 0035a: anade columna entities.group_id (TEXT NULL) si no existe.
// Devuelve true si la columna `column` existe en la tabla `table`.
static bool table_has_column(sqlite3* db, const char* table, const char* column) {
std::string sql = "PRAGMA table_info(";
sql += table;
sql += ");";
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
return false;
}
bool found = false;
while (sqlite3_step(stmt) == SQLITE_ROW) {
const unsigned char* name = sqlite3_column_text(stmt, 1);
if (name && std::strcmp(reinterpret_cast<const char*>(name), column) == 0) {
found = true;
break;
}
}
sqlite3_finalize(stmt);
return found;
}
bool project_migrate_schema(const std::string& path, std::string* error_msg) {
sqlite3* db = nullptr;
int rc = sqlite3_open_v2(path.c_str(), &db,
SQLITE_OPEN_READWRITE, nullptr);
if (rc != SQLITE_OK) {
if (error_msg) {
*error_msg = "sqlite3_open failed: ";
*error_msg += db ? sqlite3_errmsg(db) : sqlite3_errstr(rc);
}
if (db) sqlite3_close(db);
return false;
}
// 0035a: entities.group_id
if (!table_has_column(db, "entities", "group_id")) {
char* errmsg = nullptr;
rc = sqlite3_exec(db,
"ALTER TABLE entities ADD COLUMN group_id TEXT",
nullptr, nullptr, &errmsg);
if (rc != SQLITE_OK) {
if (error_msg) {
*error_msg = "ALTER TABLE entities ADD COLUMN group_id failed: ";
*error_msg += errmsg ? errmsg : "(unknown)";
}
if (errmsg) sqlite3_free(errmsg);
sqlite3_close(db);
return false;
}
if (errmsg) sqlite3_free(errmsg);
std::fprintf(stdout,
"[project_manager] migrated %s: ALTER TABLE entities ADD COLUMN group_id\n",
path.c_str());
}
sqlite3_close(db);
return true;
}
// ----------------------------------------------------------------------------
// project_create
// ----------------------------------------------------------------------------