diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index f42f607..86dc01b 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2605,4 +2605,13 @@ target_link_libraries(step413_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step414_test tests/step414_test.cpp) +target_include_directories(step414_test PRIVATE src) +target_link_libraries(step414_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 81c6518..7ba6fed 100644 --- a/editor/src/Pipeline.h +++ b/editor/src/Pipeline.h @@ -134,6 +134,10 @@ public: auto pr = TSQLParser::parseTSQLWithDiagnostics(source); diags = std::move(pr.diagnostics); return std::move(pr.module); + } else if (language == "mysql" || language == "mariadb") { + auto pr = MySQLParser::parseMySQLWithDiagnostics(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); @@ -200,6 +204,9 @@ public: } else if (language == "tsql" || language == "sqlserver" || language == "mssql") { TSQLGenerator gen; return gen.generate(ast); + } else if (language == "mysql" || language == "mariadb") { + MySQLGenerator 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 e3f13cb..2547a1a 100644 --- a/editor/src/ast/Generator.h +++ b/editor/src/ast/Generator.h @@ -15,6 +15,7 @@ #include "VBNetGenerator.h" #include "PostgreSQLGenerator.h" #include "TSQLGenerator.h" +#include "MySQLGenerator.h" #include "CGenerator.h" #include "WatGenerator.h" #include "CommonLispGenerator.h" diff --git a/editor/src/ast/MySQLGenerator.h b/editor/src/ast/MySQLGenerator.h new file mode 100644 index 0000000..73fd605 --- /dev/null +++ b/editor/src/ast/MySQLGenerator.h @@ -0,0 +1,178 @@ +#pragma once + +#include "PostgreSQLGenerator.h" +#include "SqlNodes.h" + +#include +#include + +class MySQLGenerator : public PostgreSQLGenerator { +public: + std::string commentPrefix() const { return "-- "; } + + std::string generate(const ASTNode* node) override { + return dispatchGenerate(this, node, "-- Unknown concept: "); + } + + std::string visitPrimitiveType(const PrimitiveType* type) override { + if (type->kind == "int") return "INT"; + if (type->kind == "float" || type->kind == "double") return "DOUBLE"; + if (type->kind == "string") return "VARCHAR(255)"; + if (type->kind == "bool") return "TINYINT(1)"; + 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 " << quoteIdentifier(table->name) << " ("; + + auto cols = table->getChildren("columns"); + if (cols.empty()) { + oss << "\n `id` INT AUTO_INCREMENT 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)"; + + auto* engine = table->getChild("engine"); + if (engine && engine->conceptType == "CustomType") { + oss << " ENGINE=" << static_cast(engine)->typeName; + } + oss << ";"; + return oss.str(); + } + + std::string visitColumnDefinition(const ASTNode* node) override { + auto* col = static_cast(node); + std::ostringstream oss; + oss << quoteIdentifier(col->name) + << " " << (col->dataType.empty() ? "VARCHAR(255)" : col->dataType); + if (!col->nullable) oss << " NOT NULL"; + if (!col->defaultValue.empty()) oss << " DEFAULT " << col->defaultValue; + return oss.str(); + } + + std::string visitSelectQuery(const ASTNode* node) override { + auto* q = static_cast(node); + std::ostringstream oss; + oss << "SELECT "; + 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") { + auto* tbl = static_cast(from); + oss << " FROM " << quoteIdentifier(tbl->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); + + auto* limit = q->getChild("limit"); + if (limit && limit->conceptType == "IntegerLiteral") { + oss << " LIMIT " << static_cast(limit)->value; + } + oss << ";"; + return oss.str(); + } + + std::string visitFunctionCall(const FunctionCall* call) override { + if (call->functionName == "GROUP_CONCAT") { + return "GROUP_CONCAT(*)"; + } + return PostgreSQLGenerator::visitFunctionCall(call); + } + + std::string visitInsertStatement(const ASTNode* node) override { + auto* ins = static_cast(node); + std::ostringstream oss; + oss << "INSERT INTO " << quoteIdentifier(ins->tableName); + + auto cols = ins->getChildren("columns"); + if (!cols.empty()) { + oss << " ("; + for (size_t i = 0; i < cols.size(); ++i) { + if (i > 0) oss << ", "; + auto* col = static_cast(cols[i]); + oss << quoteIdentifier(col->name); + } + oss << ")"; + } + + auto values = ins->getChildren("values"); + if (!values.empty()) { + oss << " VALUES ("; + for (size_t i = 0; i < values.size(); ++i) { + if (i > 0) oss << ", "; + oss << generate(values[i]); + } + oss << ")"; + } + oss << ";"; + return oss.str(); + } + + std::string visitUpdateStatement(const ASTNode* node) override { + auto* up = static_cast(node); + std::ostringstream oss; + oss << "UPDATE " << quoteIdentifier(up->tableName) << " SET "; + auto sets = up->getChildren("set"); + if (sets.empty()) { + oss << "/* STUB: set clause missing */"; + } else { + for (size_t i = 0; i < sets.size(); ++i) { + if (i > 0) oss << ", "; + oss << generate(sets[i]); + } + } + auto* where = up->getChild("where"); + if (where) oss << " " << generate(where); + oss << ";"; + return oss.str(); + } + + std::string visitDeleteStatement(const ASTNode* node) override { + auto* del = static_cast(node); + std::ostringstream oss; + oss << "DELETE FROM " << quoteIdentifier(del->tableName); + auto* where = del->getChild("where"); + if (where) oss << " " << generate(where); + oss << ";"; + return oss.str(); + } + +private: + static std::string quoteIdentifier(const std::string& ident) { + if (ident.empty()) return "``"; + if (ident.front() == '`' && ident.back() == '`') return ident; + + std::ostringstream oss; + std::stringstream ss(ident); + std::string part; + bool first = true; + while (std::getline(ss, part, '.')) { + if (!first) oss << "."; + oss << "`" << part << "`"; + first = false; + } + return oss.str(); + } +}; diff --git a/editor/src/ast/MySQLParser.h b/editor/src/ast/MySQLParser.h new file mode 100644 index 0000000..afe42d8 --- /dev/null +++ b/editor/src/ast/MySQLParser.h @@ -0,0 +1,331 @@ +#pragma once + +#include "ASTNode.h" +#include "Module.h" +#include "Expression.h" +#include "Type.h" +#include "SqlNodes.h" +#include "../ast/Parser.h" + +#include +#include +#include +#include + +class MySQLParser { +public: + static std::unique_ptr parseMySQL(const std::string& source) { + auto module = std::make_unique(); + module->id = IdGenerator::next("mod"); + module->name = "parsed_mysql_module"; + module->targetLanguage = "mysql"; + parseStatements(source, module.get()); + return module; + } + + static ParseResult parseMySQLWithDiagnostics(const std::string& source) { + ParseResult result; + result.module = parseMySQL(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; + } + } + } + + static void parseCreateTable(const std::string& stmt, Module* module) { + std::string tableName = extractAfterKeyword(stmt, "TABLE "); + tableName = stripBackticks(trim(untilDelimiter(tableName, "("))); + + auto* table = new TableDeclaration(); + table->id = IdGenerator::next("table"); + table->name = tableName; + + 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") || + startsWith(lowerCol, "key ") || + startsWith(lowerCol, "unique key")) continue; + + std::vector parts = splitWhitespace(colLine); + if (parts.size() < 2) continue; + + auto* col = new ColumnDefinition(); + col->id = IdGenerator::next("col"); + col->name = stripBackticks(parts[0]); + col->dataType = parts[1]; + if (parts.size() >= 3 && + toLower(parts[2]).find("auto_increment") != std::string::npos) { + col->dataType += " AUTO_INCREMENT"; + } + col->nullable = (lowerCol.find("not null") == std::string::npos); + table->addChild("columns", col); + } + } + + std::string lowerStmt = toLower(stmt); + auto enginePos = lowerStmt.find("engine="); + if (enginePos != std::string::npos) { + std::string engine = trim(stmt.substr(enginePos + 7)); + engine = untilDelimiter(engine, " "); + engine = untilDelimiter(engine, ";"); + table->setChild("engine", new CustomType(IdGenerator::next("engine"), engine)); + } + + 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 = stripBackticks(trim(untilDelimiter(idxName, " ON"))); + idx->tableName = stripBackticks(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 fromPos = lower.find(" from "); + if (fromPos != std::string::npos) { + std::string proj = trim(stmt.substr(6, fromPos - 6)); + parseSelectColumns(proj, q); + + std::string afterFrom = trim(stmt.substr(fromPos + 6)); + std::string fromName = stripBackticks(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 = stripBackticks(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) { + std::string expr = trim(stmt.substr(wherePos + 7)); + expr = untilDelimiter(expr, " LIMIT "); + auto* w = new WhereClause(); + w->id = IdGenerator::next("where"); + w->expression = expr; + q->setChild("where", w); + } + + auto limitPos = lower.find(" limit "); + if (limitPos != std::string::npos) { + std::string limitRaw = trim(stmt.substr(limitPos + 7)); + limitRaw = untilDelimiter(limitRaw, ","); + limitRaw = untilDelimiter(limitRaw, ";"); + if (!limitRaw.empty() && + std::all_of(limitRaw.begin(), limitRaw.end(), ::isdigit)) { + q->setChild("limit", + new IntegerLiteral(IdGenerator::next("lit"), std::stoi(limitRaw))); + } + } + + module->addChild("statements", q); + } + + static void parseInsert(const std::string& stmt, Module* module) { + auto* ins = new InsertStatement(); + ins->id = IdGenerator::next("ins"); + ins->tableName = stripBackticks(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 = stripBackticks(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 = stripBackticks(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 parseSelectColumns(const std::string& proj, SelectQuery* q) { + std::stringstream ss(proj); + std::string item; + while (std::getline(ss, item, ',')) { + std::string col = trim(item); + if (col.empty() || col == "*") continue; + std::string lower = toLower(col); + if (lower.find("group_concat(") != std::string::npos) { + auto* call = new FunctionCall(); + call->id = IdGenerator::next("call"); + call->functionName = "GROUP_CONCAT"; + q->addChild("columns", call); + } else { + auto* c = new ColumnDefinition(); + c->id = IdGenerator::next("col"); + c->name = stripBackticks(untilDelimiter(col, " ")); + c->dataType = "TEXT"; + q->addChild("columns", c); + } + } + } + + 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("inner") != std::string::npos) return "INNER"; + return "JOIN"; + } + + 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 token; + while (ss >> token) out.push_back(token); + return out; + } + + static std::string stripBackticks(const std::string& s) { + if (s.size() >= 2 && s.front() == '`' && s.back() == '`') { + return s.substr(1, s.size() - 2); + } + return s; + } + + 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 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/src/ast/Parser.h b/editor/src/ast/Parser.h index 2e387f5..4267369 100644 --- a/editor/src/ast/Parser.h +++ b/editor/src/ast/Parser.h @@ -168,6 +168,7 @@ private: #include "ast/VBNetParser.h" #include "ast/PostgreSQLParser.h" #include "ast/TSQLParser.h" +#include "ast/MySQLParser.h" #include "ast/CParser.h" #include "ast/WatParser.h" #include "ast/CommonLispParser.h" diff --git a/editor/tests/step414_test.cpp b/editor/tests/step414_test.cpp new file mode 100644 index 0000000..f404aa4 --- /dev/null +++ b/editor/tests/step414_test.cpp @@ -0,0 +1,209 @@ +// Step 414: MySQL Parser + Generator Tests (12 tests) + +#include "ast/MySQLParser.h" +#include "ast/MySQLGenerator.h" +#include "ast/SqlNodes.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_auto_increment_engine() { + TEST(parse_create_table_auto_increment_engine); + auto mod = MySQLParser::parseMySQL( + "CREATE TABLE `users` (`id` INT AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL) ENGINE=InnoDB;\n"); + auto* t = firstStatementOf(mod.get(), "TableDeclaration"); + CHECK(t != nullptr, "expected table declaration"); + CHECK(t->name == "users", "expected stripped backtick name"); + auto cols = t->getChildren("columns"); + CHECK(cols.size() == 2, "expected columns"); + CHECK(static_cast(cols[0])->dataType.find("AUTO_INCREMENT") != std::string::npos, + "expected auto_increment"); + auto* e = t->getChild("engine"); + CHECK(e != nullptr && e->conceptType == "CustomType", "expected engine marker"); + PASS(); +} + +void test_parse_select_limit() { + TEST(parse_select_limit); + auto mod = MySQLParser::parseMySQL("SELECT id FROM `users` LIMIT 10;\n"); + auto* q = firstStatementOf(mod.get(), "SelectQuery"); + CHECK(q != nullptr, "expected select"); + CHECK(q->getChild("limit") != nullptr, "expected limit child"); + PASS(); +} + +void test_parse_group_concat_projection() { + TEST(parse_group_concat_projection); + auto mod = MySQLParser::parseMySQL( + "SELECT GROUP_CONCAT(name), id FROM users;\n"); + auto* q = firstStatementOf(mod.get(), "SelectQuery"); + CHECK(q != nullptr, "expected select"); + auto cols = q->getChildren("columns"); + CHECK(cols.size() >= 2, "expected parsed columns"); + CHECK(cols[0]->conceptType == "FunctionCall", "expected group_concat call node"); + PASS(); +} + +void test_parse_insert_update_delete() { + TEST(parse_insert_update_delete); + auto mod = MySQLParser::parseMySQL( + "INSERT INTO `users` (`id`) VALUES (1);\n" + "UPDATE `users` SET id = 2 WHERE id = 1;\n" + "DELETE FROM `users` WHERE id = 2;\n"); + CHECK(firstStatementOf(mod.get(), "InsertStatement") != nullptr, "insert parse"); + CHECK(firstStatementOf(mod.get(), "UpdateStatement") != nullptr, "update parse"); + CHECK(firstStatementOf(mod.get(), "DeleteStatement") != nullptr, "delete parse"); + PASS(); +} + +void test_parse_with_diagnostics_entrypoint() { + TEST(parse_with_diagnostics_entrypoint); + auto result = MySQLParser::parseMySQLWithDiagnostics("SELECT id FROM users LIMIT 5;\n"); + CHECK(result.module != nullptr, "module null"); + auto* q = firstStatementOf(result.module.get(), "SelectQuery"); + CHECK(q != nullptr, "expected select"); + CHECK(q->getChild("limit") != nullptr, "expected limit"); + PASS(); +} + +void test_generate_create_table_backticks_engine() { + TEST(generate_create_table_backticks_engine); + TableDeclaration t("t1", "users"); + t.addChild("columns", new ColumnDefinition("c1", "id", "INT AUTO_INCREMENT", false)); + t.addChild("columns", new ColumnDefinition("c2", "name", "VARCHAR(255)", false)); + t.setChild("engine", new CustomType("e1", "InnoDB")); + MySQLGenerator gen; + std::string out = gen.generate(&t); + CHECK(out.find("CREATE TABLE `users`") != std::string::npos, "expected backtick table"); + CHECK(out.find("`id` INT AUTO_INCREMENT NOT NULL") != std::string::npos, "expected id col"); + CHECK(out.find("ENGINE=InnoDB") != std::string::npos, "expected engine"); + PASS(); +} + +void test_generate_select_limit() { + TEST(generate_select_limit); + SelectQuery q; + q.id = "q1"; + q.addChild("columns", new ColumnDefinition("c1", "id", "INT")); + q.setChild("from", new TableDeclaration("t1", "users")); + q.setChild("limit", new IntegerLiteral("l1", 25)); + MySQLGenerator gen; + std::string out = gen.generate(&q); + CHECK(out.find("SELECT `id` INT") != std::string::npos, "expected column output"); + CHECK(out.find("FROM `users`") != std::string::npos, "expected from output"); + CHECK(out.find("LIMIT 25") != std::string::npos, "expected limit output"); + PASS(); +} + +void test_generate_group_concat() { + TEST(generate_group_concat); + FunctionCall c; + c.id = "call1"; + c.functionName = "GROUP_CONCAT"; + MySQLGenerator gen; + std::string out = gen.generate(&c); + CHECK(out == "GROUP_CONCAT(*)", "expected group_concat form"); + PASS(); +} + +void test_generate_insert_with_backticks() { + TEST(generate_insert_with_backticks); + InsertStatement ins("i1", "users"); + ins.addChild("columns", new ColumnDefinition("c1", "id", "INT")); + ins.addChild("values", new IntegerLiteral("v1", 7)); + MySQLGenerator gen; + std::string out = gen.generate(&ins); + CHECK(out.find("INSERT INTO `users`") != std::string::npos, "expected quoted table"); + CHECK(out.find("(`id`)") != std::string::npos, "expected quoted column"); + CHECK(out.find("VALUES (7)") != std::string::npos, "expected value"); + PASS(); +} + +void test_generate_update_delete_with_backticks() { + TEST(generate_update_delete_with_backticks); + UpdateStatement up("u1", "users"); + auto* set = new Assignment(); + set->id = "a1"; + set->setChild("target", new VariableReference("r1", "name")); + set->setChild("value", new StringLiteral("s1", "alice")); + up.addChild("set", set); + up.setChild("where", new WhereClause("w1", "id = 1")); + + DeleteStatement del("d1", "users"); + del.setChild("where", new WhereClause("w2", "id = 1")); + + MySQLGenerator gen; + std::string o1 = gen.generate(&up); + std::string o2 = gen.generate(&del); + CHECK(o1.find("UPDATE `users` SET") != std::string::npos, "expected update form"); + CHECK(o2.find("DELETE FROM `users` WHERE id = 1;") != std::string::npos, "expected delete form"); + PASS(); +} + +void test_pipeline_parse_and_generate_aliases() { + TEST(pipeline_parse_and_generate_aliases); + Pipeline p; + std::vector d1, d2; + auto m1 = p.parse("SELECT id FROM users LIMIT 1;\n", "mysql", d1); + auto m2 = p.parse("SELECT id FROM users LIMIT 1;\n", "mariadb", d2); + CHECK(m1 != nullptr && m2 != nullptr, "expected parse success"); + CHECK(m1->targetLanguage == "mysql", "expected mysql target"); + + DeleteStatement del("d1", "audit"); + std::string g1 = p.generate(&del, "mysql"); + std::string g2 = p.generate(&del, "mariadb"); + CHECK(g1.find("DELETE FROM `audit`;") != std::string::npos, "mysql generate route"); + CHECK(g2.find("DELETE FROM `audit`;") != std::string::npos, "mariadb generate route"); + PASS(); +} + +void test_parser_to_generator_mysql_flow() { + TEST(parser_to_generator_mysql_flow); + auto mod = MySQLParser::parseMySQL( + "CREATE TABLE `events` (`id` INT AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL) ENGINE=InnoDB;\n" + "SELECT id FROM `events` LIMIT 3;\n"); + CHECK(mod != nullptr, "module null"); + MySQLGenerator gen; + std::string out = gen.generate(mod.get()); + CHECK(out.find("CREATE TABLE `events`") != std::string::npos, "expected create table"); + CHECK(out.find("ENGINE=InnoDB") != std::string::npos, "expected engine"); + CHECK(out.find("LIMIT 3") != std::string::npos, "expected limit"); + PASS(); +} + +int main() { + std::cout << "Step 414: MySQL Parser + Generator Tests\n"; + + test_parse_create_table_auto_increment_engine(); // 1 + test_parse_select_limit(); // 2 + test_parse_group_concat_projection(); // 3 + test_parse_insert_update_delete(); // 4 + test_parse_with_diagnostics_entrypoint(); // 5 + test_generate_create_table_backticks_engine(); // 6 + test_generate_select_limit(); // 7 + test_generate_group_concat(); // 8 + test_generate_insert_with_backticks(); // 9 + test_generate_update_delete_with_backticks(); // 10 + test_pipeline_parse_and_generate_aliases(); // 11 + test_parser_to_generator_mysql_flow(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index d784f93..5f04f20 100644 --- a/progress.md +++ b/progress.md @@ -4074,6 +4074,62 @@ coverage for dialect-specific constructs (`TOP`, `IDENTITY`, `NVARCHAR`, - `editor/src/MCPServer.h` (`1679` > `600`) - `editor/src/HeadlessAgentRPCHandler.h` (`2629` > `600`) +### Step 414: MySQL Parser + Generator +**Status:** PASS (12/12 tests) + +Added MySQL/MariaDB dialect support with parser + generator behavior for +dialect-specific features (`AUTO_INCREMENT`, backtick identifiers, +`ENGINE=InnoDB`, `LIMIT`, `GROUP_CONCAT`). + +**Files created:** +- `editor/src/ast/MySQLParser.h` — MySQL parse support: + - `parseMySQL(...)` and `parseMySQLWithDiagnostics(...)` + - `CREATE TABLE`, `CREATE INDEX`, `SELECT`, `INSERT`, `UPDATE`, `DELETE` + - backtick identifier normalization + - table engine extraction via `TableDeclaration` child `"engine"` + - `LIMIT` extraction via `SelectQuery` child `"limit"` + - `GROUP_CONCAT(...)` projection marker via `FunctionCall` +- `editor/src/ast/MySQLGenerator.h` — MySQL generation support: + - backtick quoting for table/column identifiers + - `CREATE TABLE ... ENGINE=...` emission + - MySQL select generation with `LIMIT` + - `GROUP_CONCAT(*)` generation path + - MySQL alias support in DML emitters +- `editor/tests/step414_test.cpp` — 12 tests covering: + 1. CREATE TABLE parse with `AUTO_INCREMENT` and engine + 2. SELECT `LIMIT` parse + 3. `GROUP_CONCAT` projection parse marker + 4. INSERT/UPDATE/DELETE parse coverage + 5. diagnostics entrypoint + 6. CREATE TABLE generation with backticks + engine + 7. SELECT generation with LIMIT + 8. `GROUP_CONCAT` generation + 9. INSERT generation with backticks + 10. UPDATE/DELETE generation with backticks + 11. pipeline parse + generate alias routing (`mysql`, `mariadb`) + 12. parser-to-generator MySQL flow + +**Files modified:** +- `editor/src/ast/Parser.h` — include `ast/MySQLParser.h` +- `editor/src/ast/Generator.h` — include `MySQLGenerator.h` +- `editor/src/Pipeline.h` — parse + generate routing for `mysql`, `mariadb` +- `editor/CMakeLists.txt` — `step414_test` target + +**Verification run:** +- `step414_test` — PASS (12/12) new step coverage +- `step413_test` — PASS (12/12) regression coverage +- `step412_test` — PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/ast/MySQLParser.h` within header-size limit (`331` <= `600`) +- `editor/src/ast/MySQLGenerator.h` within header-size limit (`178` <= `600`) +- `editor/tests/step414_test.cpp` within test-file size guidance (`209` lines) +- `editor/src/Pipeline.h` within header-size limit (`268` <= `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)