Skip to content
Open
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
95 changes: 59 additions & 36 deletions src/export/dump.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,54 +16,77 @@ export async function dumpDatabaseRoute(
)

const tables = tablesResult.map((row: any) => row.name)
let dumpContent = 'SQLite format 3\0' // SQLite file header

// Iterate through all tables
for (const table of tables) {
// Get table schema
const schemaResult = await executeOperation(
[
{
sql: `SELECT sql FROM sqlite_master WHERE type='table' AND name='${table}';`,
},
],
dataSource,
config
)
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
try {
controller.enqueue(encoder.encode('SQLite format 3\0'))

if (schemaResult.length) {
const schema = schemaResult[0].sql
dumpContent += `\n-- Table: ${table}\n${schema};\n\n`
}
for (const table of tables) {
const schemaResult = await executeOperation(
[
{
sql: `SELECT sql FROM sqlite_master WHERE type='table' AND name='${table}';`,
},
],
dataSource,
config
)

// Get table data
const dataResult = await executeOperation(
[{ sql: `SELECT * FROM ${table};` }],
dataSource,
config
)
if (schemaResult.length) {
const schema = schemaResult[0].sql
controller.enqueue(
encoder.encode(`\n-- Table: ${table}\n${schema};\n\n`)
)
}

for (const row of dataResult) {
const values = Object.values(row).map((value) =>
typeof value === 'string'
? `'${value.replace(/'/g, "''")}'`
: value
)
dumpContent += `INSERT INTO ${table} VALUES (${values.join(', ')});\n`
}
// Stream table rows with cursor pagination to avoid worker OOM
let offset = 0
const chunkSize = 1000
while (true) {
const dataResult = await executeOperation(
[{ sql: `SELECT * FROM ${table} LIMIT ${chunkSize} OFFSET ${offset};` }],
dataSource,
config
)

dumpContent += '\n'
}
if (!dataResult || dataResult.length === 0) break

// Create a Blob from the dump content
const blob = new Blob([dumpContent], { type: 'application/x-sqlite3' })
let chunkSql = ''
for (const row of dataResult) {
const values = Object.values(row).map((value) =>
typeof value === 'string'
? `'${value.replace(/'/g, "''")}'`
: value
)
chunkSql += `INSERT INTO ${table} VALUES (${values.join(', ')});\n`
}
controller.enqueue(encoder.encode(chunkSql))

offset += dataResult.length
if (dataResult.length < chunkSize) break

// Cooperative event-loop yielding
await new Promise((r) => setTimeout(r, 0))
}

controller.enqueue(encoder.encode('\n'))
}

controller.close()
} catch (err) {
controller.error(err)
}
},
})

const headers = new Headers({
'Content-Type': 'application/x-sqlite3',
'Content-Disposition': 'attachment; filename="database_dump.sql"',
})

return new Response(blob, { headers })
return new Response(stream, { headers })
} catch (error: any) {
console.error('Database Dump Error:', error)
return createResponse(undefined, 'Failed to create database dump', 500)
Expand Down