Fix/postgres alias column autocomplete#267
Conversation
|
@m-tonon Looks awesome! |
|
@debba you can assign it to me |
NewtTheWolf
left a comment
There was a problem hiding this comment.
Hey @m-tonon — first off, this is genuinely cool work. 🙌 Bringing autocomplete to the notebook view, pulling the registration into a shared useSqlAutocompleteRegistration hook, and especially the #266 fix itself — resolving quoted / mixed-case / schema-qualified identifiers in parseTablesFromQuery — is solid, and the test coverage is great. Really nice.
Two things I'd love to see addressed before merge:
1. (Blocking) Autocomplete dies when an unrelated connection disconnects.
disposeSqlAutocomplete() tears down the single global completion provider, but it's called on every connection's disconnect / health-failure. Repro: open two connections, work in connection A's editor, disconnect B → A's autocomplete goes dead, because A's registration hook doesn't re-run (its deps didn't change). Confirmed locally. The provider should follow the active editor's lifecycle, not get torn down on arbitrary disconnects — see the inline note in DatabaseProvider.
2. (Blocking, UX) Inserts are always fully quoted and always schema-prefixed.
Before this PR autocomplete inserted bare names; now every Postgres insert becomes "public"."users", "id". That's noisier than every reference client: Postgres' own quote_ident(), DataGrip and DBeaver all quote only when needed (reserved word / mixed case / special char) and don't prefix the default schema. Crucially, the #266 fix is about resolving quoted identifiers for lookup — it doesn't require emitting quotes on every insert, so the two can be decoupled. The mixed-case correctness ("AccountEventLog" stays quoted) must stay; only the always-on quoting of plain lowercase names + the forced public. prefix should go.
To support the inline suggestions, I'd add a small helper to src/utils/identifiers.ts (it can't be a one-click suggestion since that file isn't in this diff):
// PostgreSQL folds unquoted identifiers to lowercase and only needs quotes for
// reserved words, mixed case, or special characters — mirroring quote_ident().
const PG_SAFE_IDENTIFIER = /^[a-z_][a-z0-9_$]*$/;
const PG_RESERVED = new Set([
"select","from","where","table","user","order","group","join","and","or",
"as","in","on","by","null","true","false","default","check","column","limit","offset",
]);
/** Like formatSqlIdentifier, but quotes only when the identifier actually needs it. */
export function quoteIdentifierIfNeeded(
identifier: string,
driver: string | null | undefined,
): string {
if (!shouldQuoteIdentifiers(driver)) return identifier; // non-PG: unchanged
if (PG_SAFE_IDENTIFIER.test(identifier) && !PG_RESERVED.has(identifier)) {
return identifier;
}
return quoteIdentifier(identifier, driver);
}The column/table inline suggestions below reference this helper, so they apply as a set. Net result: SELECT id, name FROM users for plain lowercase, SELECT "AccountId" FROM "AccountEventLog" for mixed-case — exactly what you'd want. 🚀
Thanks again — really nice contribution, this is the right direction!
| const tableInsertText = ( | ||
| tableName: string, | ||
| driver?: string | null, | ||
| schema?: string | null, | ||
| ) => | ||
| schema | ||
| ? quoteTableRef(tableName, driver, schema) | ||
| : formatSqlIdentifier(tableName, driver); |
There was a problem hiding this comment.
The completion session is already scoped to the active schema (getTableColumns(..., schema)), so hard-qualifying every table as "public"."table" is redundant and noisy. Drop the schema prefix and quote only when needed (pairs with the call-site suggestion below + the quoteIdentifierIfNeeded helper from the review body):
| const tableInsertText = ( | |
| tableName: string, | |
| driver?: string | null, | |
| schema?: string | null, | |
| ) => | |
| schema | |
| ? quoteTableRef(tableName, driver, schema) | |
| : formatSqlIdentifier(tableName, driver); | |
| const tableInsertText = ( | |
| tableName: string, | |
| driver?: string | null, | |
| ) => | |
| // Already scoped to the active schema — don't prefix "public". on every | |
| // table. Quote only when the name actually requires it. | |
| quoteIdentifierIfNeeded(tableName, driver); |
If you want cross-schema qualification later, qualify only when schema !== defaultSchema rather than always.
| kind: monaco.languages.CompletionItemKind.Class, | ||
| detail: "Table", | ||
| insertText: t.name, | ||
| insertText: tableInsertText(t.name, driver, schema), |
There was a problem hiding this comment.
Match the simplified tableInsertText signature (no schema arg):
| insertText: tableInsertText(t.name, driver, schema), | |
| insertText: tableInsertText(t.name, driver), |
quoteTableRef then becomes unused in this file — drop it from the import on line 1.
| kind: monaco.languages.CompletionItemKind.Field, | ||
| detail: c.detail, | ||
| insertText: c.label, | ||
| insertText: formatSqlIdentifier(c.label, driver), |
There was a problem hiding this comment.
Columns get the same always-quote treatment — "id", "CreatedAt". Quote only when needed so plain lowercase columns stay clean while "AccountId" stays correctly quoted:
| insertText: formatSqlIdentifier(c.label, driver), | |
| insertText: quoteIdentifierIfNeeded(c.label, driver), |
| kind: monaco.languages.CompletionItemKind.Field, | ||
| detail: `${col.detail} — ${table.name}${aliasHint}`, | ||
| insertText: col.label, | ||
| insertText: formatSqlIdentifier(col.label, driver), |
There was a problem hiding this comment.
Same for the context-column insert path:
| insertText: formatSqlIdentifier(col.label, driver), | |
| insertText: quoteIdentifierIfNeeded(col.label, driver), |
| if (!targetId) return; | ||
|
|
||
| clearAutocompleteCache(targetId); | ||
| disposeSqlAutocomplete(); |
There was a problem hiding this comment.
This is the root cause of point #1 above. The global provider mirrors the active editor, so only dispose it when the connection being torn down is actually the active one — otherwise disconnecting a background connection kills autocomplete in the editor you're using:
| disposeSqlAutocomplete(); | |
| if (targetId === activeConnectionId) { | |
| disposeSqlAutocomplete(); | |
| } |
The health-check handler at L786 has the same issue, but there activeConnectionId is a stale closure — you'd need an activeConnectionIdRef (or move disposal into the active-connection-change path). Cleanest long-term: let useSqlAutocompleteRegistration own disposal in its cleanup and drop both calls from here entirely.
|
Thanks for the review! I'll take a look at the feedback, make the necessary improvements, and push an update soon. 🚀 |
This PR implements a shared SQL autocomplete registration flow for both the editor and notebook views, improving lifecycle management and preventing stale or duplicate Monaco providers.
Changes:
useSqlAutocompleteRegistrationhook to manage SQL autocomplete for active connectionsNotebookViewto utilize the new hook, passing the effective schema and active stateEditorto replace direct SQL autocomplete registration with the hookdisposeSqlAutocompleteto clean up resources on connection changes, reconnect, and failuresCloses #266