Skip to content

Commit 29b5d9a

Browse files
joaoh82claude
andauthored
fix(sql): validate table qualifiers in single-table scope (SQLR-14) (#167)
SingleTableScope now carries the user-visible scope name (FROM alias when declared, else the table name) and rejects any other qualifier with the same unknown-table-qualifier error JoinedScope produces. Validation covers the projection list, WHERE (including the index-probe shape, which previously stripped the qualifier via parts.last()), ORDER BY, GROUP BY keys, aggregate args, and UPDATE SET right-hand sides. UPDATE / DELETE now plumb their FROM alias through extract_table_name, so alias forms validate too — and per SQLite semantics a declared alias shadows the table name as a qualifier. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent b8617d2 commit 29b5d9a

4 files changed

Lines changed: 322 additions & 83 deletions

File tree

docs/sql-engine.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,11 @@ match query {
7878

7979
### The expression evaluator
8080

81-
`eval_expr(expr: &Expr, table: &Table, rowid: i64) -> Result<Value>` walks a `sqlparser::Expr` and produces a runtime [`Value`](storage-model.md#runtime-value-vs-storage-row). It's a straightforward recursive match:
81+
`eval_expr(expr: &Expr, table: &Table, rowid: i64, scope_name: &str) -> Result<Value>` walks a `sqlparser::Expr` and produces a runtime [`Value`](storage-model.md#runtime-value-vs-storage-row). It's a straightforward recursive match:
8282

8383
- `Expr::Nested(inner)` → recurse
8484
- `Expr::Identifier(ident)` → look up `ident.value` on the table at the given rowid
85-
- `Expr::CompoundIdentifier(parts)` → same, using the last component (we ignore qualifiers because there's only one table in scope)
85+
- `Expr::CompoundIdentifier(parts)` → same, after validating the qualifier against the one table in scope — `scope_name` is the FROM alias when declared, else the table name, and anything else errors with `unknown table qualifier` (SQLR-14, matching the joined scope's behavior)
8686
- `Expr::Value(v)` → convert a sqlparser literal to a runtime `Value`
8787
- `Expr::UnaryOp { op, expr }` → recurse on inner, apply `+` / `-` / `NOT`
8888
- `Expr::BinaryOp { left, op, right }` → recurse on both sides, apply the operator

docs/supported-sql.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ COUNT([DISTINCT] <column>) -- counts non-NULL values, option
202202
### What works
203203

204204
- **Projection**: `*` (all columns in declaration order), a bare column list, or an explicit list mixing bare columns and aggregate calls. Each item can carry an optional `AS alias` (the alias becomes the output column header and is recognized by `ORDER BY`).
205+
- **Qualified column references** (SQLR-14): `t.col` works in the projection, `WHERE`, `GROUP BY`, `ORDER BY`, and aggregate arguments, but the qualifier must name the table in scope — the `FROM` alias when one is declared, else the table name (ASCII case-insensitive). Anything else errors with `unknown table qualifier`, matching the joined-scope behavior and SQLite. Declaring an alias removes the table name from scope (`SELECT t.id FROM t AS a` errors), and the same validation applies to `UPDATE` / `DELETE` (including alias forms `UPDATE t AS a …` / `DELETE FROM t AS a …`).
205206
- **`WHERE`**: any [expression](#expressions). Evaluated per row; NULL-as-false in WHERE context (three-valued logic collapsed to two-valued for filtering). Includes **`IS NULL`** / **`IS NOT NULL`** for explicit null tests, **`LIKE` / `NOT LIKE` / `ILIKE`** for pattern matching, and **`IN (list) / NOT IN (list)`** for set-membership against literal lists.
206207
- **`DISTINCT`**: `SELECT DISTINCT` deduplicates result rows after projection (and after aggregation, when both apply). `NULL` values compare equal to other `NULL`s for dedupe, matching SQL's DISTINCT semantic.
207208
- **`GROUP BY`**: one or more column names, optionally qualified (`GROUP BY customers.name`) — the qualifier disambiguates same-named columns across joined tables (SQLR-6). Every non-aggregate item in the projection must appear in the `GROUP BY` list (rejected with a clear message — by the parser for single-table queries, by the executor for joined ones, where resolving qualifiers needs the schemas). `GROUP BY <col>` without any aggregate behaves like an implicit `DISTINCT <col>`.

0 commit comments

Comments
 (0)