Skip to content

Commit d9543d9

Browse files
committed
docs: update SQL_SPEC.md, CHANGELOG.md for JOIN support; bump to v0.4.0
1 parent e3648a0 commit d9543d9

3 files changed

Lines changed: 77 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,26 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.4.0] - 2026-04-13
6+
7+
### Added
8+
- Multi-sheet JOIN support: INNER JOIN and LEFT JOIN with alias syntax
9+
- JOIN parser: qualified column references (`a.id`), ON condition parsing, alias support
10+
- JOIN executor: hash join algorithm with namespaced row flattening
11+
- LEFT JOIN: NULL fill for unmatched right-table rows
12+
- WHERE, ORDER BY, LIMIT, OFFSET work with JOIN queries
13+
- 34 new JOIN tests (20 parser, 9 executor, 5 boundary)
14+
- SQL_SPEC.md updated with JOIN syntax, grammar, and constraints
15+
16+
### Changed
17+
- Parser now always emits `from` and `joins` keys in SELECT AST
18+
- Test count: 397 → 506
19+
- Version bumped to 0.4.0
20+
21+
### Rejected (by design)
22+
- RIGHT JOIN, FULL OUTER JOIN, CROSS JOIN, multiple JOINs
23+
- SELECT *, GROUP BY, HAVING, aggregates in JOIN queries
24+
525
## [0.3.0] - 2026-04-12
626

727
### Added

docs/SQL_SPEC.md

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# excel-dbapi SQL Specification
22

3-
> Version: 0.2
3+
> Version: 0.4
44
> Status: **Normative** — this document defines the SQL subset that excel-dbapi supports.
5-
> Last updated: 2026-04-12
5+
> Last updated: 2026-04-13
66
77
---
88

@@ -16,7 +16,7 @@ database engine.
1616

1717
| Statement | Supported |
1818
|-----------|-----------|
19-
| `SELECT` | ✅ Single-table, with DISTINCT / WHERE / GROUP BY / HAVING / ORDER BY / LIMIT / OFFSET / Aggregates |
19+
| `SELECT` | ✅ Single-table with DISTINCT / WHERE / GROUP BY / HAVING / ORDER BY / LIMIT / OFFSET / Aggregates; INNER/LEFT JOIN on two tables |
2020
| `INSERT` | ✅ Single-row with optional column list |
2121
| `UPDATE` | ✅ With SET assignments and optional WHERE |
2222
| `DELETE` | ✅ With optional WHERE |
@@ -27,7 +27,10 @@ database engine.
2727

2828
The following SQL features are **rejected at parse time** with `ValueError`:
2929

30-
- `JOIN` (any variant: INNER, LEFT, RIGHT, CROSS, NATURAL)
30+
- `RIGHT JOIN`, `FULL OUTER JOIN`, `CROSS JOIN`, `NATURAL JOIN` (only INNER and LEFT JOIN supported)
31+
- Multiple JOINs in one query (max one JOIN clause)
32+
- `SELECT *` with JOIN (columns must be explicitly listed with table qualifiers)
33+
- `GROUP BY`, `HAVING`, aggregates in JOIN queries
3134
- Subqueries except `WHERE col IN (SELECT single_col FROM table [WHERE ...])`
3235
- Common Table Expressions (CTEs / `WITH`)
3336
- `UNION` / `INTERSECT` / `EXCEPT`
@@ -94,6 +97,8 @@ The SQL string is tokenized with the following rules:
9497

9598
### 3.1 Syntax
9699

100+
#### Single-table
101+
97102
```
98103
SELECT [DISTINCT] columns FROM table
99104
[WHERE conditions]
@@ -104,6 +109,17 @@ SELECT [DISTINCT] columns FROM table
104109
[OFFSET n]
105110
```
106111

112+
#### JOIN (two tables)
113+
114+
```
115+
SELECT qualified_columns FROM table [alias]
116+
[INNER] JOIN table [alias] ON condition { AND condition }
117+
[WHERE conditions]
118+
[ORDER BY qualified_column [ASC|DESC]]
119+
[LIMIT n]
120+
[OFFSET n]
121+
```
122+
107123
### 3.2 Columns
108124

109125
| Form | Example | Description |
@@ -112,6 +128,7 @@ SELECT [DISTINCT] columns FROM table
112128
| Named | `SELECT id, name FROM Sheet1` | Specific columns (must exist in header) |
113129
| Aggregate | `SELECT COUNT(*) FROM Sheet1` | Aggregate function call |
114130
| Mixed | `SELECT name, COUNT(*) FROM Sheet1 GROUP BY name` | Plain + aggregate columns |
131+
| Qualified | `SELECT a.id, b.name FROM Sheet1 a JOIN Sheet2 b ON a.id = b.id` | Table-qualified columns (required in JOIN) |
115132

