@apso/cli 0.30.0 → 0.32.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.
@@ -0,0 +1,22 @@
1
+ /// <reference types="node" />
2
+ import BaseCommand from "../../lib/base-command";
3
+ import { CopyResult } from "../../lib/import";
4
+ /** Resolve the target connection string from flag/env, else undefined. */
5
+ export declare function resolveTargetConnectionString(flagValue: string | undefined, env?: NodeJS.ProcessEnv): string | undefined;
6
+ /** Render a copy result (or dry-run plan) as a human-readable summary. */
7
+ export declare function formatCopyResult(result: CopyResult): string;
8
+ export default class ImportData extends BaseCommand {
9
+ static description: string;
10
+ static examples: string[];
11
+ static flags: {
12
+ source: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
13
+ target: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
14
+ schema: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
15
+ tables: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
16
+ "batch-size": import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
17
+ "dry-run": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
18
+ yes: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
19
+ };
20
+ private promptConnection;
21
+ run(): Promise<void>;
22
+ }
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatCopyResult = exports.resolveTargetConnectionString = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const core_1 = require("@oclif/core");
6
+ const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
7
+ const base_command_1 = tslib_1.__importDefault(require("../../lib/base-command"));
8
+ const import_1 = require("../../lib/import");
9
+ const supabase_1 = require("./supabase");
10
+ /** Resolve the target connection string from flag/env, else undefined. */
11
+ function resolveTargetConnectionString(flagValue, env = process.env) {
12
+ return flagValue || env.TARGET_DATABASE_URL || env.APSO_DATABASE_URL || undefined;
13
+ }
14
+ exports.resolveTargetConnectionString = resolveTargetConnectionString;
15
+ /** Render a copy result (or dry-run plan) as a human-readable summary. */
16
+ function formatCopyResult(result) {
17
+ const lines = [];
18
+ const total = result.perTable.reduce((sum, t) => sum + t.rows, 0);
19
+ lines.push(result.dryRun
20
+ ? `Plan: ${result.perTable.length} table(s), ${total} source row(s) to copy.`
21
+ : `Copied ${total} row(s) across ${result.perTable.length} table(s).`);
22
+ for (const t of result.perTable) {
23
+ lines.push(` ${t.table}: ${t.rows} row(s)`);
24
+ }
25
+ if (result.missingTargetTables.length > 0) {
26
+ lines.push(` Skipped (no matching target table): ${result.missingTargetTables.join(", ")}`);
27
+ }
28
+ if (result.cyclicTables.length > 0) {
29
+ lines.push(` FK cycle (copied best-effort, may need a second pass): ${result.cyclicTables.join(", ")}`);
30
+ }
31
+ if (result.skippedColumns.length > 0) {
32
+ lines.push(" Skipped columns:");
33
+ for (const s of result.skippedColumns) {
34
+ lines.push(` - ${s.table}.${s.column} (${s.reason})`);
35
+ }
36
+ }
37
+ if (result.dryRun && result.blockedTables.length > 0) {
38
+ lines.push(` WARNING: target table(s) already have rows and would block a real run: ${result.blockedTables.join(", ")}`);
39
+ }
40
+ return lines.join("\n");
41
+ }
42
+ exports.formatCopyResult = formatCopyResult;
43
+ class ImportData extends base_command_1.default {
44
+ async promptConnection(role) {
45
+ const answer = await inquirer_1.default.prompt([
46
+ {
47
+ type: "password",
48
+ name: "value",
49
+ mask: "*",
50
+ message: `${role} Postgres connection string:`,
51
+ validate: (input) => input.trim().length > 0 || "A connection string is required",
52
+ },
53
+ ]);
54
+ return answer.value.trim();
55
+ }
56
+ async run() {
57
+ const { flags } = await this.parse(ImportData);
58
+ const sourceConn = (0, supabase_1.resolveConnectionString)(flags.source) ||
59
+ (await this.promptConnection("Source"));
60
+ const targetConn = resolveTargetConnectionString(flags.target) ||
61
+ (await this.promptConnection("Target"));
62
+ const tableFilter = flags.tables
63
+ ? flags.tables.split(",").map((t) => t.trim()).filter(Boolean)
64
+ : undefined;
65
+ let sourceReader;
66
+ let targetWriter;
67
+ try {
68
+ this.log("Introspecting source and target schemas...");
69
+ const sourceSchema = await new import_1.PgIntrospector(sourceConn).introspect(flags.schema);
70
+ const targetSchema = await new import_1.PgIntrospector(targetConn).introspect(flags.schema);
71
+ const plan = (0, import_1.planCopy)(sourceSchema, (0, import_1.targetColumnsFromSchema)(targetSchema), tableFilter);
72
+ if (plan.tables.length === 0) {
73
+ this.warn("No tables to copy (no source/target table overlap).");
74
+ return;
75
+ }
76
+ sourceReader = await import_1.PgSourceReader.connect(sourceConn, flags.schema);
77
+ targetWriter = await import_1.PgTargetWriter.connect(targetConn, flags.schema);
78
+ if (flags["dry-run"]) {
79
+ const result = await (0, import_1.executeCopy)(plan, sourceReader, targetWriter, {
80
+ batchSize: flags["batch-size"],
81
+ dryRun: true,
82
+ });
83
+ this.log("");
84
+ this.log(formatCopyResult(result));
85
+ this.log("");
86
+ this.log("Dry run — no rows written.");
87
+ return;
88
+ }
89
+ if (!flags.yes) {
90
+ const tableList = plan.tables.map((t) => t.targetTable).join(", ");
91
+ const { confirm } = await inquirer_1.default.prompt([
92
+ {
93
+ type: "confirm",
94
+ name: "confirm",
95
+ message: `Copy data into ${plan.tables.length} target table(s) (${tableList})?`,
96
+ default: false,
97
+ },
98
+ ]);
99
+ if (!confirm) {
100
+ this.log("Data copy cancelled.");
101
+ return;
102
+ }
103
+ }
104
+ this.log("Copying data...");
105
+ const result = await (0, import_1.executeCopy)(plan, sourceReader, targetWriter, {
106
+ batchSize: flags["batch-size"],
107
+ });
108
+ this.log("");
109
+ this.log(formatCopyResult(result));
110
+ this.log("Done.");
111
+ }
112
+ catch (error) {
113
+ const message = error instanceof Error ? error.message : String(error);
114
+ const redacted = (0, import_1.redactConnectionString)(message.split(sourceConn).join("****").split(targetConn).join("****"));
115
+ this.error(redacted);
116
+ }
117
+ finally {
118
+ if (sourceReader)
119
+ await sourceReader.close();
120
+ if (targetWriter)
121
+ await targetWriter.close();
122
+ }
123
+ }
124
+ }
125
+ ImportData.description = "Copy table data from a source Supabase (Postgres) database into the target database rebuilt from the imported schema. Reads the source read-only; inserts into empty target tables.";
126
+ ImportData.examples = [
127
+ `$ apso import data -s "postgresql://...source..." -t "postgresql://...target..."`,
128
+ `$ apso import data --dry-run`,
129
+ `$ apso import data --tables users,orders`,
130
+ ];
131
+ ImportData.flags = {
132
+ source: core_1.Flags.string({
133
+ char: "s",
134
+ description: "Source connection string. Falls back to SUPABASE_DB_URL or DATABASE_URL, then prompts.",
135
+ }),
136
+ target: core_1.Flags.string({
137
+ char: "t",
138
+ description: "Target connection string. Falls back to TARGET_DATABASE_URL or APSO_DATABASE_URL, then prompts.",
139
+ }),
140
+ schema: core_1.Flags.string({ description: "Database schema", default: "public" }),
141
+ tables: core_1.Flags.string({
142
+ description: "Comma-separated list of source tables to copy (default: all)",
143
+ }),
144
+ "batch-size": core_1.Flags.integer({
145
+ description: "Rows per insert batch",
146
+ default: 500,
147
+ }),
148
+ "dry-run": core_1.Flags.boolean({
149
+ description: "Plan and report row counts without writing",
150
+ default: false,
151
+ }),
152
+ yes: core_1.Flags.boolean({
153
+ char: "y",
154
+ description: "Skip the confirmation prompt",
155
+ default: false,
156
+ }),
157
+ };
158
+ exports.default = ImportData;
@@ -0,0 +1,42 @@
1
+ /// <reference types="node" />
2
+ import BaseCommand from "../../lib/base-command";
3
+ import { ApsorcImportOutput, ImportReport, Introspector } from "../../lib/import";
4
+ /**
5
+ * Resolve the Postgres connection string from an explicit flag or, failing
6
+ * that, the SUPABASE_DB_URL / DATABASE_URL environment variables. Returns
7
+ * undefined if none is set (the command then prompts interactively).
8
+ */
9
+ export declare function resolveConnectionString(flagValue: string | undefined, env?: NodeJS.ProcessEnv): string | undefined;
10
+ /**
11
+ * Run the full introspect -> convert -> validate pipeline against any
12
+ * Introspector. Throws if the generated relationships fail to parse. Pure of
13
+ * file/console I/O so it can be unit-tested with a fake Introspector.
14
+ */
15
+ export declare function importFromIntrospector(introspector: Introspector, schema: string): Promise<{
16
+ apsorc: ApsorcImportOutput;
17
+ report: ImportReport;
18
+ unknownTypes: string[];
19
+ }>;
20
+ /** Render the import report as a human-readable summary (contains no secrets). */
21
+ export declare function formatReport(report: ImportReport): string;
22
+ /**
23
+ * Write the generated .apsorc to disk, backing up any existing file first.
24
+ * Returns the backup path if one was made.
25
+ */
26
+ export declare function writeApsorcFile(apsorc: ApsorcImportOutput, outPath: string): {
27
+ backupPath?: string;
28
+ };
29
+ export default class ImportSupabase extends BaseCommand {
30
+ static description: string;
31
+ static examples: string[];
32
+ static flags: {
33
+ "connection-string": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
34
+ schema: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
35
+ out: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
36
+ "dry-run": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
37
+ yes: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
38
+ };
39
+ /** Overridable for testing. */
40
+ protected makeIntrospector(connectionString: string): Introspector;
41
+ run(): Promise<void>;
42
+ }
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.writeApsorcFile = exports.formatReport = exports.importFromIntrospector = exports.resolveConnectionString = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const core_1 = require("@oclif/core");
6
+ const fs = tslib_1.__importStar(require("fs"));
7
+ const path = tslib_1.__importStar(require("path"));
8
+ const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
9
+ const base_command_1 = tslib_1.__importDefault(require("../../lib/base-command"));
10
+ const config_1 = require("../../lib/config");
11
+ const apsorc_parser_1 = require("../../lib/apsorc-parser");
12
+ const relationships_1 = require("../../lib/utils/relationships");
13
+ const import_1 = require("../../lib/import");
14
+ /**
15
+ * Resolve the Postgres connection string from an explicit flag or, failing
16
+ * that, the SUPABASE_DB_URL / DATABASE_URL environment variables. Returns
17
+ * undefined if none is set (the command then prompts interactively).
18
+ */
19
+ function resolveConnectionString(flagValue, env = process.env) {
20
+ return flagValue || env.SUPABASE_DB_URL || env.DATABASE_URL || undefined;
21
+ }
22
+ exports.resolveConnectionString = resolveConnectionString;
23
+ /**
24
+ * Run the full introspect -> convert -> validate pipeline against any
25
+ * Introspector. Throws if the generated relationships fail to parse. Pure of
26
+ * file/console I/O so it can be unit-tested with a fake Introspector.
27
+ */
28
+ async function importFromIntrospector(introspector, schema) {
29
+ const introspected = await introspector.introspect(schema);
30
+ const { apsorc, report } = (0, import_1.pgToApsorc)(introspected);
31
+ // Validate by running the generated relationships through the real parser.
32
+ (0, relationships_1.parseRelationships)(apsorc.relationships);
33
+ return {
34
+ apsorc,
35
+ report,
36
+ unknownTypes: (0, import_1.findUnknownEmittedTypes)(apsorc),
37
+ };
38
+ }
39
+ exports.importFromIntrospector = importFromIntrospector;
40
+ /** Render the import report as a human-readable summary (contains no secrets). */
41
+ function formatReport(report) {
42
+ const lines = [];
43
+ lines.push(`Imported ${report.tablesImported.length} table(s) and ${report.relationships} relationship(s).`);
44
+ if (report.tablesImported.length > 0) {
45
+ lines.push(` Tables: ${report.tablesImported.join(", ")}`);
46
+ }
47
+ if (report.viewsSkipped.length > 0) {
48
+ lines.push(` Skipped views: ${report.viewsSkipped.join(", ")}`);
49
+ }
50
+ if (report.systemSchemasSkipped.length > 0) {
51
+ lines.push(` Skipped system schemas: ${report.systemSchemasSkipped.join(", ")}`);
52
+ }
53
+ const w = report.warnings;
54
+ const review = [];
55
+ if (w.compositePks.length > 0)
56
+ review.push(` - Composite primary keys (review): ${w.compositePks.join(", ")}`);
57
+ if (w.nonStandardPks.length > 0)
58
+ review.push(` - Non-"id" primary keys (kept as fields): ${w.nonStandardPks.join(", ")}`);
59
+ if (w.noPrimaryKey.length > 0)
60
+ review.push(` - Tables with no primary key: ${w.noPrimaryKey.join(", ")}`);
61
+ if (w.compositeFks.length > 0)
62
+ review.push(` - Composite foreign keys (kept as columns, no relationship): ${w.compositeFks.join(", ")}`);
63
+ if (w.fkColumnNameUnmapped.length > 0)
64
+ review.push(` - Foreign keys whose generated column may differ: ${w.fkColumnNameUnmapped.join(", ")}`);
65
+ if (w.joinTablesDetected.length > 0)
66
+ review.push(` - Join tables (could be modeled as ManyToMany): ${w.joinTablesDetected.join(", ")}`);
67
+ if (w.arraysLossy.length > 0)
68
+ review.push(` - Array columns reduced to text: ${w.arraysLossy.join(", ")}`);
69
+ if (w.typesDefaulted.length > 0)
70
+ review.push(` - Unknown types defaulted to text: ${w.typesDefaulted
71
+ .map((t) => `${t.column} (${t.udt})`)
72
+ .join(", ")}`);
73
+ if (w.defaultsDropped.length > 0)
74
+ review.push(` - Column defaults dropped: ${w.defaultsDropped.join(", ")}`);
75
+ if (review.length > 0) {
76
+ lines.push("", "Review the following (not fully represented):", ...review);
77
+ }
78
+ return lines.join("\n");
79
+ }
80
+ exports.formatReport = formatReport;
81
+ /**
82
+ * Write the generated .apsorc to disk, backing up any existing file first.
83
+ * Returns the backup path if one was made.
84
+ */
85
+ function writeApsorcFile(apsorc, outPath) {
86
+ let backupPath;
87
+ if (fs.existsSync(outPath)) {
88
+ backupPath = (0, config_1.createBackup)(outPath);
89
+ }
90
+ fs.writeFileSync(outPath, JSON.stringify(apsorc, null, 2), "utf-8");
91
+ return { backupPath };
92
+ }
93
+ exports.writeApsorcFile = writeApsorcFile;
94
+ class ImportSupabase extends base_command_1.default {
95
+ /** Overridable for testing. */
96
+ makeIntrospector(connectionString) {
97
+ return new import_1.PgIntrospector(connectionString);
98
+ }
99
+ async run() {
100
+ var _a, _b;
101
+ const { flags } = await this.parse(ImportSupabase);
102
+ let connectionString = resolveConnectionString(flags["connection-string"]);
103
+ if (!connectionString) {
104
+ const answer = await inquirer_1.default.prompt([
105
+ {
106
+ type: "password",
107
+ name: "connectionString",
108
+ mask: "*",
109
+ message: "Supabase Postgres connection string:",
110
+ validate: (input) => input.trim().length > 0 || "A connection string is required",
111
+ },
112
+ ]);
113
+ connectionString = answer.connectionString.trim();
114
+ }
115
+ try {
116
+ this.log("Connecting and introspecting schema (read-only)...");
117
+ const introspector = this.makeIntrospector(connectionString);
118
+ const { apsorc, report, unknownTypes } = await importFromIntrospector(introspector, flags.schema);
119
+ this.log("");
120
+ this.log(formatReport(report));
121
+ if (unknownTypes.length > 0) {
122
+ this.warn(`Some column types were not recognized and defaulted to text: ${[
123
+ ...new Set(unknownTypes),
124
+ ].join(", ")}`);
125
+ }
126
+ if (report.tablesImported.length === 0) {
127
+ this.warn(`No base tables found in schema "${flags.schema}". Nothing to write.`);
128
+ return;
129
+ }
130
+ if (flags["dry-run"]) {
131
+ this.log("");
132
+ this.log("Dry run — no file written.");
133
+ return;
134
+ }
135
+ const outPath = (_b = (_a = flags.out) !== null && _a !== void 0 ? _a : (0, apsorc_parser_1.findConfigPath)()) !== null && _b !== void 0 ? _b : path.join(process.cwd(), ".apsorc");
136
+ if (fs.existsSync(outPath) && !flags.yes) {
137
+ const { confirm } = await inquirer_1.default.prompt([
138
+ {
139
+ type: "confirm",
140
+ name: "confirm",
141
+ message: `Overwrite ${outPath} with the imported schema?`,
142
+ default: false,
143
+ },
144
+ ]);
145
+ if (!confirm) {
146
+ this.log("Import cancelled.");
147
+ return;
148
+ }
149
+ }
150
+ const { backupPath } = writeApsorcFile(apsorc, outPath);
151
+ if (backupPath)
152
+ this.log(`Backup created: ${backupPath}`);
153
+ this.log(`Schema written to ${outPath}`);
154
+ this.log("");
155
+ this.log("Next steps:");
156
+ this.log(" - Review the .apsorc, especially any items flagged above");
157
+ this.log(" - Run 'apso migrate' to validate the schema locally");
158
+ this.log(" - Run 'apso generate' to scaffold code");
159
+ }
160
+ catch (error) {
161
+ const message = error instanceof Error ? error.message : String(error);
162
+ // Defense-in-depth: never surface the connection string in an error.
163
+ this.error((0, import_1.redactConnectionString)(message.split(connectionString).join("****")));
164
+ }
165
+ }
166
+ }
167
+ ImportSupabase.description = "Introspect an existing Supabase (Postgres) database and generate a local .apsorc schema. Read-only: it never writes to the source database.";
168
+ ImportSupabase.examples = [
169
+ `$ apso import supabase`,
170
+ `$ apso import supabase -c "postgresql://postgres:[pw]@db.[ref].supabase.co:5432/postgres"`,
171
+ `$ SUPABASE_DB_URL="postgresql://..." apso import supabase --dry-run`,
172
+ ];
173
+ ImportSupabase.flags = {
174
+ "connection-string": core_1.Flags.string({
175
+ char: "c",
176
+ description: "Postgres connection string. Falls back to SUPABASE_DB_URL or DATABASE_URL, then prompts.",
177
+ }),
178
+ schema: core_1.Flags.string({
179
+ description: "Database schema to import",
180
+ default: "public",
181
+ }),
182
+ out: core_1.Flags.string({
183
+ char: "o",
184
+ description: "Path to write the .apsorc (default: existing .apsorc or ./.apsorc)",
185
+ }),
186
+ "dry-run": core_1.Flags.boolean({
187
+ description: "Introspect and print a summary without writing any file",
188
+ default: false,
189
+ }),
190
+ yes: core_1.Flags.boolean({
191
+ char: "y",
192
+ description: "Skip the overwrite confirmation prompt",
193
+ default: false,
194
+ }),
195
+ };
196
+ exports.default = ImportSupabase;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Database Import — data copy (phase 2)
3
+ *
4
+ * Plans and executes a row copy from a source Postgres/Supabase database into a
5
+ * target database that was rebuilt from the imported schema (phase 1). The
6
+ * planning logic is pure and fully unit-testable; the actual reads/writes go
7
+ * through the injectable `SourceReader` / `TargetWriter` interfaces so the
8
+ * executor can be tested with fakes (mirroring phase 1's `Introspector`).
9
+ *
10
+ * Mapping rules mirror phase 1 exactly:
11
+ * - target table name = snakeCase(entity name) = snakeCase(source table name)
12
+ * - single-column FK = renamed to the platform's camelCase `<ref>Id`
13
+ * - every other column = copied under its original name
14
+ */
15
+ import { IntrospectedSchema, IntrospectedTable } from "./types";
16
+ export interface ColumnMapping {
17
+ sourceColumn: string;
18
+ targetColumn: string;
19
+ }
20
+ export interface TableCopyPlan {
21
+ sourceTable: string;
22
+ targetTable: string;
23
+ /** Kept source columns (those whose mapped target column exists), in order. */
24
+ sourceColumns: string[];
25
+ /** Target column names, parallel to sourceColumns. */
26
+ targetColumns: string[];
27
+ /** Source PK column used to order paged reads (null when there is no PK). */
28
+ orderBy: string | null;
29
+ /** Target PK column whose sequence to reset after copy (null if not serial). */
30
+ serialPkColumn: string | null;
31
+ }
32
+ export interface CopyPlan {
33
+ tables: TableCopyPlan[];
34
+ skippedColumns: Array<{
35
+ table: string;
36
+ column: string;
37
+ reason: string;
38
+ }>;
39
+ /** Source tables with no corresponding target table (skipped entirely). */
40
+ missingTargetTables: string[];
41
+ /** Tables involved in a FK cycle (copied best-effort, after acyclic tables). */
42
+ cyclicTables: string[];
43
+ }
44
+ /** The target table name the generator produces for a given source table. */
45
+ export declare function targetTableName(sourceTable: string): string;
46
+ /** Map every source column to its target column name (FK rename or identity). */
47
+ export declare function buildColumnMapping(table: IntrospectedTable): ColumnMapping[];
48
+ /**
49
+ * Order tables so each is copied after the tables it references via FK
50
+ * (parents before children). Self-references are ignored; tables in a cycle are
51
+ * reported and appended best-effort at the end.
52
+ */
53
+ export declare function topoSortTables(tables: IntrospectedTable[]): {
54
+ order: string[];
55
+ cyclic: string[];
56
+ };
57
+ /**
58
+ * Build a full copy plan from the source schema and the set of columns that
59
+ * actually exist in each target table. Source columns whose mapped target
60
+ * column is missing are dropped (and reported); source tables with no target
61
+ * table are skipped entirely.
62
+ */
63
+ export declare function planCopy(source: IntrospectedSchema, targetColumnsByTable: Map<string, Set<string>>, tableFilter?: string[]): CopyPlan;
64
+ /** Coerce a value read from pg into something safe to bind on insert. */
65
+ export declare function coerceValue(value: unknown): unknown;
66
+ /** Build a parameterized multi-row INSERT statement for a batch. */
67
+ export declare function buildInsertSql(table: string, columns: string[], rows: unknown[][], schema?: string): {
68
+ text: string;
69
+ values: unknown[];
70
+ };
71
+ export interface SourceReader {
72
+ totalRows(table: string): Promise<number>;
73
+ readBatch(table: string, columns: string[], orderBy: string | null, offset: number, limit: number): Promise<unknown[][]>;
74
+ }
75
+ export interface TargetWriter {
76
+ rowCount(table: string): Promise<number>;
77
+ insertBatch(table: string, columns: string[], rows: unknown[][]): Promise<void>;
78
+ resetSequence(table: string, column: string): Promise<void>;
79
+ begin(): Promise<void>;
80
+ commit(): Promise<void>;
81
+ rollback(): Promise<void>;
82
+ }
83
+ export interface CopyResult {
84
+ dryRun: boolean;
85
+ perTable: Array<{
86
+ table: string;
87
+ rows: number;
88
+ }>;
89
+ skippedColumns: CopyPlan["skippedColumns"];
90
+ missingTargetTables: string[];
91
+ cyclicTables: string[];
92
+ /** Target tables that already had rows (blocks a real, insert-only run). */
93
+ blockedTables: string[];
94
+ }
95
+ export interface ExecuteOptions {
96
+ batchSize?: number;
97
+ dryRun?: boolean;
98
+ }
99
+ /**
100
+ * Execute a copy plan. Insert-only: if any target table already has rows the
101
+ * run aborts before writing anything. The whole copy runs in one transaction
102
+ * on the target, so a failure rolls everything back.
103
+ */
104
+ export declare function executeCopy(plan: CopyPlan, source: SourceReader, target: TargetWriter, options?: ExecuteOptions): Promise<CopyResult>;
105
+ /** Build the target column lookup used by planCopy from an introspected target. */
106
+ export declare function targetColumnsFromSchema(target: IntrospectedSchema): Map<string, Set<string>>;