diff --git a/src/export/dump.test.ts b/src/export/dump.test.ts index ca65b43..d422b3f 100644 --- a/src/export/dump.test.ts +++ b/src/export/dump.test.ts @@ -128,6 +128,44 @@ describe('Database Dump Module', () => { ) }) + it('should quote table identifiers in schema lookups, reads, and INSERT rows', async () => { + vi.mocked(executeOperation) + .mockResolvedValueOnce([{ name: "kid's profiles" }]) + .mockResolvedValueOnce([ + { + sql: 'CREATE TABLE "kid\'s profiles" (id INTEGER, name TEXT);', + }, + ]) + .mockResolvedValueOnce([{ id: 1, name: 'Alice' }]) + + const response = await dumpDatabaseRoute(mockDataSource, mockConfig) + + expect(executeOperation).toHaveBeenNthCalledWith( + 2, + [ + { + sql: "SELECT sql FROM sqlite_master WHERE type='table' AND name='kid''s profiles';", + }, + ], + mockDataSource, + mockConfig + ) + expect(executeOperation).toHaveBeenNthCalledWith( + 3, + [{ sql: 'SELECT * FROM "kid\'s profiles";' }], + mockDataSource, + mockConfig + ) + + const dumpText = await response.text() + expect(dumpText).toContain( + 'CREATE TABLE "kid\'s profiles" (id INTEGER, name TEXT);' + ) + expect(dumpText).toContain( + "INSERT INTO \"kid's profiles\" VALUES (1, 'Alice');" + ) + }) + it('should return a 500 response when an error occurs', async () => { const consoleErrorMock = vi .spyOn(console, 'error') diff --git a/src/export/dump.ts b/src/export/dump.ts index 91a2e89..12e1b32 100644 --- a/src/export/dump.ts +++ b/src/export/dump.ts @@ -3,6 +3,40 @@ import { StarbaseDBConfiguration } from '../handler' import { DataSource } from '../types' import { createResponse } from '../utils' +const sqliteKeywords = new Set([ + 'select', + 'from', + 'where', + 'table', + 'index', + 'insert', + 'values', + 'order', + 'group', + 'by', + 'limit', + 'offset', + 'join', + 'on', + 'and', + 'or', +]) + +function quoteSqlString(value: string) { + return `'${value.replace(/'/g, "''")}'` +} + +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 dumpDatabaseRoute( dataSource: DataSource, config: StarbaseDBConfiguration @@ -24,7 +58,7 @@ export async function dumpDatabaseRoute( const schemaResult = await executeOperation( [ { - sql: `SELECT sql FROM sqlite_master WHERE type='table' AND name='${table}';`, + sql: `SELECT sql FROM sqlite_master WHERE type='table' AND name=${quoteSqlString(table)};`, }, ], dataSource, @@ -38,7 +72,7 @@ export async function dumpDatabaseRoute( // Get table data const dataResult = await executeOperation( - [{ sql: `SELECT * FROM ${table};` }], + [{ sql: `SELECT * FROM ${formatIdentifier(table)};` }], dataSource, config ) @@ -49,7 +83,7 @@ export async function dumpDatabaseRoute( ? `'${value.replace(/'/g, "''")}'` : value ) - dumpContent += `INSERT INTO ${table} VALUES (${values.join(', ')});\n` + dumpContent += `INSERT INTO ${formatIdentifier(table)} VALUES (${values.join(', ')});\n` } dumpContent += '\n'