Skip to content

fix(sql): allow JOINs without an explicit alias on the joined table#97

Merged
fupelaqu merged 1 commit into
mainfrom
feature/licensingInfrastructure
Jun 1, 2026
Merged

fix(sql): allow JOINs without an explicit alias on the joined table#97
fupelaqu merged 1 commit into
mainfrom
feature/licensingInfrastructure

Conversation

@fupelaqu
Copy link
Copy Markdown
Contributor

@fupelaqu fupelaqu commented Jun 1, 2026

Closes #95.

Join.validate() rejected any join whose joined table had no alias — but standard SQL doesn't require one. The bare table name is a valid qualifier (PostgreSQL, MySQL, DuckDB, ClickHouse, SQL Server). Removing the rule surfaces two latent issues that have to be fixed together:

  1. From.scalaJoin.validate() Drop the two unconditional alias-required branches (the explicit alias match + the redundant case j if alias.isEmpty arm in the second match). Keep the ON-clause guard for non-CROSS joins. The downstream sites that key off the join alias already fall back to the source name when no alias is set (From.tableAliases, From.joinAliases, From.unnestAliases, JoinKey.apply, Unnest.innerHitsName), so no call site needs to change.

  2. From.scalaStandardJoin.update() Stop calling source.update(request). The source of a JOIN is a TABLE name, not a column expression. With the alias requirement gone, From.tableAliases now contains a customers → customers self-mapping for the alias-less join — which made GenericIdentifier.update treat name="customers" as alias.column, take parts.tail.mkString(".") of a single-part name, and end up with name="". The ON clause still gets .update(request) so column resolution within ON works.

  3. Parser.scalareservedKeywords Add inner, left, right, full, cross, outer, on. The alias regex's negative-lookahead filter excluded from/join/etc. but not the join-type keywords — so without an alias on orders, the regex greedily consumed LEFT as the alias, leaving the join rule to parse a junk JOIN with no joinType. Functions like LEFT(x, 5) and RIGHT(x, 3) are parsed by their dedicated function parsers (not via identifierRegex/regexAlias), so they continue to work — verified by ParserSpec line 225's SELECT that exercises both.

ParserSpec gets three regression cases:

  • parses LEFT JOIN customers ON orders.customer_id = customers.id and exposes join.alias = None and join.source.name = "customers".
  • alias-less joined table is reachable in From.tableAliases by its bare name ("customers" -> "customers").
  • a JOIN with no ON clause still fails — only the alias rule was relaxed, the ON rule was not.

Closes #95.

`Join.validate()` rejected any join whose joined table had no alias —
but standard SQL doesn't require one. The bare table name is a valid
qualifier (PostgreSQL, MySQL, DuckDB, ClickHouse, SQL Server). Removing
the rule surfaces two latent issues that have to be fixed together:

  1. `From.scala` — `Join.validate()`
     Drop the two unconditional alias-required branches (the explicit
     alias match + the redundant `case j if alias.isEmpty` arm in the
     second match). Keep the ON-clause guard for non-CROSS joins. The
     downstream sites that key off the join alias already fall back to
     the source name when no alias is set (`From.tableAliases`,
     `From.joinAliases`, `From.unnestAliases`, `JoinKey.apply`,
     `Unnest.innerHitsName`), so no call site needs to change.

  2. `From.scala` — `StandardJoin.update()`
     Stop calling `source.update(request)`. The source of a JOIN is a
     TABLE name, not a column expression. With the alias requirement
     gone, `From.tableAliases` now contains a `customers → customers`
     self-mapping for the alias-less join — which made
     `GenericIdentifier.update` treat `name="customers"` as
     `alias.column`, take `parts.tail.mkString(".")` of a single-part
     name, and end up with `name=""`. The ON clause still gets
     `.update(request)` so column resolution within ON works.

  3. `Parser.scala` — `reservedKeywords`
     Add `inner`, `left`, `right`, `full`, `cross`, `outer`, `on`. The
     alias regex's negative-lookahead filter excluded `from`/`join`/etc.
     but not the join-type keywords — so without an alias on `orders`,
     the regex greedily consumed `LEFT` as the alias, leaving the
     `join` rule to parse a junk JOIN with no joinType. Functions like
     `LEFT(x, 5)` and `RIGHT(x, 3)` are parsed by their dedicated
     function parsers (not via `identifierRegex`/`regexAlias`), so they
     continue to work — verified by ParserSpec line 225's SELECT that
     exercises both.

ParserSpec gets three regression cases:
  - parses `LEFT JOIN customers ON orders.customer_id = customers.id`
    and exposes `join.alias = None` and `join.source.name = "customers"`.
  - alias-less joined table is reachable in `From.tableAliases` by its
    bare name (`"customers" -> "customers"`).
  - a JOIN with no ON clause still fails — only the alias rule was
    relaxed, the ON rule was not.
@fupelaqu fupelaqu marked this pull request as ready for review June 1, 2026 11:49
@fupelaqu fupelaqu merged commit 7bb2d8e into main Jun 1, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parser: drop the mandatory alias requirement on joined tables

1 participant