alchemy 0.82.0 → 0.82.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,16 +2,18 @@ import type { Context } from "../context.ts";
2
2
  import { Resource, ResourceKind } from "../resource.ts";
3
3
  import { Scope } from "../scope.ts";
4
4
  import { logger } from "../util/logger.ts";
5
- import { CloudflareApiError, handleApiError } from "./api-error.ts";
5
+ import { CloudflareApiError } from "./api-error.ts";
6
+ import { extractCloudflareResult } from "./api-response.ts";
6
7
  import {
7
8
  createCloudflareApi,
8
9
  type CloudflareApi,
9
10
  type CloudflareApiOptions,
10
11
  } from "./api.ts";
11
- import { withJurisdiction } from "./bucket.ts";
12
12
  import { cloneD1Database } from "./d1-clone.ts";
13
+ import { importD1Database } from "./d1-import.ts";
13
14
  import { applyLocalD1Migrations } from "./d1-local-migrations.ts";
14
- import { applyMigrations, listMigrationsFiles } from "./d1-migrations.ts";
15
+ import { applyMigrations } from "./d1-migrations.ts";
16
+ import { listSqlFiles, readSqlFile, type D1SqlFile } from "./d1-sql-file.ts";
15
17
  import { deleteMiniflareBinding } from "./miniflare/delete.ts";
16
18
 
17
19
  const DEFAULT_MIGRATIONS_TABLE = "d1_migrations";
@@ -24,7 +26,7 @@ type PrimaryLocationHint =
24
26
  | "weur"
25
27
  | "eeur"
26
28
  | "apac"
27
- | "auto"
29
+ | "oc"
28
30
  | (string & {});
29
31
 
30
32
  /**
@@ -85,11 +87,10 @@ export interface D1DatabaseProps extends CloudflareApiOptions {
85
87
  clone?: D1Database | { id: string } | { name: string };
86
88
 
87
89
  /**
88
- * These files will be generated internally with the D1Database wrapper function when migrationsDir is specified
89
- *
90
- * @private
90
+ * The names of SQL files to import.
91
+ * After migrations are applied, these files will be run using [Cloudflare's D1 import API](https://developers.cloudflare.com/d1/best-practices/import-export-data/).
91
92
  */
92
- migrationsFiles?: Array<{ id: string; sql: string }>;
93
+ importFiles?: string[];
93
94
 
94
95
  /**
95
96
  * Name of the table used to track migrations. Only used if migrationsDir is specified. Defaults to 'd1_migrations'
@@ -102,6 +103,7 @@ export interface D1DatabaseProps extends CloudflareApiOptions {
102
103
  * This is analogous to wrangler's `migrations_dir`.
103
104
  */
104
105
  migrationsDir?: string;
106
+
105
107
  /**
106
108
  * Whether to emulate the database locally when Alchemy is running in watch mode.
107
109
  */
@@ -139,6 +141,7 @@ export type D1Database = Pick<
139
141
  | "migrationsTable"
140
142
  | "primaryLocationHint"
141
143
  | "readReplication"
