Skip to content

Commit 805f222

Browse files
committed
Fix CTE reassignment, self-ref, table validation, reference title
- Variable reassignment (A <- ...; A <- ...) now generates versioned CTE names (A, A_v2, A_v3) with references rewritten via AST transform - Self-referential A <- A is a no-op (skipped in CTE generation) - resolveColumns errors now propagate as RAError (e.g. "Table 'X' does not exist") instead of silently returning empty columns - Reference dialog title: "Relational Algebra Reference" - 125 tests passing https://claude.ai/code/session_01TJyw8nESra9cc5RpVUpmt6
1 parent 5545e05 commit 805f222

3 files changed

Lines changed: 104 additions & 12 deletions

File tree

src/i18n/ui-strings.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export const uiStrings: Record<string, Record<string, string>> = {
8181
generatedSQL: "Genererad SQL",
8282
raParseError: "Relationsalgebra-fel: {{message}}",
8383
raPlaceholder: "-- Skriv ett relationsalgebrauttryck\n-- Klicka på 'RA-referens' för syntax",
84-
raReference: "RA-referens",
84+
raReference: "Relationsalgebrareferens",
8585
raUnaryOps: "Unära operatorer",
8686
raBinaryOps: "Binära operatorer",
8787
raColOp: "Operator",
@@ -189,7 +189,7 @@ export const uiStrings: Record<string, Record<string, string>> = {
189189
generatedSQL: "Generated SQL",
190190
raParseError: "Relational algebra error: {{message}}",
191191
raPlaceholder: "-- Write a relational algebra expression\n-- Click 'RA Reference' for syntax",
192-
raReference: "RA Reference",
192+
raReference: "Relational Algebra Reference",
193193
raUnaryOps: "Unary Operators",
194194
raBinaryOps: "Binary Operators",
195195
raColOp: "Operator",

src/relationalAlgebra.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,4 +756,24 @@ describe("implicit return from last assignment", () => {
756756
const sql = raToSQL("A <- σ[age > 20](Person)\nA");
757757
expect(norm(sql)).toContain("SELECT * FROM A");
758758
});
759+
760+
it("should handle self-referential A <- A as a no-op", () => {
761+
const sql = raToSQL("A <- A");
762+
expect(norm(sql)).toBe(norm("SELECT * FROM A"));
763+
});
764+
765+
it("should handle variable reassignment with versioned CTE names", () => {
766+
const sql = raToSQL("A <- σ[age > 20](Person)\nA <- π[name](A)\nA");
767+
expect(norm(sql)).toContain("WITH A AS");
768+
expect(norm(sql)).toContain("A_v2 AS");
769+
expect(norm(sql)).toContain("SELECT * FROM A_v2");
770+
});
771+
772+
it("should handle triple reassignment", () => {
773+
const sql = raToSQL("X <- Person\nX <- σ[age > 20](X)\nX <- π[name](X)");
774+
expect(norm(sql)).toContain("X AS");
775+
expect(norm(sql)).toContain("X_v2 AS");
776+
expect(norm(sql)).toContain("X_v3 AS");
777+
expect(norm(sql)).toContain("SELECT * FROM X_v3");
778+
});
759779
});

src/relationalAlgebra.ts

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,8 +1029,15 @@ function resolveColumns(sql: string, db: DatabaseHandle): string[] {
10291029
const cols = stmt.getColumnNames();
10301030
stmt.free();
10311031
return cols;
1032-
} catch {
1033-
return [];
1032+
} catch (e) {
1033+
// Re-throw table-not-found errors as RAError for better messaging
1034+
const msg = (e as Error).message || String(e);
1035+
if (/no such table/i.test(msg)) {
1036+
const match = msg.match(/no such table:\s*(\S+)/i);
1037+
throw new RAError(match ? `Table '${match[1]}' does not exist` : msg);
1038+
}
1039+
// For other errors (e.g. ambiguous column), let them propagate
1040+
throw new RAError(msg);
10341041
}
10351042
}
10361043
function nodeToSQL(node: RANode, db?: DatabaseHandle): string {
@@ -1148,23 +1155,88 @@ function nodeToSQL(node: RANode, db?: DatabaseHandle): string {
11481155
}
11491156
}
11501157

