@apso/cli 0.31.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.
- package/dist/commands/import/data.d.ts +22 -0
- package/dist/commands/import/data.js +158 -0
- package/dist/lib/import/copy-data.d.ts +106 -0
- package/dist/lib/import/copy-data.js +266 -0
- package/dist/lib/import/index.d.ts +3 -0
- package/dist/lib/import/index.js +13 -1
- package/dist/lib/import/pg-data.d.ts +33 -0
- package/dist/lib/import/pg-data.js +112 -0
- package/npm-shrinkwrap.json +2 -2
- package/oclif.manifest.json +66 -1
- package/package.json +1 -1
|
@@ -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,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>>;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Database Import — data copy (phase 2)
|
|
4
|
+
*
|
|
5
|
+
* Plans and executes a row copy from a source Postgres/Supabase database into a
|
|
6
|
+
* target database that was rebuilt from the imported schema (phase 1). The
|
|
7
|
+
* planning logic is pure and fully unit-testable; the actual reads/writes go
|
|
8
|
+
* through the injectable `SourceReader` / `TargetWriter` interfaces so the
|
|
9
|
+
* executor can be tested with fakes (mirroring phase 1's `Introspector`).
|
|
10
|
+
*
|
|
11
|
+
* Mapping rules mirror phase 1 exactly:
|
|
12
|
+
* - target table name = snakeCase(entity name) = snakeCase(source table name)
|
|
13
|
+
* - single-column FK = renamed to the platform's camelCase `<ref>Id`
|
|
14
|
+
* - every other column = copied under its original name
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.targetColumnsFromSchema = exports.executeCopy = exports.buildInsertSql = exports.coerceValue = exports.planCopy = exports.topoSortTables = exports.buildColumnMapping = exports.targetTableName = void 0;
|
|
18
|
+
const casing_1 = require("../utils/casing");
|
|
19
|
+
const relationships_1 = require("../utils/relationships");
|
|
20
|
+
const pg_to_apsorc_1 = require("./pg-to-apsorc");
|
|
21
|
+
const INTEGER_PK_UDTS = new Set(["int2", "int4", "int8"]);
|
|
22
|
+
/** The target table name the generator produces for a given source table. */
|
|
23
|
+
function targetTableName(sourceTable) {
|
|
24
|
+
return (0, casing_1.snakeCase)(sourceTable);
|
|
25
|
+
}
|
|
26
|
+
exports.targetTableName = targetTableName;
|
|
27
|
+
/** Map every source column to its target column name (FK rename or identity). */
|
|
28
|
+
function buildColumnMapping(table) {
|
|
29
|
+
const fkRefByColumn = new Map();
|
|
30
|
+
for (const fk of table.foreignKeys) {
|
|
31
|
+
if (fk.columns.length === 1) {
|
|
32
|
+
fkRefByColumn.set(fk.columns[0], fk.referencedTable);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return [...table.columns]
|
|
36
|
+
.sort((a, b) => a.ordinal - b.ordinal)
|
|
37
|
+
.map((col) => {
|
|
38
|
+
const refTable = fkRefByColumn.get(col.name);
|
|
39
|
+
if (refTable) {
|
|
40
|
+
const { toName } = (0, pg_to_apsorc_1.deriveToName)(col.name, refTable);
|
|
41
|
+
const targetColumn = (0, relationships_1.getRelationshipIdField)({
|
|
42
|
+
name: refTable,
|
|
43
|
+
type: "ManyToOne",
|
|
44
|
+
referenceName: toName,
|
|
45
|
+
});
|
|
46
|
+
return { sourceColumn: col.name, targetColumn };
|
|
47
|
+
}
|
|
48
|
+
return { sourceColumn: col.name, targetColumn: col.name };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
exports.buildColumnMapping = buildColumnMapping;
|
|
52
|
+
/**
|
|
53
|
+
* Order tables so each is copied after the tables it references via FK
|
|
54
|
+
* (parents before children). Self-references are ignored; tables in a cycle are
|
|
55
|
+
* reported and appended best-effort at the end.
|
|
56
|
+
*/
|
|
57
|
+
function topoSortTables(tables) {
|
|
58
|
+
const names = new Set(tables.map((t) => t.name));
|
|
59
|
+
const deps = new Map();
|
|
60
|
+
for (const t of tables) {
|
|
61
|
+
const d = new Set();
|
|
62
|
+
for (const fk of t.foreignKeys) {
|
|
63
|
+
if (fk.referencedTable !== t.name && names.has(fk.referencedTable)) {
|
|
64
|
+
d.add(fk.referencedTable);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
deps.set(t.name, d);
|
|
68
|
+
}
|
|
69
|
+
const order = [];
|
|
70
|
+
const visited = new Set();
|
|
71
|
+
let progress = true;
|
|
72
|
+
while (order.length < tables.length && progress) {
|
|
73
|
+
progress = false;
|
|
74
|
+
for (const t of tables) {
|
|
75
|
+
if (visited.has(t.name))
|
|
76
|
+
continue;
|
|
77
|
+
if ([...deps.get(t.name)].every((x) => visited.has(x))) {
|
|
78
|
+
order.push(t.name);
|
|
79
|
+
visited.add(t.name);
|
|
80
|
+
progress = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const cyclic = tables
|
|
85
|
+
.filter((t) => !visited.has(t.name))
|
|
86
|
+
.map((t) => t.name);
|
|
87
|
+
for (const c of cyclic)
|
|
88
|
+
order.push(c); // best-effort append
|
|
89
|
+
return { order, cyclic };
|
|
90
|
+
}
|
|
91
|
+
exports.topoSortTables = topoSortTables;
|
|
92
|
+
/**
|
|
93
|
+
* Build a full copy plan from the source schema and the set of columns that
|
|
94
|
+
* actually exist in each target table. Source columns whose mapped target
|
|
95
|
+
* column is missing are dropped (and reported); source tables with no target
|
|
96
|
+
* table are skipped entirely.
|
|
97
|
+
*/
|
|
98
|
+
function planCopy(source, targetColumnsByTable, tableFilter) {
|
|
99
|
+
var _a;
|
|
100
|
+
const filter = tableFilter && tableFilter.length > 0 ? new Set(tableFilter) : null;
|
|
101
|
+
const consideredTables = source.tables.filter((t) => !filter || filter.has(t.name));
|
|
102
|
+
const { order, cyclic } = topoSortTables(consideredTables);
|
|
103
|
+
const byName = new Map(consideredTables.map((t) => [t.name, t]));
|
|
104
|
+
const tables = [];
|
|
105
|
+
const skippedColumns = [];
|
|
106
|
+
const missingTargetTables = [];
|
|
107
|
+
for (const sourceTable of order) {
|
|
108
|
+
const table = byName.get(sourceTable);
|
|
109
|
+
const targetTable = targetTableName(sourceTable);
|
|
110
|
+
const targetColumns = targetColumnsByTable.get(targetTable);
|
|
111
|
+
if (!targetColumns) {
|
|
112
|
+
missingTargetTables.push(sourceTable);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const mapping = buildColumnMapping(table);
|
|
116
|
+
const keptSource = [];
|
|
117
|
+
const keptTarget = [];
|
|
118
|
+
for (const m of mapping) {
|
|
119
|
+
if (targetColumns.has(m.targetColumn)) {
|
|
120
|
+
keptSource.push(m.sourceColumn);
|
|
121
|
+
keptTarget.push(m.targetColumn);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
skippedColumns.push({
|
|
125
|
+
table: sourceTable,
|
|
126
|
+
column: m.sourceColumn,
|
|
127
|
+
reason: `target column "${m.targetColumn}" not found in ${targetTable}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Determine ordering + serial PK handling from a single-column PK.
|
|
132
|
+
let orderBy = null;
|
|
133
|
+
let serialPkColumn = null;
|
|
134
|
+
if (table.primaryKey.length === 1) {
|
|
135
|
+
const pkCol = table.primaryKey[0];
|
|
136
|
+
orderBy = pkCol;
|
|
137
|
+
const pk = table.columns.find((c) => c.name === pkCol);
|
|
138
|
+
const targetPk = (_a = mapping.find((m) => m.sourceColumn === pkCol)) === null || _a === void 0 ? void 0 : _a.targetColumn;
|
|
139
|
+
if (pk && targetPk && INTEGER_PK_UDTS.has(pk.udtName)) {
|
|
140
|
+
serialPkColumn = targetPk;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
tables.push({
|
|
144
|
+
sourceTable,
|
|
145
|
+
targetTable,
|
|
146
|
+
sourceColumns: keptSource,
|
|
147
|
+
targetColumns: keptTarget,
|
|
148
|
+
orderBy,
|
|
149
|
+
serialPkColumn,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return { tables, skippedColumns, missingTargetTables, cyclicTables: cyclic };
|
|
153
|
+
}
|
|
154
|
+
exports.planCopy = planCopy;
|
|
155
|
+
/** Coerce a value read from pg into something safe to bind on insert. */
|
|
156
|
+
function coerceValue(value) {
|
|
157
|
+
if (value === null || value === undefined)
|
|
158
|
+
return null;
|
|
159
|
+
if (value instanceof Date)
|
|
160
|
+
return value;
|
|
161
|
+
if (Buffer.isBuffer(value))
|
|
162
|
+
return value;
|
|
163
|
+
if (typeof value === "object")
|
|
164
|
+
return JSON.stringify(value);
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
exports.coerceValue = coerceValue;
|
|
168
|
+
/** Build a parameterized multi-row INSERT statement for a batch. */
|
|
169
|
+
function buildInsertSql(table, columns, rows, schema) {
|
|
170
|
+
const tableRef = schema ? `"${schema}"."${table}"` : `"${table}"`;
|
|
171
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
172
|
+
const values = [];
|
|
173
|
+
const tuples = rows.map((row) => {
|
|
174
|
+
const placeholders = row.map((val) => {
|
|
175
|
+
values.push(coerceValue(val));
|
|
176
|
+
return `$${values.length}`;
|
|
177
|
+
});
|
|
178
|
+
return `(${placeholders.join(", ")})`;
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
text: `INSERT INTO ${tableRef} (${colList}) VALUES ${tuples.join(", ")}`,
|
|
182
|
+
values,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
exports.buildInsertSql = buildInsertSql;
|
|
186
|
+
/**
|
|
187
|
+
* Execute a copy plan. Insert-only: if any target table already has rows the
|
|
188
|
+
* run aborts before writing anything. The whole copy runs in one transaction
|
|
189
|
+
* on the target, so a failure rolls everything back.
|
|
190
|
+
*/
|
|
191
|
+
async function executeCopy(plan, source, target, options = {}) {
|
|
192
|
+
var _a, _b;
|
|
193
|
+
const batchSize = (_a = options.batchSize) !== null && _a !== void 0 ? _a : 500;
|
|
194
|
+
const dryRun = (_b = options.dryRun) !== null && _b !== void 0 ? _b : false;
|
|
195
|
+
// Pre-flight: detect non-empty target tables.
|
|
196
|
+
const blockedTables = [];
|
|
197
|
+
for (const t of plan.tables) {
|
|
198
|
+
// eslint-disable-next-line no-await-in-loop
|
|
199
|
+
const count = await target.rowCount(t.targetTable);
|
|
200
|
+
if (count > 0)
|
|
201
|
+
blockedTables.push(t.targetTable);
|
|
202
|
+
}
|
|
203
|
+
const perTable = [];
|
|
204
|
+
if (dryRun) {
|
|
205
|
+
for (const t of plan.tables) {
|
|
206
|
+
// eslint-disable-next-line no-await-in-loop
|
|
207
|
+
const rows = await source.totalRows(t.sourceTable);
|
|
208
|
+
perTable.push({ table: t.targetTable, rows });
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
dryRun: true,
|
|
212
|
+
perTable,
|
|
213
|
+
skippedColumns: plan.skippedColumns,
|
|
214
|
+
missingTargetTables: plan.missingTargetTables,
|
|
215
|
+
cyclicTables: plan.cyclicTables,
|
|
216
|
+
blockedTables,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (blockedTables.length > 0) {
|
|
220
|
+
throw new Error(`Aborting: target table(s) already contain rows: ${blockedTables.join(", ")}. Use a fresh target or clear these tables first.`);
|
|
221
|
+
}
|
|
222
|
+
await target.begin();
|
|
223
|
+
try {
|
|
224
|
+
for (const t of plan.tables) {
|
|
225
|
+
let offset = 0;
|
|
226
|
+
let copied = 0;
|
|
227
|
+
// eslint-disable-next-line no-constant-condition
|
|
228
|
+
while (true) {
|
|
229
|
+
// eslint-disable-next-line no-await-in-loop
|
|
230
|
+
const rows = await source.readBatch(t.sourceTable, t.sourceColumns, t.orderBy, offset, batchSize);
|
|
231
|
+
if (rows.length === 0)
|
|
232
|
+
break;
|
|
233
|
+
// eslint-disable-next-line no-await-in-loop
|
|
234
|
+
await target.insertBatch(t.targetTable, t.targetColumns, rows);
|
|
235
|
+
copied += rows.length;
|
|
236
|
+
offset += rows.length;
|
|
237
|
+
if (rows.length < batchSize)
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
if (t.serialPkColumn && copied > 0) {
|
|
241
|
+
// eslint-disable-next-line no-await-in-loop
|
|
242
|
+
await target.resetSequence(t.targetTable, t.serialPkColumn);
|
|
243
|
+
}
|
|
244
|
+
perTable.push({ table: t.targetTable, rows: copied });
|
|
245
|
+
}
|
|
246
|
+
await target.commit();
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
await target.rollback();
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
dryRun: false,
|
|
254
|
+
perTable,
|
|
255
|
+
skippedColumns: plan.skippedColumns,
|
|
256
|
+
missingTargetTables: plan.missingTargetTables,
|
|
257
|
+
cyclicTables: plan.cyclicTables,
|
|
258
|
+
blockedTables,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
exports.executeCopy = executeCopy;
|
|
262
|
+
/** Build the target column lookup used by planCopy from an introspected target. */
|
|
263
|
+
function targetColumnsFromSchema(target) {
|
|
264
|
+
return new Map(target.tables.map((t) => [t.name, new Set(t.columns.map((c) => c.name))]));
|
|
265
|
+
}
|
|
266
|
+
exports.targetColumnsFromSchema = targetColumnsFromSchema;
|
|
@@ -2,3 +2,6 @@ export * from "./types";
|
|
|
2
2
|
export { pgToApsorc, findUnknownEmittedTypes } from "./pg-to-apsorc";
|
|
3
3
|
export type { ApsorcImportOutput, ApsorcImportEntityOutput, ApsorcImportFieldOutput, ApsorcImportRelationshipOutput, } from "./pg-to-apsorc";
|
|
4
4
|
export { PgIntrospector, redactConnectionString, buildSchema, SYSTEM_SCHEMAS } from "./introspect";
|
|
5
|
+
export { planCopy, executeCopy, buildColumnMapping, topoSortTables, targetTableName, targetColumnsFromSchema, buildInsertSql, coerceValue, } from "./copy-data";
|
|
6
|
+
export type { CopyPlan, CopyResult, TableCopyPlan, ColumnMapping, SourceReader, TargetWriter, ExecuteOptions, } from "./copy-data";
|
|
7
|
+
export { PgSourceReader, PgTargetWriter } from "./pg-data";
|
package/dist/lib/import/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SYSTEM_SCHEMAS = exports.buildSchema = exports.redactConnectionString = exports.PgIntrospector = exports.findUnknownEmittedTypes = exports.pgToApsorc = void 0;
|
|
3
|
+
exports.PgTargetWriter = exports.PgSourceReader = exports.coerceValue = exports.buildInsertSql = exports.targetColumnsFromSchema = exports.targetTableName = exports.topoSortTables = exports.buildColumnMapping = exports.executeCopy = exports.planCopy = exports.SYSTEM_SCHEMAS = exports.buildSchema = exports.redactConnectionString = exports.PgIntrospector = exports.findUnknownEmittedTypes = exports.pgToApsorc = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
tslib_1.__exportStar(require("./types"), exports);
|
|
6
6
|
var pg_to_apsorc_1 = require("./pg-to-apsorc");
|
|
@@ -11,3 +11,15 @@ Object.defineProperty(exports, "PgIntrospector", { enumerable: true, get: functi
|
|
|
11
11
|
Object.defineProperty(exports, "redactConnectionString", { enumerable: true, get: function () { return introspect_1.redactConnectionString; } });
|
|
12
12
|
Object.defineProperty(exports, "buildSchema", { enumerable: true, get: function () { return introspect_1.buildSchema; } });
|
|
13
13
|
Object.defineProperty(exports, "SYSTEM_SCHEMAS", { enumerable: true, get: function () { return introspect_1.SYSTEM_SCHEMAS; } });
|
|
14
|
+
var copy_data_1 = require("./copy-data");
|
|
15
|
+
Object.defineProperty(exports, "planCopy", { enumerable: true, get: function () { return copy_data_1.planCopy; } });
|
|
16
|
+
Object.defineProperty(exports, "executeCopy", { enumerable: true, get: function () { return copy_data_1.executeCopy; } });
|
|
17
|
+
Object.defineProperty(exports, "buildColumnMapping", { enumerable: true, get: function () { return copy_data_1.buildColumnMapping; } });
|
|
18
|
+
Object.defineProperty(exports, "topoSortTables", { enumerable: true, get: function () { return copy_data_1.topoSortTables; } });
|
|
19
|
+
Object.defineProperty(exports, "targetTableName", { enumerable: true, get: function () { return copy_data_1.targetTableName; } });
|
|
20
|
+
Object.defineProperty(exports, "targetColumnsFromSchema", { enumerable: true, get: function () { return copy_data_1.targetColumnsFromSchema; } });
|
|
21
|
+
Object.defineProperty(exports, "buildInsertSql", { enumerable: true, get: function () { return copy_data_1.buildInsertSql; } });
|
|
22
|
+
Object.defineProperty(exports, "coerceValue", { enumerable: true, get: function () { return copy_data_1.coerceValue; } });
|
|
23
|
+
var pg_data_1 = require("./pg-data");
|
|
24
|
+
Object.defineProperty(exports, "PgSourceReader", { enumerable: true, get: function () { return pg_data_1.PgSourceReader; } });
|
|
25
|
+
Object.defineProperty(exports, "PgTargetWriter", { enumerable: true, get: function () { return pg_data_1.PgTargetWriter; } });
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Import — pg-backed data access for the copy executor.
|
|
3
|
+
*
|
|
4
|
+
* Thin wrappers around `pg` implementing the SourceReader / TargetWriter
|
|
5
|
+
* interfaces from copy-data.ts. `pg` is imported lazily and SSL is enabled by
|
|
6
|
+
* default (Supabase requires it). Integration-level: covered by manual/E2E
|
|
7
|
+
* runs, while the copy logic itself is unit-tested with fakes.
|
|
8
|
+
*/
|
|
9
|
+
import { SourceReader, TargetWriter } from "./copy-data";
|
|
10
|
+
/** Reads rows from the source database (read-only). */
|
|
11
|
+
export declare class PgSourceReader implements SourceReader {
|
|
12
|
+
private readonly client;
|
|
13
|
+
private readonly schema;
|
|
14
|
+
private constructor();
|
|
15
|
+
static connect(connectionString: string, schema: string): Promise<PgSourceReader>;
|
|
16
|
+
totalRows(table: string): Promise<number>;
|
|
17
|
+
readBatch(table: string, columns: string[], orderBy: string | null, offset: number, limit: number): Promise<unknown[][]>;
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/** Writes rows into the target database within a single transaction. */
|
|
21
|
+
export declare class PgTargetWriter implements TargetWriter {
|
|
22
|
+
private readonly client;
|
|
23
|
+
private readonly schema;
|
|
24
|
+
private constructor();
|
|
25
|
+
static connect(connectionString: string, schema: string): Promise<PgTargetWriter>;
|
|
26
|
+
rowCount(table: string): Promise<number>;
|
|
27
|
+
insertBatch(table: string, columns: string[], rows: unknown[][]): Promise<void>;
|
|
28
|
+
resetSequence(table: string, column: string): Promise<void>;
|
|
29
|
+
begin(): Promise<void>;
|
|
30
|
+
commit(): Promise<void>;
|
|
31
|
+
rollback(): Promise<void>;
|
|
32
|
+
close(): Promise<void>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Database Import — pg-backed data access for the copy executor.
|
|
4
|
+
*
|
|
5
|
+
* Thin wrappers around `pg` implementing the SourceReader / TargetWriter
|
|
6
|
+
* interfaces from copy-data.ts. `pg` is imported lazily and SSL is enabled by
|
|
7
|
+
* default (Supabase requires it). Integration-level: covered by manual/E2E
|
|
8
|
+
* runs, while the copy logic itself is unit-tested with fakes.
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
27
|
+
if (mod && mod.__esModule) return mod;
|
|
28
|
+
var result = {};
|
|
29
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
30
|
+
__setModuleDefault(result, mod);
|
|
31
|
+
return result;
|
|
32
|
+
};
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.PgTargetWriter = exports.PgSourceReader = void 0;
|
|
35
|
+
async function createClient(connectionString) {
|
|
36
|
+
var _a, _b;
|
|
37
|
+
const pg = await Promise.resolve().then(() => __importStar(require("pg")));
|
|
38
|
+
const Client = (_a = pg.Client) !== null && _a !== void 0 ? _a : (_b = pg.default) === null || _b === void 0 ? void 0 : _b.Client;
|
|
39
|
+
const client = new Client({
|
|
40
|
+
connectionString,
|
|
41
|
+
ssl: { rejectUnauthorized: false },
|
|
42
|
+
});
|
|
43
|
+
await client.connect();
|
|
44
|
+
return client;
|
|
45
|
+
}
|
|
46
|
+
function qualified(schema, table) {
|
|
47
|
+
return `"${schema}"."${table}"`;
|
|
48
|
+
}
|
|
49
|
+
/** Reads rows from the source database (read-only). */
|
|
50
|
+
class PgSourceReader {
|
|
51
|
+
constructor(client, schema) {
|
|
52
|
+
this.client = client;
|
|
53
|
+
this.schema = schema;
|
|
54
|
+
}
|
|
55
|
+
static async connect(connectionString, schema) {
|
|
56
|
+
return new PgSourceReader(await createClient(connectionString), schema);
|
|
57
|
+
}
|
|
58
|
+
async totalRows(table) {
|
|
59
|
+
const res = await this.client.query(`SELECT count(*)::int AS n FROM ${qualified(this.schema, table)}`);
|
|
60
|
+
return res.rows[0].n;
|
|
61
|
+
}
|
|
62
|
+
async readBatch(table, columns, orderBy, offset, limit) {
|
|
63
|
+
const cols = columns.map((c) => `"${c}"`).join(", ");
|
|
64
|
+
const order = orderBy ? ` ORDER BY "${orderBy}"` : "";
|
|
65
|
+
const res = await this.client.query(`SELECT ${cols} FROM ${qualified(this.schema, table)}${order} OFFSET $1 LIMIT $2`, [offset, limit]);
|
|
66
|
+
return res.rows.map((row) => columns.map((c) => row[c]));
|
|
67
|
+
}
|
|
68
|
+
async close() {
|
|
69
|
+
await this.client.end();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.PgSourceReader = PgSourceReader;
|
|
73
|
+
/** Writes rows into the target database within a single transaction. */
|
|
74
|
+
class PgTargetWriter {
|
|
75
|
+
constructor(client, schema) {
|
|
76
|
+
this.client = client;
|
|
77
|
+
this.schema = schema;
|
|
78
|
+
}
|
|
79
|
+
static async connect(connectionString, schema) {
|
|
80
|
+
return new PgTargetWriter(await createClient(connectionString), schema);
|
|
81
|
+
}
|
|
82
|
+
async rowCount(table) {
|
|
83
|
+
const res = await this.client.query(`SELECT count(*)::int AS n FROM ${qualified(this.schema, table)}`);
|
|
84
|
+
return res.rows[0].n;
|
|
85
|
+
}
|
|
86
|
+
async insertBatch(table, columns, rows) {
|
|
87
|
+
const { buildInsertSql } = await Promise.resolve().then(() => __importStar(require("./copy-data")));
|
|
88
|
+
const { text, values } = buildInsertSql(table, columns, rows, this.schema);
|
|
89
|
+
await this.client.query(text, values);
|
|
90
|
+
}
|
|
91
|
+
async resetSequence(table, column) {
|
|
92
|
+
// Advance the column's owned sequence past the max value just inserted.
|
|
93
|
+
await this.client.query(`SELECT setval(
|
|
94
|
+
pg_get_serial_sequence($1, $2),
|
|
95
|
+
(SELECT COALESCE(MAX("${column}"), 0) FROM ${qualified(this.schema, table)}),
|
|
96
|
+
true
|
|
97
|
+
)`, [`${this.schema}.${table}`, column]);
|
|
98
|
+
}
|
|
99
|
+
async begin() {
|
|
100
|
+
await this.client.query("BEGIN");
|
|
101
|
+
}
|
|
102
|
+
async commit() {
|
|
103
|
+
await this.client.query("COMMIT");
|
|
104
|
+
}
|
|
105
|
+
async rollback() {
|
|
106
|
+
await this.client.query("ROLLBACK");
|
|
107
|
+
}
|
|
108
|
+
async close() {
|
|
109
|
+
await this.client.end();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
exports.PgTargetWriter = PgTargetWriter;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apso/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@apso/cli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.32.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@biomejs/biome": "^1.9.4",
|
package/oclif.manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.32.0",
|
|
3
3
|
"commands": {
|
|
4
4
|
"config": {
|
|
5
5
|
"id": "config",
|
|
@@ -627,6 +627,71 @@
|
|
|
627
627
|
},
|
|
628
628
|
"args": {}
|
|
629
629
|
},
|
|
630
|
+
"import:data": {
|
|
631
|
+
"id": "import:data",
|
|
632
|
+
"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.",
|
|
633
|
+
"strict": true,
|
|
634
|
+
"pluginName": "@apso/cli",
|
|
635
|
+
"pluginAlias": "@apso/cli",
|
|
636
|
+
"pluginType": "core",
|
|
637
|
+
"aliases": [],
|
|
638
|
+
"hiddenAliases": [],
|
|
639
|
+
"examples": [
|
|
640
|
+
"$ apso import data -s \"postgresql://...source...\" -t \"postgresql://...target...\"",
|
|
641
|
+
"$ apso import data --dry-run",
|
|
642
|
+
"$ apso import data --tables users,orders"
|
|
643
|
+
],
|
|
644
|
+
"flags": {
|
|
645
|
+
"source": {
|
|
646
|
+
"name": "source",
|
|
647
|
+
"type": "option",
|
|
648
|
+
"char": "s",
|
|
649
|
+
"description": "Source connection string. Falls back to SUPABASE_DB_URL or DATABASE_URL, then prompts.",
|
|
650
|
+
"multiple": false
|
|
651
|
+
},
|
|
652
|
+
"target": {
|
|
653
|
+
"name": "target",
|
|
654
|
+
"type": "option",
|
|
655
|
+
"char": "t",
|
|
656
|
+
"description": "Target connection string. Falls back to TARGET_DATABASE_URL or APSO_DATABASE_URL, then prompts.",
|
|
657
|
+
"multiple": false
|
|
658
|
+
},
|
|
659
|
+
"schema": {
|
|
660
|
+
"name": "schema",
|
|
661
|
+
"type": "option",
|
|
662
|
+
"description": "Database schema",
|
|
663
|
+
"multiple": false,
|
|
664
|
+
"default": "public"
|
|
665
|
+
},
|
|
666
|
+
"tables": {
|
|
667
|
+
"name": "tables",
|
|
668
|
+
"type": "option",
|
|
669
|
+
"description": "Comma-separated list of source tables to copy (default: all)",
|
|
670
|
+
"multiple": false
|
|
671
|
+
},
|
|
672
|
+
"batch-size": {
|
|
673
|
+
"name": "batch-size",
|
|
674
|
+
"type": "option",
|
|
675
|
+
"description": "Rows per insert batch",
|
|
676
|
+
"multiple": false,
|
|
677
|
+
"default": 500
|
|
678
|
+
},
|
|
679
|
+
"dry-run": {
|
|
680
|
+
"name": "dry-run",
|
|
681
|
+
"type": "boolean",
|
|
682
|
+
"description": "Plan and report row counts without writing",
|
|
683
|
+
"allowNo": false
|
|
684
|
+
},
|
|
685
|
+
"yes": {
|
|
686
|
+
"name": "yes",
|
|
687
|
+
"type": "boolean",
|
|
688
|
+
"char": "y",
|
|
689
|
+
"description": "Skip the confirmation prompt",
|
|
690
|
+
"allowNo": false
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
"args": {}
|
|
694
|
+
},
|
|
630
695
|
"import:supabase": {
|
|
631
696
|
"id": "import:supabase",
|
|
632
697
|
"description": "Introspect an existing Supabase (Postgres) database and generate a local .apsorc schema. Read-only: it never writes to the source database.",
|