Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/export/dump.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
40 changes: 37 additions & 3 deletions src/export/dump.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
)
Expand All @@ -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'
Expand Down