@c9up/atlas 0.1.19 → 0.2.0

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.
Files changed (121) hide show
  1. package/README.md +55 -14
  2. package/db.darwin-arm64.node +0 -0
  3. package/db.darwin-x64.node +0 -0
  4. package/db.linux-arm64-gnu.node +0 -0
  5. package/db.linux-x64-gnu.node +0 -0
  6. package/db.win32-x64-msvc.node +0 -0
  7. package/dist/AtlasProvider.d.ts +6 -0
  8. package/dist/AtlasProvider.d.ts.map +1 -1
  9. package/dist/AtlasProvider.js +2 -2
  10. package/dist/AtlasProvider.js.map +1 -1
  11. package/dist/BaseEntity.d.ts +164 -5
  12. package/dist/BaseEntity.d.ts.map +1 -1
  13. package/dist/BaseEntity.js +339 -33
  14. package/dist/BaseEntity.js.map +1 -1
  15. package/dist/BaseModel.d.ts +91 -0
  16. package/dist/BaseModel.d.ts.map +1 -0
  17. package/dist/BaseModel.js +193 -0
  18. package/dist/BaseModel.js.map +1 -0
  19. package/dist/BaseRepository.d.ts +71 -15
  20. package/dist/BaseRepository.d.ts.map +1 -1
  21. package/dist/BaseRepository.js +1355 -384
  22. package/dist/BaseRepository.js.map +1 -1
  23. package/dist/ModelQuery.d.ts +341 -18
  24. package/dist/ModelQuery.d.ts.map +1 -1
  25. package/dist/ModelQuery.js +1406 -177
  26. package/dist/ModelQuery.js.map +1 -1
  27. package/dist/Transaction.d.ts +17 -0
  28. package/dist/Transaction.d.ts.map +1 -1
  29. package/dist/Transaction.js +57 -5
  30. package/dist/Transaction.js.map +1 -1
  31. package/dist/adapters/NapiDbAdapter.d.ts +33 -4
  32. package/dist/adapters/NapiDbAdapter.d.ts.map +1 -1
  33. package/dist/adapters/NapiDbAdapter.js +101 -11
  34. package/dist/adapters/NapiDbAdapter.js.map +1 -1
  35. package/dist/console/migrationCommands.d.ts +48 -0
  36. package/dist/console/migrationCommands.d.ts.map +1 -0
  37. package/dist/console/migrationCommands.js +220 -0
  38. package/dist/console/migrationCommands.js.map +1 -0
  39. package/dist/decorators/entity.d.ts +37 -6
  40. package/dist/decorators/entity.d.ts.map +1 -1
  41. package/dist/decorators/entity.js +32 -2
  42. package/dist/decorators/entity.js.map +1 -1
  43. package/dist/events.d.ts +64 -0
  44. package/dist/events.d.ts.map +1 -0
  45. package/dist/events.js +82 -0
  46. package/dist/events.js.map +1 -0
  47. package/dist/index.d.ts +5 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +3 -0
  50. package/dist/index.js.map +1 -1
  51. package/dist/metadata-keys.d.ts +3 -2
  52. package/dist/metadata-keys.d.ts.map +1 -1
  53. package/dist/naming/NamingStrategy.d.ts +7 -0
  54. package/dist/naming/NamingStrategy.d.ts.map +1 -1
  55. package/dist/naming/NamingStrategy.js +16 -0
  56. package/dist/naming/NamingStrategy.js.map +1 -1
  57. package/dist/schema/Migration.d.ts +26 -3
  58. package/dist/schema/Migration.d.ts.map +1 -1
  59. package/dist/schema/Migration.js +33 -24
  60. package/dist/schema/Migration.js.map +1 -1
  61. package/dist/schema/MigrationRunner.d.ts +43 -32
  62. package/dist/schema/MigrationRunner.d.ts.map +1 -1
  63. package/dist/schema/MigrationRunner.js +211 -26
  64. package/dist/schema/MigrationRunner.js.map +1 -1
  65. package/dist/schema/Schema.d.ts +57 -0
  66. package/dist/schema/Schema.d.ts.map +1 -1
  67. package/dist/schema/Schema.js +138 -3
  68. package/dist/schema/Schema.js.map +1 -1
  69. package/dist/schema/TableBuilder.d.ts +247 -8
  70. package/dist/schema/TableBuilder.d.ts.map +1 -1
  71. package/dist/schema/TableBuilder.js +607 -41
  72. package/dist/schema/TableBuilder.js.map +1 -1
  73. package/dist/schema/catalog.d.ts +47 -0
  74. package/dist/schema/catalog.d.ts.map +1 -0
  75. package/dist/schema/catalog.js +111 -0
  76. package/dist/schema/catalog.js.map +1 -0
  77. package/dist/schema/types.d.ts +150 -1
  78. package/dist/schema/types.d.ts.map +1 -1
  79. package/dist/schema/types.js +11 -0
  80. package/dist/schema/types.js.map +1 -1
  81. package/dist/services/db.d.ts +6 -0
  82. package/dist/services/db.d.ts.map +1 -1
  83. package/dist/services/db.js +17 -0
  84. package/dist/services/db.js.map +1 -1
  85. package/dist/testing/DatabaseCleanup.d.ts +7 -4
  86. package/dist/testing/DatabaseCleanup.d.ts.map +1 -1
  87. package/dist/testing/DatabaseCleanup.js +21 -18
  88. package/dist/testing/DatabaseCleanup.js.map +1 -1
  89. package/dist/testing/Factory.d.ts +70 -5
  90. package/dist/testing/Factory.d.ts.map +1 -1
  91. package/dist/testing/Factory.js +209 -10
  92. package/dist/testing/Factory.js.map +1 -1
  93. package/index.darwin-arm64.node +0 -0
  94. package/index.darwin-x64.node +0 -0
  95. package/index.linux-arm64-gnu.node +0 -0
  96. package/index.linux-x64-gnu.node +0 -0
  97. package/index.win32-x64-msvc.node +0 -0
  98. package/package.json +3 -1
  99. package/scripts/guard-publish.mjs +15 -0
  100. package/src/AtlasProvider.ts +8 -1
  101. package/src/BaseEntity.ts +431 -41
  102. package/src/BaseModel.ts +324 -0
  103. package/src/BaseRepository.ts +1589 -419
  104. package/src/ModelQuery.ts +1817 -203
  105. package/src/Transaction.ts +68 -5
  106. package/src/adapters/NapiDbAdapter.ts +159 -10
  107. package/src/console/migrationCommands.ts +258 -0
  108. package/src/decorators/entity.ts +53 -6
  109. package/src/events.ts +112 -0
  110. package/src/index.ts +19 -0
  111. package/src/metadata-keys.ts +3 -2
  112. package/src/naming/NamingStrategy.ts +23 -0
  113. package/src/schema/Migration.ts +42 -3
  114. package/src/schema/MigrationRunner.ts +270 -27
  115. package/src/schema/Schema.ts +210 -3
  116. package/src/schema/TableBuilder.ts +735 -41
  117. package/src/schema/catalog.ts +166 -0
  118. package/src/schema/types.ts +137 -2
  119. package/src/services/db.ts +28 -0
  120. package/src/testing/DatabaseCleanup.ts +23 -22
  121. package/src/testing/Factory.ts +332 -15