116133
- Column aliases (`AS`) are **not supported**.
117134
- Arithmetic expressions in SELECT list are **not supported** (no `col + 1`, no `UPPER(name)`).
@@ -209,6 +226,31 @@ See [Section 7: WHERE Clause](#7-where-clause).
209226
Clauses must appear in this order: `WHERE``GROUP BY``HAVING``ORDER BY``LIMIT``OFFSET`.
210227
Any other ordering raises `ValueError`.
211228

229+
### 3.11 JOIN
230+
231+
| Feature | Supported | Example |
232+
|---------|-----------|---------|
233+
| INNER JOIN || `SELECT a.id, b.name FROM t1 a JOIN t2 b ON a.id = b.id` |
234+
| LEFT JOIN || `SELECT a.id, b.name FROM t1 a LEFT JOIN t2 b ON a.id = b.id` |
235+
| RIGHT JOIN || Not supported |
236+
| FULL OUTER JOIN || Not supported |
237+
| CROSS JOIN || Not supported |
238+
| NATURAL JOIN || Not supported |
239+
| Multiple JOINs || Only one JOIN clause per query |
240+
241+
**Requirements**:
242+
- Table aliases are recommended (e.g., `FROM users a JOIN orders b ON ...`).
243+
- All SELECT columns must use qualified names (`a.id`, not just `id`).
244+
- `SELECT *` is **not supported** with JOIN.
245+
- The ON clause requires at least one equality condition (e.g., `a.id = b.user_id`).
246+
- Multiple ON conditions are joined with `AND`.
247+
- `WHERE`, `ORDER BY`, `LIMIT`, `OFFSET` work with JOIN queries.
248+
- `GROUP BY`, `HAVING`, and aggregates are **not supported** in JOIN queries.
249+
250+
**Execution**:
251+
- INNER JOIN returns only rows where the ON condition matches in both tables.
252+
- LEFT JOIN returns all rows from the left table, with NULL values for unmatched right-table columns.
253+
- The join algorithm uses hash matching for efficient lookups.
212254
---
213255

214256
## 4. INSERT
@@ -412,14 +454,15 @@ For UPDATE with WHERE:
412454
| Column not found | `ValueError` | `Unknown column(s): ...` |
413455
| Sheet already exists (CREATE) | `ValueError` | `Sheet '{name}' already exists` |
414456
| Param count mismatch | `ValueError` | `Not enough / Too many parameters` |
415-
| Unsupported grammar (JOIN, etc.) | `ValueError` | `Unsupported SQL grammar: {feature}` |
457+
| Unsupported grammar | `ValueError` | `Unsupported SQL grammar: {feature}` |
416458
| Parenthesized WHERE | `ValueError` | `Unsupported SQL grammar: parenthesized expressions` |
417459
| Aggregate in WHERE clause | `ValueError` | `Aggregate functions are not allowed in WHERE clause; use HAVING instead` |
418460
| Invalid HAVING column reference | `ValueError` | `HAVING column '{column}' must be a GROUP BY column or aggregate` |
419461
| Read-only backend mutation | `NotSupportedError` | `{action} is not supported by the read-only backend` |
420462
| Invalid LIMIT (non-integer) | `ValueError` | `LIMIT must be an integer` |
421463
| INSERT into headless sheet | `ValueError` | `Cannot insert into sheet without headers` |
422464
| Empty IN clause | `ValueError` | `IN clause cannot be empty` |
465+
| Unsupported JOIN variant | `ValueError` | `RIGHT JOIN is not supported...` |
423466

424467
---
425468

@@ -428,11 +471,12 @@ For UPDATE with WHERE:
428471
```ebnf
429472
statement = select | insert | update | delete | create | drop ;
430473
431-
select = "SELECT" [ "DISTINCT" ] select_columns "FROM" table
474+
select = "SELECT" [ "DISTINCT" ] select_columns "FROM" table_ref
475+
[ join_clause ]
432476
[ "WHERE" where_expr ]
433477
[ "GROUP" "BY" column { "," column } ]
434478
[ "HAVING" where_expr ]
435-
[ "ORDER" "BY" column [ direction ] ]
479+
[ "ORDER" "BY" qualified_column [ direction ] ]
436480
[ "LIMIT" integer ]
437481
[ "OFFSET" integer ] ;
438482
@@ -456,6 +500,11 @@ column_list = column { "," column } ;
456500
value_list = value { "," value } ;
457501
column_def = column [ type_name ] ;
458502
assignment = column "=" value ;
503+
qualified_col = [ table_or_alias "." ] column ;
504+
table_ref = table [ alias ] ;
505+
join_clause = [ "INNER" | "LEFT" ] "JOIN" table_ref "ON" join_cond { "AND" join_cond } ;
506+
join_cond = qualified_col "=" qualified_col ;
507+
alias = identifier ;
459508
460509
where_expr = condition { ("AND" | "OR") condition } ;
461510
condition = column operator value

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "excel-dbapi"
7-
version = "0.3.0"
7+
version = "0.4.0"
88
description = "PEP 249 compliant DB-API driver for Excel files"
99
readme = { file = "README.md", content-type = "text/markdown" }
1010
license = "MIT"

0 commit comments

Comments
 (0)