144
+ | "importFiles"
142
145
  > & {
143
146
  type: "d1";
144
147
  /**
@@ -253,13 +256,21 @@ export async function D1Database(
253
256
  id: string,
254
257
  props: Omit<D1DatabaseProps, "migrationsFiles"> = {},
255
258
  ): Promise<D1Database> {
256
- const migrationsFiles = props.migrationsDir
257
- ? await listMigrationsFiles(props.migrationsDir)
258
- : [];
259
+ const [migrationsFiles, importFiles] = await Promise.all([
260
+ props.migrationsDir ? await listSqlFiles(props.migrationsDir) : [],
261
+ props.importFiles
262
+ ? await Promise.all(
263
+ props.importFiles.map((file) =>
264
+ readSqlFile(Scope.current.rootDir, file),
265
+ ),
266
+ )
267
+ : [],
268
+ ]);
259
269
 
260
270
  return _D1Database(id, {
261
271
  ...props,
262
272
  migrationsFiles,
273
+ importFiles,
263
274
  dev: {
264
275
  ...(props.dev ?? {}),
265
276
  // force local migrations to run even if the database was already deployed live
@@ -274,7 +285,10 @@ const _D1Database = Resource(
274
285
  async function (
275
286
  this: Context<D1Database>,
276
287
  id: string,
277
- props: D1DatabaseProps,
288
+ props: Omit<D1DatabaseProps, "importFiles"> & {
289
+ migrationsFiles: D1SqlFile[] | undefined;
290
+ importFiles: D1SqlFile[] | undefined;
291
+ },
278
292
  ): Promise<D1Database> {
279
293
  const databaseName =
280
294
  props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
@@ -286,17 +300,18 @@ const _D1Database = Resource(
286
300
 
287
301
  const local = this.scope.local && !props.dev?.remote;
288
302
  const dev = {
289
- id: this.output?.dev?.id ?? this.output?.id ?? id,
303
+ id: this.output?.dev?.id ?? this.output?.id ?? databaseName,
290
304
  remote: props.dev?.remote ?? false,
291
305
  };
292
306
  const adopt = props.adopt ?? this.scope.adopt;
293
307
 
294
308
  if (local) {
295
- if (props.migrationsFiles && props.migrationsFiles.length > 0) {
309
+ if (props.migrationsFiles?.length || props.importFiles?.length) {
296
310
  await applyLocalD1Migrations({
297
311
  databaseId: dev.id,
298
312
  migrationsTable: props.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE,
299
- migrations: props.migrationsFiles,
313
+ migrations: props.migrationsFiles ?? [],
314
+ imports: props.importFiles ?? [],
300
315
  rootDir: this.scope.rootDir,
301
316
  });
302
317
  }
@@ -320,12 +335,12 @@ const _D1Database = Resource(
320
335
  await deleteMiniflareBinding(this.scope, "d1", this.output.dev.id);
321
336
  }
322
337
  if (props.delete !== false && this.output?.id) {
323
- await deleteDatabase(api, this.output.id, props);
338
+ await deleteDatabase(api, this.output.id);
324
339
  }
325
340
  // Return void (a deleted database has no content)
326
341
  return this.destroy();
327
342
  }
328
- let dbData: CloudflareD1Response;
343
+ let dbData: D1ResponseObject;
329
344
 
330
345
  if (
331
346
  this.phase === "create" ||
@@ -337,14 +352,9 @@ const _D1Database = Resource(
337
352
  try {
338
353
  dbData = await createDatabase(api, databaseName, props);
339
354
 
340
- // Read replication cannot be set during creation, so update it after creation
341
- if (props.readReplication && dbData.result.uuid) {
342
- dbData = await updateDatabase(api, dbData.result.uuid, props);
343
- }
344
-
345
355
  // If clone property is provided, perform cloning after database creation
346
- if (props.clone && dbData.result.uuid) {
347
- await cloneDb(api, props.clone, dbData.result.uuid, jurisdiction);
356
+ if (props.clone && dbData.uuid) {
357
+ await cloneDb(api, props.clone, dbData.uuid);
348
358
  }
349
359
  } catch (error) {
350
360
  // Check if this is a "database already exists" error and adopt is enabled
@@ -355,7 +365,7 @@ const _D1Database = Resource(
355
365
  ) {
356
366
  logger.log(`Database ${databaseName} already exists, adopting it`);
357
367
  // Find the existing database by name
358
- const databases = await listDatabases(api, databaseName, props);
368
+ const databases = await listDatabases(api, databaseName);
359
369
  const existingDb = databases.find((db) => db.name === databaseName);
360
370
 
361
371
  if (!existingDb) {
@@ -365,11 +375,15 @@ const _D1Database = Resource(
365
375
  }
366
376
 
367
377
  // Get the database details using its ID
368
- dbData = await getDatabase(api, existingDb.id, props);
378
+ dbData = await getDatabase(api, existingDb.uuid);
369
379
 
370
380
  // Update the database with the provided properties
371
381
  if (props.readReplication) {
372
- dbData = await updateDatabase(api, existingDb.id, props);
382
+ dbData = await updateReadReplicationMode(
383
+ api,
384
+ existingDb.uuid,
385
+ props.readReplication?.mode,
386
+ );
373
387
  }
374
388
  } else {
375
389
  // Re-throw the error if adopt is false or it's not a "database already exists" error
@@ -395,7 +409,11 @@ const _D1Database = Resource(
395
409
  );
396
410
  }
397
411
  // Update the database with new properties
398
- dbData = await updateDatabase(api, this.output.id, props);
412
+ dbData = await updateReadReplicationMode(
413
+ api,
414
+ this.output.id,
415
+ props.readReplication?.mode ?? "disabled", // disabled seems to be the default
416
+ );
399
417
  } else {
400
418
  // If no ID exists, fall back to creating a new database
401
419
  dbData = await createDatabase(api, databaseName, props);
@@ -406,17 +424,11 @@ const _D1Database = Resource(
406
424
  try {
407
425
  const migrationsTable =
408
426
  props.migrationsTable || DEFAULT_MIGRATIONS_TABLE;
409
- const databaseId = dbData.result.uuid || this.output?.id;
410
-
411
- if (!databaseId) {
412
- throw new Error("Database ID not found for migrations");
413
- }
414
-
415
427
  await applyMigrations({
416
428
  migrationsFiles: props.migrationsFiles,
417
429
  migrationsTable,
418
430
  accountId: api.accountId,
419
- databaseId,
431
+ databaseId: dbData.uuid,
420
432
  api,
421
433
  });
422
434
  } catch (migrationErr) {
@@ -424,13 +436,20 @@ const _D1Database = Resource(
424
436
  throw migrationErr;
425
437
  }
426
438
  }
427
- if (!dbData.result.uuid) {
428
- // TODO(sam): why would this ever happen?
429
- throw new Error("Database ID not found");
439
+ if (props.importFiles?.length) {
440
+ await Promise.all(
441
+ props.importFiles.map(async (file) => {
442
+ await importD1Database(api, {
443
+ databaseId: dbData.uuid,
444
+ sqlData: file.sql,
445
+ filename: file.id,
446
+ });
447
+ }),
448
+ );
430
449
  }
431
450
  return {
432
451
  type: "d1",
433
- id: dbData.result.uuid!,
452
+ id: dbData.uuid,
434
453
  name: databaseName,
435
454
  readReplication: props.readReplication,
436
455
  primaryLocationHint: props.primaryLocationHint,
@@ -442,22 +461,16 @@ const _D1Database = Resource(
442
461
  },
443
462
  );
444
463
 
445
- interface CloudflareD1Response {
446
- result: {
447
- uuid?: string;
448
- name: string;
449
- file_size: number;
450
- num_tables: number;
451
- version: string;
452
- primary_location_hint?: string;
453
- read_replication?: {
454
- mode: "auto" | "disabled";
455
- };
456
- jurisdiction?: string;
457
- };
458
- success: boolean;
459
- errors: Array<{ code: number; message: string }>;
460
- messages: string[];
464
+ interface D1ResponseObject {
465
+ uuid: string;
466
+ name: string;
467
+ created_at: string;
468
+ version: string;
469
+ num_tables: number;
470
+ file_size: number;
471
+ running_in_region: "EEUR" | "APAC" | "WNAM" | "ENAM" | "WEUR" | "EEUR" | "OC";
472
+ read_replication: { mode: "auto" | "disabled" };
473
+ jurisdiction: "eu" | "fedramp" | null;
461
474
  }
462
475
 
463
476
  /**
@@ -466,37 +479,45 @@ interface CloudflareD1Response {
466
479
  export async function createDatabase(
467
480
  api: CloudflareApi,
468
481
  databaseName: string,
469
- props: D1DatabaseProps,
470
- ): Promise<CloudflareD1Response> {
482
+ props: Pick<
483
+ D1DatabaseProps,
484
+ "jurisdiction" | "primaryLocationHint" | "readReplication"
485
+ >,
486
+ ): Promise<D1ResponseObject> {
471
487
  // Create new D1 database
472
- const createPayload: any = {
488
+ const createPayload: {
489
+ name: string;
490
+ jurisdiction?: "eu" | "fedramp";
491
+ primary_location_hint?:
492
+ | "wnam"
493
+ | "enam"
494
+ | "weur"
495
+ | "eeur"
496
+ | "apac"
497
+ | "oc"
498
+ | (string & {});
499
+ } = {
473
500
  name: databaseName,
474
501
  jurisdiction:
475
502
  props.jurisdiction !== "default" ? props.jurisdiction : undefined,
503
+ primary_location_hint: props.primaryLocationHint,
476
504
  };
477
-
478
- if (props.primaryLocationHint) {
479
- createPayload.primary_location_hint = props.primaryLocationHint;
480
- }
481
-
482
- const createResponse = await api.post(
483
- `/accounts/${api.accountId}/d1/database`,
484
- createPayload,
485
- {
486
- headers: withJurisdiction(props),
487
- },
505
+ const database = await extractCloudflareResult<D1ResponseObject>(
506
+ `create D1 database "${databaseName}"`,
507
+ api.post(`/accounts/${api.accountId}/d1/database`, createPayload),
488
508
  );
489
-
490
- if (!createResponse.ok) {
491
- return await handleApiError(
492
- createResponse,
493
- "creating",
494
- "D1 database",
495
- databaseName,
509
+ if (!database.uuid) {
510
+ // this is included in the spec as optional... why is it optional? we may never know...
511
+ throw new Error("Missing UUID for created database");
512
+ }
513
+ if (props.readReplication?.mode) {
514
+ return await updateReadReplicationMode(
515
+ api,
516
+ database.uuid,
517
+ props.readReplication?.mode,
496
518
  );
497
519
  }
498
-
499
- return (await createResponse.json()) as CloudflareD1Response;
520
+ return database;
500
521
  }
501
522
 
502
523
  /**
@@ -504,25 +525,12 @@ export async function createDatabase(
504
525
  */
505
526
  export async function getDatabase(
506
527
  api: CloudflareApi,
507
- databaseId?: string,
508
- props: D1DatabaseProps = {},
509
- ): Promise<CloudflareD1Response> {
510
- if (!databaseId) {
511
- throw new Error("Database ID is required");
512
- }
513
-
514
- const response = await api.get(
515
- `/accounts/${api.accountId}/d1/database/${databaseId}`,
516
- {
517
- headers: withJurisdiction(props),
518
- },
528
+ databaseId: string,
529
+ ): Promise<D1ResponseObject> {
530
+ return await extractCloudflareResult<D1ResponseObject>(
531
+ `get D1 database "${databaseId}"`,
532
+ api.get(`/accounts/${api.accountId}/d1/database/${databaseId}`),
519
533
  );
520
-
521
- if (!response.ok) {
522
- return await handleApiError(response, "getting", "D1 database", databaseId);
523
- }
524
-
525
- return (await response.json()) as CloudflareD1Response;
526
534
  }
527
535
 
528
536
  /**
@@ -530,31 +538,12 @@ export async function getDatabase(
530
538
  */
531
539
  export async function deleteDatabase(
532
540
  api: CloudflareApi,
533
- databaseId?: string,
534
- props: D1DatabaseProps = {},
541
+ databaseId: string,
535
542
  ): Promise<void> {
536
- if (!databaseId) {
537
- logger.log("No database ID provided, skipping delete");
538
- return;
539
- }
540
-
541
- // Delete D1 database
542
- const deleteResponse = await api.delete(
543
- `/accounts/${api.accountId}/d1/database/${databaseId}`,
544
- {
545
- headers: withJurisdiction(props),
546
- },
543
+ await extractCloudflareResult(
544
+ `delete D1 database "${databaseId}"`,
545
+ api.delete(`/accounts/${api.accountId}/d1/database/${databaseId}`),
547
546
  );
548
-
549
- if (!deleteResponse.ok && deleteResponse.status !== 404) {
550
- const errorData: any = await deleteResponse.json().catch(() => ({
551
- errors: [{ message: deleteResponse.statusText }],
552
- }));
553
- throw new CloudflareApiError(
554
- `Error deleting D1 database '${databaseId}': ${errorData.errors?.[0]?.message || deleteResponse.statusText}`,
555
- deleteResponse,
556
- );
557
- }
558
547
  }
559
548
 
560
549
  /**
@@ -563,44 +552,15 @@ export async function deleteDatabase(
563
552
  export async function listDatabases(
564
553
  api: CloudflareApi,
565
554
  name?: string,
566
- props: D1DatabaseProps = {},
567
- ): Promise<{ name: string; id: string }[]> {
555
+ ): Promise<D1ResponseObject[]> {
568
556
  // Construct query string if name is provided
569
557
  const queryParams = name ? `?name=${encodeURIComponent(name)}` : "";
570
558
 
571
- const response = await api.get(
572
- `/accounts/${api.accountId}/d1/database${queryParams}`,
573
- {
574
- headers: withJurisdiction(props),
575
- },
559
+ // TODO(john): handle pagination (wasn't handled originally)
560
+ return await extractCloudflareResult<D1ResponseObject[]>(
561
+ `list D1 databases${name ? ` with name "${name}"` : ""}`,
562
+ api.get(`/accounts/${api.accountId}/d1/database${queryParams}`),
576
563
  );
577
-
578
- if (!response.ok) {
579
- throw new CloudflareApiError(
580
- `Failed to list databases: ${response.statusText}`,
581
- response,
582
- );
583
- }
584
-
585
- const data = (await response.json()) as {
586
- success: boolean;
587
- errors?: Array<{ code: number; message: string }>;
588
- result?: Array<{
589
- name: string;
590
- uuid: string;
591
- }>;
592
- };
593
-
594
- if (!data.success) {
595
- const errorMessage = data.errors?.[0]?.message || "Unknown error";
596
- throw new Error(`Failed to list databases: ${errorMessage}`);
597
- }
598
-
599
- // Transform API response
600
- return (data.result || []).map((db) => ({
601
- name: db.name,
602
- id: db.uuid,
603
- }));
604
564
  }
605
565
 
606
566
  /**
@@ -608,38 +568,19 @@ export async function listDatabases(
608
568
  *
609
569
  * Note: According to Cloudflare API, only read_replication.mode can be modified during updates.
610
570
  */
611
- export async function updateDatabase(
571
+ export async function updateReadReplicationMode(
612
572
  api: CloudflareApi,
613
573
  databaseId: string,
614
- props: D1DatabaseProps,
615
- ): Promise<CloudflareD1Response> {
616
- const updatePayload: any = {};
617
-
618
- // Only include read_replication in update payload
619
- if (props.readReplication) {
620
- updatePayload.read_replication = {
621
- mode: props.readReplication.mode,
622
- };
623
- }
624
-
625
- const updateResponse = await api.patch(
626
- `/accounts/${api.accountId}/d1/database/${databaseId}`,
627
- updatePayload,
628
- {
629
- headers: withJurisdiction(props),
630
- },
574
+ readReplicationMode: "auto" | "disabled",
575
+ ): Promise<D1ResponseObject> {
576
+ return await extractCloudflareResult<D1ResponseObject>(
577
+ `update read replication mode for D1 database "${databaseId}"`,
578
+ api.patch(`/accounts/${api.accountId}/d1/database/${databaseId}`, {
579
+ read_replication: {
580
+ mode: readReplicationMode,
581
+ },
582
+ }),
631
583
  );
632
-
633
- if (!updateResponse.ok) {
634
- return await handleApiError(
635
- updateResponse,
636
- "updating",
637
- "D1 database",
638
- databaseId,
639
- );
640
- }
641
-
642
- return (await updateResponse.json()) as CloudflareD1Response;
643
584
  }
644
585
 
645
586
  /**
@@ -654,7 +595,6 @@ async function cloneDb(
654
595
  api: CloudflareApi,
655
596
  sourceDb: D1Database | { id: string } | { name: string },
656
597
  targetDbId: string,
657
- jurisdiction: D1DatabaseJurisdiction,
658
598
  ): Promise<void> {
659
599
  let sourceId: string;
660
600
 
@@ -664,9 +604,7 @@ async function cloneDb(
664
604
  sourceId = sourceDb.id;
665
605
  } else if ("name" in sourceDb && sourceDb.name) {
666
606
  // Look up ID by name
667
- const databases = await listDatabases(api, sourceDb.name, {
668
- jurisdiction,
669
- });
607
+ const databases = await listDatabases(api, sourceDb.name);
670
608
  const foundDb = databases.find((db) => db.name === sourceDb.name);
671
609
 
672
610
  if (!foundDb) {
@@ -675,7 +613,7 @@ async function cloneDb(
675
613
  );
676
614
  }
677
615
 
678
- sourceId = foundDb.id;
616
+ sourceId = foundDb.uuid;
679
617
  } else if ("type" in sourceDb && sourceDb.type === "d1" && "id" in sourceDb) {
680
618
  // It's a D1Database object
681
619
  sourceId = sourceDb.id;
@@ -1,11 +1,13 @@
1
1
  import * as mf from "miniflare";
2
+ import type { D1SqlFile } from "./d1-sql-file.ts";
2
3
  import { getDefaultPersistPath } from "./miniflare/paths.ts";
3
4
 
4
5
  export interface D1LocalMigrationOptions {
5
6
  rootDir: string;
6
7
  databaseId: string;
7
8
  migrationsTable: string;
8
- migrations: { id: string; sql: string }[];
9
+ migrations: Array<D1SqlFile>;
10
+ imports: Array<D1SqlFile>;
9
11
  }
10
12
 
11
13
  export const applyLocalD1Migrations = async (
@@ -21,34 +23,70 @@ export const applyLocalD1Migrations = async (
21
23
  });
22
24
  try {
23
25
  await miniflare.ready;
24
- // TODO(sam): don't use `any` once prisma is fixed upstream
25
- const db: any = await miniflare.getD1Database("DB");
26
- const session: any = db.withSession("first-primary");
27
- await session
28
- .prepare(
29
- `CREATE TABLE IF NOT EXISTS ${options.migrationsTable} (
26
+ const db = await miniflare.getD1Database("DB");
27
+ const session = db.withSession("first-primary");
28
+ const tableInfo = await session
29
+ .prepare(`PRAGMA table_info(${options.migrationsTable});`)
30
+ .all<{
31
+ cid: number;
32
+ name: string;
33
+ type: string;
34
+ notnull: number;
35
+ dflt_value: string | null;
36
+ pk: number;
37
+ }>();
38
+ if (tableInfo.results.length === 0) {
39
+ await session
40
+ .prepare(
41
+ `CREATE TABLE ${options.migrationsTable} (
30
42
  id INTEGER PRIMARY KEY AUTOINCREMENT,
31
43
  name TEXT NOT NULL,
32
- applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
44
+ applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
45
+ type TEXT NOT NULL
33
46
  )`,
34
- )
35
- .run();
36
- const appliedMigrations: {
37
- results: { name: string }[];
47
+ )
48
+ .run();
49
+ } else if (!tableInfo.results.some((col) => col.name === "type")) {
50
+ await session
51
+ .prepare(
52
+ `ALTER TABLE ${options.migrationsTable} ADD COLUMN type TEXT NOT NULL DEFAULT 'migration';`,
53
+ )
54
+ .run();
55
+ }
56
+ const applied: {
57
+ results: { name: string; type: "migration" | "import" }[];
38
58
  } = await session
39
59
  .prepare(
40
- `SELECT name FROM ${options.migrationsTable} ORDER BY applied_at ASC`,
60
+ `SELECT name, type FROM ${options.migrationsTable} ORDER BY applied_at ASC`,
41
61
  )
42
62
  .all();
43
63
  const insertRecord = session.prepare(
44
- `INSERT INTO ${options.migrationsTable} (name) VALUES (?)`,
64
+ `INSERT INTO ${options.migrationsTable} (name, type) VALUES (?, ?)`,
45
65
  );
46
- for (const migration of options.migrations) {
47
- if (appliedMigrations.results.some((m) => m.name === migration.id)) {
66
+ for (const { id, sql } of options.migrations) {
67
+ if (applied.results.some((m) => m.name === id)) {
68
+ continue;
69
+ }
70
+ const statements = sql
71
+ .split("--> statement-breakpoint")
72
+ .filter((s) => s.trim())
73
+ .map((s) => session.prepare(s));
74
+ statements.push(insertRecord.bind(id, "migration"));
75
+ await session.batch(statements);
76
+ }
77
+ for (const { id, sql, hash } of options.imports) {
78
+ const name = `${id}-${hash}`;
79
+ if (applied.results.some((m) => m.name === name)) {
48
80
  continue;
49
81
  }
50
- await session.prepare(migration.sql).run();
51
- await insertRecord.bind(migration.id).run();
82
+ // Split into statements to prevent D1_ERROR: statement too long: SQLITE_TOOBIG.
83
+ // This is split naively by semicolons followed by newlines - not perfect but should work 99% of the time.
84
+ const statements = sql
85
+ .split(/;\r?\n/)
86
+ .filter((s) => s.trim())
87
+ .map((s) => session.prepare(s));
88
+ statements.push(insertRecord.bind(name, "import"));
89
+ await session.batch(statements);
52
90
  }
53
91
  } finally {
54
92
  await miniflare.dispose();
@@ -1,6 +1,3 @@
1
- import { glob } from "glob";
2
- import * as fs from "node:fs/promises";
3
- import path from "pathe";
4
1
  import { logger } from "../util/logger.ts";
5
2
  import { handleApiError } from "./api-error.ts";
6
3
  import type { CloudflareApi } from "./api.ts";
@@ -14,16 +11,6 @@ export interface D1MigrationOptions {
14
11
  quiet?: boolean;
15
12
  }
16
13
 
17
- const getPrefix = (name: string) => {
18
- const prefix = name.split("_")[0];
19
- const num = Number.parseInt(prefix, 10);
20
- return Number.isNaN(num) ? null : num;
21
- };
22
-
23
- async function readMigrationFile(filePath: string): Promise<string> {
24
- return fs.readFile(filePath, "utf-8");
25
- }
26
-
27
14
  /**
28
15
  * Detects the current schema of the migration table.
29
16
  * Returns info about the table structure to determine if migration is needed.
@@ -153,36 +140,6 @@ async function migrateLegacySchema(
153
140
  }
154
141
  }
155
142
 
156
- /**
157
- * Reads migration SQL files from the migrationsDir, sorted by filename.
158
- * @param migrationsDir Directory containing .sql migration files
159
- */
160
- export async function listMigrationsFiles(
161
- migrationsDir: string,
162
- ): Promise<Array<{ id: string; sql: string }>> {
163
- const entries = await glob("**/*.sql", {
164
- cwd: migrationsDir,
165
- });
166
-
167
- const sqlFiles = entries.sort((a: string, b: string) => {
168
- const aNum = getPrefix(a);
169
- const bNum = getPrefix(b);
170
-
171
- if (aNum !== null && bNum !== null) return aNum - bNum;
172
- if (aNum !== null) return -1;
173
- if (bNum !== null) return 1;
174
-
175
- return a.localeCompare(b);
176
- });
177
-
178
- return await Promise.all(
179
- sqlFiles.map(async (file) => ({
180
- id: file,
181
- sql: await readMigrationFile(path.join(migrationsDir, file)),
182
- })),
183
- );
184
- }
185
-
186
143
  /**
187
144
  * Ensures the migrations table exists in the D1 database with wrangler-compatible schema.
188
145
  * Handles migration from legacy 2-column schema to 3-column schema if needed.