@@ -10,6 +10,7 @@ import {
10
10
  COMPUTED_KEY,
11
11
  type ColumnSerializeConfig,
12
12
  } from "../metadata-keys.js";
13
+ import { getNamingStrategy } from "../naming/NamingStrategy.js";
13
14
 
14
15
  const ENTITY_KEY = Symbol("atlas:entity");
15
16
  const COLUMNS_KEY = Symbol("atlas:columns");
@@ -62,8 +63,14 @@ export interface ColumnAdapter {
62
63
  *
63
64
  * MUST stay synchronous; the bind layer cannot await before handing values
64
65
  * to the Rust DML compiler. Returning a Promise throws.
66
+ *
67
+ * Signature mirrors Adonis Lucid: `(value, attribute, model)`. `attribute` is
68
+ * the model property key; `model` is the entity instance being persisted, or
69
+ * `undefined` on query-builder paths that carry no instance (e.g.
70
+ * `updateWhere`). Both extra args are optional — a one-argument adapter keeps
71
+ * working unchanged.
65
72
  */
66
- prepare?: (value: unknown) => unknown;
73
+ prepare?: (value: unknown, attribute?: string, model?: unknown) => unknown;
67
74
  /**
68
75
  * Transform the raw DB value into the model attribute (DB → model). Mirror
69
76
  * of Adonis Lucid's `@column.consume`. For entity columns, runs in
@@ -76,8 +83,12 @@ export interface ColumnAdapter {
76
83
  * null/undefined inputs to preserve nullable semantics.
77
84
  *
78
85
  * MUST stay synchronous (same constraint as `prepare`).
86
+ *
87
+ * Signature mirrors Adonis Lucid: `(value, attribute, model)`. `attribute` is
88
+ * the model property key; `model` is the entity being hydrated. Both extra
89
+ * args are optional — a one-argument adapter keeps working unchanged.
79
90
  */
80
- consume?: (value: unknown) => unknown;
91
+ consume?: (value: unknown, attribute?: string, model?: unknown) => unknown;
81
92
  }
