Skip to content

fix(editor): suggest columns for derived-table and CTE aliases in autocomplete#1712

Merged
datlechin merged 3 commits into
mainfrom
fix/derived-table-alias-completion
Jun 18, 2026
Merged

fix(editor): suggest columns for derived-table and CTE aliases in autocomplete#1712
datlechin merged 3 commits into
mainfrom
fix/derived-table-alias-completion

Conversation

@datlechin

Copy link
Copy Markdown
Member

Fixes #1697.

Problem

Typing alias. for a derived table (subquery with an alias) returned no column suggestions. Ordinary table aliases worked, but LEFT JOIN (SELECT ...) ahs did not, so ahs. suggested nothing. CTEs had the same gap.

Root cause

Two layers, so a regex tweak alone would not fix it:

  1. Parsing never registered the subquery alias. fromListRegex stops at the first (, and the JOIN regex requires an identifier right after JOIN, so JOIN (SELECT ...) ahs matched nothing.
  2. Data model had no concept of a derived column source. resolveAlias only maps an alias to a real schema table; a derived table's columns live in the subquery's SELECT list, which the schema knows nothing about.

The same class of bug already affected CTEs (WITH cte AS (SELECT ...)).

Fix

A small model extension, not a symptom patch:

  • DerivedTableParser (new): one paren/string/comment-aware scan that extracts each FROM/JOIN subquery and CTE with the column names its SELECT list produces. Resolves explicit AS names, bare and qualified columns; skips SELECT * and unaliased expressions (no reliable name). Honors explicit CTE column lists (WITH c(a, b) AS ...). Uses O(1) NSString access.
  • TableReference gains an optional derivedColumns source (isDerived).
  • SQLContextAnalyzer.analyze merges parsed derived tables into the in-scope references; a derived alias wins over a plain reference of the same name (so a CTE used directly in FROM resolves).
  • SQLCompletionProvider serves derived columns in the alias. path; SQLSchemaProvider.allColumnsInScope includes them in unqualified completion too.

This matches DataGrip and DBeaver's semantic engine, which parse the subquery output columns rather than only real tables.

Tests

  • DerivedTableParserTests: the exact issue query, FROM/JOIN/comma-list derived tables, qualified columns, AS renames, SELECT * (empty), nested subqueries, CTEs and explicit column lists, quoted aliases, and non-matches (IN (SELECT ...), function calls).
  • SQLContextAnalyzerTests: derived-table and CTE references carry their columns.
  • CompletionEngineTests: end-to-end ahs. and CTE t. return the subquery's columns.

Known limitation

A SELECT * subquery and unaliased expressions like AVG(score) have no name to suggest, so they are skipped (documented). This is where DataGrip and DBeaver also degrade.

Docs / CHANGELOG

docs/features/autocomplete.mdx gains a "Derived Tables and CTEs" example; CHANGELOG updated under Fixed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2cad9d004c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +255 to +257
let end = matchingQuote(in: ns, from: index, limit: limit, quote: c)
guard end > index + 1 else { return nil }
return ns.substring(with: NSRange(location: index + 1, length: end - index - 1))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fix quoted alias extraction to drop the closing quote

When the SELECT list or derived-table alias is backtick- or double-quoted, matchingQuote returns the index after the closing quote, but this substring length treats that value as the closing-quote index. For inputs like SELECT AVG(score) AS avg_score the derived column becomes `avg_score instead of avg_score, so autocomplete returns an invalid label; quoted derived aliases similarly fail to match the user's qualifier.

Useful? React with 👍 / 👎.

Comment on lines +87 to +88
if kind == "FROM" || kind == "JOIN" || preceding.text == "," {
return parseDerived(in: ns, subOpen: open, subClose: close, limit: limit)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict comma-derived parsing to FROM lists

Treating any (SELECT ...) preceded by a comma as a derived table also matches scalar subqueries in the SELECT list, e.g. SELECT name, (SELECT id FROM orders) AS order_id FROM users. In that case order_id is a column alias, not a table alias, but it gets added to tableReferences, so order_id. and unqualified column completion can suggest columns from the scalar subquery that are not valid in the outer scope.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 56b232514e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
guard cursor < limit, let alias = readAliasForward(in: ns, at: cursor, limit: limit) else { return nil }
guard !Self.nonAliasKeywords.contains(alias.uppercased()) else { return nil }
return DerivedTable(alias: alias, columns: columns)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor derived-table column alias lists

When a derived table supplies a column alias list after the table alias, e.g. FROM (SELECT id, name FROM users) AS u(user_id, display_name), SQL exposes user_id and display_name, not the inner select-list names. This path always returns the parsed select-list columns and never looks for the optional alias column list after u, so u. completions suggest invalid column names for a valid query shape.

Useful? React with 👍 / 👎.

Comment on lines +820 to +822
for derived in derivedTables where present.insert(derived.alias.lowercased()).inserted {
references.append(
TableReference(tableName: derived.alias, alias: derived.alias, derivedColumns: derived.columns)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep unused CTEs out of column scope

When a statement defines a CTE but does not reference it in the current FROM/JOIN, this loop still appends it as a TableReference with derivedColumns. For a query like WITH unused AS (SELECT secret FROM users) SELECT * FROM orders o WHERE |, column completion sees both orders and unused, so it can suggest unused.secret even though the CTE is only available as a table operand and is not in the query's range table.

Useful? React with 👍 / 👎.

@mintlify

mintlify Bot commented Jun 17, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
TablePro 🟢 Ready View Preview Jun 17, 2026, 5:10 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@datlechin datlechin merged commit a98740b into main Jun 18, 2026
3 of 4 checks passed
@datlechin datlechin deleted the fix/derived-table-alias-completion branch June 18, 2026 02:36

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e99664f251

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +137 to +139
if let derived = context.tableReferences.first(where: {
$0.isDerived && $0.identifier.caseInsensitiveCompare(dotPrefix) == .orderedSame
}), let columns = derived.derivedColumns, !columns.isEmpty {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop derived aliases from falling through to table lookup

When a derived table has no parsed columns, such as FROM (SELECT * FROM users) d, derivedColumns is an empty array, so this condition skips the derived match and falls through to resolveAlias. Because derived references are stored with tableName == alias, d. can then fetch and show columns from a real base table named d, even though d is a derived table whose columns this parser intentionally skipped. Return immediately for a derived match even when the column list is empty.

Useful? React with 👍 / 👎.

Comment on lines +808 to +812
for key in [ref.tableName.lowercased(), ref.identifier.lowercased()] {
guard let columns = derivedColumnsByAlias[key] else { continue }
consumed.insert(key)
references[index] = TableReference(
tableName: ref.tableName, alias: ref.alias, schema: ref.schema, derivedColumns: columns

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep CTE columns off unrelated aliases

This matches every parsed CTE/derived name against both the table operand and the effective alias, so a query with WITH c AS (...) SELECT c.| FROM users c rewrites the in-scope users c reference as derived and c. suggests the CTE columns instead of users columns. That is separate from appending unused CTEs later: the actual table reference is mutated here before the append path. CTE columns should attach only when the referenced table name is the CTE, not when an unrelated table alias happens to share the CTE name.

Useful? React with 👍 / 👎.

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.

SQL Auto-completion Does Not Work for Aliases from Derived Tables

1 participant