Skip to content

Commit 516ff38

Browse files
committed
Support curly braces, implicit subscripts, and subscript notation
- Curly braces as alternative to square brackets: σ_{cond}(R) - LaTeX-style underscore prefix: σ_{...} (underscore silently skipped) - Implicit subscripts without any delimiters: σ cond (R), π cols (R) - Line comments with -- are ignored - 14 new tests (96 total), all passing https://claude.ai/code/session_01TJyw8nESra9cc5RpVUpmt6
1 parent ead2eb7 commit 516ff38

2 files changed

Lines changed: 208 additions & 33 deletions

File tree

src/relationalAlgebra.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,3 +597,97 @@ describe("multi-line and assignments", () => {
597597
expect(norm(sql)).toContain("WHERE name = 'Peter'");
598598
});
599599
});
600+
601+
// ─── Alternative notation styles ────────────────────────────────────────────
602+
603+
describe("curly braces (LaTeX-style)", () => {
604+
it("should support σ_{condition}(R)", () => {
605+
expect(norm(raToSQL("σ_{age > 20}(Person)"))).toBe(
606+
norm("SELECT * FROM (Person) WHERE age > 20")
607+
);
608+
});
609+
610+
it("should support π_{cols}(R)", () => {
611+
expect(norm(raToSQL("π_{name, city}(Person)"))).toBe(
612+
norm("SELECT name, city FROM (Person)")
613+
);
614+
});
615+
616+
it("should support ρ_{old→new}(R)", () => {
617+
expect(norm(raToSQL("ρ_{name→fullName}(Person)"))).toBe(
618+
norm("SELECT name AS fullName FROM (Person)")
619+
);
620+
});
621+
622+
it("should support curly braces without underscore", () => {
623+
expect(norm(raToSQL("σ{age > 20}(Person)"))).toBe(
624+
norm("SELECT * FROM (Person) WHERE age > 20")
625+
);
626+
});
627+
628+
it("should support theta join with curly braces", () => {
629+
const sql = raToSQL("Person ⋈{Person.id = Student.id} Student");
630+
expect(norm(sql)).toContain("JOIN");
631+
expect(norm(sql)).toContain("ON Person.id = Student.id");
632+
});
633+
});
634+
635+
describe("implicit subscripts (no brackets)", () => {
636+
it("should support σ condition (R)", () => {
637+
expect(norm(raToSQL("σ age > 20 (Person)"))).toBe(
638+
norm("SELECT * FROM (Person) WHERE age > 20")
639+
);
640+
});
641+
642+
it("should support π cols (R)", () => {
643+
expect(norm(raToSQL("π name, city (Person)"))).toBe(
644+
norm("SELECT name, city FROM (Person)")
645+
);
646+
});
647+
648+
it("should support sigma condition (R) with keyword", () => {
649+
expect(norm(raToSQL("sigma age > 20 (Person)"))).toBe(
650+
norm("SELECT * FROM (Person) WHERE age > 20")
651+
);
652+
});
653+
654+
it("should support pi cols (R) with keyword", () => {
655+
expect(norm(raToSQL("pi name (Person)"))).toBe(
656+
norm("SELECT name FROM (Person)")
657+
);
658+
});
659+
660+
it("should support ρ old→new (R) implicit", () => {
661+
expect(norm(raToSQL("ρ name→fullName (Person)"))).toBe(
662+
norm("SELECT name AS fullName FROM (Person)")
663+
);
664+
});
665+
666+
it("should support τ col (R) implicit", () => {
667+
expect(norm(raToSQL("τ name (Person)"))).toContain("ORDER BY name");
668+
});
669+
670+
it("should support nested implicit subscripts", () => {
671+
const sql = raToSQL("π name (σ age > 20 (Person))");
672+
expect(norm(sql)).toContain("SELECT name FROM");
673+
expect(norm(sql)).toContain("WHERE age > 20");
674+
});
675+
676+
it("should support compound implicit condition with AND", () => {
677+
const sql = raToSQL("σ age > 20 and city = 'Stockholm' (Person)");
678+
expect(norm(sql)).toContain("WHERE");
679+
expect(norm(sql)).toContain("AND");
680+
expect(norm(sql)).toContain("age > 20");
681+
});
682+
683+
it("should work with multi-line and implicit subscripts", () => {
684+
const input = [
685+
"A <- σ age > 20 (Person)",
686+
"π name (A)",
687+
].join("\n");
688+
const sql = raToSQL(input);
689+
expect(norm(sql)).toContain("WITH");
690+
expect(norm(sql)).toContain("WHERE age > 20");
691+
expect(norm(sql)).toContain("SELECT name FROM");
692+
});
693+
});

