From 7dbfbab63a5d35d91b94685210facbead8b3b945 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 16 Feb 2026 15:45:27 -0700 Subject: [PATCH] Step 412: add PostgreSQL generator and pipeline routing --- editor/CMakeLists.txt | 9 + editor/src/Pipeline.h | 3 + editor/src/ast/Generator.h | 1 + editor/src/ast/PostgreSQLGenerator.h | 412 +++++++++++++++++++++++++++ editor/src/ast/ProjectionGenerator.h | 29 ++ editor/tests/step412_test.cpp | 203 +++++++++++++ progress.md | 55 ++++ 7 files changed, 712 insertions(+) create mode 100644 editor/src/ast/PostgreSQLGenerator.h create mode 100644 editor/tests/step412_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 541ea72..c406869 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2587,4 +2587,13 @@ target_link_libraries(step411_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step412_test tests/step412_test.cpp) +target_include_directories(step412_test PRIVATE src) +target_link_libraries(step412_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 77e5253..7b91758 100644 --- a/editor/src/Pipeline.h +++ b/editor/src/Pipeline.h @@ -190,6 +190,9 @@ public: } else if (language == "vbnet" || language == "vb" || language == "vb.net") { VBNetGenerator gen; return gen.generate(ast); + } else if (language == "postgresql" || language == "postgres") { + PostgreSQLGenerator 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 44a8cac..df4f590 100644 --- a/editor/src/ast/Generator.h +++ b/editor/src/ast/Generator.h @@ -13,6 +13,7 @@ #include "CSharpGenerator.h" #include "FSharpGenerator.h" #include "VBNetGenerator.h" +#include "PostgreSQLGenerator.h" #include "CGenerator.h" #include "WatGenerator.h" #include "CommonLispGenerator.h" diff --git a/editor/src/ast/PostgreSQLGenerator.h b/editor/src/ast/PostgreSQLGenerator.h new file mode 100644 index 0000000..fafcc51 --- /dev/null +++ b/editor/src/ast/PostgreSQLGenerator.h @@ -0,0 +1,412 @@ +#pragma once + +#include "ProjectionGenerator.h" +#include "SqlNodes.h" +#include "../SemannoAnnotationImpl.h" + +#include +#include + +class PostgreSQLGenerator : public ProjectionGenerator, + public SemannoAnnotationImpl { +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* stmt : module->getChildren("statements")) { + oss << generate(stmt) << "\n"; + } + for (const auto* fn : module->getChildren("functions")) { + oss << generate(fn) << "\n"; + } + return trimTrailingNewlines(oss.str()); + } + + std::string visitFunction(const Function* function) override { + std::ostringstream oss; + for (const auto* anno : function->getChildren("annotations")) { + oss << generate(anno) << "\n"; + } + + oss << "CREATE FUNCTION " << function->name << "("; + auto params = function->getChildren("parameters"); + for (size_t i = 0; i < params.size(); ++i) { + if (i > 0) oss << ", "; + oss << generate(params[i]); + } + oss << ")\nRETURNS VOID\nLANGUAGE plpgsql\nAS $$\nBEGIN\n"; + for (const auto* stmt : function->getChildren("body")) { + oss << " " << generate(stmt) << "\n"; + } + oss << "END;\n$$;"; + return oss.str(); + } + + std::string visitVariable(const Variable* variable) override { + std::ostringstream oss; + oss << variable->name; + auto* init = variable->getChild("initializer"); + if (init) oss << " = " << generate(init); + return oss.str(); + } + + std::string visitParameter(const Parameter* parameter) override { + auto* type = parameter->getChild("type"); + return parameter->name + " " + (type ? generate(type) : "TEXT"); + } + + std::string visitAssignment(const Assignment* assignment) override { + auto* t = assignment->getChild("target"); + auto* v = assignment->getChild("value"); + return (t ? generate(t) : "column") + " = " + (v ? generate(v) : "NULL"); + } + + std::string visitReturn(const Return* ret) override { + auto* value = ret->getChild("value"); + return value ? "RETURN " + generate(value) + ";" : "RETURN;"; + } + + std::string visitBinaryOperation(const BinaryOperation* binOp) override { + auto* l = binOp->getChild("left"); + auto* r = binOp->getChild("right"); + return (l ? generate(l) : "NULL") + " " + binOp->op + " " + (r ? generate(r) : "NULL"); + } + + std::string visitVariableReference(const VariableReference* varRef) override { + return varRef->variableName; + } + + std::string visitIntegerLiteral(const IntegerLiteral* lit) override { + return std::to_string(lit->value); + } + + std::string visitFloatLiteral(const FloatLiteral* lit) override { + return lit->value; + } + + std::string visitStringLiteral(const StringLiteral* lit) override { + return "'" + lit->value + "'"; + } + + std::string visitBooleanLiteral(const BooleanLiteral* lit) override { + return lit->value ? "TRUE" : "FALSE"; + } + + std::string visitNullLiteral(const NullLiteral*) override { + return "NULL"; + } + + std::string visitIfStatement(const IfStatement* stmt) override { + std::ostringstream oss; + auto* cond = stmt->getChild("condition"); + oss << "IF " << (cond ? generate(cond) : "TRUE") << " THEN"; + for (const auto* s : stmt->getChildren("thenBranch")) { + oss << "\n " << generate(s); + } + auto elseBranch = stmt->getChildren("elseBranch"); + if (!elseBranch.empty()) { + oss << "\nELSE"; + for (const auto* s : elseBranch) { + oss << "\n " << generate(s); + } + } + oss << "\nEND IF;"; + return oss.str(); + } + + std::string visitWhileLoop(const WhileLoop* loop) override { + std::ostringstream oss; + auto* cond = loop->getChild("condition"); + oss << "WHILE " << (cond ? generate(cond) : "TRUE") << " LOOP"; + for (const auto* s : loop->getChildren("body")) { + oss << "\n " << generate(s); + } + oss << "\nEND LOOP;"; + return oss.str(); + } + + std::string visitForLoop(const ForLoop* loop) override { + std::ostringstream oss; + auto* iter = loop->getChild("iterable"); + oss << "FOR " << (loop->iteratorName.empty() ? "item" : loop->iteratorName) + << " IN " << (iter ? generate(iter) : "SELECT 1") << " LOOP"; + for (const auto* s : loop->getChildren("body")) { + oss << "\n " << generate(s); + } + oss << "\nEND LOOP;"; + return oss.str(); + } + + std::string visitExpressionStatement(const ExpressionStatement* stmt) override { + auto* expr = stmt->getChild("expression"); + return expr ? generate(expr) + ";" : ";"; + } + + std::string visitUnaryOperation(const UnaryOperation* unOp) override { + auto* operand = unOp->getChild("operand"); + return unOp->op + (operand ? generate(operand) : ""); + } + + std::string visitFunctionCall(const FunctionCall* call) override { + std::ostringstream oss; + 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 visitBlock(const Block* block) override { + std::ostringstream oss; + for (const auto* s : block->getChildren("statements")) { + oss << generate(s) << "\n"; + } + return trimTrailingNewlines(oss.str()); + } + + std::string visitListLiteral(const ListLiteral* lit) override { + std::ostringstream oss; + oss << "ARRAY["; + auto elems = lit->getChildren("elements"); + for (size_t i = 0; i < elems.size(); ++i) { + if (i > 0) oss << ", "; + oss << generate(elems[i]); + } + oss << "]"; + return oss.str(); + } + + std::string visitIndexAccess(const IndexAccess* access) override { + auto* t = access->getChild("target"); + auto* i = access->getChild("index"); + return (t ? generate(t) : "") + "[" + (i ? generate(i) : "1") + "]"; + } + + std::string visitMemberAccess(const MemberAccess* access) override { + auto* target = access->getChild("target"); + return (target ? generate(target) : "") + "." + access->memberName; + } + + std::string visitPrimitiveType(const PrimitiveType* type) override { + if (type->kind == "int") return "INTEGER"; + if (type->kind == "float" || type->kind == "double") return "DOUBLE PRECISION"; + if (type->kind == "string") return "TEXT"; + if (type->kind == "bool") return "BOOLEAN"; + if (type->kind == "void") return "VOID"; + return type->kind; + } + + std::string visitListType(const ListType* type) override { + auto* e = type->getChild("elementType"); + return (e ? generate(e) : "TEXT") + "[]"; + } + + std::string visitSetType(const SetType* type) override { + auto* e = type->getChild("elementType"); + return (e ? generate(e) : "TEXT") + "[]"; + } + + std::string visitMapType(const MapType*) override { + return "JSONB"; + } + + std::string visitTupleType(const TupleType*) override { + return "RECORD"; + } + + std::string visitArrayType(const ArrayType* type) override { + auto* e = type->getChild("elementType"); + return (e ? generate(e) : "TEXT") + "[]"; + } + + std::string visitOptionalType(const OptionalType* type) override { + auto* inner = type->getChild("innerType"); + return inner ? generate(inner) : "TEXT"; + } + + std::string visitCustomType(const CustomType* type) override { + return type->typeName; + } + + std::string visitDerefStrategy(const DerefStrategy* a) override { return commentPrefix() + SemannoEmitter::emit(a); } + std::string visitOptimizationLock(const OptimizationLock* a) override { return commentPrefix() + SemannoEmitter::emit(a); } + std::string visitLangSpecific(const LangSpecific* a) override { return commentPrefix() + SemannoEmitter::emit(a); } + std::string visitDeallocateAnnotation(const DeallocateAnnotation* a) override { return commentPrefix() + SemannoEmitter::emit(a); } + std::string visitLifetimeAnnotation(const LifetimeAnnotation* a) override { return commentPrefix() + SemannoEmitter::emit(a); } + std::string visitReclaimAnnotation(const ReclaimAnnotation* a) override { return commentPrefix() + SemannoEmitter::emit(a); } + std::string visitOwnerAnnotation(const OwnerAnnotation* a) override { return commentPrefix() + SemannoEmitter::emit(a); } + std::string visitAllocateAnnotation(const AllocateAnnotation* a) override { return commentPrefix() + SemannoEmitter::emit(a); } + std::string visitHotColdAnnotation(const HotColdAnnotation* a) override { return commentPrefix() + SemannoEmitter::emit(a); } + std::string visitInlineAnnotation(const InlineAnnotation* a) override { return commentPrefix() + SemannoEmitter::emit(a); } + std::string visitPureAnnotation(const PureAnnotation* a) override { return commentPrefix() + SemannoEmitter::emit(a); } + std::string visitConstExprAnnotation(const ConstExprAnnotation* a) override { return commentPrefix() + SemannoEmitter::emit(a); } + + 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 SERIAL 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);"; + + for (const auto* idx : table->getChildren("indexes")) { + oss << "\n" << generate(idx); + } + return oss.str(); + } + + std::string visitColumnDefinition(const ASTNode* node) override { + auto* col = static_cast(node); + std::ostringstream oss; + oss << col->name << " " << (col->dataType.empty() ? "TEXT" : 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) 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 visitInsertStatement(const ASTNode* node) override { + auto* ins = static_cast(node); + std::ostringstream oss; + oss << "INSERT INTO " << 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 << 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 " << 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 " << del->tableName; + auto* where = del->getChild("where"); + if (where) oss << " " << generate(where); + oss << ";"; + return oss.str(); + } + + std::string visitJoinClause(const ASTNode* node) override { + auto* join = static_cast(node); + std::ostringstream oss; + oss << (join->joinType.empty() ? "INNER" : join->joinType) + << " JOIN " << join->tableName; + auto* on = join->getChild("on"); + if (on) oss << " ON " << generate(on); + return oss.str(); + } + + std::string visitWhereClause(const ASTNode* node) override { + auto* where = static_cast(node); + return "WHERE " + where->expression; + } + + std::string visitIndexDefinition(const ASTNode* node) override { + auto* idx = static_cast(node); + std::ostringstream oss; + oss << "CREATE "; + if (idx->unique) oss << "UNIQUE "; + oss << "INDEX " << idx->name << " ON " << idx->tableName; + auto cols = idx->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 << col->name; + } + oss << ")"; + } + oss << ";"; + return oss.str(); + } + +private: + static std::string trimTrailingNewlines(std::string s) { + while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) s.pop_back(); + return s; + } +}; diff --git a/editor/src/ast/ProjectionGenerator.h b/editor/src/ast/ProjectionGenerator.h index 449e44f..8233f1d 100644 --- a/editor/src/ast/ProjectionGenerator.h +++ b/editor/src/ast/ProjectionGenerator.h @@ -16,6 +16,7 @@ #include "HostBoundary.h" #include "PreprocessorNodes.h" #include "EnumNamespaceNodes.h" +#include "SqlNodes.h" class ProjectionGenerator : public virtual AnnotationVisitorExtended { public: @@ -84,6 +85,16 @@ public: virtual std::string visitEnumDeclaration(const ASTNode* node) { return ""; } virtual std::string visitNamespaceDeclaration(const ASTNode* node) { return ""; } virtual std::string visitTypeAlias(const ASTNode* node) { return ""; } + // SQL visitors (Step 412) + virtual std::string visitTableDeclaration(const ASTNode* node) { return ""; } + virtual std::string visitColumnDefinition(const ASTNode* node) { return ""; } + virtual std::string visitSelectQuery(const ASTNode* node) { return ""; } + virtual std::string visitInsertStatement(const ASTNode* node) { return ""; } + virtual std::string visitUpdateStatement(const ASTNode* node) { return ""; } + virtual std::string visitDeleteStatement(const ASTNode* node) { return ""; } + virtual std::string visitJoinClause(const ASTNode* node) { return ""; } + virtual std::string visitWhereClause(const ASTNode* node) { return ""; } + virtual std::string visitIndexDefinition(const ASTNode* node) { return ""; } // Host boundary visitors virtual std::string visitHostCall(const HostCall* node) { return ""; } @@ -373,6 +384,24 @@ std::string dispatchGenerate(Gen* gen, const ASTNode* node, const std::string& u return gen->visitNamespaceDeclaration(node); } else if (node->conceptType == "TypeAlias") { return gen->visitTypeAlias(node); + } else if (node->conceptType == "TableDeclaration") { + return gen->visitTableDeclaration(node); + } else if (node->conceptType == "ColumnDefinition") { + return gen->visitColumnDefinition(node); + } else if (node->conceptType == "SelectQuery") { + return gen->visitSelectQuery(node); + } else if (node->conceptType == "InsertStatement") { + return gen->visitInsertStatement(node); + } else if (node->conceptType == "UpdateStatement") { + return gen->visitUpdateStatement(node); + } else if (node->conceptType == "DeleteStatement") { + return gen->visitDeleteStatement(node); + } else if (node->conceptType == "JoinClause") { + return gen->visitJoinClause(node); + } else if (node->conceptType == "WhereClause") { + return gen->visitWhereClause(node); + } else if (node->conceptType == "IndexDefinition") { + return gen->visitIndexDefinition(node); } return unknownPrefix + node->conceptType; diff --git a/editor/tests/step412_test.cpp b/editor/tests/step412_test.cpp new file mode 100644 index 0000000..75dd3c0 --- /dev/null +++ b/editor/tests/step412_test.cpp @@ -0,0 +1,203 @@ +// Step 412: PostgreSQL Generator Tests (12 tests) + +#include "ast/PostgreSQLGenerator.h" +#include "ast/PostgreSQLParser.h" +#include "Pipeline.h" +#include "ast/SqlNodes.h" +#include "ast/Annotation.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 {} + +void test_generate_create_table_with_columns() { + TEST(generate_create_table_with_columns); + TableDeclaration t("t1", "users"); + t.addChild("columns", new ColumnDefinition("c1", "id", "SERIAL", false)); + t.addChild("columns", new ColumnDefinition("c2", "name", "TEXT", false)); + + PostgreSQLGenerator gen; + std::string out = gen.generate(&t); + CHECK(out.find("CREATE TABLE users") != std::string::npos, "expected create table"); + CHECK(out.find("id SERIAL NOT NULL") != std::string::npos, "expected id column"); + CHECK(out.find("name TEXT NOT NULL") != std::string::npos, "expected name column"); + PASS(); +} + +void test_generate_select_distinct_join_where() { + TEST(generate_select_distinct_join_where); + SelectQuery q; + q.id = "q1"; + q.distinct = true; + q.addChild("columns", new ColumnDefinition("c1", "id", "INTEGER")); + q.setChild("from", new TableDeclaration("t1", "users")); + auto* j = new JoinClause("j1", "LEFT", "orders"); + j->setChild("on", new WhereClause("w1", "users.id = orders.user_id")); + q.addChild("joins", j); + q.setChild("where", new WhereClause("w2", "users.active = TRUE")); + + PostgreSQLGenerator gen; + std::string out = gen.generate(&q); + CHECK(out.find("SELECT DISTINCT") != std::string::npos, "expected distinct"); + CHECK(out.find("LEFT JOIN orders") != std::string::npos, "expected join"); + CHECK(out.find("WHERE users.active = TRUE") != std::string::npos, "expected where"); + PASS(); +} + +void test_generate_insert_statement() { + TEST(generate_insert_statement); + InsertStatement ins("i1", "users"); + ins.addChild("columns", new ColumnDefinition("c1", "id", "INTEGER")); + ins.addChild("columns", new ColumnDefinition("c2", "name", "TEXT")); + ins.addChild("values", new IntegerLiteral("v1", 1)); + ins.addChild("values", new StringLiteral("v2", "alice")); + + PostgreSQLGenerator gen; + std::string out = gen.generate(&ins); + CHECK(out.find("INSERT INTO users") != std::string::npos, "expected insert target"); + CHECK(out.find("(id, name)") != std::string::npos, "expected column list"); + CHECK(out.find("VALUES (1, 'alice')") != std::string::npos, "expected values list"); + PASS(); +} + +void test_generate_update_statement() { + TEST(generate_update_statement); + UpdateStatement up("u1", "users"); + auto* set = new Assignment(); + set->id = "a1"; + set->setChild("target", new VariableReference("vr1", "name")); + set->setChild("value", new StringLiteral("s1", "bob")); + up.addChild("set", set); + up.setChild("where", new WhereClause("w1", "id = 1")); + + PostgreSQLGenerator gen; + std::string out = gen.generate(&up); + CHECK(out.find("UPDATE users SET") != std::string::npos, "expected update"); + CHECK(out.find("name = 'bob'") != std::string::npos, "expected set assignment"); + CHECK(out.find("WHERE id = 1") != std::string::npos, "expected where"); + PASS(); +} + +void test_generate_delete_statement() { + TEST(generate_delete_statement); + DeleteStatement del("d1", "users"); + del.setChild("where", new WhereClause("w1", "id = 7")); + + PostgreSQLGenerator gen; + std::string out = gen.generate(&del); + CHECK(out == "DELETE FROM users WHERE id = 7;", "expected delete sql"); + PASS(); +} + +void test_generate_index_definition() { + TEST(generate_index_definition); + IndexDefinition idx("x1", "idx_users_email", "users", true); + idx.addChild("columns", new ColumnDefinition("c1", "email", "TEXT")); + + PostgreSQLGenerator gen; + std::string out = gen.generate(&idx); + CHECK(out.find("CREATE UNIQUE INDEX idx_users_email ON users") != std::string::npos, + "expected unique index"); + CHECK(out.find("(email)") != std::string::npos, "expected index column"); + PASS(); +} + +void test_generate_module_with_statements() { + TEST(generate_module_with_statements); + Module mod; + mod.id = "m1"; + mod.name = "sql_mod"; + mod.addChild("statements", new DeleteStatement("d1", "logs")); + + PostgreSQLGenerator gen; + std::string out = gen.generate(&mod); + CHECK(out.find("-- Module: sql_mod") != std::string::npos, "expected module header"); + CHECK(out.find("DELETE FROM logs;") != std::string::npos, "expected statement content"); + PASS(); +} + +void test_generate_where_clause_passthrough() { + TEST(generate_where_clause_passthrough); + WhereClause where("w1", "status = 'ok'"); + PostgreSQLGenerator gen; + std::string out = gen.generate(&where); + CHECK(out == "WHERE status = 'ok'", "expected where passthrough"); + PASS(); +} + +void test_comment_prefix_and_semanno_output() { + TEST(comment_prefix_and_semanno_output); + Function fn("f1", "process"); + auto* risk = new RiskAnnotation(); + risk->id = "r1"; + risk->level = "medium"; + fn.addChild("annotations", risk); + + PostgreSQLGenerator gen; + std::string out = gen.generate(&fn); + CHECK(gen.commentPrefix() == "-- ", "expected postgres comment prefix"); + CHECK(out.find("@semanno:risk") != std::string::npos, "expected semanno output"); + PASS(); +} + +void test_pipeline_generate_routing_aliases() { + TEST(pipeline_generate_routing_aliases); + DeleteStatement del("d1", "audit"); + Pipeline p; + std::string o1 = p.generate(&del, "postgresql"); + std::string o2 = p.generate(&del, "postgres"); + CHECK(o1 == "DELETE FROM audit;", "postgresql route"); + CHECK(o2 == "DELETE FROM audit;", "postgres route"); + PASS(); +} + +void test_parser_to_generator_create_table_flow() { + TEST(parser_to_generator_create_table_flow); + auto mod = PostgreSQLParser::parsePostgreSQL( + "CREATE TABLE events (id SERIAL, payload JSONB, tags TEXT[]);\n"); + CHECK(mod != nullptr, "module null"); + + PostgreSQLGenerator gen; + std::string out = gen.generate(mod.get()); + CHECK(out.find("CREATE TABLE events") != std::string::npos, "expected create table"); + CHECK(out.find("payload JSONB") != std::string::npos, "expected jsonb column"); + CHECK(out.find("tags TEXT[]") != std::string::npos, "expected array column"); + PASS(); +} + +void test_generate_select_edge_case_defaults_to_star() { + TEST(generate_select_edge_case_defaults_to_star); + SelectQuery q; + q.id = "q2"; + PostgreSQLGenerator gen; + std::string out = gen.generate(&q); + CHECK(out == "SELECT *;", "expected default select star"); + PASS(); +} + +int main() { + std::cout << "Step 412: PostgreSQL Generator Tests\n"; + + test_generate_create_table_with_columns(); // 1 + test_generate_select_distinct_join_where(); // 2 + test_generate_insert_statement(); // 3 + test_generate_update_statement(); // 4 + test_generate_delete_statement(); // 5 + test_generate_index_definition(); // 6 + test_generate_module_with_statements(); // 7 + test_generate_where_clause_passthrough(); // 8 + test_comment_prefix_and_semanno_output(); // 9 + test_pipeline_generate_routing_aliases(); // 10 + test_parser_to_generator_create_table_flow(); // 11 + test_generate_select_edge_case_defaults_to_star(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 221219a..778f354 100644 --- a/progress.md +++ b/progress.md @@ -3964,6 +3964,61 @@ and procedural patterns. - `editor/src/MCPServer.h` (`1679` > `600`) - `editor/src/HeadlessAgentRPCHandler.h` (`2623` > `600`) +### Step 412: PostgreSQL Generator +**Status:** PASS (12/12 tests) + +Added a dedicated PostgreSQL generator and end-to-end generation coverage over +the SQL AST nodes introduced in Step 410 and parsed in Step 411. + +**Files created:** +- `editor/src/ast/PostgreSQLGenerator.h` — PostgreSQL code generation support: + - SQL DDL/DML emitters for: + - `TableDeclaration` + - `ColumnDefinition` + - `SelectQuery` + - `InsertStatement` + - `UpdateStatement` + - `DeleteStatement` + - `JoinClause` + - `WhereClause` + - `IndexDefinition` + - module/function support for SQL-oriented output + - annotation output via Semanno mixin with PostgreSQL comment prefix (`-- `) +- `editor/tests/step412_test.cpp` — 12 tests covering: + 1. CREATE TABLE generation with columns + 2. SELECT DISTINCT + JOIN + WHERE generation + 3. INSERT generation with columns and values + 4. UPDATE generation with SET and WHERE + 5. DELETE generation + 6. UNIQUE INDEX generation + 7. module-level SQL statement emission + 8. WHERE clause passthrough behavior + 9. PostgreSQL comment prefix + semanno annotation output + 10. pipeline generate routing aliases (`postgresql`, `postgres`) + 11. parser-to-generator flow for CREATE TABLE + 12. edge case: empty SELECT defaults to `SELECT *;` + +**Files modified:** +- `editor/src/ast/ProjectionGenerator.h` — SQL visitor hooks and dispatch branches +- `editor/src/ast/Generator.h` — include `PostgreSQLGenerator.h` +- `editor/src/Pipeline.h` — generate routing for `postgresql`, `postgres` +- `editor/CMakeLists.txt` — `step412_test` target + +**Verification run:** +- `step412_test` — PASS (12/12) new step coverage +- `step411_test` — PASS (12/12) regression coverage +- `step410_test` — PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/ast/PostgreSQLGenerator.h` within header-size limit (`412` <= `600`) +- `editor/tests/step412_test.cpp` within test-file size guidance (`203` lines) +- `editor/src/Pipeline.h` within header-size limit (`254` <= `600`) +- `editor/src/ast/ProjectionGenerator.h` within header-size limit (`408` <= `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)