From b575d569d6f7ee4a265f3cda3a3496227264d1a0 Mon Sep 17 00:00:00 2001 From: Seowoo Han Date: Thu, 21 May 2026 00:10:34 +0900 Subject: [PATCH] fix export table data identifiers --- src/export/index.test.ts | 51 ++++++++++++++++++++++++++++++++++++++++ src/export/index.ts | 32 ++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/export/index.test.ts b/src/export/index.test.ts index 48de76e..ee3ed5e 100644 --- a/src/export/index.test.ts +++ b/src/export/index.test.ts @@ -83,6 +83,57 @@ describe('Database Operations Module', () => { ]) }) + it('should quote table names that require identifiers in data queries', async () => { + vi.mocked(executeTransaction) + .mockResolvedValueOnce([{ name: "kid's profiles" }]) + .mockResolvedValueOnce([{ id: 1, name: 'Alice' }]) + + const result = await getTableData( + "kid's profiles", + mockDataSource, + mockConfig + ) + + expect(executeTransaction).toHaveBeenNthCalledWith(1, { + queries: [ + { + sql: "SELECT name FROM sqlite_master WHERE type='table' AND name=?;", + params: ["kid's profiles"], + }, + ], + isRaw: false, + dataSource: mockDataSource, + config: mockConfig, + }) + expect(executeTransaction).toHaveBeenNthCalledWith(2, { + queries: [{ sql: 'SELECT * FROM "kid\'s profiles";' }], + isRaw: false, + dataSource: mockDataSource, + config: mockConfig, + }) + expect(result).toEqual([{ id: 1, name: 'Alice' }]) + }) + + it('should quote reserved table names in data queries', async () => { + vi.mocked(executeTransaction) + .mockResolvedValueOnce([{ name: 'order' }]) + .mockResolvedValueOnce([{ id: 1 }]) + + const result = await getTableData( + 'order', + mockDataSource, + mockConfig + ) + + expect(executeTransaction).toHaveBeenNthCalledWith(2, { + queries: [{ sql: 'SELECT * FROM "order";' }], + isRaw: false, + dataSource: mockDataSource, + config: mockConfig, + }) + expect(result).toEqual([{ id: 1 }]) + }) + it('should return null if table does not exist', async () => { vi.mocked(executeTransaction).mockResolvedValueOnce([]) diff --git a/src/export/index.ts b/src/export/index.ts index 9c40119..aa05a38 100644 --- a/src/export/index.ts +++ b/src/export/index.ts @@ -2,6 +2,36 @@ import { DataSource } from '../types' import { executeTransaction } from '../operation' import { StarbaseDBConfiguration } from '../handler' +const sqliteKeywords = new Set([ + 'select', + 'from', + 'where', + 'table', + 'index', + 'insert', + 'values', + 'order', + 'group', + 'by', + 'limit', + 'offset', + 'join', + 'on', + 'and', + 'or', +]) + +function formatIdentifier(identifier: string) { + if ( + /^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier) && + !sqliteKeywords.has(identifier.toLowerCase()) + ) { + return identifier + } + + return `"${identifier.replace(/"/g, '""')}"` +} + export async function executeOperation( queries: { sql: string; params?: any[] }[], dataSource: DataSource, @@ -43,7 +73,7 @@ export async function getTableData( // Get table data const dataResult = await executeOperation( - [{ sql: `SELECT * FROM ${tableName};` }], + [{ sql: `SELECT * FROM ${formatIdentifier(tableName)};` }], dataSource, config )