1158+
/**
1159+
* Rewrite table references in an AST node using a name mapping.
1160+
* Used to handle variable reassignment (A <- ...; A <- ...) by pointing
1161+
* references to the correct versioned CTE name.
1162+
*/
1163+
function rewriteTableRefs(node: RANode, nameMap: Record<string, string>): RANode {
1164+
switch (node.type) {
1165+
case "table":
1166+
return nameMap[node.name] ? { type: "table", name: nameMap[node.name] } : node;
1167+
case "selection":
1168+
return { ...node, relation: rewriteTableRefs(node.relation, nameMap) };
1169+
case "projection":
1170+
return { ...node, relation: rewriteTableRefs(node.relation, nameMap) };
1171+
case "rename":
1172+
return { ...node, relation: rewriteTableRefs(node.relation, nameMap) };
1173+
case "group":
1174+
return { ...node, relation: rewriteTableRefs(node.relation, nameMap) };
1175+
case "sort":
1176+
return { ...node, relation: rewriteTableRefs(node.relation, nameMap) };
1177+
case "distinct":
1178+
return { ...node, relation: rewriteTableRefs(node.relation, nameMap) };
1179+
case "crossProduct":
1180+
case "naturalJoin":
1181+
case "union":
1182+
case "intersect":
1183+
case "difference":
1184+
case "division":
1185+
case "leftSemiJoin":
1186+
case "rightSemiJoin":
1187+
case "antiJoin":
1188+
return { ...node, left: rewriteTableRefs(node.left, nameMap), right: rewriteTableRefs(node.right, nameMap) };
1189+
case "thetaJoin":
1190+
case "leftJoin":
1191+
case "rightJoin":
1192+
case "fullJoin":
1193+
return { ...node, left: rewriteTableRefs(node.left, nameMap), right: rewriteTableRefs(node.right, nameMap) };
1194+
default:
1195+
return node;
1196+
}
1197+
}
1198+
11511199
function programToSQL(program: RAProgram, db?: DatabaseHandle): string {
11521200
if (program.assignments.length === 0) {
11531201
return nodeToSQL(program.result, db);
11541202
}
11551203

1156-
// Use CTEs (WITH clauses) for assignments
1157-
const ctes = program.assignments.map(a => {
1158-
const sql = nodeToSQL(a.expr, db);
1159-
// Wrap non-table expressions in SELECT * FROM (...) for CTE compatibility
1204+
// Use CTEs (WITH clauses) for assignments.
1205+
// Handle reassignment (A <- ..., A <- ...) by versioning CTE names
1206+
// and rewriting references in subsequent expressions.
1207+
const ctes: string[] = [];
1208+
// Maps variable name -> current CTE name (may be versioned like A_v2)
1209+
const nameMap: Record<string, string> = {};
1210+
// Track how many times each name has been assigned
1211+
const assignCount: Record<string, number> = {};
1212+
1213+
for (const a of program.assignments) {
1214+
if (a.expr.type === "table" && a.expr.name === a.name && !nameMap[a.name]) {
1215+
// Self-referential assignment (A <- A) where A is a real table — skip, it's a no-op
1216+
continue;
1217+
}
1218+
1219+
// Rewrite the expression: replace table references with their current CTE aliases
1220+
const rewrittenExpr = rewriteTableRefs(a.expr, nameMap);
1221+
const sql = nodeToSQL(rewrittenExpr, db);
11601222
const wrappedSQL = /^\w+$/.test(sql) ? `SELECT * FROM ${sql}` : sql;
1161-
return `${a.name} AS (${wrappedSQL})`;
1162-
});
11631223

1164-
const resultSQL = nodeToSQL(program.result, db);
1165-
// Wrap bare table reference in SELECT for the final expression
1224+
// Determine the CTE name for this assignment
1225+
assignCount[a.name] = (assignCount[a.name] || 0) + 1;
1226+
const cteName = assignCount[a.name] > 1 ? `${a.name}_v${assignCount[a.name]}` : a.name;
1227+
nameMap[a.name] = cteName;
1228+
1229+
ctes.push(`${cteName} AS (${wrappedSQL})`);
1230+
}
1231+
1232+
// Rewrite the result expression with final name mappings
1233+
const rewrittenResult = rewriteTableRefs(program.result, nameMap);
1234+
const resultSQL = nodeToSQL(rewrittenResult, db);
11661235
const wrappedResult = /^\w+$/.test(resultSQL) ? `SELECT * FROM ${resultSQL}` : resultSQL;
11671236

1237+
if (ctes.length === 0) {
1238+
return wrappedResult;
1239+
}
11681240
return `WITH ${ctes.join(", ")} ${wrappedResult}`;
11691241
}
11701242

0 commit comments

Comments
 (0)