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
2828The 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```
98103SELECT [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).
209226Clauses must appear in this order: ` WHERE ` → ` GROUP BY ` → ` HAVING ` → ` ORDER BY ` → ` LIMIT ` → ` OFFSET ` .
210227Any 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
429472statement = 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 } ;
456500value_list = value { "," value } ;
457501column_def = column [ type_name ] ;
458502assignment = 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
460509where_expr = condition { ("AND" | "OR") condition } ;
461510condition = column operator value
0 commit comments