├── .gitignore ├── .prettierignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── pnpm-lock.yaml ├── src ├── cli.ts └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.2.1 - 2023-12-21 2 | 3 | - Fix table priority 4 | ([#2](https://github.com/Cretezy/cloudflare-d1-backup/pull/2), thanks 5 | [@nora-soderlund](https://github.com/nora-soderlund)!) 6 | 7 | ## v0.2.0 - 2023-11-06 8 | 9 | - Add `--limit` option to add LIMIT to each SELECT query to fix D1 10 | `Isolate Has exceeded Memory Size`. Defaults to 1000. 11 | 12 | ## v0.1.1 - 2023-10-10 13 | 14 | - Fix running script with `npx` (missing shebang). 15 | 16 | ## v0.1.0 - 2023-10-10 17 | 18 | - Initial release. 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Nora Söderlund 2 | Copyright 2023 Charles Crete 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloudflare D1 Backup 2 | 3 | This script creates an backup/export of a Cloudflare D1 SQLite database. It uses 4 | the 5 | [D1 HTTP API](https://developers.cloudflare.com/api/operations/cloudflare-d1-query-database) 6 | to query for table definitions and data, then outputs SQL commands to recreate 7 | the database as-is. 8 | 9 | This script has only been tested on small databases (~700KB). Please report any 10 | bugs using 11 | [GitHub Issues](https://github.com/Cretezy/cloudflare-d1-backup/issues). 12 | 13 | Based on 14 | [nora-soderlund/cloudflare-d1-backups](https://github.com/nora-soderlund/cloudflare-d1-backups), 15 | which requires to be ran inside a Worker. This repository uses the 16 | [D1 HTTP API](https://developers.cloudflare.com/api/operations/cloudflare-d1-query-database). 17 | 18 | ## Usage 19 | 20 | To create a backup, you must obtain: 21 | 22 | - Your Cloudflare account ID. This can be found as the ID in the URL on the 23 | dashboard after `dash.cloudflare.com/`, or in the sidebar of a zone. 24 | - Your Cloudflare D1 database ID. This can be found on the D1 page. 25 | - Your Cloudflare API key. This can be created under the user icon in the 26 | top-right under "My Profile", then "API Tokens" in the sidebar. Make sure to 27 | have D1 write access (the script does not write to your database). 28 | 29 | ### CLI 30 | 31 | This will create the backup at `backup.sql`. 32 | 33 | ```bash 34 | CLOUDFLARE_D1_ACCOUNT_ID=... CLOUDFLARE_D1_DATABASE_ID=... CLOUDFLARE_D1_API_KEY=... \ 35 | npx @cretezy/cloudflare-d1-backup backup.sql 36 | ``` 37 | 38 | The CLI also supports reading from `.env`. 39 | 40 | You may also pass the `--limit` to add a LIMIT clause for each SELECT query. 41 | Default is 1000. You may need to lower if D1 crashes due to 42 | `Isolate Has exceeded Memory Size`. You can increase to speed up exports. 43 | 44 | ### Library 45 | 46 | ```bash 47 | npm i @cretezy/cloudflare-d1-backup 48 | ``` 49 | 50 | ```ts 51 | import { createBackup } from "@cretezy/cloudflare-d1-backup"; 52 | 53 | const backup = await createBackup({ 54 | accountId: "...", 55 | databaseId: "...", 56 | apiKey: "...", 57 | // Optional, see note above on --limit 58 | limit: 1000, 59 | }); 60 | ``` 61 | 62 | `backup` will be the string of the backup commands. 63 | 64 | ## Restoring a backup 65 | 66 | ```bash 67 | npx wrangler d1 execute --file= 68 | ``` 69 | 70 | `` must be the ID or name of the D1 database. 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cretezy/cloudflare-d1-backup", 3 | "main": "dist/index.js", 4 | "version": "0.2.1", 5 | "license": "Apache-2.0", 6 | "type": "module", 7 | "scripts": { 8 | "build": "tsc", 9 | "start": "ts-node-esm src/cli.ts", 10 | "prepack": "pnpm build", 11 | "format": "prettier -w ." 12 | }, 13 | "bin": { 14 | "cloudflare-d1-backup": "./dist/cli.js" 15 | }, 16 | "types": "./dist/index.d.ts", 17 | "files": [ 18 | "dist", 19 | "src" 20 | ], 21 | "devDependencies": { 22 | "@types/node": "^20.8.4", 23 | "prettier": "^3.0.3", 24 | "ts-node": "^10.9.1", 25 | "typescript": "^5.2.2" 26 | }, 27 | "dependencies": { 28 | "dotenv": "^16.3.1", 29 | "node-fetch": "^3.3.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | dotenv: 9 | specifier: ^16.3.1 10 | version: 16.3.1 11 | node-fetch: 12 | specifier: ^3.3.2 13 | version: 3.3.2 14 | 15 | devDependencies: 16 | '@types/node': 17 | specifier: ^20.8.4 18 | version: 20.8.4 19 | prettier: 20 | specifier: ^3.0.3 21 | version: 3.0.3 22 | ts-node: 23 | specifier: ^10.9.1 24 | version: 10.9.1(@types/node@20.8.4)(typescript@5.2.2) 25 | typescript: 26 | specifier: ^5.2.2 27 | version: 5.2.2 28 | 29 | packages: 30 | 31 | /@cspotcode/source-map-support@0.8.1: 32 | resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 33 | engines: {node: '>=12'} 34 | dependencies: 35 | '@jridgewell/trace-mapping': 0.3.9 36 | dev: true 37 | 38 | /@jridgewell/resolve-uri@3.1.1: 39 | resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} 40 | engines: {node: '>=6.0.0'} 41 | dev: true 42 | 43 | /@jridgewell/sourcemap-codec@1.4.15: 44 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 45 | dev: true 46 | 47 | /@jridgewell/trace-mapping@0.3.9: 48 | resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 49 | dependencies: 50 | '@jridgewell/resolve-uri': 3.1.1 51 | '@jridgewell/sourcemap-codec': 1.4.15 52 | dev: true 53 | 54 | /@tsconfig/node10@1.0.9: 55 | resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} 56 | dev: true 57 | 58 | /@tsconfig/node12@1.0.11: 59 | resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} 60 | dev: true 61 | 62 | /@tsconfig/node14@1.0.3: 63 | resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} 64 | dev: true 65 | 66 | /@tsconfig/node16@1.0.4: 67 | resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} 68 | dev: true 69 | 70 | /@types/node@20.8.4: 71 | resolution: {integrity: sha512-ZVPnqU58giiCjSxjVUESDtdPk4QR5WQhhINbc9UBrKLU68MX5BF6kbQzTrkwbolyr0X8ChBpXfavr5mZFKZQ5A==} 72 | dependencies: 73 | undici-types: 5.25.3 74 | dev: true 75 | 76 | /acorn-walk@8.2.0: 77 | resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} 78 | engines: {node: '>=0.4.0'} 79 | dev: true 80 | 81 | /acorn@8.10.0: 82 | resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} 83 | engines: {node: '>=0.4.0'} 84 | hasBin: true 85 | dev: true 86 | 87 | /arg@4.1.3: 88 | resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} 89 | dev: true 90 | 91 | /create-require@1.1.1: 92 | resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} 93 | dev: true 94 | 95 | /data-uri-to-buffer@4.0.1: 96 | resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} 97 | engines: {node: '>= 12'} 98 | dev: false 99 | 100 | /diff@4.0.2: 101 | resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} 102 | engines: {node: '>=0.3.1'} 103 | dev: true 104 | 105 | /dotenv@16.3.1: 106 | resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} 107 | engines: {node: '>=12'} 108 | dev: false 109 | 110 | /fetch-blob@3.2.0: 111 | resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} 112 | engines: {node: ^12.20 || >= 14.13} 113 | dependencies: 114 | node-domexception: 1.0.0 115 | web-streams-polyfill: 3.2.1 116 | dev: false 117 | 118 | /formdata-polyfill@4.0.10: 119 | resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} 120 | engines: {node: '>=12.20.0'} 121 | dependencies: 122 | fetch-blob: 3.2.0 123 | dev: false 124 | 125 | /make-error@1.3.6: 126 | resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} 127 | dev: true 128 | 129 | /node-domexception@1.0.0: 130 | resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} 131 | engines: {node: '>=10.5.0'} 132 | dev: false 133 | 134 | /node-fetch@3.3.2: 135 | resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} 136 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 137 | dependencies: 138 | data-uri-to-buffer: 4.0.1 139 | fetch-blob: 3.2.0 140 | formdata-polyfill: 4.0.10 141 | dev: false 142 | 143 | /prettier@3.0.3: 144 | resolution: {integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==} 145 | engines: {node: '>=14'} 146 | hasBin: true 147 | dev: true 148 | 149 | /ts-node@10.9.1(@types/node@20.8.4)(typescript@5.2.2): 150 | resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} 151 | hasBin: true 152 | peerDependencies: 153 | '@swc/core': '>=1.2.50' 154 | '@swc/wasm': '>=1.2.50' 155 | '@types/node': '*' 156 | typescript: '>=2.7' 157 | peerDependenciesMeta: 158 | '@swc/core': 159 | optional: true 160 | '@swc/wasm': 161 | optional: true 162 | dependencies: 163 | '@cspotcode/source-map-support': 0.8.1 164 | '@tsconfig/node10': 1.0.9 165 | '@tsconfig/node12': 1.0.11 166 | '@tsconfig/node14': 1.0.3 167 | '@tsconfig/node16': 1.0.4 168 | '@types/node': 20.8.4 169 | acorn: 8.10.0 170 | acorn-walk: 8.2.0 171 | arg: 4.1.3 172 | create-require: 1.1.1 173 | diff: 4.0.2 174 | make-error: 1.3.6 175 | typescript: 5.2.2 176 | v8-compile-cache-lib: 3.0.1 177 | yn: 3.1.1 178 | dev: true 179 | 180 | /typescript@5.2.2: 181 | resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} 182 | engines: {node: '>=14.17'} 183 | hasBin: true 184 | dev: true 185 | 186 | /undici-types@5.25.3: 187 | resolution: {integrity: sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==} 188 | dev: true 189 | 190 | /v8-compile-cache-lib@3.0.1: 191 | resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} 192 | dev: true 193 | 194 | /web-streams-polyfill@3.2.1: 195 | resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} 196 | engines: {node: '>= 8'} 197 | dev: false 198 | 199 | /yn@3.1.1: 200 | resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} 201 | engines: {node: '>=6'} 202 | dev: true 203 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from "fs"; 4 | import "dotenv/config"; 5 | import { createBackup } from "./index.js"; 6 | import { parseArgs } from "node:util"; 7 | 8 | const { values, positionals } = parseArgs({ 9 | options: { 10 | limit: { 11 | type: "string", 12 | short: "l", 13 | default: "1000", 14 | }, 15 | }, 16 | allowPositionals: true, 17 | }); 18 | 19 | const path = positionals[0]; 20 | if (!path) { 21 | console.error("Must supply path as first argument."); 22 | process.exit(1); 23 | } 24 | 25 | const limit = parseInt(values.limit ?? ""); 26 | if (Number.isNaN(limit)) { 27 | console.error("Limit must be a number."); 28 | process.exit(1); 29 | } 30 | if (limit <= 0) { 31 | console.error("Limit must be higher than 0."); 32 | process.exit(1); 33 | } 34 | 35 | const backup = await createBackup({ 36 | accountId: process.env.CLOUDFLARE_D1_ACCOUNT_ID!, 37 | databaseId: process.env.CLOUDFLARE_D1_DATABASE_ID!, 38 | apiKey: process.env.CLOUDFLARE_D1_API_KEY!, 39 | limit, 40 | }); 41 | 42 | fs.writeFileSync(path, backup); 43 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | 3 | export async function createBackup(options: { 4 | accountId: string; 5 | databaseId: string; 6 | apiKey: string; 7 | // Default to 1000 8 | limit?: number; 9 | }) { 10 | const limit = options.limit ?? 1000; 11 | const lines: string[] = []; 12 | function append(command: string) { 13 | lines.push(command); 14 | } 15 | 16 | async function fetchD1>( 17 | sql: string, 18 | params?: unknown[], 19 | ) { 20 | const response = await fetch( 21 | `https://api.cloudflare.com/client/v4/accounts/${options.accountId}/d1/database/${options.databaseId}/query`, 22 | { 23 | method: "POST", 24 | headers: { 25 | "Content-Type": "application/json", 26 | Authorization: `Bearer ${options.apiKey}`, 27 | }, 28 | body: JSON.stringify({ 29 | sql, 30 | params, 31 | }), 32 | }, 33 | ); 34 | 35 | const body = (await response.json()) as any; 36 | if ("errors" in body && body.errors.length) { 37 | throw new Error( 38 | `D1 Error: ${body.errors 39 | .map((error: { message: string }) => error.message) 40 | .join(", ")}`, 41 | ); 42 | } 43 | 44 | return body.result as { 45 | meta: {}; 46 | results: T[]; 47 | success: boolean; 48 | }[]; 49 | } 50 | 51 | let writableSchema: boolean = false; 52 | 53 | { 54 | const [tables] = await fetchD1<{ name: string; type: string; sql: string }>( 55 | "SELECT name, type, sql FROM sqlite_master WHERE sql IS NOT NULL AND type = 'table' ORDER BY rootpage DESC", 56 | ); 57 | 58 | for (let table of tables.results) { 59 | if (typeof table.name !== "string") { 60 | console.warn(`Table name is not string: ${table.name}`); 61 | continue; 62 | } 63 | if (table.name.startsWith("_cf_")) { 64 | continue; // we're not allowed access to these 65 | } else if (table.name === "sqlite_sequence") { 66 | append("DELETE FROM sqlite_sequence;"); 67 | } else if (table.name === "sqlite_stat1") { 68 | append("ANALYZE sqlite_master;"); 69 | } else if (table.name.startsWith("sqlite_")) { 70 | continue; 71 | } else if ( 72 | typeof table.sql === "string" && 73 | table.sql.startsWith("CREATE VIRTUAL TABLE") 74 | ) { 75 | if (!writableSchema) { 76 | append("PRAGMA writable_schema=ON;"); 77 | 78 | writableSchema = true; 79 | } 80 | 81 | const tableName = table.name.replace("'", "''"); 82 | 83 | append( 84 | `INSERT INTO sqlite_master (type, name, tbl_name, rootpage, sql) VALUES ('table', '${tableName}', '${tableName}', 0, '${table.sql.replace( 85 | /'/g, 86 | "''", 87 | )}');`, 88 | ); 89 | 90 | continue; 91 | } else if ( 92 | typeof table.sql === "string" && 93 | table.sql.toUpperCase().startsWith("CREATE TABLE ") 94 | ) { 95 | append( 96 | `CREATE TABLE IF NOT EXISTS ${table.sql.substring( 97 | "CREATE TABLE ".length, 98 | )};`, 99 | ); 100 | } else { 101 | append(`${table.sql};`); 102 | } 103 | 104 | const tableNameIndent = table.name.replace('"', '""'); 105 | 106 | // PRAGMA table_info is returning unauthorized on experimental D1 backend 107 | 108 | //const tableInfo = await originDatabase.prepare(`PRAGMA table_info("${tableNameIndent}")`).all(); 109 | //const columnNames = tableInfo.results.map((row) => row.name); 110 | 111 | const [tableRow] = await fetchD1( 112 | `SELECT * FROM "${tableNameIndent}" LIMIT 1`, 113 | ); 114 | 115 | if (tableRow.results[0]) { 116 | const columnNames = Object.keys(tableRow.results[0]); 117 | 118 | const [tableRowCount] = await fetchD1<{ count: number }>( 119 | `SELECT COUNT(*) AS count FROM "${tableNameIndent}"`, 120 | ); 121 | 122 | if (tableRowCount === null) { 123 | throw new Error("Failed to get table row count from table."); 124 | } 125 | 126 | for ( 127 | let offset = 0; 128 | offset <= tableRowCount.results[0].count; 129 | offset += limit 130 | ) { 131 | const queries = []; 132 | 133 | // D1 said maximum depth is 20, but the limit is seemingly at 9. 134 | for (let index = 0; index < columnNames.length; index += 9) { 135 | const currentColumnNames = columnNames.slice( 136 | index, 137 | Math.min(index + 9, columnNames.length), 138 | ); 139 | 140 | queries.push( 141 | `SELECT '${currentColumnNames 142 | .map( 143 | (columnName) => 144 | `'||quote("${columnName.replace('"', '""')}")||'`, 145 | ) 146 | .join( 147 | ", ", 148 | )}' AS partialCommand FROM "${tableNameIndent}" LIMIT ${limit} OFFSET ${offset}`, 149 | ); 150 | } 151 | 152 | const results = await fetchD1<{ partialCommand: string }>( 153 | queries.join(";\n"), 154 | ); 155 | 156 | if (results.length && results[0].results.length) { 157 | for (let result = 1; result < results.length; result++) { 158 | if ( 159 | results[result].results.length !== results[0].results.length 160 | ) { 161 | throw new Error( 162 | "Failed to split expression tree into several queries properly.", 163 | ); 164 | } 165 | } 166 | 167 | for (let row = 0; row < results[0].results.length; row++) { 168 | let columns: string[] = []; 169 | 170 | for (let result = 0; result < results.length; result++) { 171 | columns.push( 172 | results[result].results[row].partialCommand as string, 173 | ); 174 | } 175 | 176 | append( 177 | `INSERT INTO "${tableNameIndent}" (${columnNames 178 | .map((columnName) => `"${columnName}"`) 179 | .join(", ")}) VALUES (${columns 180 | .map((column) => column.replace("\n", "\\n")) 181 | .join(", ")});`, 182 | ); 183 | } 184 | } 185 | } 186 | } 187 | } 188 | } 189 | 190 | { 191 | const [schemas] = await fetchD1( 192 | "SELECT name, type, sql FROM sqlite_master WHERE sql IS NOT NULL AND type IN ('index', 'trigger', 'view')", 193 | ); 194 | 195 | if (schemas.results.length) { 196 | for (let schema of schemas.results) { 197 | append(`${schema.sql};`); 198 | } 199 | } 200 | } 201 | 202 | if (writableSchema) { 203 | append("PRAGMA writable_schema=OFF;"); 204 | } 205 | 206 | return lines.join("\n"); 207 | } 208 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2022", "DOM"], 5 | "module": "NodeNext", 6 | "strict": true, 7 | "outDir": "dist", 8 | "declaration": true 9 | } 10 | } 11 | --------------------------------------------------------------------------------