feat(cpp/core): añadir sql_parse pure

This commit is contained in:
2026-04-28 23:51:23 +02:00
parent b2d7b29e00
commit dd3f73905f
4 changed files with 298 additions and 1 deletions
+2 -1
View File
@@ -21,8 +21,9 @@ add_imgui_app(primitives_gallery
${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)
# 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
+172
View File
@@ -0,0 +1,172 @@
#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 (!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;
}
if (c == '-' && n == '-') {
m = Mode::LineComment;
++i;
if (n == '\n') ++cur_line; // (no aplica, n es '-')
} else if (c == '/' && n == '*') {
m = Mode::BlockComment;
++i;
} 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.