82
93
 
83
94
  export interface ColumnMetadata extends ColumnAdapter {
@@ -86,17 +97,25 @@ export interface ColumnMetadata extends ColumnAdapter {
86
97
  nullable?: boolean;
87
98
  default?: unknown;
88
99
  serializeAs?: string | null;
89
- serialize?: (value: unknown) => unknown;
100
+ serialize?: (value: unknown, attribute?: string, model?: unknown) => unknown;
101
+ /** Explicit DB column name override (AdonisJS Lucid `columnName`). */
102
+ columnName?: string;
90
103
  }
91
104
 
92
105
  export interface ColumnOptions extends ColumnAdapter {
93
106
  type?: string;
94
107
  nullable?: boolean;
95
108
  default?: unknown;
109
+ /**
110
+ * Map this property to an explicitly-named DB column (AdonisJS Lucid
111
+ * `columnName`), instead of the default `camelCase → snake_case` convention.
112
+ * For legacy/non-conventional schemas, e.g. `@Column({ columnName: 'USR_MAIL' })`.
113
+ */
114
+ columnName?: string;
96
115
  /** Rename this column at `toJSON` time. Use `null` to hide it entirely. */
97
116
  serializeAs?: string | null;
98
117
  /** Transform the value at `toJSON` time (e.g. mask a phone number, coerce a Date). */
99
- serialize?: (value: unknown) => unknown;
118
+ serialize?: (value: unknown, attribute?: string, model?: unknown) => unknown;
100
119
  }
101
120
 
102
121
  type Constructor = new (...args: unknown[]) => unknown;
@@ -202,6 +221,7 @@ export function Column(options?: ColumnOptions): PropertyDecorator {
202
221
  serialize: options?.serialize,
203
222
  prepare: options?.prepare,
204
223
  consume: options?.consume,
224
+ columnName: options?.columnName,
205
225
  });
206
226
  Reflect.defineMetadata(COLUMNS_KEY, columns, target.constructor);
207
227
  }
@@ -445,7 +465,12 @@ export function HasMany(
445
465
  };
446
466
  }
447
467
 
448
- /** @HasOneThrough(() => Related, () => Through, { firstKey, secondKey, localKey, secondLocalKey, onQuery }) */
468
+ /**
469
+ * `@HasOneThrough(() => Related, () => Through, { firstKey, secondKey, localKey, secondLocalKey, onQuery })`
470
+ *
471
+ * An atlas addition — Lucid has no hasOneThrough relation. Same two-hop
472
+ * traversal as {@link HasManyThrough}, returning a single row.
473
+ */
449
474
  export function HasOneThrough(
450
475
  target: () => Constructor,
451
476
  through: () => Constructor,
@@ -490,6 +515,10 @@ export function ManyToMany(
490
515
  type: "manyToMany",
491
516
  target,
492
517
  pivot: options,
518
+ // `localKey` selects which parent column the pivot FK references
519
+ // (default: the parent PK). Without copying it here the value the type
520
+ // accepts is silently dropped and the pivot always targets the PK.
521
+ localKey: options.localKey,
493
522
  onQuery: options.onQuery,
494
523
  serializeAs: options.serializeAs,
495
524
  });
@@ -510,13 +539,31 @@ export function getEntityMetadata(
510
539
  return Reflect.getMetadata(ENTITY_KEY, target);
511
540
  }
512
541
 
542
+ /**
543
+ * Return the class's `@Entity` metadata, SYNTHESIZING it when the decorator is
544
+ * absent: the table name is inferred from the class name via the naming strategy
545
+ * (or an explicit `static table`). AdonisJS Lucid parity — a model needs no
546
+ * explicit `@Entity('table')`. Used by both `BaseModel` and the `BaseRepository`
547
+ * constructor so the Data-Mapper and Active-Record paths share one convention.
548
+ */
549
+ export function ensureEntityMetadata(target: Constructor): EntityMetadata {
550
+ const existing = getEntityMetadata(target);
551
+ if (existing) return existing;
552
+ const staticTable = (target as { table?: string }).table;
553
+ const table = staticTable ?? getNamingStrategy(target).tableName(target.name);
554
+ Entity(table)(target);
555
+ return getEntityMetadata(target) ?? { tableName: table };
556
+ }
557
+
513
558
  /** Get column metadata for a class (returns a copy). */
