diff --git a/src/export/dump.ts b/src/export/dump.ts index 91a2e89..e0fa843 100644 --- a/src/export/dump.ts +++ b/src/export/dump.ts @@ -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)