Step 415: add SQL annotation mapping heuristics

This commit is contained in:
Bill
2026-02-16 16:00:27 -07:00
parent 439a8a5838
commit 79767743c6
4 changed files with 531 additions and 0 deletions

View File

@@ -2614,4 +2614,13 @@ target_link_libraries(step414_test PRIVATE
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go)
add_executable(step415_test tests/step415_test.cpp)
target_include_directories(step415_test PRIVATE src)
target_link_libraries(step415_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

@@ -0,0 +1,246 @@
#pragma once
#include "ast/ASTNode.h"
#include "ast/Module.h"
#include "ast/ClassDeclaration.h"
#include "ast/Variable.h"
#include "ast/SqlNodes.h"
#include "ast/Annotation.h"
#include "ast/Expression.h"
#include <algorithm>
#include <cctype>
#include <string>
#include <unordered_set>
#include <vector>
class SqlAnnotationMapper {
public:
struct InferredAnnotation {
std::string nodeId;
std::string annotationType;
std::string key;
std::string value;
std::string reason;
double confidence = 0.0;
};
std::vector<InferredAnnotation> inferForModule(const Module* module) const {
std::vector<InferredAnnotation> out;
if (!module) return out;
for (const auto* stmt : module->getChildren("statements")) {
inferForNode(stmt, out);
}
return out;
}
std::vector<InferredAnnotation> inferForSqlText(
const std::string& sqlText,
const std::string& nodeId = "sql_text") const {
std::vector<InferredAnnotation> out;
std::string lower = toLower(sqlText);
if (containsPhrase(lower, "drop table")) {
out.push_back({nodeId, "RiskAnnotation", "level", "high",
"DROP TABLE is destructive and high-risk", 0.95});
}
if (containsPhrase(lower, "delete from") && !containsPhrase(lower, " where ")) {
out.push_back({nodeId, "RiskAnnotation", "level", "high",
"DELETE without WHERE can remove all rows", 0.95});
}
if (containsPhrase(lower, " limit ") || containsPhrase(lower, " top ")) {
out.push_back({nodeId, "BoundsCheckAnnotation", "enabled", "true",
"LIMIT/TOP bounds result set size", 0.85});
}
return out;
}
std::vector<InferredAnnotation> inferCrossStackOrmMapping(
const ClassDeclaration* ormClass,
const TableDeclaration* table) const {
std::vector<InferredAnnotation> out;
if (!ormClass || !table) return out;
auto classFields = collectClassFields(ormClass);
auto tableColumns = collectTableColumns(table);
const int matched = countIntersection(classFields, tableColumns);
const int denom = std::max(1, std::max((int)classFields.size(), (int)tableColumns.size()));
out.push_back({ormClass->id, "ContractAnnotation", "preconditions",
"sql table " + table->name + " exists", "ORM class depends on backing SQL table", 0.88});
out.push_back({table->id, "ContractAnnotation", "preconditions",
"orm class " + ormClass->name + " stays schema-compatible",
"SQL table participates in ORM mapping contract", 0.84});
if (matched * 2 < denom) {
out.push_back({ormClass->id, "RiskAnnotation", "level", "medium",
"ORM fields diverge from SQL columns; migration risk", 0.82});
}
return out;
}
void applyInferredAnnotations(
ASTNode* root,
const std::vector<InferredAnnotation>& inferred) const {
if (!root) return;
for (const auto& inf : inferred) {
ASTNode* node = findNodeById(root, inf.nodeId);
if (!node) continue;
Annotation* anno = createAnnotationNode(inf);
if (!anno) continue;
if (anno->id.empty()) anno->id = "sql_inferred_" + inf.nodeId + "_" + inf.annotationType;
node->addChild("annotations", anno);
}
}
private:
void inferForNode(const ASTNode* node, std::vector<InferredAnnotation>& out) const {
if (!node) return;
if (node->conceptType == "DeleteStatement") {
auto* del = static_cast<const DeleteStatement*>(node);
if (del->getChild("where") == nullptr) {
out.push_back({node->id, "RiskAnnotation", "level", "high",
"DELETE without WHERE can remove all rows", 0.95});
}
} else if (node->conceptType == "UpdateStatement") {
auto* up = static_cast<const UpdateStatement*>(node);
if (up->getChild("where") == nullptr) {
out.push_back({node->id, "RiskAnnotation", "level", "medium",
"UPDATE without WHERE can overwrite all rows", 0.86});
}
} else if (node->conceptType == "SelectQuery") {
auto* q = static_cast<const SelectQuery*>(node);
const int joinDepth = (int)q->getChildren("joins").size();
const int subqueries = countSubqueryHints(q);
out.push_back({node->id, "ComplexityAnnotation", "timeComplexity",
complexityFromQuery(joinDepth, subqueries),
"JOIN depth and subquery nesting estimated from query structure",
confidenceFromComplexity(joinDepth, subqueries)});
if (hasBoundsClause(q)) {
out.push_back({node->id, "BoundsCheckAnnotation", "enabled", "true",
"LIMIT/TOP bounds result set size", 0.86});
}
if (dependsOnIndexes(q)) {
out.push_back({node->id, "ContractAnnotation", "preconditions",
"index exists on predicate/join columns",
"Query shape indicates index dependency for expected performance",
0.78});
}
}
}
static bool hasBoundsClause(const SelectQuery* q) {
return q->getChild("limit") != nullptr || q->getChild("top") != nullptr;
}
static bool dependsOnIndexes(const SelectQuery* q) {
return !q->getChildren("joins").empty() || q->getChild("where") != nullptr;
}
static int countSubqueryHints(const SelectQuery* q) {
int hints = 0;
auto* where = q->getChild("where");
if (where && where->conceptType == "WhereClause") {
const auto* w = static_cast<const WhereClause*>(where);
if (containsPhrase(toLower(w->expression), "(select")) hints++;
}
auto* from = q->getChild("from");
if (from && from->conceptType == "TableDeclaration") {
const auto* t = static_cast<const TableDeclaration*>(from);
if (containsPhrase(toLower(t->name), "(select")) hints++;
}
for (const auto* j : q->getChildren("joins")) {
auto* jc = static_cast<const JoinClause*>(j);
if (containsPhrase(toLower(jc->tableName), "(select")) hints++;
}
return hints;
}
static std::string complexityFromQuery(int joinDepth, int subqueries) {
if (joinDepth >= 3 || subqueries >= 2) return "O(n^3)";
if (joinDepth >= 2 || subqueries >= 1) return "O(n^2)";
if (joinDepth >= 1) return "O(n log n)";
return "O(n)";
}
static double confidenceFromComplexity(int joinDepth, int subqueries) {
return std::min(0.95, 0.55 + 0.1 * joinDepth + 0.15 * subqueries);
}
static std::unordered_set<std::string> collectClassFields(const ClassDeclaration* cls) {
std::unordered_set<std::string> out;
for (const auto* f : cls->getChildren("fields")) {
if (f->conceptType != "Variable") continue;
out.insert(toLower(static_cast<const Variable*>(f)->name));
}
return out;
}
static std::unordered_set<std::string> collectTableColumns(const TableDeclaration* table) {
std::unordered_set<std::string> out;
for (const auto* c : table->getChildren("columns")) {
if (c->conceptType != "ColumnDefinition") continue;
out.insert(toLower(static_cast<const ColumnDefinition*>(c)->name));
}
return out;
}
static int countIntersection(const std::unordered_set<std::string>& a,
const std::unordered_set<std::string>& b) {
int count = 0;
for (const auto& v : a) if (b.count(v)) ++count;
return count;
}
static Annotation* createAnnotationNode(const InferredAnnotation& inf) {
if (inf.annotationType == "RiskAnnotation") {
auto* a = new RiskAnnotation();
a->level = inf.value;
a->reason = inf.reason;
return a;
}
if (inf.annotationType == "ComplexityAnnotation") {
auto* a = new ComplexityAnnotation();
a->timeComplexity = inf.value;
return a;
}
if (inf.annotationType == "BoundsCheckAnnotation") {
auto* a = new BoundsCheckAnnotation();
a->enabled = (toLower(inf.value) != "false");
return a;
}
if (inf.annotationType == "ContractAnnotation") {
auto* a = new ContractAnnotation();
if (inf.key == "preconditions") a->preconditions = inf.value;
else a->preconditions = inf.key + "=" + inf.value;
return a;
}
return nullptr;
}
static ASTNode* findNodeById(ASTNode* node, const std::string& id) {
if (!node) return nullptr;
if (node->id == id) return node;
for (auto* c : node->allChildren()) {
if (auto* hit = findNodeById(c, id)) return hit;
}
return nullptr;
}
static bool containsPhrase(const std::string& text, const std::string& phrase) {
return text.find(phrase) != std::string::npos;
}
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;
}
};

