@@ -15,7 +15,7 @@ import {
1515 type CompletionResult ,
1616 startCompletion ,
1717} from '@codemirror/autocomplete' ;
18- import { PostgreSQL , type SQLNamespace , sql as sqlLanguage } from '@codemirror/lang-sql' ;
18+ import { PostgreSQL , type SQLNamespace , schemaCompletionSource , sql as sqlLanguage } from '@codemirror/lang-sql' ;
1919import { HighlightStyle , indentUnit , syntaxHighlighting , syntaxTree } from '@codemirror/language' ;
2020import { EditorState , type Extension , Prec } from '@codemirror/state' ;
2121import { EditorView , keymap } from '@codemirror/view' ;
@@ -192,12 +192,9 @@ function tableNamespace(table: TableRef): SQLNamespace {
192192 } ;
193193}
194194
195- // Builds the lang-sql completion schema from the loaded catalog tree: bare
196- // table names → columns. Tables are deliberately NOT nested under their
197- // catalog — Redpanda SQL (Oxla) addresses catalog tables with arrow notation
198- // (`catalog=>table`), which catalogArrowSource below handles; dot-style
199- // nesting would advertise syntax the server rejects. Bare entries still give
200- // alias/column resolution (`FROM default_redpanda_catalog=>cars c` → `c.`).
195+ // Bare table name → columns. Powers alias/column resolution only
196+ // (schemaColumnSource); tables aren't nested under catalogs since Oxla uses
197+ // `catalog=>table` arrow notation, handled by catalogArrowSource.
201198function buildSchema ( catalogs : Catalog [ ] ) : SQLNamespace {
202199 const root : Record < string , SQLNamespace > = { } ;
203200 for ( const catalog of catalogs ) {
@@ -212,6 +209,20 @@ function buildSchema(catalogs: Catalog[]): SQLNamespace {
212209 return root ;
213210}
214211
212+ // Schema completion limited to dotted members (`c.` → columns); bare table
213+ // names are suppressed at the top level so catalogArrowSource owns them.
214+ function schemaColumnSource ( catalogs : Catalog [ ] ) : ( context : CompletionContext ) => CompletionResult | null {
215+ const source = schemaCompletionSource ( { dialect : PostgreSQL , schema : buildSchema ( catalogs ) } ) ;
216+ return ( context ) => {
217+ const result = source ( context ) ;
218+ if ( ! result || result instanceof Promise ) {
219+ return null ;
220+ }
221+ const dotted = context . state . sliceDoc ( result . from - 1 , result . from ) === '.' ;
222+ return dotted ? result : null ;
223+ } ;
224+ }
225+
215226// Matches an identifier followed by `=>` or `.` and a partial table name,
216227// anchored at the cursor: [, name, gap1, separator, gap2, quote, partial].
217228const CATALOG_REF_RE = / ( [ A - Z a - z _ ] [ \w $ ] * ) ( \s * ) ( = > | \. ) ( \s * ) ( " ? ) ( [ \w $ ] * ) $ / ;
@@ -415,7 +426,9 @@ export const SqlEditor = forwardRef<SqlEditorHandle, SqlEditorProps>(
415426 } ;
416427
417428 const extensions = useMemo ( ( ) => {
418- const sqlSupport = sqlLanguage ( { dialect : PostgreSQL , schema : buildSchema ( catalogs ) , upperCaseKeywords : true } ) ;
429+ // No `schema` here — schemaColumnSource adds it back for dotted
430+ // completions only, avoiding bare table names at the top level.
431+ const sqlSupport = sqlLanguage ( { dialect : PostgreSQL , upperCaseKeywords : true } ) ;
419432 return [
420433 // Prec.highest so Mod-Enter beats the default keymap's insertBlankLine.
421434 Prec . highest (
@@ -441,6 +454,7 @@ export const SqlEditor = forwardRef<SqlEditorHandle, SqlEditorProps>(
441454 ) ,
442455 sqlSupport ,
443456 sqlSupport . language . data . of ( { autocomplete : catalogArrowSource ( catalogs ) } ) ,
457+ sqlSupport . language . data . of ( { autocomplete : schemaColumnSource ( catalogs ) } ) ,
444458 isDark ? DARK_THEME : LIGHT_THEME ,
445459 EditorView . updateListener . of ( ( update ) => {
446460 if ( update . selectionSet ) {
0 commit comments