merge: issue/0045-cpp-extract-pure-logic — implementación paralela

This commit is contained in:
2026-04-28 23:59:45 +02:00
27 changed files with 1104 additions and 68 deletions
+5 -2
View File
@@ -17,12 +17,14 @@ add_imgui_app(primitives_gallery
${CMAKE_SOURCE_DIR}/functions/core/timeline.cpp
demos_sql.cpp
demos_scientific.cpp
# text_editor + file_watcher (issue 0025)
# text_editor + file_watcher (issue 0025) + file_poll_diff pure (issue 0045)
${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp
${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp
${CMAKE_SOURCE_DIR}/functions/core/file_poll_diff.cpp
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit/TextEditor.cpp
# sql_workbench (issue 0032)
# sql_workbench (issue 0032) + sql_parse pure (issue 0045)
${CMAKE_SOURCE_DIR}/functions/core/sql_workbench.cpp
${CMAKE_SOURCE_DIR}/functions/core/sql_parse.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
@@ -38,6 +40,7 @@ add_imgui_app(primitives_gallery
${CMAKE_SOURCE_DIR}/functions/core/toast.cpp
${CMAKE_SOURCE_DIR}/functions/core/tree_view.cpp
${CMAKE_SOURCE_DIR}/functions/core/process_runner.cpp
${CMAKE_SOURCE_DIR}/functions/core/process_state_machine.cpp
# Viz primitives demoed
${CMAKE_SOURCE_DIR}/functions/viz/kpi_card.cpp
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
+1
View File
@@ -1,5 +1,6 @@
add_imgui_app(shaders_lab
main.cpp
compiler.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/gl_loader.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/gl_shader.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/gl_framebuffer.cpp
+63
View File
@@ -0,0 +1,63 @@
#include "compiler.h"
#include "gfx/shader_canvas.h"
#include "gfx/gl_shader.h"
#include "gfx/uniform_parser.h"
#include "gfx/uniform_panel.h"
#include "gfx/dag_compile.h"
#include "gfx/dag_uniforms.h"
#include <chrono>
#include <string>
#include <vector>
// ── Globals declarados en main.cpp (single source of truth) ─────────────────
extern fn::gfx::ShaderCanvas g_canvas_code;
extern fn::gfx::ShaderCanvas g_canvas_dag;
extern std::string g_source;
extern std::string g_code_err;
extern int g_code_err_line;
extern std::chrono::steady_clock::time_point g_code_last_edit;
extern bool g_code_dirty;
extern std::vector<fn::gfx::UniformDescriptor> g_descs;
extern fn::gfx::UniformStore g_store;
extern std::vector<fn::gfx::DagStep> g_pipeline;
extern std::string g_dag_glsl;
extern std::string g_dag_err;
extern int g_dag_err_line;
namespace shaders_lab {
void compile_code() {
auto r = fn::gfx::compile_fragment(g_source);
if (r.ok) {
g_descs = fn::gfx::parse_uniforms(g_source);
fn::gfx::uniforms_sync(g_store, g_descs);
fn::gfx::canvas_set_program(g_canvas_code, r.program);
g_code_err.clear();
g_code_err_line = -1;
} else {
g_code_err = r.err_msg;
g_code_err_line = r.err_line;
}
}
void compile_dag() {
g_dag_glsl = fn::gfx::compile_dag_to_glsl(g_pipeline);
auto r = fn::gfx::compile_fragment(g_dag_glsl);
if (r.ok) {
fn::gfx::canvas_set_program(g_canvas_dag, r.program);
g_dag_err.clear();
g_dag_err_line = -1;
} else {
g_dag_err = r.err_msg;
g_dag_err_line = r.err_line;
}
}
void mark_code_dirty() {
g_code_last_edit = std::chrono::steady_clock::now();
g_code_dirty = true;
}
} // namespace shaders_lab
+24
View File
@@ -0,0 +1,24 @@
#pragma once
// shaders_lab/compiler — extrae las rutinas impuras de compilacion del shader
// (compile_code, compile_dag, mark_code_dirty) desde main.cpp para que el
// archivo principal quede acotado a la composicion de paneles ImGui.
//
// Las globals (g_source, g_descs, g_store, g_pipeline, etc.) se declaran
// extern y viven en main.cpp; aqui solo orquestamos compilacion.
namespace shaders_lab {
// Compila g_source -> programa OpenGL para g_canvas_code, refresca g_descs
// y sincroniza g_store. Actualiza g_code_err / g_code_err_line.
void compile_code();
// Compila g_pipeline -> g_dag_glsl -> programa OpenGL para g_canvas_dag.
// Actualiza g_dag_err / g_dag_err_line.
void compile_dag();
// Marca el shader Code como dirty y registra el timestamp del ultimo edit
// (para debounce de 250ms en el render loop).
void mark_code_dirty();
} // namespace shaders_lab
+21 -45
View File
@@ -19,6 +19,8 @@
#include "core/app_menubar.h"
#include "core/layout_storage.h"
#include "compiler.h"
#include <chrono>
#include <cctype>
#include <cstring>
@@ -27,8 +29,9 @@
#include <utility>
#include <vector>
static fn::gfx::ShaderCanvas g_canvas_code;
static fn::gfx::ShaderCanvas g_canvas_dag;
// Globals: linked extern desde compiler.cpp. NO `static` aqui.
fn::gfx::ShaderCanvas g_canvas_code;
fn::gfx::ShaderCanvas g_canvas_dag;
// Default placeholder so the Code panel does something useful on first launch
// without committing to one specific look.
@@ -47,19 +50,19 @@ void main() {
}
)glsl";
static std::string g_source = CODE_PLACEHOLDER;
static std::string g_code_err;
static int g_code_err_line = -1;
static std::chrono::steady_clock::time_point g_code_last_edit;
static bool g_code_dirty = true;
static std::vector<fn::gfx::UniformDescriptor> g_descs;
static fn::gfx::UniformStore g_store;
std::string g_source = CODE_PLACEHOLDER;
std::string g_code_err;
int g_code_err_line = -1;
std::chrono::steady_clock::time_point g_code_last_edit;
bool g_code_dirty = true;
std::vector<fn::gfx::UniformDescriptor> g_descs;
fn::gfx::UniformStore g_store;
static std::vector<fn::gfx::DagStep> g_pipeline;
static std::string g_dag_glsl;
static std::string g_dag_err;
static int g_dag_err_line = -1;
static bool g_dag_dirty = true;
std::vector<fn::gfx::DagStep> g_pipeline;
std::string g_dag_glsl;
std::string g_dag_err;
int g_dag_err_line = -1;
static bool g_dag_dirty = true; // solo lo usa main.cpp
// ── Panel visibility (toggled from View menu and panel close button) ──────
static bool g_show_code = true;
@@ -84,37 +87,10 @@ static char g_save_desc[256] = "";
static char g_save_tags[128] = "shaders_lab,user";
static std::string g_save_err;
static void compile_code() {
auto r = fn::gfx::compile_fragment(g_source);
if (r.ok) {
g_descs = fn::gfx::parse_uniforms(g_source);
fn::gfx::uniforms_sync(g_store, g_descs);
fn::gfx::canvas_set_program(g_canvas_code, r.program);
g_code_err.clear();
g_code_err_line = -1;
} else {
g_code_err = r.err_msg;
g_code_err_line = r.err_line;
}
}
static void compile_dag() {
g_dag_glsl = fn::gfx::compile_dag_to_glsl(g_pipeline);
auto r = fn::gfx::compile_fragment(g_dag_glsl);
if (r.ok) {
fn::gfx::canvas_set_program(g_canvas_dag, r.program);
g_dag_err.clear();
g_dag_err_line = -1;
} else {
g_dag_err = r.err_msg;
g_dag_err_line = r.err_line;
}
}
static void mark_code_dirty() {
g_code_last_edit = std::chrono::steady_clock::now();
g_code_dirty = true;
}
// compile_code, compile_dag, mark_code_dirty viven en compiler.cpp
using shaders_lab::compile_code;
using shaders_lab::compile_dag;
using shaders_lab::mark_code_dirty;
static void ensure_dag_default() {
if (g_pipeline.empty()) {
@@ -7,6 +7,7 @@ add_imgui_app(text_editor_smoke
main.cpp
${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp
${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp
${CMAKE_SOURCE_DIR}/functions/core/file_poll_diff.cpp
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit/TextEditor.cpp
)
target_include_directories(text_editor_smoke PRIVATE
+45
View File
@@ -0,0 +1,45 @@
#include "core/file_poll_diff.h"
#include <unordered_map>
#include <unordered_set>
namespace fn_ui {
FileDiff file_poll_diff(const std::vector<FileEntry>& before,
const std::vector<FileEntry>& after) {
FileDiff out;
// Index before by path para lookup O(1).
std::unordered_map<std::string, const FileEntry*> idx_before;
idx_before.reserve(before.size());
for (const auto& e : before) {
idx_before[e.path] = &e;
}
// Recorrer after: clasificar added/modified.
std::unordered_set<std::string> seen_in_after;
seen_in_after.reserve(after.size());
for (const auto& e : after) {
seen_in_after.insert(e.path);
auto it = idx_before.find(e.path);
if (it == idx_before.end()) {
out.added.push_back(e.path);
} else {
const FileEntry* prev = it->second;
if (prev->size != e.size || prev->mtime != e.mtime) {
out.modified.push_back(e.path);
}
}
}
// Recorrer before: detectar removed (paths que no estan en after).
for (const auto& e : before) {
if (seen_in_after.find(e.path) == seen_in_after.end()) {
out.removed.push_back(e.path);
}
}
return out;
}
} // namespace fn_ui
+34
View File
@@ -0,0 +1,34 @@
#pragma once
// file_poll_diff — diff puro entre dos snapshots de filesystem.
//
// Pareja natural de file_watcher: en plataformas sin inotify/RDCW (o cuando
// se quiere fallback portable), el caller toma snapshots periodicos via
// stat()/opendir() y los compara con esta funcion para emitir eventos
// added/modified/removed. Sin I/O, sin estado.
#include <cstdint>
#include <string>
#include <vector>
namespace fn_ui {
struct FileEntry {
std::string path;
uint64_t size = 0;
int64_t mtime = 0; // unix seconds
};
struct FileDiff {
std::vector<std::string> added;
std::vector<std::string> modified; // size o mtime distinto
std::vector<std::string> removed;
};
// Pura: calcula diff entre dos snapshots por path. Asume entries con paths
// unicos en cada snapshot. Orden de los vectores: el orden en que aparecen
// los paths en `after` (added/modified) y en `before` (removed).
FileDiff file_poll_diff(const std::vector<FileEntry>& before,
const std::vector<FileEntry>& after);
} // namespace fn_ui
+77
View File
@@ -0,0 +1,77 @@
---
name: file_poll_diff
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "fn_ui::FileDiff fn_ui::file_poll_diff(const std::vector<fn_ui::FileEntry>& before, const std::vector<fn_ui::FileEntry>& after)"
description: "Diff puro entre dos snapshots de filesystem (path/size/mtime). Devuelve vectores added/modified/removed. Sin I/O. Pareja del file_watcher para fallback portable basado en polling."
tags: [filesystem, diff, poll, snapshot, pure]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: true
tests: ["file_poll_diff detects added/modified/removed", "file_poll_diff: empty inputs", "file_poll_diff: identical snapshots"]
test_file_path: "cpp/tests/test_file_poll_diff.cpp"
file_path: "cpp/functions/core/file_poll_diff.cpp"
params:
- name: before
desc: "Snapshot anterior. Vector de FileEntry {path, size, mtime}, paths unicos"
- name: after
desc: "Snapshot actual. Mismo formato. Paths unicos"
output: "FileDiff con tres vectores de paths: added (en after y no en before), modified (path comun pero size o mtime distinto), removed (en before y no en after)."
---
# file_poll_diff
Logica pura para calcular cambios de filesystem entre dos snapshots. Sin
I/O, sin estado: el caller hace `stat()`/`opendir()` por su lado, construye
los `FileEntry`, y esta funcion los compara.
## API
```cpp
namespace fn_ui {
struct FileEntry {
std::string path;
uint64_t size = 0;
int64_t mtime = 0; // unix seconds
};
struct FileDiff {
std::vector<std::string> added;
std::vector<std::string> modified;
std::vector<std::string> removed;
};
FileDiff file_poll_diff(const std::vector<FileEntry>& before,
const std::vector<FileEntry>& after);
}
```
## Reglas
- Comparacion por `path` exacto.
- "Modified": al menos uno de `size` o `mtime` cambia.
- Asume paths unicos por snapshot (la funcion no deduplica).
- Complejidad: O(N+M) con un `unordered_map<string, const FileEntry*>` para
indexar `before`.
## Cuando usar
- En plataformas donde `file_watcher` no tiene backend nativo (stub) y se
necesita un fallback basado en polling.
- Para consolidar bursts de eventos: tomar dos snapshots en el tiempo y
reportar solo el cambio neto (sin los intermedios).
- Tests del watcher: simular cambios de FS sin tocar disco.
## Por que pura
No abre archivos, no llama `stat`, no usa el reloj. Misma entrada produce la
misma salida — testeable sin fixtures de FS.
+27
View File
@@ -1,5 +1,7 @@
#include "file_watcher.h"
#include "core/file_poll_diff.h"
#include <cstring>
#include <unordered_map>
#include <vector>
@@ -280,4 +282,29 @@ const char* file_watcher_last_error(const FileWatcher* w) { return w ? w->last_
#endif
// ─────────────────────────────────────────────────────────────────────────────
// Helper portable basado en file_poll_diff (puro). Comun a todas las plataformas.
// ─────────────────────────────────────────────────────────────────────────────
std::vector<FileEvent> file_watcher_events_from_diff(
const std::vector<FileSnapshotEntry>& before,
const std::vector<FileSnapshotEntry>& after) {
// Convertir a fn_ui::FileEntry para llamar a la funcion pura.
std::vector<fn_ui::FileEntry> a, b;
a.reserve(before.size());
b.reserve(after.size());
for (const auto& e : before) a.push_back({e.path, e.size, e.mtime});
for (const auto& e : after) b.push_back({e.path, e.size, e.mtime});
fn_ui::FileDiff diff = fn_ui::file_poll_diff(a, b);
std::vector<FileEvent> out;
out.reserve(diff.added.size() + diff.modified.size() + diff.removed.size());
for (const auto& p : diff.added) out.push_back({p, FileEvent::Created});
for (const auto& p : diff.modified) out.push_back({p, FileEvent::Modified});
for (const auto& p : diff.removed) out.push_back({p, FileEvent::Deleted});
return out;
}
} // namespace fn
+23
View File
@@ -7,7 +7,12 @@
// Otros: stub (poll() devuelve siempre vacio)
//
// API no bloqueante: poll() drena los eventos disponibles desde la ultima llamada.
//
// Para plataformas sin backend nativo, el caller puede usar la helper
// file_watcher_events_from_diff que delega en `file_poll_diff` (puro) para
// derivar FileEvents a partir de dos snapshots tomados con stat()/opendir().
#include <cstdint>
#include <string>
#include <vector>
@@ -21,6 +26,14 @@ struct FileEvent {
Kind kind;
};
// Snapshot de filesystem (forma simple, identica al fn_ui::FileEntry de
// file_poll_diff_cpp_core). Se replica aqui para no acoplar headers en apps.
struct FileSnapshotEntry {
std::string path;
uint64_t size = 0;
int64_t mtime = 0;
};
// Crea un watcher vacio. El caller llama destroy.
FileWatcher* file_watcher_create();
@@ -38,4 +51,14 @@ std::vector<FileEvent> file_watcher_poll(FileWatcher* w);
// Devuelve el mensaje del ultimo error (vacio si no hay).
const char* file_watcher_last_error(const FileWatcher* w);
// Helper portable: convierte dos snapshots de FS en FileEvents usando la
// logica pura `file_poll_diff_cpp_core`. Util en plataformas sin inotify/
// RDCW o como fallback de polling. El caller construye los snapshots con
// stat()/opendir().
//
// Mapeo: added -> Created, modified -> Modified, removed -> Deleted.
std::vector<FileEvent> file_watcher_events_from_diff(
const std::vector<FileSnapshotEntry>& before,
const std::vector<FileSnapshotEntry>& after);
} // namespace fn
+1 -1
View File
@@ -8,7 +8,7 @@ purity: impure
signature: "fn::FileWatcher* fn::file_watcher_create(); void fn::file_watcher_destroy(fn::FileWatcher*); bool fn::file_watcher_add(fn::FileWatcher*, const char* path); std::vector<fn::FileEvent> fn::file_watcher_poll(fn::FileWatcher*); const char* fn::file_watcher_last_error(const fn::FileWatcher*)"
description: "Watcher de archivos/directorios cross-platform (Linux inotify, Windows ReadDirectoryChangesW). API no bloqueante: registra paths con add() y consulta eventos con poll(). Cada poll() drena todos los eventos pendientes desde la llamada anterior."
tags: [filesystem, watcher, inotify, file_events, io]
uses_functions: []
uses_functions: ["file_poll_diff_cpp_core"]
uses_types: []
returns: []
returns_optional: false
+23 -3
View File
@@ -1,10 +1,26 @@
#include "core/process_runner.h"
#include "core/process_state_machine.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cmath>
namespace fn_ui {
// Helpers para puentear el RunnerState (publico) con la SM pura.
// RunnerState y ProcessState comparten orden y valores enteros (0..3).
namespace {
ProcessState to_ps(RunnerState s) { return static_cast<ProcessState>(static_cast<int>(s)); }
RunnerState to_rs(ProcessState s) { return static_cast<RunnerState>(static_cast<int>(s)); }
// Aplica un evento al estado actual del runner via process_transition.
void apply_event(ProcessRunner& r, std::atomic<int>& state, ProcessEvent ev) {
(void)r;
int cur = state.load();
ProcessState ns = process_transition(to_ps(static_cast<RunnerState>(cur)), ev);
state.store(static_cast<int>(to_rs(ns)));
}
} // namespace
ProcessRunner::ProcessRunner() = default;
ProcessRunner::~ProcessRunner() {
@@ -26,7 +42,8 @@ std::string ProcessRunner::message() const {
void ProcessRunner::reset() {
if (is_busy()) return;
state_.store(static_cast<int>(RunnerState::Idle));
// Reset event: Success/Error -> Idle (no-op si ya en Idle).
apply_event(*this, state_, ProcessEvent::Reset);
std::lock_guard<std::mutex> lk(mu_);
message_.clear();
}
@@ -36,7 +53,9 @@ void runner_trigger(ProcessRunner& r,
if (r.is_busy()) return;
if (r.th_.joinable()) r.th_.join();
r.state_.store(static_cast<int>(RunnerState::Running));
// Si venimos de Success/Error, primero Reset -> Idle, luego Spawned -> Running.
apply_event(r, r.state_, ProcessEvent::Reset);
apply_event(r, r.state_, ProcessEvent::Spawned);
{
std::lock_guard<std::mutex> lk(r.mu_);
r.message_.clear();
@@ -55,7 +74,8 @@ void runner_trigger(ProcessRunner& r,
std::lock_guard<std::mutex> lk(r.mu_);
r.message_ = std::move(out);
}
r.state_.store(static_cast<int>(ok ? RunnerState::Success : RunnerState::Error));
// Running -> Success/Error via SM pura.
apply_event(r, r.state_, ok ? ProcessEvent::Finished : ProcessEvent::Failed);
});
}
+1 -1
View File
@@ -8,7 +8,7 @@ purity: impure
signature: "class fn_ui::ProcessRunner { is_busy(); state(); message(); reset(); }; void runner_trigger(ProcessRunner&, std::function<bool(std::string&)>); void runner_status(const ProcessRunner&, const char* label)"
description: "Ejecuta una tarea en std::thread en background y expone estado thread-safe (idle/running/success/error). Incluye widget inline con spinner + resultado."
tags: [imgui, ui, async, thread, runner, tokens]
uses_functions: ["tokens_cpp_core"]
uses_functions: ["process_state_machine_cpp_core", "tokens_cpp_core"]
uses_types: []
returns: []
returns_optional: false
@@ -0,0 +1,47 @@
#include "core/process_state_machine.h"
namespace fn_ui {
ProcessState process_transition(ProcessState s, ProcessEvent e) {
using S = ProcessState;
using E = ProcessEvent;
switch (s) {
case S::Idle:
// Trigger es una solicitud, el cambio real ocurre con Spawned.
if (e == E::Spawned) return S::Running;
return s;
case S::Running:
if (e == E::Finished) return S::Success;
if (e == E::Failed || e == E::Timeout) return S::Error;
return s;
case S::Success:
case S::Error:
if (e == E::Reset) return S::Idle;
return s;
}
return s;
}
const char* process_state_name(ProcessState s) {
switch (s) {
case ProcessState::Idle: return "Idle";
case ProcessState::Running: return "Running";
case ProcessState::Success: return "Success";
case ProcessState::Error: return "Error";
}
return "Unknown";
}
const char* process_event_name(ProcessEvent e) {
switch (e) {
case ProcessEvent::Trigger: return "Trigger";
case ProcessEvent::Spawned: return "Spawned";
case ProcessEvent::Finished: return "Finished";
case ProcessEvent::Failed: return "Failed";
case ProcessEvent::Timeout: return "Timeout";
case ProcessEvent::Reset: return "Reset";
}
return "Unknown";
}
} // namespace fn_ui
@@ -0,0 +1,36 @@
#pragma once
// process_state_machine — logica pura de transiciones de un proceso async
// (idle/running/success/error) a partir de eventos. Sin threads, sin I/O.
//
// Diseñada para que `process_runner` (impuro, threads + popen) delegue las
// transiciones aqui y la UI/CLI puedan razonar sobre el estado sin depender
// de la implementacion concreta del runner.
namespace fn_ui {
enum class ProcessState {
Idle,
Running,
Success,
Error,
};
enum class ProcessEvent {
Trigger, // se solicita lanzar (no cambia estado por si solo)
Spawned, // el proceso/task realmente arranco
Finished, // termino con exito
Failed, // termino con error
Timeout, // se cancelo por timeout (-> Error)
Reset, // limpiar a Idle (solo desde Success/Error)
};
// Pura: dado (state, event) devuelve el nuevo estado. Transiciones invalidas
// devuelven el mismo estado sin mutar.
ProcessState process_transition(ProcessState s, ProcessEvent e);
// Nombres legibles para logs/UI (sin alocar — punteros a literales staticos).
const char* process_state_name(ProcessState s);
const char* process_event_name(ProcessEvent e);
} // namespace fn_ui
@@ -0,0 +1,71 @@
---
name: process_state_machine
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "fn_ui::ProcessState fn_ui::process_transition(fn_ui::ProcessState s, fn_ui::ProcessEvent e); const char* fn_ui::process_state_name(fn_ui::ProcessState); const char* fn_ui::process_event_name(fn_ui::ProcessEvent)"
description: "State machine puro para procesos async. Estados: Idle, Running, Success, Error. Eventos: Trigger, Spawned, Finished, Failed, Timeout, Reset. Transiciones invalidas devuelven el mismo estado sin mutar."
tags: [state_machine, process, async, runner, pure]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: true
tests: ["process_transition: idle/spawned -> running", "process_transition: running -> success/error", "process_transition: reset from terminal", "process_transition: invalid events keep state", "process_state_name and process_event_name"]
test_file_path: "cpp/tests/test_process_state_machine.cpp"
file_path: "cpp/functions/core/process_state_machine.cpp"
params:
- name: s
desc: "Estado actual (Idle, Running, Success, Error)"
- name: e
desc: "Evento a aplicar (Trigger, Spawned, Finished, Failed, Timeout, Reset)"
output: "Nuevo ProcessState. Si la transicion (s, e) no esta definida, devuelve s sin cambios. process_state_name y process_event_name retornan punteros a literales staticos (no alocan)."
---
# process_state_machine
Las apps del registry usan `process_runner` (impuro: threads, mutex, popen)
para lanzar reindex, builds, deploys... Esta funcion extrae el contrato de
estados a una tabla pura que se puede testear, simular y compartir con CLIs
y bots sin depender del runner concreto.
## Tabla de transiciones
| Estado actual | Evento | Nuevo estado |
|---------------|------------|--------------|
| Idle | Spawned | Running |
| Idle | Trigger | Idle (la solicitud no cambia el estado por si sola — el runner debe Spawnar) |
| Running | Finished | Success |
| Running | Failed | Error |
| Running | Timeout | Error |
| Success | Reset | Idle |
| Error | Reset | Idle |
| cualquier otra combinacion | | mismo estado |
`Trigger` se mantiene como evento explicito para que el runner pueda emitir
"se pidio lanzar" antes de saber si el proceso arranca (util para logs y UI).
## API
```cpp
namespace fn_ui {
enum class ProcessState { Idle, Running, Success, Error };
enum class ProcessEvent { Trigger, Spawned, Finished, Failed, Timeout, Reset };
ProcessState process_transition(ProcessState s, ProcessEvent e);
const char* process_state_name(ProcessState s);
const char* process_event_name(ProcessEvent e);
}
```
## Uso desde process_runner
`process_runner` mantiene su `std::atomic<int>` con el estado, pero llama a
`process_transition` para decidir el siguiente valor. Esto evita que la
logica de transicion se duplique entre el thread productor y la UI consumer.
+179
View File
@@ -0,0 +1,179 @@
#include "core/sql_parse.h"
#include <cctype>
#include <string>
namespace fn_ui {
namespace {
// Devuelve la version uppercase ASCII del primer token (delimitado por
// whitespace) de `s`, asumiendo que `s` empieza ya en el token.
std::string first_token_upper(const std::string& s, size_t start) {
std::string out;
while (start < s.size()) {
unsigned char c = static_cast<unsigned char>(s[start]);
if (std::isspace(c)) break;
if (!std::isalpha(c)) break; // keywords son solo letras
out.push_back(static_cast<char>(std::toupper(c)));
++start;
}
return out;
}
// Devuelve el indice del primer caracter "real" (no whitespace, no comentario)
// a partir de `i`. Avanza saltando -- ... \n y /* ... */.
size_t skip_ws_and_comments(const std::string& s, size_t i) {
while (i < s.size()) {
unsigned char c = static_cast<unsigned char>(s[i]);
if (std::isspace(c)) { ++i; continue; }
if (c == '-' && i + 1 < s.size() && s[i + 1] == '-') {
// line comment hasta \n
i += 2;
while (i < s.size() && s[i] != '\n') ++i;
continue;
}
if (c == '/' && i + 1 < s.size() && s[i + 1] == '*') {
// block comment hasta */
i += 2;
while (i + 1 < s.size() && !(s[i] == '*' && s[i + 1] == '/')) ++i;
if (i + 1 < s.size()) i += 2;
else i = s.size();
continue;
}
break;
}
return i;
}
std::string trim(const std::string& s) {
size_t a = 0, b = s.size();
while (a < b && std::isspace(static_cast<unsigned char>(s[a]))) ++a;
while (b > a && std::isspace(static_cast<unsigned char>(s[b - 1]))) --b;
return s.substr(a, b - a);
}
} // namespace
SqlStmtKind sql_classify(const std::string& stmt) {
size_t i = skip_ws_and_comments(stmt, 0);
if (i >= stmt.size()) return SqlStmtKind::Unknown;
std::string head = first_token_upper(stmt, i);
if (head == "SELECT" || head == "WITH") return SqlStmtKind::Select;
if (head == "INSERT") return SqlStmtKind::Insert;
if (head == "UPDATE") return SqlStmtKind::Update;
if (head == "DELETE") return SqlStmtKind::Delete;
if (head == "CREATE") return SqlStmtKind::Create;
if (head == "DROP") return SqlStmtKind::Drop;
if (head == "ALTER") return SqlStmtKind::Alter;
if (head == "PRAGMA") return SqlStmtKind::Pragma;
if (head == "EXPLAIN") return SqlStmtKind::Explain;
return SqlStmtKind::Unknown;
}
std::vector<SqlStatement> sql_parse(const std::string& input) {
std::vector<SqlStatement> out;
enum class Mode { Normal, LineComment, BlockComment, SingleStr, DoubleStr, BackTick };
Mode m = Mode::Normal;
size_t stmt_start = 0;
int cur_line = 1;
int stmt_line = 1;
bool stmt_has_content = false;
auto flush = [&](size_t end) {
std::string raw = input.substr(stmt_start, end - stmt_start);
std::string t = trim(raw);
if (!t.empty()) {
SqlStatement s;
s.text = t;
s.kind = sql_classify(t);
s.line = stmt_line;
out.push_back(std::move(s));
}
};
for (size_t i = 0; i < input.size(); ++i) {
char c = input[i];
char n = (i + 1 < input.size()) ? input[i + 1] : '\0';
if (c == '\n') ++cur_line;
switch (m) {
case Mode::Normal:
if (c == '-' && n == '-') {
m = Mode::LineComment;
++i;
} else if (c == '/' && n == '*') {
m = Mode::BlockComment;
++i;
} else if (!stmt_has_content && !std::isspace(static_cast<unsigned char>(c))) {
// marca inicio "real" de un statement (despues de skip ws/comments)
stmt_line = cur_line;
stmt_has_content = true;
// procesar el caracter actual abajo (no es comentario ni ws)
if (c == '\'') m = Mode::SingleStr;
else if (c == '"') m = Mode::DoubleStr;
else if (c == '`') m = Mode::BackTick;
else if (c == ';') {
flush(i);
stmt_start = i + 1;
stmt_has_content = false;
}
} else if (c == '\'') {
m = Mode::SingleStr;
} else if (c == '"') {
m = Mode::DoubleStr;
} else if (c == '`') {
m = Mode::BackTick;
} else if (c == ';') {
flush(i);
stmt_start = i + 1;
stmt_has_content = false;
}
break;
case Mode::LineComment:
if (c == '\n') m = Mode::Normal;
break;
case Mode::BlockComment:
if (c == '*' && n == '/') {
m = Mode::Normal;
++i;
}
break;
case Mode::SingleStr:
if (c == '\'') {
// SQL escapa '' como literal, sigue dentro de la cadena.
if (n == '\'') { ++i; }
else m = Mode::Normal;
}
break;
case Mode::DoubleStr:
if (c == '"') {
if (n == '"') { ++i; }
else m = Mode::Normal;
}
break;
case Mode::BackTick:
if (c == '`') {
if (n == '`') { ++i; }
else m = Mode::Normal;
}
break;
}
}
// ultimo statement sin ';' final
if (stmt_start < input.size()) {
flush(input.size());
}
return out;
}
} // namespace fn_ui
+48
View File
@@ -0,0 +1,48 @@
#pragma once
// sql_parse — tokenizer y clasificador de statements SQL (logica pura).
//
// Separa una cadena multi-statement por ';' (fuera de strings y comentarios)
// y clasifica cada statement por su keyword inicial. No ejecuta nada — esta
// funcion es 100% pura: misma entrada, misma salida.
//
// Uso tipico:
//
// auto stmts = fn_ui::sql_parse("SELECT 1; INSERT INTO t VALUES (2);");
// for (auto& s : stmts) {
// switch (s.kind) { ... }
// }
#include <string>
#include <vector>
namespace fn_ui {
enum class SqlStmtKind {
Unknown,
Select,
Insert,
Update,
Delete,
Create,
Drop,
Alter,
Pragma,
Explain,
};
struct SqlStatement {
SqlStmtKind kind = SqlStmtKind::Unknown;
std::string text; // texto trimeado (sin ';' final)
int line = 1; // linea de inicio en el input (1-based)
};
// Tokeniza SQL multi-statement. Salta cadenas '...' "..." `...` y comentarios
// -- linea y /* bloque */. Devuelve los statements no vacios.
std::vector<SqlStatement> sql_parse(const std::string& input);
// Clasifica un statement individual por su keyword inicial (case-insensitive,
// despues de saltar whitespace y comentarios iniciales).
SqlStmtKind sql_classify(const std::string& stmt);
} // namespace fn_ui
+76
View File
@@ -0,0 +1,76 @@
---
name: sql_parse
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "std::vector<fn_ui::SqlStatement> fn_ui::sql_parse(const std::string& input); fn_ui::SqlStmtKind fn_ui::sql_classify(const std::string& stmt)"
description: "Tokenizer y clasificador puro de SQL multi-statement. Separa por ';' fuera de strings ('...', \"...\", `...`) y comentarios (-- linea, /* bloque */), trimea, y clasifica cada statement por su keyword inicial (SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, PRAGMA, EXPLAIN, WITH→Select)."
tags: [sql, parser, tokenizer, pure, sqlite]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: true
tests: ["sql_parse classifies common statements", "sql_parse handles strings and comments", "sql_parse trims and ignores empty"]
test_file_path: "cpp/tests/test_sql_parse.cpp"
file_path: "cpp/functions/core/sql_parse.cpp"
params:
- name: input
desc: "Texto SQL completo, posiblemente multi-statement, con ';' opcional al final"
- name: stmt
desc: "(sql_classify) Un solo statement ya separado, sin ';' final"
output: "sql_parse: vector con un SqlStatement por statement no vacio (kind, text trimeado, line 1-based de inicio en el input). sql_classify: SqlStmtKind segun la primera keyword del statement."
---
# sql_parse
Logica pura para entender un script SQL antes de pasarlo al motor: separar
statements y clasificarlos. Sin estado, sin I/O, sin SQLite. Reutilizable
desde `sql_workbench` y desde cualquier app/CLI que necesite distinguir
SELECT de DDL para mostrar info al usuario o decidir como ejecutar.
## API
```cpp
namespace fn_ui {
enum class SqlStmtKind {
Unknown, Select, Insert, Update, Delete, Create, Drop, Alter, Pragma, Explain
};
struct SqlStatement {
SqlStmtKind kind;
std::string text; // texto trimeado
int line; // linea de inicio en el input (1-based)
};
std::vector<SqlStatement> sql_parse(const std::string& input);
SqlStmtKind sql_classify(const std::string& stmt);
}
```
## Reglas del tokenizer
- Strings: `'...'`, `"..."` y `` `...` `` se saltan enteras. Soporta el escape
SQL estandar de doblar la quote (`'don''t'`, `"a""b"`).
- Comentarios: `-- linea` hasta `\n` y `/* bloque */`.
- El separador `;` solo divide cuando aparece en modo Normal (fuera de
strings/comments).
- Statements vacios (`;;`, `; ;`) se descartan tras trimear.
- Si el ultimo statement no termina en `;`, se incluye igualmente.
## Clasificacion
`sql_classify` mira la primera palabra alfabetica despues de saltar whitespace
y comentarios. `WITH` se clasifica como `Select` porque es la forma comun de
iniciar CTEs que devuelven filas.
## Por que pura
No abre conexiones, no toca SQLite, no consulta el reloj. Misma entrada → misma
salida. Esto permite testearla sin depender de un fixture de DB.
+11 -15
View File
@@ -1,5 +1,6 @@
#include "core/sql_workbench.h"
#include "core/sql_parse.h"
#include "core/text_editor.h"
#include "core/tokens.h"
#include "core/button.h"
@@ -25,24 +26,19 @@ namespace fn {
namespace {
std::string ltrim(const std::string& s) {
size_t i = 0;
while (i < s.size() && std::isspace(static_cast<unsigned char>(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<unsigned char>(t[end]))) ++end;
std::string tok = t.substr(0, end);
for (auto& c : tok) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
return tok;
// Clasifica el primer statement con sql_parse pure. Si el input no contiene
// statements (todo whitespace/comentarios), devuelve Unknown.
fn_ui::SqlStmtKind first_stmt_kind(const std::string& sql) {
auto stmts = fn_ui::sql_parse(sql);
if (stmts.empty()) return fn_ui::SqlStmtKind::Unknown;
return stmts.front().kind;
}
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";
using K = fn_ui::SqlStmtKind;
K k = first_stmt_kind(sql);
// sql_parse clasifica WITH como Select; Pragma y Explain son readonly.
return k == K::Select || k == K::Pragma || k == K::Explain;
}
TextEditorState* editor_of(SqlWorkbenchState& state) {
+1 -1
View File
@@ -8,7 +8,7 @@ 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: ["button_cpp_core", "table_view_cpp_viz", "text_editor_cpp_core", "tokens_cpp_core"]
uses_functions: ["button_cpp_core", "sql_parse_cpp_core", "table_view_cpp_viz", "text_editor_cpp_core", "tokens_cpp_core"]
uses_types: []
returns: []
returns_optional: false
+8
View File
@@ -31,6 +31,14 @@ add_fn_test(test_pie_chart_math test_pie_chart_math.cpp)
add_fn_test(test_kpi_card_math test_kpi_card_math.cpp)
add_fn_test(test_bar_chart_math test_bar_chart_math.cpp)
# Issue 0045 — tests de la logica pura extraida.
add_fn_test(test_sql_parse test_sql_parse.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../functions/core/sql_parse.cpp)
add_fn_test(test_process_state_machine test_process_state_machine.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../functions/core/process_state_machine.cpp)
add_fn_test(test_file_poll_diff test_file_poll_diff.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../functions/core/file_poll_diff.cpp)
# --- Placeholders para primitivos UI (logica visual cubierta en 0048) ------
add_fn_test(test_tokens test_tokens.cpp)
add_fn_test(test_button test_button.cpp)
+105
View File
@@ -0,0 +1,105 @@
// Tests for fn_ui::file_poll_diff (cpp/functions/core/file_poll_diff).
// Pura: compara dos snapshots de FS y devuelve added/modified/removed.
#define CATCH_CONFIG_MAIN
#include "catch_amalgamated.hpp"
#include "core/file_poll_diff.h"
#include <algorithm>
#include <vector>
#include <string>
using fn_ui::file_poll_diff;
using fn_ui::FileEntry;
namespace {
// Helper: ordena los vectores para comparaciones estables (el orden interno
// depende del orden de iteracion de unordered_map, no es estable).
void sort_diff(fn_ui::FileDiff& d) {
std::sort(d.added.begin(), d.added.end());
std::sort(d.modified.begin(), d.modified.end());
std::sort(d.removed.begin(), d.removed.end());
}
}
TEST_CASE("file_poll_diff detects added/modified/removed") {
std::vector<FileEntry> before = {{"a", 10, 1}, {"b", 20, 2}, {"c", 30, 3}};
std::vector<FileEntry> after = {{"a", 10, 1}, {"b", 25, 2}, {"d", 40, 4}};
auto d = file_poll_diff(before, after);
sort_diff(d);
REQUIRE(d.added == std::vector<std::string>{"d"});
REQUIRE(d.modified == std::vector<std::string>{"b"});
REQUIRE(d.removed == std::vector<std::string>{"c"});
}
TEST_CASE("file_poll_diff: empty inputs") {
auto d = file_poll_diff({}, {});
REQUIRE(d.added.empty());
REQUIRE(d.modified.empty());
REQUIRE(d.removed.empty());
}
TEST_CASE("file_poll_diff: identical snapshots") {
std::vector<FileEntry> snap = {{"a", 1, 100}, {"b", 2, 200}};
auto d = file_poll_diff(snap, snap);
REQUIRE(d.added.empty());
REQUIRE(d.modified.empty());
REQUIRE(d.removed.empty());
}
TEST_CASE("file_poll_diff: all added (before vacio)") {
std::vector<FileEntry> after = {{"a", 1, 1}, {"b", 2, 2}};
auto d = file_poll_diff({}, after);
sort_diff(d);
REQUIRE(d.added == std::vector<std::string>{"a", "b"});
REQUIRE(d.modified.empty());
REQUIRE(d.removed.empty());
}
TEST_CASE("file_poll_diff: all removed (after vacio)") {
std::vector<FileEntry> before = {{"a", 1, 1}, {"b", 2, 2}};
auto d = file_poll_diff(before, {});
sort_diff(d);
REQUIRE(d.removed == std::vector<std::string>{"a", "b"});
REQUIRE(d.modified.empty());
REQUIRE(d.added.empty());
}
TEST_CASE("file_poll_diff: solo size cambia -> modified") {
std::vector<FileEntry> before = {{"a", 100, 5}};
std::vector<FileEntry> after = {{"a", 200, 5}};
auto d = file_poll_diff(before, after);
REQUIRE(d.modified == std::vector<std::string>{"a"});
REQUIRE(d.added.empty());
REQUIRE(d.removed.empty());
}
TEST_CASE("file_poll_diff: solo mtime cambia -> modified") {
std::vector<FileEntry> before = {{"a", 100, 5}};
std::vector<FileEntry> after = {{"a", 100, 9}};
auto d = file_poll_diff(before, after);
REQUIRE(d.modified == std::vector<std::string>{"a"});
REQUIRE(d.added.empty());
REQUIRE(d.removed.empty());
}
TEST_CASE("file_poll_diff: combinacion compleja") {
std::vector<FileEntry> before = {
{"keep", 10, 1},
{"mod_size", 20, 2},
{"mod_mtime", 30, 3},
{"removed", 40, 4},
};
std::vector<FileEntry> after = {
{"keep", 10, 1},
{"mod_size", 21, 2},
{"mod_mtime", 30, 9},
{"new", 50, 5},
};
auto d = file_poll_diff(before, after);
sort_diff(d);
REQUIRE(d.added == std::vector<std::string>{"new"});
REQUIRE(d.modified == std::vector<std::string>{"mod_mtime", "mod_size"});
REQUIRE(d.removed == std::vector<std::string>{"removed"});
}
+83
View File
@@ -0,0 +1,83 @@
// Tests for fn_ui::process_transition (cpp/functions/core/process_state_machine).
// Tabla de transiciones definida en .md.
#define CATCH_CONFIG_MAIN
#include "catch_amalgamated.hpp"
#include "core/process_state_machine.h"
#include <cstring>
using fn_ui::process_transition;
using fn_ui::process_state_name;
using fn_ui::process_event_name;
using fn_ui::ProcessState;
using fn_ui::ProcessEvent;
TEST_CASE("process_transition: idle -> running on Spawned") {
REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Spawned) == ProcessState::Running);
}
TEST_CASE("process_transition: idle + Trigger sigue Idle (la solicitud no muta el estado)") {
REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Trigger) == ProcessState::Idle);
}
TEST_CASE("process_transition: running -> success on Finished") {
REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Finished) == ProcessState::Success);
}
TEST_CASE("process_transition: running -> error on Failed/Timeout") {
REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Failed) == ProcessState::Error);
REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Timeout) == ProcessState::Error);
}
TEST_CASE("process_transition: success/error -> idle on Reset") {
REQUIRE(process_transition(ProcessState::Success, ProcessEvent::Reset) == ProcessState::Idle);
REQUIRE(process_transition(ProcessState::Error, ProcessEvent::Reset) == ProcessState::Idle);
}
TEST_CASE("process_transition: invalid events keep state") {
// Idle no acepta Finished/Failed/Timeout.
REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Finished) == ProcessState::Idle);
REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Failed) == ProcessState::Idle);
REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Timeout) == ProcessState::Idle);
REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Reset) == ProcessState::Idle);
// Running ignora Trigger/Spawned/Reset.
REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Trigger) == ProcessState::Running);
REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Spawned) == ProcessState::Running);
REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Reset) == ProcessState::Running);
// Success ignora todo menos Reset.
REQUIRE(process_transition(ProcessState::Success, ProcessEvent::Trigger) == ProcessState::Success);
REQUIRE(process_transition(ProcessState::Success, ProcessEvent::Spawned) == ProcessState::Success);
REQUIRE(process_transition(ProcessState::Success, ProcessEvent::Finished) == ProcessState::Success);
REQUIRE(process_transition(ProcessState::Success, ProcessEvent::Failed) == ProcessState::Success);
// Error idem.
REQUIRE(process_transition(ProcessState::Error, ProcessEvent::Finished) == ProcessState::Error);
REQUIRE(process_transition(ProcessState::Error, ProcessEvent::Spawned) == ProcessState::Error);
}
TEST_CASE("process_state_name and process_event_name") {
REQUIRE(std::strcmp(process_state_name(ProcessState::Idle), "Idle") == 0);
REQUIRE(std::strcmp(process_state_name(ProcessState::Running), "Running") == 0);
REQUIRE(std::strcmp(process_state_name(ProcessState::Success), "Success") == 0);
REQUIRE(std::strcmp(process_state_name(ProcessState::Error), "Error") == 0);
REQUIRE(std::strcmp(process_event_name(ProcessEvent::Trigger), "Trigger") == 0);
REQUIRE(std::strcmp(process_event_name(ProcessEvent::Spawned), "Spawned") == 0);
REQUIRE(std::strcmp(process_event_name(ProcessEvent::Finished), "Finished") == 0);
REQUIRE(std::strcmp(process_event_name(ProcessEvent::Failed), "Failed") == 0);
REQUIRE(std::strcmp(process_event_name(ProcessEvent::Timeout), "Timeout") == 0);
REQUIRE(std::strcmp(process_event_name(ProcessEvent::Reset), "Reset") == 0);
}
TEST_CASE("process_transition: ciclo completo idle -> running -> success -> idle") {
ProcessState s = ProcessState::Idle;
s = process_transition(s, ProcessEvent::Spawned);
REQUIRE(s == ProcessState::Running);
s = process_transition(s, ProcessEvent::Finished);
REQUIRE(s == ProcessState::Success);
s = process_transition(s, ProcessEvent::Reset);
REQUIRE(s == ProcessState::Idle);
}
+93
View File
@@ -0,0 +1,93 @@
// Tests for fn_ui::sql_parse / sql_classify (cpp/functions/core/sql_parse).
// Pura: tokeniza SQL multi-statement saltando strings y comentarios, y
// clasifica por keyword inicial.
#define CATCH_CONFIG_MAIN
#include "catch_amalgamated.hpp"
#include "core/sql_parse.h"
using fn_ui::sql_parse;
using fn_ui::sql_classify;
using fn_ui::SqlStmtKind;
TEST_CASE("sql_parse classifies common statements") {
auto stmts = sql_parse("SELECT * FROM t; INSERT INTO t VALUES (1);");
REQUIRE(stmts.size() == 2);
REQUIRE(stmts[0].kind == SqlStmtKind::Select);
REQUIRE(stmts[1].kind == SqlStmtKind::Insert);
}
TEST_CASE("sql_parse handles strings and comments") {
auto stmts = sql_parse("-- comment\nSELECT 'a;b' FROM t; /* x */ DELETE FROM t;");
REQUIRE(stmts.size() == 2);
REQUIRE(stmts[0].kind == SqlStmtKind::Select);
REQUIRE(stmts[1].kind == SqlStmtKind::Delete);
}
TEST_CASE("sql_parse trims and ignores empty") {
auto stmts = sql_parse("; ; SELECT 1;;");
REQUIRE(stmts.size() == 1);
REQUIRE(stmts[0].kind == SqlStmtKind::Select);
}
TEST_CASE("sql_parse: ddl and dcl keywords") {
auto stmts = sql_parse("CREATE TABLE t(a); DROP TABLE t; ALTER TABLE t ADD b; UPDATE t SET a=1;");
REQUIRE(stmts.size() == 4);
REQUIRE(stmts[0].kind == SqlStmtKind::Create);
REQUIRE(stmts[1].kind == SqlStmtKind::Drop);
REQUIRE(stmts[2].kind == SqlStmtKind::Alter);
REQUIRE(stmts[3].kind == SqlStmtKind::Update);
}
TEST_CASE("sql_parse: pragma and explain") {
auto stmts = sql_parse("PRAGMA foreign_keys = ON; EXPLAIN SELECT 1;");
REQUIRE(stmts.size() == 2);
REQUIRE(stmts[0].kind == SqlStmtKind::Pragma);
REQUIRE(stmts[1].kind == SqlStmtKind::Explain);
}
TEST_CASE("sql_parse: WITH clasifica como Select") {
auto stmts = sql_parse("WITH x AS (SELECT 1) SELECT * FROM x;");
REQUIRE(stmts.size() == 1);
REQUIRE(stmts[0].kind == SqlStmtKind::Select);
}
TEST_CASE("sql_parse: case-insensitive y trim") {
auto stmts = sql_parse(" select 1;\n INSERT INTO t VALUES (2);");
REQUIRE(stmts.size() == 2);
REQUIRE(stmts[0].kind == SqlStmtKind::Select);
REQUIRE(stmts[1].kind == SqlStmtKind::Insert);
}
TEST_CASE("sql_parse: line tracking") {
auto stmts = sql_parse("\n\nSELECT 1;\n-- skip\nDELETE FROM t;\n");
REQUIRE(stmts.size() == 2);
REQUIRE(stmts[0].line == 3);
REQUIRE(stmts[1].line == 5);
}
TEST_CASE("sql_parse: ultimo statement sin ;") {
auto stmts = sql_parse("SELECT 1");
REQUIRE(stmts.size() == 1);
REQUIRE(stmts[0].kind == SqlStmtKind::Select);
}
TEST_CASE("sql_classify: standalone") {
REQUIRE(sql_classify("SELECT 1") == SqlStmtKind::Select);
REQUIRE(sql_classify("garbage") == SqlStmtKind::Unknown);
REQUIRE(sql_classify("") == SqlStmtKind::Unknown);
REQUIRE(sql_classify("/* c */ DELETE FROM t") == SqlStmtKind::Delete);
}
TEST_CASE("sql_parse: ; dentro de string no separa") {
auto stmts = sql_parse("SELECT 'a;b;c' FROM t;");
REQUIRE(stmts.size() == 1);
REQUIRE(stmts[0].kind == SqlStmtKind::Select);
}
TEST_CASE("sql_parse: backtick identifier") {
auto stmts = sql_parse("SELECT * FROM `weird;name`;");
REQUIRE(stmts.size() == 1);
REQUIRE(stmts[0].kind == SqlStmtKind::Select);
}