From 8ae2902c8e5eb796d0c22245620b72ea868b4491 Mon Sep 17 00:00:00 2001 From: Jan Werner Date: Wed, 28 Oct 2026 10:54:15 +0100 Subject: [PATCH] fix: quote identifiers in object preview queries --- src/components/sql-editor/SQLEditorLayout.tsx | 4 ++-- .../sql-editor/schema/TableSchemaContent.tsx | 3 ++- .../sql-editor/schema/ViewSchemaContent.tsx | 3 ++- src/lib/sql/generate.ts | 6 ++++++ tests/sql-generate.test.ts | 13 +++++++++++++ 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/components/sql-editor/SQLEditorLayout.tsx b/src/components/sql-editor/SQLEditorLayout.tsx index f636b11..66fe982 100644 --- a/src/components/sql-editor/SQLEditorLayout.tsx +++ b/src/components/sql-editor/SQLEditorLayout.tsx @@ -8,7 +8,7 @@ import { useExecuteSQL } from '../../hooks/useQuery' import { useSchemaStoreSync } from '../../hooks/useSchemaStoreSync' import { schemaStore } from '@/lib/schema-store' import { queryClient as rpcClient } from '@/lib/connect-client' -import { generateSelect, generateInsert, generateUpdate, generateDelete, generateCreateTable, generateAlterAddColumn } from '@/lib/sql/generate' +import { generateObjectSelect, generateSelect, generateInsert, generateUpdate, generateDelete, generateCreateTable, generateAlterAddColumn } from '@/lib/sql/generate' import { toastManager } from '../ui/toast' import type { ObjectType } from './ObjectTree' import type { SelectedObject } from '@/hooks/useEditorNavigation' @@ -177,7 +177,7 @@ export function SQLEditorLayout({ } // For tables/views, run SELECT query - const query = `SELECT * FROM ${schema}.${objectName} LIMIT 100;` + const query = generateObjectSelect(schema, objectName) // Check if a tab with the same SQL statement already exists const existingTab = editorTabs.tabs.find( diff --git a/src/components/sql-editor/schema/TableSchemaContent.tsx b/src/components/sql-editor/schema/TableSchemaContent.tsx index be258cc..7d6a698 100644 --- a/src/components/sql-editor/schema/TableSchemaContent.tsx +++ b/src/components/sql-editor/schema/TableSchemaContent.tsx @@ -6,6 +6,7 @@ import { Button } from '../../ui/button' import { ScrollArea } from '../../ui/scroll-area' import { Tabs, TabsList, TabsTab, TabsPanel } from '../../ui/tabs' import { InfoItem, formatBytes, formatRowCount, getKindLabel, ColumnsTable, IndexesTable, ConstraintsTable, TriggersTable, PoliciesTable, GrantsTable } from './shared' +import { generateObjectSelect } from '@/lib/sql/generate' interface TableSchemaContentProps { connectionId: string @@ -37,7 +38,7 @@ export function TableSchemaContent({ connectionId, schema, table, onNewQuery }: const { data: grants, isLoading: grantsLoading } = useGrants(connectionId, schema, table, visitedTabs.has('grants')) const handleNewQuery = () => { - onNewQuery?.(`SELECT * FROM "${schema}"."${table}" LIMIT 100;`) + onNewQuery?.(generateObjectSelect(schema, table)) } // Build ER diagram showing table structure and FK relationships diff --git a/src/components/sql-editor/schema/ViewSchemaContent.tsx b/src/components/sql-editor/schema/ViewSchemaContent.tsx index f3ff42e..22b3042 100644 --- a/src/components/sql-editor/schema/ViewSchemaContent.tsx +++ b/src/components/sql-editor/schema/ViewSchemaContent.tsx @@ -3,6 +3,7 @@ import { Badge } from '../../ui/badge' import { Button } from '../../ui/button' import { ScrollArea } from '../../ui/scroll-area' import { InfoItem, formatBytes, formatRowCount, getKindLabel, SQLDefinition, ColumnsTable } from './shared' +import { generateObjectSelect } from '@/lib/sql/generate' interface ViewSchemaContentProps { connectionId: string @@ -17,7 +18,7 @@ export function ViewSchemaContent({ connectionId, schema, view, objectType, onNe const { data: tableInfo, isLoading: tableInfoLoading } = useTableInfo(connectionId, schema, view) const handleNewQuery = () => { - onNewQuery?.(`SELECT * FROM "${schema}"."${view}" LIMIT 100;`) + onNewQuery?.(generateObjectSelect(schema, view)) } const isMaterializedView = objectType === 'materialized_view' diff --git a/src/lib/sql/generate.ts b/src/lib/sql/generate.ts index 268576b..e928b82 100644 --- a/src/lib/sql/generate.ts +++ b/src/lib/sql/generate.ts @@ -19,6 +19,12 @@ export async function generateSelect(table: string): Promise { return formatSql(sql) } +/** Generate a preview query from raw PostgreSQL object names. */ +export function generateObjectSelect(schema: string, name: string): string { + const quote = (identifier: string) => `"${identifier.replace(/"/g, '""')}"` + return `SELECT * FROM ${quote(schema)}.${quote(name)} LIMIT 100;` +} + /** * Generate an INSERT statement with column placeholders. */ diff --git a/tests/sql-generate.test.ts b/tests/sql-generate.test.ts index 1572aa6..9a012b2 100644 --- a/tests/sql-generate.test.ts +++ b/tests/sql-generate.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest' import { + generateObjectSelect, generateSelect, generateInsert, generateUpdate, @@ -19,6 +20,18 @@ LIMIT }) }) + describe('generateObjectSelect', () => { + it.each([ + ['public', 'users', 'SELECT * FROM "public"."users" LIMIT 100;'], + ['ExampleSchema', 'ExampleTable_V01', 'SELECT * FROM "ExampleSchema"."ExampleTable_V01" LIMIT 100;'], + ['select', 'order', 'SELECT * FROM "select"."order" LIMIT 100;'], + ['schema.with.dots', 'table name', 'SELECT * FROM "schema.with.dots"."table name" LIMIT 100;'], + ['a"b', 'c"d', 'SELECT * FROM "a""b"."c""d" LIMIT 100;'], + ])('quotes schema %s and object %s', (schema, name, expected) => { + expect(generateObjectSelect(schema, name)).toBe(expected) + }) + }) + describe('generateInsert', () => { it('generates an INSERT statement with column placeholders', async () => { const columns = [