@@ -3,67 +3,101 @@ import { StarbaseDBConfiguration } from '../handler'
33import { DataSource } from '../types'
44import { createResponse } from '../utils'
55
6+ // Number of rows fetched per page when dumping a table. Keeping this
7+ // bounded avoids loading entire tables into Worker memory, which is
8+ // what previously caused dumps of large databases to fail.
9+ const DUMP_PAGE_SIZE = 1000
10+
11+ function formatValue ( value : unknown ) : string {
12+ if ( value === null || value === undefined ) return 'NULL'
13+ if ( typeof value === 'string' ) return `'${ value . replace ( / ' / g, "''" ) } '`
14+ return String ( value )
15+ }
16+
617export async function dumpDatabaseRoute (
718 dataSource : DataSource ,
819 config : StarbaseDBConfiguration
920) : Promise < Response > {
1021 try {
11- // Get all table names
22+ // Resolve the list of tables up front so any failure surfaces as a
23+ // 500 (matching prior behavior) rather than mid-stream.
1224 const tablesResult = await executeOperation (
1325 [ { sql : "SELECT name FROM sqlite_master WHERE type='table';" } ] ,
1426 dataSource ,
1527 config
1628 )
17-
1829 const tables = tablesResult . map ( ( row : any ) => row . name )
19- let dumpContent = 'SQLite format 3\0' // SQLite file header
2030
21- // Iterate through all tables
22- for ( const table of tables ) {
23- // Get table schema
24- const schemaResult = await executeOperation (
25- [
26- {
27- sql : `SELECT sql FROM sqlite_master WHERE type='table' AND name='${ table } ';` ,
28- } ,
29- ] ,
30- dataSource ,
31- config
32- )
31+ const encoder = new TextEncoder ( )
32+ const stream = new ReadableStream < Uint8Array > ( {
33+ async start ( controller ) {
34+ try {
35+ controller . enqueue ( encoder . encode ( 'SQLite format 3\0' ) )
3336
34- if ( schemaResult . length ) {
35- const schema = schemaResult [ 0 ] . sql
36- dumpContent += `\n-- Table: ${ table } \n${ schema } ;\n\n`
37- }
37+ for ( const table of tables ) {
38+ const schemaResult = await executeOperation (
39+ [
40+ {
41+ sql : `SELECT sql FROM sqlite_master WHERE type='table' AND name='${ table } ';` ,
42+ } ,
43+ ] ,
44+ dataSource ,
45+ config
46+ )
3847
39- // Get table data
40- const dataResult = await executeOperation (
41- [ { sql : `SELECT * FROM ${ table } ;` } ] ,
42- dataSource ,
43- config
44- )
48+ if ( schemaResult . length ) {
49+ const schema = schemaResult [ 0 ] . sql
50+ controller . enqueue (
51+ encoder . encode (
52+ `\n-- Table: ${ table } \n${ schema } ;\n\n`
53+ )
54+ )
55+ }
4556
46- for ( const row of dataResult ) {
47- const values = Object . values ( row ) . map ( ( value ) =>
48- typeof value === 'string'
49- ? `'${ value . replace ( / ' / g, "''" ) } '`
50- : value
51- )
52- dumpContent += `INSERT INTO ${ table } VALUES (${ values . join ( ', ' ) } );\n`
53- }
57+ // Page through the table so we never materialize the
58+ // full result set in memory.
59+ let offset = 0
60+ while ( true ) {
61+ const dataResult = await executeOperation (
62+ [
63+ {
64+ sql : `SELECT * FROM ${ table } LIMIT ${ DUMP_PAGE_SIZE } OFFSET ${ offset } ;` ,
65+ } ,
66+ ] ,
67+ dataSource ,
68+ config
69+ )
5470
55- dumpContent += '\n'
56- }
71+ if ( ! dataResult . length ) break
5772
58- // Create a Blob from the dump content
59- const blob = new Blob ( [ dumpContent ] , { type : 'application/x-sqlite3' } )
73+ let chunk = ''
74+ for ( const row of dataResult ) {
75+ const values =
76+ Object . values ( row ) . map ( formatValue )
77+ chunk += `INSERT INTO ${ table } VALUES (${ values . join ( ', ' ) } );\n`
78+ }
79+ controller . enqueue ( encoder . encode ( chunk ) )
80+
81+ if ( dataResult . length < DUMP_PAGE_SIZE ) break
82+ offset += DUMP_PAGE_SIZE
83+ }
84+
85+ controller . enqueue ( encoder . encode ( '\n' ) )
86+ }
87+
88+ controller . close ( )
89+ } catch ( error ) {
90+ controller . error ( error )
91+ }
92+ } ,
93+ } )
6094
6195 const headers = new Headers ( {
6296 'Content-Type' : 'application/x-sqlite3' ,
6397 'Content-Disposition' : 'attachment; filename="database_dump.sql"' ,
6498 } )
6599
66- return new Response ( blob , { headers } )
100+ return new Response ( stream , { headers } )
67101 } catch ( error : any ) {
68102 console . error ( 'Database Dump Error:' , error )
69103 return createResponse ( undefined , 'Failed to create database dump' , 500 )
0 commit comments