feat(cpp/core): añadir sql_parse pure
This commit is contained in:
@@ -21,8 +21,9 @@ add_imgui_app(primitives_gallery
|
|||||||
${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp
|
${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp
|
||||||
${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp
|
${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp
|
||||||
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit/TextEditor.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_workbench.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/functions/core/sql_parse.cpp
|
||||||
# Core primitives demoed (tokens vive en fn_framework)
|
# Core primitives demoed (tokens vive en fn_framework)
|
||||||
${CMAKE_SOURCE_DIR}/functions/core/fullscreen_window.cpp
|
${CMAKE_SOURCE_DIR}/functions/core/fullscreen_window.cpp
|
||||||
${CMAKE_SOURCE_DIR}/functions/core/page_header.cpp
|
${CMAKE_SOURCE_DIR}/functions/core/page_header.cpp
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user