View File

@@ -0,0 +1,230 @@
// Step 415: SQL Annotation Mapping Tests (12 tests)
#include "SqlAnnotationMapper.h"
#include "ast/Module.h"
#include "ast/SqlNodes.h"
#include "ast/ClassDeclaration.h"
#include "ast/Variable.h"
#include "ast/Annotation.h"
#include <iostream>
#include <string>
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 {}
static bool hasInferred(const std::vector<SqlAnnotationMapper::InferredAnnotation>& v,
const std::string& nodeId,
const std::string& annoType,
const std::string& valueContains = "") {
for (const auto& a : v) {
if (a.nodeId == nodeId && a.annotationType == annoType) {
if (valueContains.empty()) return true;
if (a.value.find(valueContains) != std::string::npos ||
a.reason.find(valueContains) != std::string::npos) return true;
}
}
return false;
}
void test_risk_high_delete_without_where() {
TEST(risk_high_delete_without_where);
Module mod;
mod.id = "m1";
auto* d = new DeleteStatement("del1", "users");
mod.addChild("statements", d);
SqlAnnotationMapper mapper;
auto inf = mapper.inferForModule(&mod);
CHECK(hasInferred(inf, "del1", "RiskAnnotation", "high"), "expected high risk");
PASS();
}
void test_no_high_risk_delete_with_where() {
TEST(no_high_risk_delete_with_where);
Module mod;
mod.id = "m1";
auto* d = new DeleteStatement("del1", "users");
d->setChild("where", new WhereClause("w1", "id = 1"));
mod.addChild("statements", d);
SqlAnnotationMapper mapper;
auto inf = mapper.inferForModule(&mod);
CHECK(!hasInferred(inf, "del1", "RiskAnnotation", "high"), "should not flag high risk");
PASS();
}
void test_risk_high_drop_table_text() {
TEST(risk_high_drop_table_text);
SqlAnnotationMapper mapper;
auto inf = mapper.inferForSqlText("DROP TABLE users;");
CHECK(hasInferred(inf, "sql_text", "RiskAnnotation", "high"), "expected drop table high risk");
PASS();
}
void test_complexity_from_join_depth() {
TEST(complexity_from_join_depth);
Module mod;
mod.id = "m1";
auto* q = new SelectQuery();
q->id = "q1";
q->addChild("joins", new JoinClause("j1", "LEFT", "orders"));
q->addChild("joins", new JoinClause("j2", "LEFT", "profiles"));
mod.addChild("statements", q);
SqlAnnotationMapper mapper;
auto inf = mapper.inferForModule(&mod);
CHECK(hasInferred(inf, "q1", "ComplexityAnnotation", "O(n^2)"),
"expected O(n^2) for multi-join query");
PASS();
}
void test_complexity_from_subquery_hint() {
TEST(complexity_from_subquery_hint);
Module mod;
mod.id = "m1";
auto* q = new SelectQuery();
q->id = "q1";
q->setChild("where", new WhereClause("w1", "id IN (SELECT user_id FROM orders)"));
mod.addChild("statements", q);
SqlAnnotationMapper mapper;
auto inf = mapper.inferForModule(&mod);
CHECK(hasInferred(inf, "q1", "ComplexityAnnotation", "O(n^2)"),
"expected O(n^2) for subquery");
PASS();
}
void test_boundscheck_from_limit_node() {
TEST(boundscheck_from_limit_node);
Module mod;
mod.id = "m1";
auto* q = new SelectQuery();
q->id = "q1";
q->setChild("limit", new IntegerLiteral("i1", 10));
mod.addChild("statements", q);
SqlAnnotationMapper mapper;
auto inf = mapper.inferForModule(&mod);
CHECK(hasInferred(inf, "q1", "BoundsCheckAnnotation"), "expected bounds check");
PASS();
}
void test_boundscheck_from_top_node() {
TEST(boundscheck_from_top_node);
Module mod;
mod.id = "m1";
auto* q = new SelectQuery();
q->id = "q1";
q->setChild("top", new IntegerLiteral("i1", 5));
mod.addChild("statements", q);
SqlAnnotationMapper mapper;
auto inf = mapper.inferForModule(&mod);
CHECK(hasInferred(inf, "q1", "BoundsCheckAnnotation"), "expected bounds check");
PASS();
}
void test_no_boundscheck_without_limit_or_top() {
TEST(no_boundscheck_without_limit_or_top);
Module mod;
mod.id = "m1";
auto* q = new SelectQuery();
q->id = "q1";
mod.addChild("statements", q);
SqlAnnotationMapper mapper;
auto inf = mapper.inferForModule(&mod);
CHECK(!hasInferred(inf, "q1", "BoundsCheckAnnotation"), "unexpected bounds check");
PASS();
}
void test_contract_for_index_dependent_query() {
TEST(contract_for_index_dependent_query);
Module mod;
mod.id = "m1";
auto* q = new SelectQuery();
q->id = "q1";
q->setChild("where", new WhereClause("w1", "email = 'x'"));
mod.addChild("statements", q);
SqlAnnotationMapper mapper;
auto inf = mapper.inferForModule(&mod);
CHECK(hasInferred(inf, "q1", "ContractAnnotation", "index exists"),
"expected index precondition contract");
PASS();
}
void test_cross_stack_orm_to_sql_contract() {
TEST(cross_stack_orm_to_sql_contract);
ClassDeclaration cls("c1", "User");
cls.addChild("fields", new Variable("f1", "id"));
cls.addChild("fields", new Variable("f2", "email"));
TableDeclaration table("t1", "users");
table.addChild("columns", new ColumnDefinition("col1", "id", "INT"));
table.addChild("columns", new ColumnDefinition("col2", "email", "TEXT"));
SqlAnnotationMapper mapper;
auto inf = mapper.inferCrossStackOrmMapping(&cls, &table);
CHECK(hasInferred(inf, "c1", "ContractAnnotation", "sql table users exists"),
"expected class contract");
CHECK(hasInferred(inf, "t1", "ContractAnnotation", "orm class User"),
"expected table contract");
PASS();
}
void test_cross_stack_mismatch_risk() {
TEST(cross_stack_mismatch_risk);
ClassDeclaration cls("c1", "Account");
cls.addChild("fields", new Variable("f1", "name"));
cls.addChild("fields", new Variable("f2", "createdAt"));
TableDeclaration table("t1", "accounts");
table.addChild("columns", new ColumnDefinition("col1", "id", "INT"));
table.addChild("columns", new ColumnDefinition("col2", "email", "TEXT"));
SqlAnnotationMapper mapper;
auto inf = mapper.inferCrossStackOrmMapping(&cls, &table);
CHECK(hasInferred(inf, "c1", "RiskAnnotation", "medium"), "expected mismatch risk");
PASS();
}
void test_apply_inferred_annotations_to_ast() {
TEST(apply_inferred_annotations_to_ast);
Module mod;
mod.id = "m1";
auto* q = new SelectQuery();
q->id = "q1";
q->setChild("limit", new IntegerLiteral("i1", 10));
mod.addChild("statements", q);
SqlAnnotationMapper mapper;
auto inf = mapper.inferForModule(&mod);
mapper.applyInferredAnnotations(&mod, inf);
auto annos = q->getChildren("annotations");
bool hasBounds = false;
bool hasComplex = false;
for (auto* a : annos) {
if (a->conceptType == "BoundsCheckAnnotation") hasBounds = true;
if (a->conceptType == "ComplexityAnnotation") hasComplex = true;
}
CHECK(hasBounds, "expected applied BoundsCheckAnnotation");
CHECK(hasComplex, "expected applied ComplexityAnnotation");
PASS();
}
int main() {
std::cout << "Step 415: SQL Annotation Mapping Tests\n";
test_risk_high_delete_without_where(); // 1
test_no_high_risk_delete_with_where(); // 2
test_risk_high_drop_table_text(); // 3
test_complexity_from_join_depth(); // 4
test_complexity_from_subquery_hint(); // 5
test_boundscheck_from_limit_node(); // 6
test_boundscheck_from_top_node(); // 7
test_no_boundscheck_without_limit_or_top(); // 8
test_contract_for_index_dependent_query(); // 9
test_cross_stack_orm_to_sql_contract(); // 10
test_cross_stack_mismatch_risk(); // 11
test_apply_inferred_annotations_to_ast(); // 12
std::cout << "\nResults: " << passed << "/" << (passed + failed)
<< " passed\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -4130,6 +4130,52 @@ dialect-specific features (`AUTO_INCREMENT`, backtick identifiers,
- `editor/src/MCPServer.h` (`1679` > `600`)
- `editor/src/HeadlessAgentRPCHandler.h` (`2629` > `600`)
### Step 415: SQL Annotation Mapping
**Status:** PASS (12/12 tests)
Added SQL-specific semantic annotation mapping focused on risk, complexity,
bounds checks, index contracts, and cross-stack ORM<->schema consistency.
**Files created:**
- `editor/src/SqlAnnotationMapper.h` — SQL annotation inference/mapping support:
- module-level AST inference for:
- `RiskAnnotation` on destructive SQL patterns (DELETE without WHERE, UPDATE without WHERE)
- `ComplexityAnnotation` from JOIN depth + subquery hints
- `BoundsCheckAnnotation` from `LIMIT`/`TOP` clauses
- `ContractAnnotation` precondition for index-dependent queries
- text-level SQL inference for `DROP TABLE` and bounds clause patterns
- cross-stack mapping for Python ORM class fields vs SQL table columns
- inferred-annotation application helper to attach generated annotations to AST nodes
- `editor/tests/step415_test.cpp` — 12 tests covering:
1. high risk on DELETE without WHERE
2. no high-risk false positive on DELETE with WHERE
3. high risk on DROP TABLE text
4. complexity from multi-join depth
5. complexity from subquery nesting hint
6. bounds check inference from LIMIT
7. bounds check inference from TOP
8. no bounds-check false positive without LIMIT/TOP
9. index precondition contract inference
10. cross-stack ORM<->SQL contract mapping
11. cross-stack schema mismatch risk mapping
12. inferred-annotation application to AST nodes
**Files modified:**
- `editor/CMakeLists.txt``step415_test` target
**Verification run:**
- `step415_test` — PASS (12/12) new step coverage
- `step414_test` — PASS (12/12) regression coverage
- `step413_test` — PASS (12/12) regression coverage
**Architecture gate check:**
- `editor/src/SqlAnnotationMapper.h` within header-size limit (`246` <= `600`)
- `editor/tests/step415_test.cpp` within test-file size guidance (`230` lines)
- 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)