Step 414: add MySQL parser and generator support

This commit is contained in:
Bill
2026-02-16 15:54:37 -07:00
parent 0e201e2f8c
commit 439a8a5838
8 changed files with 792 additions and 0 deletions

View File

@@ -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)

View File

@@ -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);

View File

@@ -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"

View File

@@ -0,0 +1,178 @@
#pragma once
#include "PostgreSQLGenerator.h"
#include "SqlNodes.h"
#include <sstream>
#include <string>
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<const TableDeclaration*>(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<const CustomType*>(engine)->typeName;
}
oss << ";";
return oss.str();
}
std::string visitColumnDefinition(const ASTNode* node) override {
auto* col = static_cast<const ColumnDefinition*>(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<const SelectQuery*>(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<const TableDeclaration*>(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<const IntegerLiteral*>(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<const InsertStatement*>(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<const ColumnDefinition*>(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<const UpdateStatement*>(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<const DeleteStatement*>(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();
}
};

View File

@@ -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 <algorithm>
#include <sstream>
#include <string>
#include <vector>
class MySQLParser {
public:
static std::unique_ptr<Module> parseMySQL(const std::string& source) {
auto module = std::make_unique<Module>();
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<std::string> 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<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 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<char>(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);
}
};

View File

@@ -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"

View File

@@ -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 <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_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<TableDeclaration>(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<ColumnDefinition*>(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<SelectQuery>(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<SelectQuery>(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<InsertStatement>(mod.get(), "InsertStatement") != nullptr, "insert parse");
CHECK(firstStatementOf<UpdateStatement>(mod.get(), "UpdateStatement") != nullptr, "update parse");
CHECK(firstStatementOf<DeleteStatement>(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<SelectQuery>(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<ParseDiagnostic> 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;
}

View File

@@ -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)