src/relationalAlgebra.ts

Lines changed: 114 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,11 @@ function tokenize(input: string): Token[] {
176176
if (ch === ")") { tokens.push({ type: TokenType.RPAREN, value: ch, pos }); i++; continue; }
177177
if (ch === "[") { tokens.push({ type: TokenType.LBRACKET, value: ch, pos }); i++; continue; }
178178
if (ch === "]") { tokens.push({ type: TokenType.RBRACKET, value: ch, pos }); i++; continue; }
179+
// Curly braces as alternative to square brackets (LaTeX-style σ_{cond})
180+
if (ch === "{") { tokens.push({ type: TokenType.LBRACKET, value: ch, pos }); i++; continue; }
181+
if (ch === "}") { tokens.push({ type: TokenType.RBRACKET, value: ch, pos }); i++; continue; }
182+
// Underscore: skip silently (allows LaTeX-style σ_{cond} — the _ is just decoration)
183+
if (ch === "_") { i++; continue; }
179184
if (ch === ",") { tokens.push({ type: TokenType.COMMA, value: ch, pos }); i++; continue; }
180185
if (ch === ".") { tokens.push({ type: TokenType.DOT, value: ch, pos }); i++; continue; }
181186
if (ch === ";") { tokens.push({ type: TokenType.SEMICOLON, value: ch, pos }); i++; continue; }
@@ -531,60 +536,136 @@ class Parser {
531536
return left;
532537
}
533538

539+
/**
540+
* Check if the next token starts a subscript (bracket or implicit).
541+
* Returns true if it's `[`, `{`, or any non-`(` token that could begin
542+
* an implicit subscript (i.e., the subscript content before `(`).
543+
*/
544+
private hasSubscript(): boolean {
545+
return this.peek().type === TokenType.LBRACKET || this.peek().type !== TokenType.LPAREN;
546+
}
547+
548+
/**
549+
* Parse a subscript that may be:
550+
* 1. Bracketed: `[...]` or `{...}`
551+
* 2. Implicit: tokens up to the next unmatched `(`
552+
*
553+
* For implicit subscripts, we temporarily inject LBRACKET/RBRACKET
554+
* around the subscript tokens so the same parsing logic can be reused.
555+
*/
556+
private parseSubscriptCondition(): ConditionNode {
557+
if (this.peek().type === TokenType.LBRACKET) {
558+
return this.parseBracketedCondition();
559+
}
560+
// Implicit subscript: parse condition tokens until we hit LPAREN
561+
// We parse directly — the condition ends when we encounter LPAREN at depth 0
562+
const cond = this.parseOrCondition();
563+
return cond;
564+
}
565+
566+
private parseSubscriptColumns(): ColumnRef[] {
567+
if (this.peek().type === TokenType.LBRACKET) {
568+
this.expect(TokenType.LBRACKET);
569+
const cols = this.parseColumnList();
570+
this.expect(TokenType.RBRACKET);
571+
return cols;
572+
}
573+
// Implicit: parse column list until LPAREN
574+
return this.parseColumnList();
575+
}
576+
577+
private parseSubscriptRenameMappings(): RenameMapping[] {
578+
if (this.peek().type === TokenType.LBRACKET) {
579+
this.expect(TokenType.LBRACKET);
580+
const mappings = this.parseRenameMappings();
581+
this.expect(TokenType.RBRACKET);
582+
return mappings;
583+
}
584+
return this.parseRenameMappings();
585+
}
586+
587+
private parseSubscriptGroupSpec(): { groupBy: ColumnRef[]; aggregates: AggregateExpr[] } {
588+
if (this.peek().type === TokenType.LBRACKET) {
589+
this.expect(TokenType.LBRACKET);
590+
const spec = this.parseGroupSpec();
591+
this.expect(TokenType.RBRACKET);
592+
return spec;
593+
}
594+
return this.parseGroupSpec();
595+
}
596+
597+
private parseSubscriptSortColumns(): SortColumn[] {
598+
if (this.peek().type === TokenType.LBRACKET) {
599+
this.expect(TokenType.LBRACKET);
600+
const cols = this.parseSortColumns();
601+
this.expect(TokenType.RBRACKET);
602+
return cols;
603+
}
604+
return this.parseSortColumns();
605+
}
606+
534607
private parseUnaryExpr(): RANode {
535608
const t = this.peek().type;
536609

537610
if (t === TokenType.SIGMA) {
538611
this.advance();
539-
const condition = this.parseBracketedCondition();
540-
this.expect(TokenType.LPAREN);
541-
const relation = this.parseUnionExpr();
542-
this.expect(TokenType.RPAREN);
543-
return { type: "selection", condition, relation };
612+
if (this.hasSubscript()) {
613+
const condition = this.parseSubscriptCondition();
614+
this.expect(TokenType.LPAREN);
615+
const relation = this.parseUnionExpr();
616+
this.expect(TokenType.RPAREN);
617+
return { type: "selection", condition, relation };
618+
}
619+
// σ(R) with no subscript — error
620+
throw new RAError("Selection (σ) requires a condition — use σ[condition](R) or σ condition (R)");
544621
}
545622

546623
if (t === TokenType.PI) {
547624
this.advance();
548-
this.expect(TokenType.LBRACKET);
549-
const columns = this.parseColumnList();
550-
this.expect(TokenType.RBRACKET);
551-
this.expect(TokenType.LPAREN);
552-
const relation = this.parseUnionExpr();
553-
this.expect(TokenType.RPAREN);
554-
return { type: "projection", columns, relation };
625+
if (this.hasSubscript()) {
626+
const columns = this.parseSubscriptColumns();
627+
this.expect(TokenType.LPAREN);
628+
const relation = this.parseUnionExpr();
629+
this.expect(TokenType.RPAREN);
630+
return { type: "projection", columns, relation };
631+
}
632+
throw new RAError("Projection (π) requires column list — use π[cols](R) or π cols (R)");
555633
}
556634

557635
if (t === TokenType.RHO) {
558636
this.advance();
559-
this.expect(TokenType.LBRACKET);
560-
const mappings = this.parseRenameMappings();
561-
this.expect(TokenType.RBRACKET);
562-
this.expect(TokenType.LPAREN);
563-
const relation = this.parseUnionExpr();
564-
this.expect(TokenType.RPAREN);
565-
return { type: "rename", mappings, relation };
637+
if (this.hasSubscript()) {
638+
const mappings = this.parseSubscriptRenameMappings();
639+
this.expect(TokenType.LPAREN);
640+
const relation = this.parseUnionExpr();
641+
this.expect(TokenType.RPAREN);
642+
return { type: "rename", mappings, relation };
643+
}
644+
throw new RAError("Rename (ρ) requires mappings — use ρ[old→new](R) or ρ old→new (R)");
566645
}
567646

568647
if (t === TokenType.GAMMA) {
569648
this.advance();
570-
this.expect(TokenType.LBRACKET);
571-
const { groupBy, aggregates } = this.parseGroupSpec();
572-
this.expect(TokenType.RBRACKET);
573-
this.expect(TokenType.LPAREN);
574-
const relation = this.parseUnionExpr();
575-
this.expect(TokenType.RPAREN);
576-
return { type: "group", groupBy, aggregates, relation };
649+
if (this.hasSubscript()) {
650+
const { groupBy, aggregates } = this.parseSubscriptGroupSpec();
651+
this.expect(TokenType.LPAREN);
652+
const relation = this.parseUnionExpr();
653+
this.expect(TokenType.RPAREN);
654+
return { type: "group", groupBy, aggregates, relation };
655+
}
656+
throw new RAError("Grouping (γ) requires specification — use γ[groupCols; AGG(col)](R)");
577657
}
578658

579659
if (t === TokenType.TAU) {
580660
this.advance();
581-
this.expect(TokenType.LBRACKET);
582-
const columns = this.parseSortColumns();
583-
this.expect(TokenType.RBRACKET);
584-
this.expect(TokenType.LPAREN);
585-
const relation = this.parseUnionExpr();
586-
this.expect(TokenType.RPAREN);
587-
return { type: "sort", columns, relation };
661+
if (this.hasSubscript()) {
662+
const columns = this.parseSubscriptSortColumns();
663+
this.expect(TokenType.LPAREN);
664+
const relation = this.parseUnionExpr();
665+
this.expect(TokenType.RPAREN);
666+
return { type: "sort", columns, relation };
667+
}
668+
throw new RAError("Sort (τ) requires column list — use τ[col](R) or τ col (R)");
588669
}
589670

590671
if (t === TokenType.DELTA) {

0 commit comments

Comments
 (0)