Step 413: add T-SQL parser and generator support
This commit is contained in:
@@ -2596,4 +2596,13 @@ target_link_libraries(step412_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step413_test tests/step413_test.cpp)
|
||||
target_include_directories(step413_test PRIVATE src)
|
||||
target_link_libraries(step413_test PRIVATE
|
||||
nlohmann_json::nlohmann_json
|
||||
unofficial::tree-sitter::tree-sitter
|
||||
tree_sitter_python tree_sitter_cpp tree_sitter_elisp
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||
|
||||
@@ -130,6 +130,10 @@ public:
|
||||
auto pr = PostgreSQLParser::parsePostgreSQLWithDiagnostics(source);
|
||||
diags = std::move(pr.diagnostics);
|
||||
return std::move(pr.module);
|
||||
} else if (language == "tsql" || language == "sqlserver" || language == "mssql") {
|
||||
auto pr = TSQLParser::parseTSQLWithDiagnostics(source);
|
||||
diags = std::move(pr.diagnostics);
|
||||
return std::move(pr.module);
|
||||
} else if (language == "c") {
|
||||
auto pr = CParser::parseCWithDiagnostics(source);
|
||||
diags = std::move(pr.diagnostics);
|
||||
@@ -193,6 +197,9 @@ public:
|
||||
} else if (language == "postgresql" || language == "postgres") {
|
||||
PostgreSQLGenerator gen;
|
||||
return gen.generate(ast);
|
||||
} else if (language == "tsql" || language == "sqlserver" || language == "mssql") {
|
||||
TSQLGenerator gen;
|
||||
return gen.generate(ast);
|
||||
} else if (language == "c") {
|
||||
CGenerator gen;
|
||||
return gen.generate(ast);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "FSharpGenerator.h"
|
||||
#include "VBNetGenerator.h"
|
||||
#include "PostgreSQLGenerator.h"
|
||||
#include "TSQLGenerator.h"
|
||||
#include "CGenerator.h"
|
||||
#include "WatGenerator.h"
|
||||
#include "CommonLispGenerator.h"
|
||||
|
||||
@@ -167,6 +167,7 @@ private:
|
||||
#include "ast/FSharpParser.h"
|
||||
#include "ast/VBNetParser.h"
|
||||
#include "ast/PostgreSQLParser.h"
|
||||
#include "ast/TSQLParser.h"
|
||||
#include "ast/CParser.h"
|
||||
#include "ast/WatParser.h"
|
||||
#include "ast/CommonLispParser.h"
|
||||
|
||||
161
editor/src/ast/TSQLGenerator.h
Normal file
161
editor/src/ast/TSQLGenerator.h
Normal file
@@ -0,0 +1,161 @@
|
||||
#pragma once
|
||||
|
||||
#include "PostgreSQLGenerator.h"
|
||||
#include "SqlNodes.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
class TSQLGenerator : public PostgreSQLGenerator {
|
||||
public:
|
||||
std::string commentPrefix() const { return "-- "; }
|
||||
|
||||
std::string generate(const ASTNode* node) override {
|
||||
return dispatchGenerate(this, node, "-- Unknown concept: ");
|
||||
}
|
||||
|
||||
std::string visitModule(const Module* module) override {
|
||||
std::ostringstream oss;
|
||||
oss << "-- Module: " << module->name << "\n\n";
|
||||
for (const auto* v : module->getChildren("variables")) {
|
||||
oss << generate(v) << "\n";
|
||||
}
|
||||
for (const auto* stmt : module->getChildren("statements")) {
|
||||
oss << generate(stmt) << "\n";
|
||||
}
|
||||
for (const auto* fn : module->getChildren("functions")) {
|
||||
oss << generate(fn) << "\n";
|
||||
}
|
||||
std::string out = oss.str();
|
||||
while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) out.pop_back();
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string visitFunction(const Function* function) override {
|
||||
std::ostringstream oss;
|
||||
for (const auto* anno : function->getChildren("annotations")) {
|
||||
oss << generate(anno) << "\n";
|
||||
}
|
||||
oss << "CREATE PROCEDURE " << function->name << "\nAS\nBEGIN\n";
|
||||
for (const auto* stmt : function->getChildren("body")) {
|
||||
oss << " " << generate(stmt) << "\n";
|
||||
}
|
||||
oss << "END;";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string visitVariable(const Variable* variable) override {
|
||||
std::ostringstream oss;
|
||||
oss << "DECLARE " << variable->name;
|
||||
auto* type = variable->getChild("type");
|
||||
if (type) oss << " " << generate(type);
|
||||
auto* init = variable->getChild("initializer");
|
||||
if (init) oss << " = " << generate(init);
|
||||
oss << ";";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string visitAssignment(const Assignment* assignment) override {
|
||||
auto* t = assignment->getChild("target");
|
||||
auto* v = assignment->getChild("value");
|
||||
return "SET " + std::string(t ? generate(t) : "@value") +
|
||||
" = " + std::string(v ? generate(v) : "NULL") + ";";
|
||||
}
|
||||
|
||||
std::string visitExpressionStatement(const ExpressionStatement* stmt) override {
|
||||
auto* expr = stmt->getChild("expression");
|
||||
return expr ? generate(expr) : "";
|
||||
}
|
||||
|
||||
std::string visitFunctionCall(const FunctionCall* call) override {
|
||||
std::ostringstream oss;
|
||||
if (call->functionName == "PRINT") {
|
||||
oss << "PRINT ";
|
||||
auto args = call->getChildren("arguments");
|
||||
if (!args.empty()) oss << generate(args.front());
|
||||
oss << ";";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
oss << call->functionName << "(";
|
||||
auto args = call->getChildren("arguments");
|
||||
for (size_t i = 0; i < args.size(); ++i) {
|
||||
if (i > 0) oss << ", ";
|
||||
oss << generate(args[i]);
|
||||
}
|
||||
oss << ")";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string visitPrimitiveType(const PrimitiveType* type) override {
|
||||
if (type->kind == "int" || type->kind == "INTEGER") return "INT";
|
||||
if (type->kind == "float" || type->kind == "double") return "FLOAT";
|
||||
if (type->kind == "string" || type->kind == "TEXT") return "NVARCHAR(MAX)";
|
||||
if (type->kind == "bool") return "BIT";
|
||||
if (type->kind == "void") return "VOID";
|
||||
return type->kind;
|
||||
}
|
||||
|
||||
std::string visitTableDeclaration(const ASTNode* node) override {
|
||||
auto* table = static_cast<const TableDeclaration*>(node);
|
||||
std::ostringstream oss;
|
||||
oss << "CREATE TABLE " << table->name << " (";
|
||||
|
||||
auto cols = table->getChildren("columns");
|
||||
if (cols.empty()) {
|
||||
oss << "\n id INT IDENTITY(1,1) PRIMARY KEY";
|
||||
} else {
|
||||
for (size_t i = 0; i < cols.size(); ++i) {
|
||||
oss << "\n " << generate(cols[i]);
|
||||
if (i + 1 < cols.size()) oss << ",";
|
||||
}
|
||||
}
|
||||
oss << "\n);";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string visitSelectQuery(const ASTNode* node) override {
|
||||
auto* q = static_cast<const SelectQuery*>(node);
|
||||
std::ostringstream oss;
|
||||
oss << "SELECT ";
|
||||
auto* topNode = q->getChild("top");
|
||||
if (topNode) {
|
||||
auto* top = static_cast<const IntegerLiteral*>(topNode);
|
||||
oss << "TOP " << top->value << " ";
|
||||
}
|
||||
if (q->distinct) oss << "DISTINCT ";
|
||||
|
||||
auto cols = q->getChildren("columns");
|
||||
if (cols.empty()) {
|
||||
oss << "*";
|
||||
} else {
|
||||
for (size_t i = 0; i < cols.size(); ++i) {
|
||||
if (i > 0) oss << ", ";
|
||||
oss << generate(cols[i]);
|
||||
}
|
||||
}
|
||||
|
||||
auto* from = q->getChild("from");
|
||||
if (from && from->conceptType == "TableDeclaration") {
|
||||
oss << " FROM " << static_cast<const TableDeclaration*>(from)->name;
|
||||
} else if (from) {
|
||||
oss << " FROM " << generate(from);
|
||||
}
|
||||
for (const auto* join : q->getChildren("joins")) {
|
||||
oss << " " << generate(join);
|
||||
}
|
||||
auto* where = q->getChild("where");
|
||||
if (where) oss << " " << generate(where);
|
||||
oss << ";";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string visitColumnDefinition(const ASTNode* node) override {
|
||||
auto* col = static_cast<const ColumnDefinition*>(node);
|
||||
std::ostringstream oss;
|
||||
oss << col->name << " " << (col->dataType.empty() ? "NVARCHAR(MAX)" : col->dataType);
|
||||
if (!col->nullable) oss << " NOT NULL";
|
||||
if (!col->defaultValue.empty()) oss << " DEFAULT " << col->defaultValue;
|
||||
return oss.str();
|
||||
}
|
||||
};
|
||||
377
editor/src/ast/TSQLParser.h
Normal file
377
editor/src/ast/TSQLParser.h
Normal file
@@ -0,0 +1,377 @@
|
||||
#pragma once
|
||||
|
||||
#include "ASTNode.h"
|
||||
#include "Module.h"
|
||||
#include "Function.h"
|
||||
#include "Variable.h"
|
||||
#include "Statement.h"
|
||||
#include "Expression.h"
|
||||
#include "Type.h"
|
||||
#include "SqlNodes.h"
|
||||
#include "../ast/Parser.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class TSQLParser {
|
||||
public:
|
||||
static std::unique_ptr<Module> parseTSQL(const std::string& source) {
|
||||
auto module = std::make_unique<Module>();
|
||||
module->id = IdGenerator::next("mod");
|
||||
module->name = "parsed_tsql_module";
|
||||
module->targetLanguage = "tsql";
|
||||
parseStatements(source, module.get());
|
||||
return module;
|
||||
}
|
||||
|
||||
static ParseResult parseTSQLWithDiagnostics(const std::string& source) {
|
||||
ParseResult result;
|
||||
result.module = parseTSQL(source);
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
static void parseStatements(const std::string& source, Module* module) {
|
||||
for (const auto& stmtRaw : splitStatements(source)) {
|
||||
std::string stmt = trim(stmtRaw);
|
||||
if (stmt.empty()) continue;
|
||||
std::string lower = toLower(stmt);
|
||||
|
||||
if (startsWith(lower, "create table ")) {
|
||||
parseCreateTable(stmt, module);
|
||||
continue;
|
||||
}
|
||||
if (startsWith(lower, "create index ") || startsWith(lower, "create unique index ")) {
|
||||
parseCreateIndex(stmt, module);
|
||||
continue;
|
||||
}
|
||||
if (startsWith(lower, "select ")) {
|
||||
parseSelect(stmt, module);
|
||||
continue;
|
||||
}
|
||||
if (startsWith(lower, "insert into ")) {
|
||||
parseInsert(stmt, module);
|
||||
continue;
|
||||
}
|
||||
if (startsWith(lower, "update ")) {
|
||||
parseUpdate(stmt, module);
|
||||
continue;
|
||||
}
|
||||
if (startsWith(lower, "delete from ")) {
|
||||
parseDelete(stmt, module);
|
||||
continue;
|
||||
}
|
||||
if (startsWith(lower, "create procedure ")) {
|
||||
parseProcedure(stmt, module);
|
||||
continue;
|
||||
}
|
||||
if (startsWith(lower, "declare ")) {
|
||||
parseDeclare(stmt, module);
|
||||
continue;
|
||||
}
|
||||
if (startsWith(lower, "set ")) {
|
||||
parseSet(stmt, module);
|
||||
continue;
|
||||
}
|
||||
if (startsWith(lower, "print ")) {
|
||||
parsePrint(stmt, module);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void parseCreateTable(const std::string& stmt, Module* module) {
|
||||
std::string tableName = extractAfterKeyword(stmt, "TABLE ");
|
||||
tableName = trim(untilDelimiter(tableName, "("));
|
||||
|
||||
auto* table = new TableDeclaration();
|
||||
table->id = IdGenerator::next("table");
|
||||
table->name = tableName;
|
||||
auto dotPos = tableName.find('.');
|
||||
if (dotPos != std::string::npos) table->schema = tableName.substr(0, dotPos);
|
||||
|
||||
auto lp = stmt.find('(');
|
||||
auto rp = stmt.rfind(')');
|
||||
if (lp != std::string::npos && rp != std::string::npos && rp > lp) {
|
||||
std::string cols = stmt.substr(lp + 1, rp - lp - 1);
|
||||
std::stringstream ss(cols);
|
||||
std::string item;
|
||||
while (std::getline(ss, item, ',')) {
|
||||
std::string colLine = trim(item);
|
||||
if (colLine.empty()) continue;
|
||||
std::string lowerCol = toLower(colLine);
|
||||
if (startsWith(lowerCol, "constraint ") ||
|
||||
startsWith(lowerCol, "primary key") ||
|
||||
startsWith(lowerCol, "foreign key")) continue;
|
||||
|
||||
std::vector<std::string> parts = splitWhitespace(colLine);
|
||||
if (parts.size() < 2) continue;
|
||||
auto* col = new ColumnDefinition();
|
||||
col->id = IdGenerator::next("col");
|
||||
col->name = parts[0];
|
||||
col->dataType = parts[1];
|
||||
if (parts.size() >= 3 &&
|
||||
(toLower(parts[2]).find("identity") != std::string::npos ||
|
||||
toLower(parts[2]).find("(") != std::string::npos)) {
|
||||
col->dataType += " " + parts[2];
|
||||
}
|
||||
col->nullable = (lowerCol.find("not null") == std::string::npos);
|
||||
table->addChild("columns", col);
|
||||
}
|
||||
}
|
||||
|
||||
module->addChild("statements", table);
|
||||
}
|
||||
|
||||
static void parseCreateIndex(const std::string& stmt, Module* module) {
|
||||
std::string lower = toLower(stmt);
|
||||
bool unique = startsWith(lower, "create unique index ");
|
||||
|
||||
std::string idxName = extractAfterKeyword(stmt, "INDEX ");
|
||||
std::string tableName = extractAfterKeyword(stmt, "ON ");
|
||||
auto* idx = new IndexDefinition();
|
||||
idx->id = IdGenerator::next("idx");
|
||||
idx->name = trim(untilDelimiter(idxName, " ON"));
|
||||
idx->tableName = trim(untilDelimiter(tableName, "("));
|
||||
idx->unique = unique;
|
||||
module->addChild("statements", idx);
|
||||
}
|
||||
|
||||
static void parseSelect(const std::string& stmt, Module* module) {
|
||||
std::string lower = toLower(stmt);
|
||||
auto* q = new SelectQuery();
|
||||
q->id = IdGenerator::next("sel");
|
||||
q->distinct = (lower.find("select distinct") == 0);
|
||||
|
||||
auto topPos = lower.find("select top ");
|
||||
if (topPos == 0) {
|
||||
std::string afterTop = trim(stmt.substr(11));
|
||||
std::string topCount = firstToken(afterTop);
|
||||
if (!topCount.empty() && std::all_of(topCount.begin(), topCount.end(), ::isdigit)) {
|
||||
q->setChild("top", new IntegerLiteral(IdGenerator::next("lit"), std::stoi(topCount)));
|
||||
}
|
||||
}
|
||||
|
||||
auto fromPos = lower.find(" from ");
|
||||
if (fromPos != std::string::npos) {
|
||||
std::string afterFrom = trim(stmt.substr(fromPos + 6));
|
||||
std::string fromName = firstToken(afterFrom);
|
||||
auto* from = new TableDeclaration();
|
||||
from->id = IdGenerator::next("tbl");
|
||||
from->name = fromName;
|
||||
q->setChild("from", from);
|
||||
}
|
||||
|
||||
size_t search = 0;
|
||||
while (true) {
|
||||
auto joinPos = lower.find(" join ", search);
|
||||
if (joinPos == std::string::npos) break;
|
||||
auto tableStart = joinPos + 6;
|
||||
std::string joinRest = stmt.substr(tableStart);
|
||||
std::string tableName = firstToken(joinRest);
|
||||
auto* j = new JoinClause();
|
||||
j->id = IdGenerator::next("join");
|
||||
j->joinType = detectJoinType(lower, joinPos);
|
||||
j->tableName = tableName;
|
||||
q->addChild("joins", j);
|
||||
search = tableStart + 1;
|
||||
}
|
||||
|
||||
auto wherePos = lower.find(" where ");
|
||||
if (wherePos != std::string::npos) {
|
||||
auto* w = new WhereClause();
|
||||
w->id = IdGenerator::next("where");
|
||||
w->expression = trim(stmt.substr(wherePos + 7));
|
||||
q->setChild("where", w);
|
||||
}
|
||||
|
||||
module->addChild("statements", q);
|
||||
}
|
||||
|
||||
static void parseInsert(const std::string& stmt, Module* module) {
|
||||
auto* ins = new InsertStatement();
|
||||
ins->id = IdGenerator::next("ins");
|
||||
ins->tableName = extractAfterKeyword(stmt, "INTO ");
|
||||
ins->tableName = trim(untilDelimiter(ins->tableName, "("));
|
||||
module->addChild("statements", ins);
|
||||
}
|
||||
|
||||
static void parseUpdate(const std::string& stmt, Module* module) {
|
||||
auto* up = new UpdateStatement();
|
||||
up->id = IdGenerator::next("upd");
|
||||
up->tableName = trim(untilDelimiter(extractAfterKeyword(stmt, "UPDATE "), " SET"));
|
||||
|
||||
std::string lower = toLower(stmt);
|
||||
auto wherePos = lower.find(" where ");
|
||||
if (wherePos != std::string::npos) {
|
||||
auto* w = new WhereClause();
|
||||
w->id = IdGenerator::next("where");
|
||||
w->expression = trim(stmt.substr(wherePos + 7));
|
||||
up->setChild("where", w);
|
||||
}
|
||||
module->addChild("statements", up);
|
||||
}
|
||||
|
||||
static void parseDelete(const std::string& stmt, Module* module) {
|
||||
auto* del = new DeleteStatement();
|
||||
del->id = IdGenerator::next("del");
|
||||
del->tableName = trim(untilDelimiter(extractAfterKeyword(stmt, "FROM "), " WHERE"));
|
||||
std::string lower = toLower(stmt);
|
||||
auto wherePos = lower.find(" where ");
|
||||
if (wherePos != std::string::npos) {
|
||||
auto* w = new WhereClause();
|
||||
w->id = IdGenerator::next("where");
|
||||
w->expression = trim(stmt.substr(wherePos + 7));
|
||||
del->setChild("where", w);
|
||||
}
|
||||
module->addChild("statements", del);
|
||||
}
|
||||
|
||||
static void parseProcedure(const std::string& stmt, Module* module) {
|
||||
std::string raw = extractAfterKeyword(stmt, "PROCEDURE ");
|
||||
auto* fn = new Function();
|
||||
fn->id = IdGenerator::next("proc");
|
||||
fn->name = trim(untilDelimiter(raw, " AS"));
|
||||
module->addChild("functions", fn);
|
||||
}
|
||||
|
||||
static void parseDeclare(const std::string& stmt, Module* module) {
|
||||
std::string raw = extractAfterKeyword(stmt, "DECLARE ");
|
||||
std::vector<std::string> parts = splitWhitespace(raw);
|
||||
if (parts.size() < 2) return;
|
||||
|
||||
auto* v = new Variable();
|
||||
v->id = IdGenerator::next("var");
|
||||
v->name = parts[0];
|
||||
v->setChild("type", new PrimitiveType(IdGenerator::next("type"), toUpper(parts[1])));
|
||||
|
||||
auto eqPos = raw.find('=');
|
||||
if (eqPos != std::string::npos) {
|
||||
std::string rhs = trim(raw.substr(eqPos + 1));
|
||||
v->setChild("initializer", parseLiteral(rhs));
|
||||
}
|
||||
module->addChild("variables", v);
|
||||
}
|
||||
|
||||
static void parseSet(const std::string& stmt, Module* module) {
|
||||
std::string raw = extractAfterKeyword(stmt, "SET ");
|
||||
auto eqPos = raw.find('=');
|
||||
if (eqPos == std::string::npos) return;
|
||||
|
||||
auto* assign = new Assignment();
|
||||
assign->id = IdGenerator::next("set");
|
||||
std::string lhs = trim(raw.substr(0, eqPos));
|
||||
std::string rhs = trim(raw.substr(eqPos + 1));
|
||||
assign->setChild("target", new VariableReference(IdGenerator::next("ref"), lhs));
|
||||
assign->setChild("value", parseLiteral(rhs));
|
||||
module->addChild("statements", assign);
|
||||
}
|
||||
|
||||
static void parsePrint(const std::string& stmt, Module* module) {
|
||||
std::string raw = extractAfterKeyword(stmt, "PRINT ");
|
||||
auto* call = new FunctionCall();
|
||||
call->id = IdGenerator::next("call");
|
||||
call->functionName = "PRINT";
|
||||
call->addChild("arguments", parseLiteral(raw));
|
||||
|
||||
auto* es = new ExpressionStatement();
|
||||
es->id = IdGenerator::next("expr");
|
||||
es->setChild("expression", call);
|
||||
module->addChild("statements", es);
|
||||
}
|
||||
|
||||
static ASTNode* parseLiteral(const std::string& tokenRaw) {
|
||||
std::string t = trim(tokenRaw);
|
||||
if (t.empty()) return new NullLiteral();
|
||||
if ((t.front() == '\'' && t.back() == '\'') || (t.front() == '"' && t.back() == '"')) {
|
||||
return new StringLiteral(IdGenerator::next("str"), t.substr(1, t.size() - 2));
|
||||
}
|
||||
if (t == "1" || t == "0") return new IntegerLiteral(IdGenerator::next("int"), std::stoi(t));
|
||||
if (std::all_of(t.begin(), t.end(), [](unsigned char c) { return std::isdigit(c); })) {
|
||||
return new IntegerLiteral(IdGenerator::next("int"), std::stoi(t));
|
||||
}
|
||||
return new VariableReference(IdGenerator::next("ref"), t);
|
||||
}
|
||||
|
||||
static std::string detectJoinType(const std::string& lower, size_t joinPos) {
|
||||
std::string prefix = lower.substr((joinPos > 8 ? joinPos - 8 : 0), 8);
|
||||
if (prefix.find("left") != std::string::npos) return "LEFT";
|
||||
if (prefix.find("right") != std::string::npos) return "RIGHT";
|
||||
if (prefix.find("full") != std::string::npos) return "FULL";
|
||||
return "INNER";
|
||||
}
|
||||
|
||||
static std::vector<std::string> splitStatements(const std::string& source) {
|
||||
std::vector<std::string> out;
|
||||
std::string cur;
|
||||
for (char c : source) {
|
||||
if (c == ';') {
|
||||
if (!trim(cur).empty()) out.push_back(cur);
|
||||
cur.clear();
|
||||
continue;
|
||||
}
|
||||
cur.push_back(c);
|
||||
}
|
||||
if (!trim(cur).empty()) out.push_back(cur);
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::string extractAfterKeyword(const std::string& s, const std::string& keywordUpper) {
|
||||
std::string lower = toLower(s);
|
||||
std::string kw = toLower(keywordUpper);
|
||||
auto pos = lower.find(kw);
|
||||
if (pos == std::string::npos) return "";
|
||||
pos += kw.size();
|
||||
return trim(s.substr(pos));
|
||||
}
|
||||
|
||||
static std::string firstToken(const std::string& s) {
|
||||
std::string t = trim(s);
|
||||
auto end = t.find_first_of(" \t\r\n");
|
||||
if (end == std::string::npos) return t;
|
||||
return t.substr(0, end);
|
||||
}
|
||||
|
||||
static std::string untilDelimiter(const std::string& s, const std::string& delimiterUpper) {
|
||||
std::string lower = toLower(s);
|
||||
std::string d = toLower(delimiterUpper);
|
||||
auto pos = lower.find(d);
|
||||
if (pos == std::string::npos) return s;
|
||||
return s.substr(0, pos);
|
||||
}
|
||||
|
||||
static std::vector<std::string> splitWhitespace(const std::string& s) {
|
||||
std::vector<std::string> out;
|
||||
std::stringstream ss(s);
|
||||
std::string tok;
|
||||
while (ss >> tok) out.push_back(tok);
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool startsWith(const std::string& s, const std::string& prefix) {
|
||||
return s.rfind(prefix, 0) == 0;
|
||||
}
|
||||
|
||||
static std::string toLower(const std::string& s) {
|
||||
std::string out = s;
|
||||
std::transform(out.begin(), out.end(), out.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::string toUpper(const std::string& s) {
|
||||
std::string out = s;
|
||||
std::transform(out.begin(), out.end(), out.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::toupper(c)); });
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::string trim(const std::string& s) {
|
||||
auto start = s.find_first_not_of(" \t\r\n");
|
||||
if (start == std::string::npos) return "";
|
||||
auto end = s.find_last_not_of(" \t\r\n");
|
||||
return s.substr(start, end - start + 1);
|
||||
}
|
||||
};
|
||||
213
editor/tests/step413_test.cpp
Normal file
213
editor/tests/step413_test.cpp
Normal file
@@ -0,0 +1,213 @@
|
||||
// Step 413: T-SQL Parser + Generator Tests (12 tests)
|
||||
|
||||
#include "ast/TSQLParser.h"
|
||||
#include "ast/TSQLGenerator.h"
|
||||
#include "ast/SqlNodes.h"
|
||||
#include "ast/Function.h"
|
||||
#include "ast/Variable.h"
|
||||
#include "Pipeline.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
template <typename T>
|
||||
T* firstStatementOf(const Module* mod, const std::string& conceptName) {
|
||||
for (auto* s : mod->getChildren("statements")) {
|
||||
if (s->conceptType == conceptName) return static_cast<T*>(s);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void test_parse_create_table_identity_nvarchar() {
|
||||
TEST(parse_create_table_identity_nvarchar);
|
||||
auto mod = TSQLParser::parseTSQL(
|
||||
"CREATE TABLE users (id INT IDENTITY(1,1), name NVARCHAR(100) NOT NULL);\n");
|
||||
CHECK(mod != nullptr, "module null");
|
||||
auto* t = firstStatementOf<TableDeclaration>(mod.get(), "TableDeclaration");
|
||||
CHECK(t != nullptr, "expected table declaration");
|
||||
CHECK(t->name == "users", "expected users table");
|
||||
auto cols = t->getChildren("columns");
|
||||
CHECK(cols.size() == 2, "expected two columns");
|
||||
CHECK(static_cast<ColumnDefinition*>(cols[0])->dataType.find("IDENTITY") != std::string::npos,
|
||||
"expected identity type");
|
||||
CHECK(static_cast<ColumnDefinition*>(cols[1])->dataType.find("NVARCHAR") != std::string::npos,
|
||||
"expected nvarchar type");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_parse_select_top_where() {
|
||||
TEST(parse_select_top_where);
|
||||
auto mod = TSQLParser::parseTSQL("SELECT TOP 5 id FROM users WHERE active = 1;\n");
|
||||
auto* q = firstStatementOf<SelectQuery>(mod.get(), "SelectQuery");
|
||||
CHECK(q != nullptr, "expected select query");
|
||||
CHECK(q->getChild("top") != nullptr, "expected top child");
|
||||
CHECK(q->getChild("where") != nullptr, "expected where child");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_parse_create_procedure_marker() {
|
||||
TEST(parse_create_procedure_marker);
|
||||
auto mod = TSQLParser::parseTSQL(
|
||||
"CREATE PROCEDURE usp_cleanup AS BEGIN DELETE FROM logs; END;\n");
|
||||
auto fns = mod->getChildren("functions");
|
||||
CHECK(fns.size() == 1, "expected one procedure marker");
|
||||
auto* fn = static_cast<Function*>(fns.front());
|
||||
CHECK(fn->name == "usp_cleanup", "expected procedure name");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_parse_declare_variable() {
|
||||
TEST(parse_declare_variable);
|
||||
auto mod = TSQLParser::parseTSQL("DECLARE @count INT = 10;\n");
|
||||
auto vars = mod->getChildren("variables");
|
||||
CHECK(vars.size() == 1, "expected one variable");
|
||||
auto* v = static_cast<Variable*>(vars.front());
|
||||
CHECK(v->name == "@count", "expected variable name");
|
||||
CHECK(v->getChild("initializer") != nullptr, "expected initializer");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_parse_set_assignment() {
|
||||
TEST(parse_set_assignment);
|
||||
auto mod = TSQLParser::parseTSQL("SET @count = 11;\n");
|
||||
auto* a = firstStatementOf<Assignment>(mod.get(), "Assignment");
|
||||
CHECK(a != nullptr, "expected assignment statement");
|
||||
auto* target = static_cast<VariableReference*>(a->getChild("target"));
|
||||
CHECK(target != nullptr && target->variableName == "@count", "expected assignment target");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_parse_print_statement() {
|
||||
TEST(parse_print_statement);
|
||||
auto mod = TSQLParser::parseTSQL("PRINT 'done';\n");
|
||||
auto* es = firstStatementOf<ExpressionStatement>(mod.get(), "ExpressionStatement");
|
||||
CHECK(es != nullptr, "expected expression statement");
|
||||
auto* call = static_cast<FunctionCall*>(es->getChild("expression"));
|
||||
CHECK(call != nullptr && call->functionName == "PRINT", "expected print call");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_generate_select_top_clause() {
|
||||
TEST(generate_select_top_clause);
|
||||
SelectQuery q;
|
||||
q.id = "q1";
|
||||
q.setChild("top", new IntegerLiteral("i1", 3));
|
||||
q.setChild("from", new TableDeclaration("t1", "users"));
|
||||
TSQLGenerator gen;
|
||||
std::string out = gen.generate(&q);
|
||||
CHECK(out.find("SELECT TOP 3") != std::string::npos, "expected top syntax");
|
||||
CHECK(out.find("FROM users") != std::string::npos, "expected from clause");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_generate_create_table_identity() {
|
||||
TEST(generate_create_table_identity);
|
||||
TableDeclaration t("t1", "users");
|
||||
t.addChild("columns", new ColumnDefinition("c1", "id", "INT IDENTITY(1,1)", false));
|
||||
t.addChild("columns", new ColumnDefinition("c2", "name", "NVARCHAR(100)", false));
|
||||
TSQLGenerator gen;
|
||||
std::string out = gen.generate(&t);
|
||||
CHECK(out.find("CREATE TABLE users") != std::string::npos, "expected create table");
|
||||
CHECK(out.find("INT IDENTITY(1,1)") != std::string::npos, "expected identity");
|
||||
CHECK(out.find("NVARCHAR(100)") != std::string::npos, "expected nvarchar");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_generate_procedure_begin_end() {
|
||||
TEST(generate_procedure_begin_end);
|
||||
Function fn("f1", "usp_sync");
|
||||
fn.addChild("body", new Return());
|
||||
TSQLGenerator gen;
|
||||
std::string out = gen.generate(&fn);
|
||||
CHECK(out.find("CREATE PROCEDURE usp_sync") != std::string::npos, "expected procedure keyword");
|
||||
CHECK(out.find("BEGIN") != std::string::npos, "expected begin block");
|
||||
CHECK(out.find("END;") != std::string::npos, "expected end block");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_generate_declare_set_print() {
|
||||
TEST(generate_declare_set_print);
|
||||
Variable v("v1", "@n");
|
||||
v.setChild("type", new PrimitiveType("t1", "int"));
|
||||
v.setChild("initializer", new IntegerLiteral("i1", 1));
|
||||
|
||||
Assignment a;
|
||||
a.id = "a1";
|
||||
a.setChild("target", new VariableReference("r1", "@n"));
|
||||
a.setChild("value", new IntegerLiteral("i2", 2));
|
||||
|
||||
FunctionCall printCall;
|
||||
printCall.id = "p1";
|
||||
printCall.functionName = "PRINT";
|
||||
printCall.addChild("arguments", new StringLiteral("s1", "ok"));
|
||||
|
||||
TSQLGenerator gen;
|
||||
std::string d = gen.generate(&v);
|
||||
std::string s = gen.generate(&a);
|
||||
std::string p = gen.generate(&printCall);
|
||||
CHECK(d.find("DECLARE @n INT = 1;") != std::string::npos, "expected declare");
|
||||
CHECK(s.find("SET @n = 2;") != std::string::npos, "expected set");
|
||||
CHECK(p == "PRINT 'ok';", "expected print syntax");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_pipeline_parse_and_generate_aliases() {
|
||||
TEST(pipeline_parse_and_generate_aliases);
|
||||
Pipeline p;
|
||||
std::vector<ParseDiagnostic> d1, d2, d3;
|
||||
auto m1 = p.parse("SELECT TOP 1 id FROM users;\n", "tsql", d1);
|
||||
auto m2 = p.parse("SELECT TOP 1 id FROM users;\n", "sqlserver", d2);
|
||||
auto m3 = p.parse("SELECT TOP 1 id FROM users;\n", "mssql", d3);
|
||||
CHECK(m1 && m2 && m3, "expected parse via aliases");
|
||||
CHECK(m1->targetLanguage == "tsql", "expected tsql target");
|
||||
|
||||
DeleteStatement del("d1", "audit");
|
||||
std::string o1 = p.generate(&del, "tsql");
|
||||
std::string o2 = p.generate(&del, "sqlserver");
|
||||
std::string o3 = p.generate(&del, "mssql");
|
||||
CHECK(o1.find("DELETE FROM audit;") != std::string::npos, "tsql route");
|
||||
CHECK(o2.find("DELETE FROM audit;") != std::string::npos, "sqlserver route");
|
||||
CHECK(o3.find("DELETE FROM audit;") != std::string::npos, "mssql route");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_parser_to_generator_tsql_flow() {
|
||||
TEST(parser_to_generator_tsql_flow);
|
||||
auto mod = TSQLParser::parseTSQL(
|
||||
"DECLARE @limit INT = 10;\n"
|
||||
"SELECT TOP 10 id FROM users WHERE active = 1;\n");
|
||||
CHECK(mod != nullptr, "module null");
|
||||
TSQLGenerator gen;
|
||||
std::string out = gen.generate(mod.get());
|
||||
CHECK(out.find("DECLARE @limit INT = 10;") != std::string::npos, "expected declare output");
|
||||
CHECK(out.find("SELECT TOP 10") != std::string::npos, "expected top output");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 413: T-SQL Parser + Generator Tests\n";
|
||||
|
||||
test_parse_create_table_identity_nvarchar(); // 1
|
||||
test_parse_select_top_where(); // 2
|
||||
test_parse_create_procedure_marker(); // 3
|
||||
test_parse_declare_variable(); // 4
|
||||
test_parse_set_assignment(); // 5
|
||||
test_parse_print_statement(); // 6
|
||||
test_generate_select_top_clause(); // 7
|
||||
test_generate_create_table_identity(); // 8
|
||||
test_generate_procedure_begin_end(); // 9
|
||||
test_generate_declare_set_print(); // 10
|
||||
test_pipeline_parse_and_generate_aliases(); // 11
|
||||
test_parser_to_generator_tsql_flow(); // 12
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed)
|
||||
<< " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
55
progress.md
55
progress.md
@@ -4019,6 +4019,61 @@ the SQL AST nodes introduced in Step 410 and parsed in Step 411.
|
||||
- `editor/src/MCPServer.h` (`1679` > `600`)
|
||||
- `editor/src/HeadlessAgentRPCHandler.h` (`2629` > `600`)
|
||||
|
||||
### Step 413: T-SQL Parser + Generator
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Added Microsoft SQL Server (T-SQL) dialect support with parser and generator
|
||||
coverage for dialect-specific constructs (`TOP`, `IDENTITY`, `NVARCHAR`,
|
||||
`DECLARE`/`SET`/`PRINT`, stored procedures).
|
||||
|
||||
**Files created:**
|
||||
- `editor/src/ast/TSQLParser.h` — T-SQL parse support:
|
||||
- `parseTSQL(...)` and `parseTSQLWithDiagnostics(...)`
|
||||
- `CREATE TABLE`, `CREATE INDEX`, `SELECT`, `INSERT`, `UPDATE`, `DELETE`
|
||||
- `SELECT TOP n` captured via `SelectQuery` child `"top"`
|
||||
- `CREATE PROCEDURE ... AS BEGIN ... END` mapped to `Function` markers
|
||||
- `DECLARE`, `SET`, `PRINT` mapped to `Variable`, `Assignment`,
|
||||
and `ExpressionStatement(FunctionCall)`
|
||||
- `editor/src/ast/TSQLGenerator.h` — T-SQL generation support:
|
||||
- `SELECT TOP n` emission
|
||||
- `CREATE PROCEDURE ... AS BEGIN ... END`
|
||||
- `DECLARE` / `SET` / `PRINT` statements
|
||||
- T-SQL-oriented primitive type mapping (`INT`, `FLOAT`, `NVARCHAR(MAX)`, `BIT`)
|
||||
- `editor/tests/step413_test.cpp` — 12 tests covering:
|
||||
1. CREATE TABLE with IDENTITY and NVARCHAR parse
|
||||
2. SELECT TOP + WHERE parse
|
||||
3. CREATE PROCEDURE parse marker
|
||||
4. DECLARE parse
|
||||
5. SET parse
|
||||
6. PRINT parse
|
||||
7. SELECT TOP generation
|
||||
8. CREATE TABLE with IDENTITY generation
|
||||
9. PROCEDURE BEGIN/END generation
|
||||
10. DECLARE/SET/PRINT generation
|
||||
11. pipeline parse/generate alias routing (`tsql`, `sqlserver`, `mssql`)
|
||||
12. parser-to-generator T-SQL flow
|
||||
|
||||
**Files modified:**
|
||||
- `editor/src/ast/Parser.h` — include `ast/TSQLParser.h`
|
||||
- `editor/src/ast/Generator.h` — include `TSQLGenerator.h`
|
||||
- `editor/src/Pipeline.h` — parse + generate routing for `tsql`, `sqlserver`, `mssql`
|
||||
- `editor/CMakeLists.txt` — `step413_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `step413_test` — PASS (12/12) new step coverage
|
||||
- `step412_test` — PASS (12/12) regression coverage
|
||||
- `step411_test` — PASS (12/12) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/ast/TSQLParser.h` within header-size limit (`377` <= `600`)
|
||||
- `editor/src/ast/TSQLGenerator.h` within header-size limit (`161` <= `600`)
|
||||
- `editor/tests/step413_test.cpp` within test-file size guidance (`213` lines)
|
||||
- `editor/src/Pipeline.h` within header-size limit (`261` <= `600`)
|
||||
- Legacy oversized headers persist:
|
||||
- `editor/src/ast/Serialization.h` (`1427` > `600`)
|
||||
- `editor/src/MCPServer.h` (`1679` > `600`)
|
||||
- `editor/src/HeadlessAgentRPCHandler.h` (`2629` > `600`)
|
||||
|
||||
# Roadmap Planning — Sprints 12-25+
|
||||
|
||||
## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)
|
||||
|
||||
Reference in New Issue
Block a user