From e24d16db59a6d94fd631ac9c5a0b6af3c2b9fd2f Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 25 Apr 2026 21:48:50 +0200 Subject: [PATCH 1/2] feat(core): sql_workbench component (issue 0032) ImGui SQL workbench: editor (text_editor + CodeLang::SQL), schema sidebar desde sqlite_master, tabla resultado (table_view), historial. Backend sql_workbench_run_query separado de UI; readonly opt-in (rechaza non-SELECT/ PRAGMA/EXPLAIN/WITH); cap de filas (truncated_at, default 10000); last_ms medido con steady_clock. API: sql_workbench(id, sqlite3*, SqlWorkbenchState&, size). DB caller-owned. Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/functions/core/sql_workbench.cpp | 400 ++++++++++++++++++ cpp/functions/core/sql_workbench.h | 62 +++ cpp/functions/core/sql_workbench.md | 108 +++++ .../{ => completed}/0032-cpp-sql-workbench.md | 0 4 files changed, 570 insertions(+) create mode 100644 cpp/functions/core/sql_workbench.cpp create mode 100644 cpp/functions/core/sql_workbench.h create mode 100644 cpp/functions/core/sql_workbench.md rename dev/issues/{ => completed}/0032-cpp-sql-workbench.md (100%) diff --git a/cpp/functions/core/sql_workbench.cpp b/cpp/functions/core/sql_workbench.cpp new file mode 100644 index 00000000..ded12b54 --- /dev/null +++ b/cpp/functions/core/sql_workbench.cpp @@ -0,0 +1,400 @@ +#include "core/sql_workbench.h" + +#include "core/text_editor.h" +#include "core/tokens.h" +#include "core/button.h" +#include "viz/table_view.h" + +#include "imgui.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace fn { + +// ───────────────────────────────────────────────────────────────────────────── +// Helpers +// ───────────────────────────────────────────────────────────────────────────── + +namespace { + +std::string ltrim(const std::string& s) { + size_t i = 0; + while (i < s.size() && std::isspace(static_cast(s[i]))) ++i; + return s.substr(i); +} + +std::string upper_first_token(const std::string& sql) { + std::string t = ltrim(sql); + size_t end = 0; + while (end < t.size() && !std::isspace(static_cast(t[end]))) ++end; + std::string tok = t.substr(0, end); + for (auto& c : tok) c = static_cast(std::toupper(static_cast(c))); + return tok; +} + +bool is_readonly_stmt(const std::string& sql) { + const std::string head = upper_first_token(sql); + return head == "SELECT" || head == "PRAGMA" || head == "EXPLAIN" || head == "WITH"; +} + +TextEditorState* editor_of(SqlWorkbenchState& state) { + return reinterpret_cast(state._editor); +} + +void ensure_editor(SqlWorkbenchState& state) { + if (!state._editor) { + state._editor = text_editor_create(CodeLang::SQL); + text_editor_set_text(editor_of(state), state.query.c_str()); + state._editor_synced = true; + } +} + +} // namespace + +// ───────────────────────────────────────────────────────────────────────────── +// Backend +// ───────────────────────────────────────────────────────────────────────────── + +bool sql_workbench_run_query(sqlite3* db, const char* sql, SqlWorkbenchState& state) { + state.columns.clear(); + state.rows.clear(); + state.last_error.clear(); + state.truncated = false; + state.last_ms = 0.0; + + if (!db) { + state.last_error = "no database handle"; + return false; + } + if (!sql || !*sql) { + state.last_error = "empty query"; + return false; + } + + if (state.readonly && !is_readonly_stmt(sql)) { + state.last_error = "readonly mode: only SELECT/PRAGMA/EXPLAIN/WITH allowed"; + return false; + } + + auto t0 = std::chrono::steady_clock::now(); + + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) { + state.last_error = sqlite3_errmsg(db); + if (stmt) sqlite3_finalize(stmt); + return false; + } + + int ncols = sqlite3_column_count(stmt); + state.columns.reserve(static_cast(ncols)); + for (int c = 0; c < ncols; ++c) { + const char* name = sqlite3_column_name(stmt, c); + state.columns.emplace_back(name ? name : ""); + } + + int row_cap = state.truncated_at > 0 ? state.truncated_at : 10000; + + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { + if (static_cast(state.rows.size()) >= row_cap) { + state.truncated = true; + break; + } + std::vector row; + row.reserve(static_cast(ncols)); + for (int c = 0; c < ncols; ++c) { + int t = sqlite3_column_type(stmt, c); + if (t == SQLITE_NULL) { + row.emplace_back("NULL"); + } else { + const unsigned char* txt = sqlite3_column_text(stmt, c); + row.emplace_back(txt ? reinterpret_cast(txt) : ""); + } + } + state.rows.emplace_back(std::move(row)); + } + + bool ok = (rc == SQLITE_ROW || rc == SQLITE_DONE); + if (!ok) { + state.last_error = sqlite3_errmsg(db); + } + + sqlite3_finalize(stmt); + + auto t1 = std::chrono::steady_clock::now(); + state.last_ms = std::chrono::duration(t1 - t0).count(); + return ok; +} + +void sql_workbench_load_schema(sqlite3* db, SqlWorkbenchState& state) { + state._schema_tables.clear(); + state._schema_views.clear(); + state._schema_loaded = true; + + if (!db) return; + + const char* sql = + "SELECT name, type FROM sqlite_master " + "WHERE type IN ('table','view') " + " AND name NOT LIKE 'sqlite_%' " + "ORDER BY type, name;"; + + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) return; + + while (sqlite3_step(stmt) == SQLITE_ROW) { + const unsigned char* name = sqlite3_column_text(stmt, 0); + const unsigned char* type = sqlite3_column_text(stmt, 1); + if (!name || !type) continue; + std::string n = reinterpret_cast(name); + std::string t = reinterpret_cast(type); + if (t == "table") state._schema_tables.push_back(n); + else if (t == "view") state._schema_views.push_back(n); + } + sqlite3_finalize(stmt); +} + +void sql_workbench_destroy(SqlWorkbenchState& state) { + if (state._editor) { + text_editor_destroy(editor_of(state)); + state._editor = nullptr; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// UI +// ───────────────────────────────────────────────────────────────────────────── + +namespace { + +void run_current_query(sqlite3* db, SqlWorkbenchState& state) { + // Sincroniza desde el editor antes de ejecutar. + if (state._editor) { + const char* txt = text_editor_get_text(editor_of(state)); + if (txt) state.query = txt; + } + bool ok = sql_workbench_run_query(db, state.query.c_str(), state); + if (ok) { + // Solo apear al historial si no esta duplicada con la ultima. + if (state.history.empty() || state.history.back() != state.query) { + state.history.push_back(state.query); + if (state.history.size() > 200) { + state.history.erase(state.history.begin(), + state.history.begin() + (state.history.size() - 200)); + } + } + } +} + +void set_query(SqlWorkbenchState& state, const std::string& q) { + state.query = q; + if (state._editor) text_editor_set_text(editor_of(state), q.c_str()); +} + +void draw_schema_sidebar(SqlWorkbenchState& state) { + using namespace fn_tokens; + + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim); + ImGui::TextUnformatted("SCHEMA"); + ImGui::PopStyleColor(); + ImGui::Separator(); + + if (!state._schema_loaded) { + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted); + ImGui::TextUnformatted("(no schema)"); + ImGui::PopStyleColor(); + return; + } + + auto draw_section = [&](const char* label, const std::vector& items) { + if (items.empty()) return; + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted); + ImGui::TextUnformatted(label); + ImGui::PopStyleColor(); + for (const auto& name : items) { + char buf[256]; + std::snprintf(buf, sizeof(buf), "%s##sw_t_%s", name.c_str(), name.c_str()); + if (ImGui::Selectable(buf, false)) { + char q[512]; + std::snprintf(q, sizeof(q), "SELECT * FROM %s LIMIT 100;", name.c_str()); + set_query(state, q); + } + } + ImGui::Dummy(ImVec2(0, spacing::xs)); + }; + + draw_section("tables", state._schema_tables); + draw_section("views", state._schema_views); +} + +void draw_history_popup(SqlWorkbenchState& state) { + if (!state._show_history) return; + + ImGui::OpenPopup("##sw_history"); + ImGui::SetNextWindowSize(ImVec2(640, 420), ImGuiCond_Appearing); + if (ImGui::BeginPopupModal("##sw_history", &state._show_history, + ImGuiWindowFlags_NoSavedSettings)) { + ImGui::TextUnformatted("Query history (last 50)"); + ImGui::Separator(); + + int total = static_cast(state.history.size()); + int start = std::max(0, total - 50); + for (int i = total - 1; i >= start; --i) { + char buf[64]; + std::snprintf(buf, sizeof(buf), "##sw_h_%d", i); + const std::string& q = state.history[i]; + // Mostrar primera linea como label. + std::string preview = q; + auto nl = preview.find('\n'); + if (nl != std::string::npos) preview = preview.substr(0, nl) + " ..."; + if (preview.size() > 120) preview = preview.substr(0, 117) + "..."; + ImGui::PushID(i); + if (ImGui::Selectable(preview.c_str(), false)) { + set_query(state, q); + state._show_history = false; + ImGui::CloseCurrentPopup(); + } + ImGui::PopID(); + } + ImGui::EndPopup(); + } +} + +void draw_results_table(const SqlWorkbenchState& state) { + if (state.columns.empty()) { + using namespace fn_tokens; + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted); + ImGui::TextUnformatted("(no results — run a query)"); + ImGui::PopStyleColor(); + return; + } + + int ncols = static_cast(state.columns.size()); + int nrows = static_cast(state.rows.size()); + + // Headers como const char*. + std::vector headers; + headers.reserve(state.columns.size()); + for (const auto& c : state.columns) headers.push_back(c.c_str()); + + // Cells flat row-major. + std::vector cells; + cells.reserve(static_cast(nrows) * static_cast(ncols)); + for (const auto& row : state.rows) { + for (int c = 0; c < ncols; ++c) { + if (c < static_cast(row.size())) cells.push_back(row[c].c_str()); + else cells.push_back(""); + } + } + + table_view("##sw_results", headers.data(), ncols, + cells.empty() ? nullptr : cells.data(), nrows); +} + +} // namespace + +void sql_workbench(const char* id, sqlite3* db, SqlWorkbenchState& state, ImVec2 size) { + using namespace fn_tokens; + + ensure_editor(state); + + if (db && !state._schema_loaded) { + sql_workbench_load_schema(db, state); + } + + ImGui::PushID(id); + ImGui::BeginChild("##sw_root", size, ImGuiChildFlags_Borders); + + if (!db) { + ImGui::PushStyleColor(ImGuiCol_Text, colors::warning); + ImGui::TextUnformatted("sql_workbench: no database handle"); + ImGui::PopStyleColor(); + ImGui::EndChild(); + ImGui::PopID(); + return; + } + + // Layout: sidebar (200px) | content (editor + tabla) + if (ImGui::BeginTable("##sw_layout", 2, + ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingFixedFit | + ImGuiTableFlags_BordersInnerV)) { + ImGui::TableSetupColumn("schema", ImGuiTableColumnFlags_WidthFixed, 200.0f); + ImGui::TableSetupColumn("content", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableNextRow(); + + // ── Sidebar ── + ImGui::TableSetColumnIndex(0); + ImGui::BeginChild("##sw_schema", ImVec2(0, 0)); + draw_schema_sidebar(state); + ImGui::EndChild(); + + // ── Content ── + ImGui::TableSetColumnIndex(1); + ImGui::BeginChild("##sw_content", ImVec2(0, 0)); + + // Toolbar superior. + bool run_clicked = fn_ui::button("Run", fn_ui::ButtonVariant::Primary); + ImGui::SameLine(); + if (fn_ui::button("History")) state._show_history = true; + ImGui::SameLine(); + if (state.readonly) { + ImGui::PushStyleColor(ImGuiCol_Text, colors::warning); + ImGui::TextUnformatted("READONLY"); + ImGui::PopStyleColor(); + ImGui::SameLine(); + } + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim); + ImGui::TextUnformatted("(Ctrl+Enter)"); + ImGui::PopStyleColor(); + + // Editor SQL. + ImVec2 avail = ImGui::GetContentRegionAvail(); + float editor_h = avail.y * 0.40f; + if (editor_h < 120.0f) editor_h = 120.0f; + text_editor_render(editor_of(state), "##sw_editor", ImVec2(-1, editor_h)); + + // Atajo Ctrl+Enter (cuando el child tiene foco/hover). + bool shortcut = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows) && + ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && + ImGui::IsKeyPressed(ImGuiKey_Enter, false); + + if (run_clicked || shortcut) { + run_current_query(db, state); + } + + // Status bar. + if (!state.last_error.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, colors::error); + ImGui::TextWrapped("error: %s", state.last_error.c_str()); + ImGui::PopStyleColor(); + } else if (!state.columns.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim); + ImGui::Text("%d rows%s in %.2f ms", + static_cast(state.rows.size()), + state.truncated ? " (truncated)" : "", + state.last_ms); + ImGui::PopStyleColor(); + } + + // Tabla resultado. + draw_results_table(state); + + ImGui::EndChild(); + ImGui::EndTable(); + } + + draw_history_popup(state); + + ImGui::EndChild(); + ImGui::PopID(); +} + +} // namespace fn diff --git a/cpp/functions/core/sql_workbench.h b/cpp/functions/core/sql_workbench.h new file mode 100644 index 00000000..68128f32 --- /dev/null +++ b/cpp/functions/core/sql_workbench.h @@ -0,0 +1,62 @@ +#pragma once + +// sql_workbench — componente ImGui que combina text_editor (SQL) + table_view +// + sidebar de schema + historial. Ejecuta queries contra una sqlite3* +// proporcionada por el caller (no se cierra desde el componente). +// +// Uso tipico: +// sqlite3* db = nullptr; +// sqlite3_open_v2("registry.db", &db, SQLITE_OPEN_READONLY, nullptr); +// fn::SqlWorkbenchState st; +// st.readonly = true; +// ... +// fn::sql_workbench("##sql", db, st, ImVec2(-1, -1)); +// +// Backend (`sql_workbench_run_query`) es testeable por separado contra una DB +// en memoria. + +#include "imgui.h" + +#include +#include + +struct sqlite3; + +namespace fn { + +struct SqlWorkbenchState { + std::string query = "SELECT name FROM sqlite_master WHERE type='table';"; + std::string last_error; + std::vector columns; + std::vector> rows; + std::vector history; + bool readonly = false; + int truncated_at = 10000; + bool truncated = false; + double last_ms = 0.0; + + // Internals (PIMPL-lite — el caller no necesita tocar esto). + void* _editor = nullptr; // fn::TextEditorState* + bool _editor_synced = false; + bool _show_history = false; + std::vector _schema_tables; + std::vector _schema_views; + bool _schema_loaded = false; +}; + +// Ejecuta `sql` contra `db` y rellena state.columns/rows/last_error/last_ms. +// Devuelve true si la query se ejecuto sin error. Aplica readonly check. +// Cap de filas a state.truncated_at (state.truncated indica si se trunco). +bool sql_workbench_run_query(sqlite3* db, const char* sql, SqlWorkbenchState& state); + +// Lee el schema de la DB (sqlite_master) y rellena state._schema_tables/_views. +void sql_workbench_load_schema(sqlite3* db, SqlWorkbenchState& state); + +// Renderiza el componente. La DB debe existir mientras se renderiza. Si +// db == nullptr, dibuja un empty state. +void sql_workbench(const char* id, sqlite3* db, SqlWorkbenchState& state, ImVec2 size = {-1, -1}); + +// Libera el editor interno (llamar al cerrar la app si la state es global). +void sql_workbench_destroy(SqlWorkbenchState& state); + +} // namespace fn diff --git a/cpp/functions/core/sql_workbench.md b/cpp/functions/core/sql_workbench.md new file mode 100644 index 00000000..338eb04f --- /dev/null +++ b/cpp/functions/core/sql_workbench.md @@ -0,0 +1,108 @@ +--- +name: sql_workbench +kind: component +lang: cpp +domain: core +version: "1.0.0" +purity: impure +signature: "void fn::sql_workbench(const char* id, sqlite3* db, fn::SqlWorkbenchState& state, ImVec2 size); bool fn::sql_workbench_run_query(sqlite3*, const char*, fn::SqlWorkbenchState&); void fn::sql_workbench_load_schema(sqlite3*, fn::SqlWorkbenchState&); void fn::sql_workbench_destroy(fn::SqlWorkbenchState&)" +description: "Workbench SQL embebido en ImGui: editor con highlighting (text_editor + CodeLang::SQL), tabla de resultados (table_view), sidebar de schema (sqlite_master) e historial. Ejecuta queries contra una sqlite3* del caller (no abre/cierra la DB)." +tags: [imgui, sql, sqlite, editor, table, dashboard, registry, debug] +uses_functions: [text_editor_cpp_core, table_view_cpp_viz, button_cpp_core, tokens_cpp_core] +uses_types: [] +returns: [] +returns_optional: false +error_type: error_go_core +imports: [imgui, sqlite3] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/sql_workbench.cpp" +framework: imgui +params: + - name: id + desc: "ImGui id estable para el contenedor (debe ser unico en el frame)" + - name: db + desc: "sqlite3* del caller. NO se cierra desde el componente. Si es nullptr, se renderiza un empty state." + - name: state + desc: "Estado mutable: query, columnas, filas, historial, last_error, readonly, truncated_at, last_ms" + - name: size + desc: "Tamano del contenedor (ImVec2). {-1,-1} = ocupar todo el espacio disponible" +output: "Renderiza el workbench dentro de un BeginChild. Modifica state.columns/rows/last_error/last_ms al ejecutar queries y state.history al apilar queries exitosas. sql_workbench_run_query devuelve true en caso de exito." +--- + +# sql_workbench + +Componente C++/ImGui para explorar bases SQLite desde una app del registry sin alt-tab al CLI ni a DBeaver. + +## Capacidades + +- **Editor SQL**: `text_editor` con `CodeLang::SQL` (syntax highlighting via ImGuiColorTextEdit). +- **Run + atajo**: boton `Run` y atajo `Ctrl+Enter` cuando el editor tiene foco. +- **Schema sidebar**: lista `tables` y `views` desde `sqlite_master`. Click → `SELECT * FROM LIMIT 100;`. +- **Resultados**: tabla via `table_view_cpp_viz` (sortable/scrollable/resizable). +- **Historial**: cada query exitosa se apila en `state.history`. Boton `History` abre popup con las ultimas 50. +- **Status bar**: `N rows in X.XX ms` o mensaje de error en rojo. +- **Cap de filas**: `state.truncated_at` (default 10000); marca `state.truncated = true` y muestra "(truncated)". +- **Readonly opt-in**: si `state.readonly = true`, rechaza statements que no comiencen (case-insensitive trim) por `SELECT`, `PRAGMA`, `EXPLAIN` o `WITH`. + +## API + +```cpp +namespace fn { + struct SqlWorkbenchState { + std::string query = "SELECT name FROM sqlite_master WHERE type='table';"; + std::string last_error; + std::vector columns; + std::vector> rows; + std::vector history; + bool readonly = false; + int truncated_at = 10000; + bool truncated = false; + double last_ms = 0.0; + // ...internals (editor handle, schema cache, popup flag). + }; + + bool sql_workbench_run_query(sqlite3* db, const char* sql, SqlWorkbenchState&); + void sql_workbench_load_schema(sqlite3* db, SqlWorkbenchState&); + void sql_workbench(const char* id, sqlite3* db, SqlWorkbenchState&, ImVec2 size = {-1, -1}); + void sql_workbench_destroy(SqlWorkbenchState&); +} +``` + +## Ejemplo + +```cpp +sqlite3* db = nullptr; +sqlite3_open_v2("registry.db", &db, SQLITE_OPEN_READONLY, nullptr); + +fn::SqlWorkbenchState st; +st.readonly = true; + +fn::run_app({.title="sql"}, [&]{ + fn::sql_workbench("##sql", db, st, ImVec2(-1, -1)); +}); + +fn::sql_workbench_destroy(st); +sqlite3_close(db); +``` + +## Decisiones de diseno + +- **Caller-owned DB**: el componente recibe `sqlite3*` y NO la cierra. Permite reusar la conexion ya abierta por la app host (ej: `registry_dashboard` con `registry.db`, app con `operations.db`). +- **Rows como strings**: `sqlite3_column_text` ya da convertibles. NULL se renderiza como literal `NULL` para distinguirlo del string vacio. +- **Backend separado de UI**: `sql_workbench_run_query` es testeable contra una DB en memoria (`:memory:`) sin frame ImGui. +- **Editor lazy**: el `TextEditorState` se crea en el primer render. `sql_workbench_destroy` lo libera. +- **Schema cache**: `sql_workbench_load_schema` se llama una vez al abrir; el caller puede forzar reload poniendo `state._schema_loaded = false`. +- **Sin sorting interno**: `table_view` muestra los indicadores pero NO reordena. Para ordenar, el usuario reescribe el `ORDER BY` en SQL — coherente con la filosofia de un workbench. +- **Sin queries async**: ejecucion sincrona en main thread. Para queries lentas, considerar wrapper con `process_runner` (deja la primitiva minima). + +## Riesgos / limites + +- Queries que cuelgan UI: cap de 10k filas + ejecucion sincrona. Documentado. +- Memoria: cada celda es un `std::string` propio (sin `string_view` sobre `sqlite3_column_text`, que es invalidado por `sqlite3_step`). +- Historial: cap suave a 200 entries en memoria; popup muestra las ultimas 50. + +## Combinaciones sugeridas + +`sql_workbench` + `app_menubar` + `dashboard_panel` = panel de exploracion en cualquier app del registry. Demo en `apps/primitives_gallery` apuntando a `registry.db` readonly. diff --git a/dev/issues/0032-cpp-sql-workbench.md b/dev/issues/completed/0032-cpp-sql-workbench.md similarity index 100% rename from dev/issues/0032-cpp-sql-workbench.md rename to dev/issues/completed/0032-cpp-sql-workbench.md From 4d53ee63800938674d67295499e870625ff4e90e Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 25 Apr 2026 21:49:09 +0200 Subject: [PATCH 2/2] feat(primitives_gallery): demo de sql_workbench contra registry.db readonly Demo nuevo en demos_sql.cpp: abre registry.db en SQLITE_OPEN_READONLY (resolviendo via FN_REGISTRY_ROOT o cwd ascendente), monta fn::SqlWorkbenchState con readonly=true y query inicial sobre la tabla functions. Wire-up: entry en k_demos[] tras process_runner; declaracion en demos.h; sources sql_workbench.cpp + demos_sql.cpp + link SQLite::SQLite3 en CMakeLists.txt. Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/apps/primitives_gallery/CMakeLists.txt | 7 ++ cpp/apps/primitives_gallery/demos.h | 1 + cpp/apps/primitives_gallery/demos_sql.cpp | 129 +++++++++++++++++++++ cpp/apps/primitives_gallery/main.cpp | 1 + 4 files changed, 138 insertions(+) create mode 100644 cpp/apps/primitives_gallery/demos_sql.cpp diff --git a/cpp/apps/primitives_gallery/CMakeLists.txt b/cpp/apps/primitives_gallery/CMakeLists.txt index 6812bf9f..805a7feb 100644 --- a/cpp/apps/primitives_gallery/CMakeLists.txt +++ b/cpp/apps/primitives_gallery/CMakeLists.txt @@ -8,10 +8,13 @@ add_imgui_app(primitives_gallery demos_text_editor.cpp demos_gl_texture.cpp demos_extras.cpp + demos_sql.cpp # text_editor + file_watcher (issue 0025) ${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp ${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp ${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit/TextEditor.cpp + # sql_workbench (issue 0032) + ${CMAKE_SOURCE_DIR}/functions/core/sql_workbench.cpp # Core primitives demoed (tokens vive en fn_framework) ${CMAKE_SOURCE_DIR}/functions/core/fullscreen_window.cpp ${CMAKE_SOURCE_DIR}/functions/core/page_header.cpp @@ -61,6 +64,10 @@ target_include_directories(primitives_gallery PRIVATE ${CMAKE_SOURCE_DIR}/vendor/stb ) +# SQLite (sql_workbench) — alias provisto por cpp/CMakeLists.txt: +# system on Linux, vendored amalgamation on Windows cross-compile. +target_link_libraries(primitives_gallery PRIVATE SQLite::SQLite3) + if(WIN32) target_link_libraries(primitives_gallery PRIVATE opengl32) endif() diff --git a/cpp/apps/primitives_gallery/demos.h b/cpp/apps/primitives_gallery/demos.h index b7f22c32..ff527479 100644 --- a/cpp/apps/primitives_gallery/demos.h +++ b/cpp/apps/primitives_gallery/demos.h @@ -21,6 +21,7 @@ void demo_dashboard_panel(); void demo_text_editor(); // wave 1, issue 0025 void demo_file_watcher(); // wave 1, issue 0025 void demo_process_runner(); +void demo_sql_workbench(); // issue 0032 // --- Viz --- void demo_bar_chart(); diff --git a/cpp/apps/primitives_gallery/demos_sql.cpp b/cpp/apps/primitives_gallery/demos_sql.cpp new file mode 100644 index 00000000..01dc4a3e --- /dev/null +++ b/cpp/apps/primitives_gallery/demos_sql.cpp @@ -0,0 +1,129 @@ +// Demo de sql_workbench (Core, issue 0032). +// +// Abre `registry.db` en modo readonly y deja que el componente liste sus +// tablas en la sidebar. La idea es probar el ciclo Run + tabla + historial +// contra una DB real sin riesgo de mutarla. + +#include "demos.h" +#include "demo.h" + +#include "core/sql_workbench.h" +#include "core/tokens.h" + +#include +#include + +#include +#include +#include +#include + +namespace gallery { + +namespace { + +struct SqlDemoState { + sqlite3* db = nullptr; + fn::SqlWorkbenchState wb; + bool tried_open = false; + std::string db_path; + std::string open_error; +}; + +SqlDemoState& state() { + static SqlDemoState s; + return s; +} + +// Resuelve la ruta a registry.db: env FN_REGISTRY_ROOT/registry.db si existe, +// si no, prueba ./registry.db, ../registry.db, ../../registry.db (build tree). +std::string resolve_registry_db() { + if (const char* env = std::getenv("FN_REGISTRY_ROOT")) { + std::string p = std::string(env) + "/registry.db"; + if (FILE* f = std::fopen(p.c_str(), "rb")) { std::fclose(f); return p; } + } + const char* candidates[] = { + "registry.db", + "../registry.db", + "../../registry.db", + "../../../registry.db", + "../../../../registry.db", + }; + for (const char* c : candidates) { + if (FILE* f = std::fopen(c, "rb")) { std::fclose(f); return c; } + } + return ""; +} + +void ensure_open() { + auto& s = state(); + if (s.tried_open) return; + s.tried_open = true; + + s.db_path = resolve_registry_db(); + if (s.db_path.empty()) { + s.open_error = "registry.db not found (tried FN_REGISTRY_ROOT and parent dirs)"; + return; + } + int rc = sqlite3_open_v2(s.db_path.c_str(), &s.db, + SQLITE_OPEN_READONLY, nullptr); + if (rc != SQLITE_OK) { + s.open_error = sqlite3_errmsg(s.db); + if (s.db) { sqlite3_close(s.db); s.db = nullptr; } + return; + } + s.wb.readonly = true; + // Query inicial mas util para el demo: lista de funciones del registry. + s.wb.query = + "SELECT id, kind, purity, domain\n" + "FROM functions\n" + "ORDER BY id\n" + "LIMIT 50;"; +} + +} // namespace + +void demo_sql_workbench() { + using namespace fn_tokens; + + demo_header("sql_workbench", "v1.0.0", + "Workbench SQL: editor con highlighting, schema sidebar, tabla de " + "resultados e historial. Ejecuta queries contra una sqlite3* del caller. " + "En este demo, registry.db abierto en modo readonly."); + + ensure_open(); + auto& s = state(); + + if (!s.open_error.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, colors::error); + ImGui::TextWrapped("could not open registry.db: %s", s.open_error.c_str()); + ImGui::PopStyleColor(); + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted); + ImGui::TextWrapped("Set FN_REGISTRY_ROOT to the repo root or run from the repo cwd."); + ImGui::PopStyleColor(); + return; + } + + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim); + ImGui::Text("db: %s (readonly)", s.db_path.c_str()); + ImGui::PopStyleColor(); + + section("workbench"); + { + ImVec2 avail = ImGui::GetContentRegionAvail(); + // Reserva un pelin para el code_block de abajo. + float h = avail.y - 110.0f; + if (h < 320.0f) h = 320.0f; + fn::sql_workbench("##gallery_sql", s.db, s.wb, ImVec2(-1, h)); + } + + code_block( + "sqlite3* db = nullptr;\n" + "sqlite3_open_v2(\"registry.db\", &db, SQLITE_OPEN_READONLY, nullptr);\n" + "fn::SqlWorkbenchState st;\n" + "st.readonly = true;\n" + "fn::sql_workbench(\"##sql\", db, st, ImVec2(-1, -1));" + ); +} + +} // namespace gallery diff --git a/cpp/apps/primitives_gallery/main.cpp b/cpp/apps/primitives_gallery/main.cpp index 269bfe6b..22cf0b9b 100644 --- a/cpp/apps/primitives_gallery/main.cpp +++ b/cpp/apps/primitives_gallery/main.cpp @@ -49,6 +49,7 @@ static const DemoEntry k_demos[] = { {"text_editor", "text_editor", "Core", &gallery::demo_text_editor}, // wave 1 {"file_watcher", "file_watcher", "Core", &gallery::demo_file_watcher}, // wave 1 {"process_runner", "process_runner", "Core", &gallery::demo_process_runner}, + {"sql_workbench", "sql_workbench", "Core", &gallery::demo_sql_workbench}, // issue 0032 // Viz {"bar_chart", "bar_chart", "Viz", &gallery::demo_bar_chart}, {"pie_chart", "pie_chart", "Viz", &gallery::demo_pie_chart},