diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index c406869..f42f607 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -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) diff --git a/editor/src/Pipeline.h b/editor/src/Pipeline.h index 7b91758..81c6518 100644 --- a/editor/src/Pipeline.h +++ b/editor/src/Pipeline.h @@ -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); diff --git a/editor/src/ast/Generator.h b/editor/src/ast/Generator.h index df4f590..e3f13cb 100644 --- a/editor/src/ast/Generator.h +++ b/editor/src/ast/Generator.h @@ -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" diff --git a/editor/src/ast/Parser.h b/editor/src/ast/Parser.h index 0cbcec8..2e387f5 100644 --- a/editor/src/ast/Parser.h +++ b/editor/src/ast/Parser.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" diff --git a/editor/src/ast/TSQLGenerator.h b/editor/src/ast/TSQLGenerator.h new file mode 100644 index 0000000..d229e0f --- /dev/null +++ b/editor/src/ast/TSQLGenerator.h @@ -0,0 +1,161 @@ +#pragma once + +#include "PostgreSQLGenerator.h" +#include "SqlNodes.h" + +#include +#include + +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(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(node); + std::ostringstream oss; + oss << "SELECT "; + auto* topNode = q->getChild("top"); + if (topNode) { + auto* top = static_cast(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(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(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(); + } +}; diff --git a/editor/src/ast/TSQLParser.h b/editor/src/ast/TSQLParser.h new file mode 100644 index 0000000..dbcc52c --- /dev/null +++ b/editor/src/ast/TSQLParser.h @@ -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 +#include +#include +#include + +class TSQLParser { +public: + static std::unique_ptr parseTSQL(const std::string& source) { + auto module = std::make_unique(); + 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 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 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 splitStatements(const std::string& source) { + std::vector 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 splitWhitespace(const std::string& s) { + std::vector 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(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(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); + } +}; diff --git a/editor/tests/step413_test.cpp b/editor/tests/step413_test.cpp new file mode 100644 index 0000000..4371c3f --- /dev/null +++ b/editor/tests/step413_test.cpp @@ -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 +#include +#include + +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 +T* firstStatementOf(const Module* mod, const std::string& conceptName) { + for (auto* s : mod->getChildren("statements")) { + if (s->conceptType == conceptName) return static_cast(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(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(cols[0])->dataType.find("IDENTITY") != std::string::npos, + "expected identity type"); + CHECK(static_cast(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(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(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(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(mod.get(), "Assignment"); + CHECK(a != nullptr, "expected assignment statement"); + auto* target = static_cast(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(mod.get(), "ExpressionStatement"); + CHECK(es != nullptr, "expected expression statement"); + auto* call = static_cast(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 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; +} diff --git a/progress.md b/progress.md index 778f354..d784f93 100644 --- a/progress.md +++ b/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)