514
559
  export function getColumnMetadata(target: Constructor): ColumnMetadata[] {
515
560
  return [...(Reflect.getMetadata(COLUMNS_KEY, target) ?? [])];
516
561
  }
517
562
 
518
- /** Get primary key property name. */
563
+ /** Get primary key property name. A `static primaryKey` (AdonisJS Lucid) wins over the `@PrimaryKey()` decorator. */
519
564
  export function getPrimaryKey(target: Constructor): string | undefined {
565
+ const staticPk = (target as { primaryKey?: string }).primaryKey;
566
+ if (staticPk) return staticPk;
520
567
  return Reflect.getMetadata(PRIMARY_KEY, target);
521
568
  }
522
569
 
package/src/events.ts ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Query observability — atlas's equivalent of Lucid's `db:query` event.
3
+ *
4
+ * Agnostic by design: atlas cannot import the framework's emitter (it is a
5
+ * standalone package), so it owns a tiny listener registry instead. An
6
+ * integration package bridges it to whatever emitter the app uses:
7
+ *
8
+ * onDbQuery((event) => emitter.emit('db:query', event))
9
+ *
10
+ * Emission is opt-in. It costs a `performance.now()` pair per query, so it only
11
+ * happens when a connection is configured with `debug: true` or a single query
12
+ * asks via `.debug()` — and even then, only if someone is listening.
13
+ */
14
+
15
+ /**
16
+ * A single executed SQL statement. Mirrors Lucid's `db:query` payload, minus
17
+ * the fields that only make sense inside AdonisJS.
18
+ */
19
+ export interface DbQueryEvent {
20
+ /** The SQL as sent to the driver, placeholders included. */
21
+ sql: string;
22
+ /** The bound parameters. Never interpolated into `sql`. */
23
+ bindings: readonly unknown[];
24
+ /** Wall-clock duration in milliseconds, including the NAPI round-trip. */
25
+ duration: number;
26
+ /** Connection name, when the app named it. */
27
+ connection?: string;
28
+ /** Entity class name, when the query came from a repository/model. */
29
+ model?: string;
30
+ /** The call that produced it (`exec`, `first`, `paginate`, …). */
31
+ method?: string;
32
+ /** True for schema statements (migrations), false for DML/queries. */
33
+ ddl?: boolean;
34
+ /** True when the statement ran inside an interactive transaction. */
35
+ inTransaction?: boolean;
36
+ /** Set when the statement threw — the event is emitted either way. */
37
+ error?: Error;
38
+ }
39
+
40
+ export type DbQueryListener = (event: DbQueryEvent) => void;
41
+
42
+ const listeners = new Set<DbQueryListener>();
43
+
44
+ /**
45
+ * Subscribe to every observed query. Returns an unsubscribe function.
46
+ *
47
+ * A listener that throws would otherwise take down the query that triggered it,
48
+ * so throws are swallowed — observability must never change behaviour.
49
+ */
50
+ export function onDbQuery(listener: DbQueryListener): () => void {
51
+ listeners.add(listener);
52
+ return () => {
53
+ listeners.delete(listener);
54
+ };
55
+ }
56
+
57
+ /** Remove every listener. Intended for test teardown. */
58
+ export function clearDbQueryListeners(): void {
59
+ listeners.clear();
60
+ }
61
+
62
+ /**
63
+ * Whether anyone is listening. Checked before timing a query so the
64
+ * instrumentation costs nothing when unused.
65
+ */
66
+ export function hasDbQueryListeners(): boolean {
67
+ return listeners.size > 0;
68
+ }
69
+
70
+ /** Emit to every listener. Package-internal. */
71
+ export function emitDbQuery(event: DbQueryEvent): void {
72
+ for (const listener of listeners) {
73
+ try {
74
+ listener(event);
75
+ } catch {
76
+ // A broken listener must not fail the query it is reporting on.
77
+ }
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Render a query event as a single log line (Lucid's `prettyPrint`).
83
+ *
84
+ * The bindings are appended as JSON, NOT interpolated into the SQL: an
85
+ * interpolated line reads like runnable SQL while having none of the escaping
86
+ * that made the real statement safe, and it is exactly the string someone
87
+ * copies into a console later.
88
+ */
89
+ export function prettyPrintQuery(event: DbQueryEvent): string {
90
+ const parts = [`${event.duration.toFixed(2)}ms`];
91
+ if (event.connection) parts.push(event.connection);
92
+ if (event.model) parts.push(event.model);
93
+ if (event.method) parts.push(event.method);
94
+ if (event.inTransaction) parts.push("trx");
95
+ if (event.error) parts.push(`ERROR: ${event.error.message}`);
96
+
97
+ const head = `[atlas] ${parts.join(" ")}`;
98
+ const bindings =
99
+ event.bindings.length > 0 ? ` -- ${safeJson(event.bindings)}` : "";
100
+ return `${head} ${event.sql}${bindings}`;
101
+ }
102
+
103
+ /** JSON that can't throw on a circular / non-serialisable binding. */
104
+ function safeJson(value: unknown): string {
105
+ try {
106
+ return JSON.stringify(value, (_k, v) =>
107
+ typeof v === "bigint" ? `${v}n` : v,
108
+ );
109
+ } catch {
110
+ return "[unserialisable bindings]";
111
+ }
112
+ }
package/src/index.ts CHANGED
@@ -10,14 +10,27 @@ export { SQLITE_PROD_PRAGMAS } from "./AtlasProvider.js";
10
10
  export type {
11
11
  AsyncDatabaseConnection,
12
12
  ConnectRetryOptions,
13
+ ObservabilityOptions,
14
+ QueryMeta,
13
15
  } from "./adapters/NapiDbAdapter.js";
14
16
  export { createNapiConnection } from "./adapters/NapiDbAdapter.js";
15
17
  export type { DomainEvent } from "./BaseEntity.js";
16
18
  export { BaseEntity } from "./BaseEntity.js";
19
+ export { BaseModel } from "./BaseModel.js";
17
20
  export type { DatabaseConnection } from "./BaseRepository.js";
18
21
  export { BaseRepository } from "./BaseRepository.js";
19
22
  export { defineConfig } from "./config.js";
20
23
  export { configure } from "./configure.js";
24
+ export {
25
+ dbWipeCommand,
26
+ type MigrationCommandOptions,
27
+ makeMigrationCommand,
28
+ migrationRefreshCommand,
29
+ migrationResetCommand,
30
+ migrationRollbackCommand,
31
+ migrationRunCommand,
32
+ migrationStatusCommand,
33
+ } from "./console/migrationCommands.js";
21
34
  export {
22
35
  type AtlasCommand,
23
36
  schemaCheckCommand,
@@ -77,6 +90,12 @@ export {
77
90
  OptimisticLockError,
78
91
  RelationNotLoadedError,
79
92
  } from "./errors.js";
93
+ export type { DbQueryEvent, DbQueryListener } from "./events.js";
94
+ export {
95
+ clearDbQueryListeners,
96
+ onDbQuery,
97
+ prettyPrintQuery,
98
+ } from "./events.js";
80
99
  export {
81
100
  isAtlasStrictMode,
82
101
  ModelQuery,
@@ -17,6 +17,7 @@ export const COLUMN_SERIALIZE_KEY = Symbol.for("atlas:columnSerialize");
17
17
  export interface ColumnSerializeConfig {
18
18
  /** Rename this column at toJSON time (e.g. `password` → `passwordHash`). Null = hidden. */
19
19
  serializeAs?: string | null;
20
- /** Transform function applied to the value at toJSON time. */
21
- serialize?: (value: unknown) => unknown;
20
+ /** Transform function applied to the value at toJSON time. Signature mirrors
21
+ * Adonis Lucid: `(value, attribute, model)`; extra args optional. */
22
+ serialize?: (value: unknown, attribute?: string, model?: unknown) => unknown;
22
23
  }
@@ -38,6 +38,12 @@ export interface NamingStrategy {
38
38
  ): string;
39
39
  /** Default pivot table name for a manyToMany relation. */
40
40
  relationPivotTable(aClass: string, bClass: string): string;
41
+ /**
42
+ * Remap the pagination `meta` key names emitted by `Paginator.toJSON()`
43
+ * (AdonisJS Lucid `paginationMetaKeys`), e.g. `{ total: 'count', perPage:
44
+ * 'per_page' }`. Optional — omitted keys keep their default name.
45
+ */
46
+ paginationMetaKeys?(): Record<string, string>;
41
47
  }
42
48
 
43
49
  /**
@@ -84,6 +90,23 @@ export class CamelCaseNamingStrategy implements NamingStrategy {
84
90
  const [x, y] = [camelToSnake(aClass), camelToSnake(bClass)].sort();
85
91
  return `${x}_${y}`;
86
92
  }
93
+
94
+ paginationMetaKeys(): Record<string, string> {
95
+ // snake_case, matching Adonis Lucid's default paginator meta shape — even
96
+ // though model attributes serialize as camelCase, Lucid emits pagination
97
+ // meta in snake_case. Override this method to customize.
98
+ return {
99
+ total: "total",
100
+ perPage: "per_page",
101
+ currentPage: "current_page",
102
+ lastPage: "last_page",
103
+ firstPage: "first_page",
104
+ firstPageUrl: "first_page_url",
105
+ lastPageUrl: "last_page_url",
106
+ nextPageUrl: "next_page_url",
107
+ previousPageUrl: "previous_page_url",
108
+ };
109
+ }
87
110
  }
88
111
 
89
112
  /** The default singleton — used when an entity doesn't override `static namingStrategy`. */
@@ -6,6 +6,7 @@
6
6
 
7
7
  import type { AtlasDialect } from "../query/native.js";
8
8
  import { RawSql } from "../query/QueryBuilder.js";
9
+ import type { CatalogConnection } from "./catalog.js";
9
10
  import { Schema } from "./SchemaBuilder.js";
10
11
 
11
12
  /**
@@ -29,9 +30,18 @@ import { Schema } from "./SchemaBuilder.js";
29
30
  * }
30
31
  * }
31
32
  */
33
+ /**
34
+ * A deferred operation registered with `this.defer()`. Runs after the
35
+ * migration's schema statements, with a live connection handle (execute/query).
36
+ */
37
+ export type DeferredMigrationCallback = (
38
+ db: CatalogConnection,
39
+ ) => Promise<void> | void;
40
+
32
41
  export abstract class Migration {
33
42
  readonly schema: Schema;
34
43
  readonly #dialect: AtlasDialect;
44
+ #deferred: DeferredMigrationCallback[] = [];
35
45
 
36
46
  constructor(dialect?: AtlasDialect) {
37
47
  this.schema = new Schema(dialect);
@@ -41,6 +51,27 @@ export abstract class Migration {
41
51
  this.#dialect = dialect ?? "sqlite";
42
52
  }
43
53
 
54
+ /**
55
+ * Register a callback to run AFTER this migration's schema statements execute
56
+ * (Adonis Lucid `this.defer`). Typically used to seed data into a table the
57
+ * same migration just created. The callback gets a live connection handle.
58
+ *
59
+ * Note: deferred callbacks run after the schema statements have committed, so
60
+ * they are outside the schema transaction (atlas builds schema as a batch);
61
+ * the migration is only recorded as applied once every deferred callback
62
+ * succeeds. Make deferred work idempotent.
63
+ */
64
+ defer(callback: DeferredMigrationCallback): void {
65
+ this.#deferred.push(callback);
66
+ }
67
+
68
+ /** @internal Return and clear the deferred callbacks — called by the runner. */
69
+ consumeDeferred(): DeferredMigrationCallback[] {
70
+ const deferred = this.#deferred;
71
+ this.#deferred = [];
72
+ return deferred;
73
+ }
74
+
44
75
  /** Apply the migration. */
45
76
  abstract up(): Promise<void> | void;
46
77
 
@@ -69,15 +100,23 @@ export abstract class Migration {
69
100
  );
70
101
  }
71
102
 
72
- /** Get the SQL statements generated by this migration. */
73
- async getUpSQL(): Promise<string[]> {
103
+ /**
104
+ * Get the SQL statements generated by this migration. A `connection` (passed
105
+ * by the runner) is bound to the schema so `this.schema.hasTable()` /
106
+ * `hasColumn()` can query the live catalog from inside `up`/`down`.
107
+ */
108
+ async getUpSQL(connection?: CatalogConnection): Promise<string[]> {
109
+ if (connection) this.schema.bindConnection(connection);
74
110
  this.schema.reset();
111
+ this.#deferred = [];
75
112
  await this.up();
76
113
  return this.schema.toSQL();
77
114
  }
78
115
 
79
- async getDownSQL(): Promise<string[]> {
116
+ async getDownSQL(connection?: CatalogConnection): Promise<string[]> {
117
+ if (connection) this.schema.bindConnection(connection);
80
118
  this.schema.reset();
119
+ this.#deferred = [];
81
120
  await this.down();
82
121
  return this.schema.toSQL();
83
122
  }