@pgpmjs/core 7.13.0 → 7.15.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,172 @@
1
+ /**
2
+ * The apply-spec ("proxy module") types.
3
+ *
4
+ * A proxy module is a directory containing a single `pgpm.apply.json` file:
5
+ * "apply <source module> transpiled with this schema map, under my name". It
6
+ * needs no `.control`, no `pgpm.plan`, and no deploy/revert/verify scripts —
7
+ * workspace discovery synthesizes a module entry from the spec, so the proxy
8
+ * participates in dependency resolution by name exactly like a regular
9
+ * module. At deploy/verify/revert time the engine transpiles the source
10
+ * module in memory and runs the normal migration path against the result;
11
+ * transpiled code is never committed.
12
+ */
13
+ /** File name of the apply spec inside a proxy module directory. */
14
+ export declare const APPLY_SPEC_FILE = "pgpm.apply.json";
15
+ /** Expanded reference to the source module a proxy module applies. */
16
+ export interface ApplySpecSource {
17
+ /**
18
+ * Control/module name of the source module as it appears in the workspace
19
+ * module map (e.g. `pgpm-totp`). The source is typically installed into the
20
+ * workspace extensions directory via `pgpm install`; it is source material,
21
+ * not a runtime dependency, so it must not appear in `requires`.
22
+ */
23
+ module: string;
24
+ /** Optional npm package name the source is installed from (e.g. `@pgpm/totp`). */
25
+ package?: string;
26
+ /** Optional exact version pin for the source package. */
27
+ version?: string;
28
+ /**
29
+ * Optional content-addressed pin: the expected `manifest.digest` of the
30
+ * source bundle. When present, apply refuses to proceed if the installed
31
+ * source does not hash to exactly this digest.
32
+ */
33
+ bundleDigest?: string;
34
+ }
35
+ /**
36
+ * An object-level route in a `pgpm.apply.json` spec: send one named object out
37
+ * of a source schema to a target schema, overriding the whole-schema default
38
+ * for that object only. Kinds are expressed as separate properties (no dotted
39
+ * identity strings), so a table and a function of the same name route
40
+ * independently.
41
+ */
42
+ export interface ApplyRouteEntry {
43
+ /** Source schema the object is defined in (e.g. `users`). */
44
+ fromSchema: string;
45
+ /** Object namespace: `table`/`view` → relation, `procedure` → function. */
46
+ kind: 'table' | 'view' | 'function' | 'procedure' | 'type';
47
+ /** Unqualified object name (e.g. `accounts`). */
48
+ name: string;
49
+ /** Target schema the object is routed to (e.g. `reporting`). */
50
+ toSchema: string;
51
+ }
52
+ /**
53
+ * A per-tenant seed object in a reuse spec: the objects that must be
54
+ * materialized once *per tenant*. `@pgpmjs/slice`'s `partitionModule` starts
55
+ * from these seeds and marks every change that transitively reaches one as
56
+ * per-tenant; everything else is proven shared and emitted once. Expressed as
57
+ * separate properties (no dotted identity strings), matching {@link ApplyRouteEntry}.
58
+ */
59
+ export interface ApplyReuseSeed {
60
+ /** Source schema the seed object is defined in (e.g. `catalog`). */
61
+ fromSchema: string;
62
+ /** Object namespace: `table`/`view` → relation, `procedure` → function. */
63
+ kind: 'table' | 'view' | 'function' | 'procedure' | 'type';
64
+ /** Unqualified object name (e.g. `products`). */
65
+ name: string;
66
+ }
67
+ /**
68
+ * Reuse declaration: split the source into a *shared* module (deployed once,
69
+ * reused by every tenant via ordinary `requires`) and a *per-tenant* module
70
+ * (materialized per instance). The classifier proves the split from the
71
+ * `perTenant` seeds; a shared object that would depend on a per-tenant one is
72
+ * rejected as unsound rather than silently deployed.
73
+ */
74
+ export interface ApplyReuseSpec {
75
+ /**
76
+ * Source schema → *shared* target schema. Shared objects land here; this
77
+ * mapping must be stable across tenant instances so the shared module has a
78
+ * deterministic identity and deploys exactly once.
79
+ */
80
+ sharedSchema: Record<string, string>;
81
+ /** Per-tenant seed objects (non-empty). */
82
+ perTenant: ApplyReuseSeed[];
83
+ /**
84
+ * Optional explicit shared-module name. Defaults to a deterministic name
85
+ * derived from the source module and the shared-schema mapping, so two tenant
86
+ * proxies over the same source + shared mapping resolve to one shared module.
87
+ */
88
+ sharedName?: string;
89
+ }
90
+ /**
91
+ * Extension-routing declaration in a `pgpm.apply.json`: where the transpiled
92
+ * instance should install extensions and resolve the symbols they provide
93
+ * (e.g. isolate `pgcrypto` in a dedicated `extensions` schema, qualifying bare
94
+ * `crypt(...)` calls, or the reverse). A distinct dimension from schema
95
+ * routing — driven by a version-aware symbol inventory, not by the objects the
96
+ * SQL itself creates. Forwarded to `@pgpmjs/transform`'s extension router.
97
+ */
98
+ export interface ApplyExtensionsSpec {
99
+ /**
100
+ * Install the matched extensions and route their provided symbols to this
101
+ * schema. `null` strips qualification (rely on `search_path`). Ignored when
102
+ * `routes` is given.
103
+ */
104
+ toSchema?: string | null;
105
+ /** With `toSchema`: limit to these extensions (default: every inventoried one). */
106
+ only?: string[];
107
+ /**
108
+ * With `toSchema`: which source qualifications to rewrite (a `null` entry
109
+ * also rewrites bare references). Defaults to `public` + bare.
110
+ */
111
+ from?: (string | null)[];
112
+ /**
113
+ * Advanced: explicit per-extension routes (`{ "<ext>": { "to": "<schema>|null",
114
+ * "from"?: [...] } }`). Overrides `toSchema`/`only`/`from`.
115
+ */
116
+ routes?: Record<string, {
117
+ to: string | null;
118
+ from?: (string | null)[];
119
+ }>;
120
+ /** Target PostgreSQL major version, for core-graduation awareness. */
121
+ serverVersion?: number;
122
+ }
123
+ /** Role-name translation: source role name → target role name. */
124
+ export type ApplyRolesSpec = Record<string, string>;
125
+ /** The parsed, normalized `pgpm.apply.json` spec. */
126
+ export interface PgpmApplySpec {
127
+ /**
128
+ * Module name this instance is deployed as. Defaults to the proxy
129
+ * directory's name.
130
+ */
131
+ name?: string;
132
+ /** Source module reference: a module name, or the expanded object form. */
133
+ source: string | ApplySpecSource;
134
+ /**
135
+ * Whole-schema default: source schema → target schema (drives the AST + plan
136
+ * rewrite). Optional when `route` fully covers the objects being moved.
137
+ */
138
+ schemas?: Record<string, string>;
139
+ /**
140
+ * Object-level routes. Each entry overrides the `schemas` default for its
141
+ * specific object, letting one source schema fan out per object (e.g. a
142
+ * table into a tenant schema and a function into a shared schema).
143
+ */
144
+ route?: ApplyRouteEntry[];
145
+ /**
146
+ * Reuse declaration: split shared vs per-tenant objects (see
147
+ * {@link ApplyReuseSpec}). When present, this proxy expands into two modules —
148
+ * a shared module and a per-tenant module that `requires` it.
149
+ */
150
+ reuse?: ApplyReuseSpec;
151
+ /**
152
+ * Extension routing: where to install extensions and resolve their provided
153
+ * symbols in the transpiled instance (see {@link ApplyExtensionsSpec}).
154
+ */
155
+ extensions?: ApplyExtensionsSpec;
156
+ /**
157
+ * Role-name translation applied to the transpiled instance's SQL
158
+ * (see {@link ApplyRolesSpec}). Renames role identifiers only.
159
+ */
160
+ roles?: ApplyRolesSpec;
161
+ /**
162
+ * Runtime requires of the transpiled output (native extensions and other
163
+ * modules). Defaults to the source module's own requires.
164
+ */
165
+ requires?: string[];
166
+ /** Version reported for this instance (default: the source's version). */
167
+ version?: string;
168
+ }
169
+ /** {@link PgpmApplySpec} with `source` normalized to the object form. */
170
+ export interface ResolvedApplySpec extends Omit<PgpmApplySpec, 'source'> {
171
+ source: ApplySpecSource;
172
+ }
package/apply/types.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ /**
3
+ * The apply-spec ("proxy module") types.
4
+ *
5
+ * A proxy module is a directory containing a single `pgpm.apply.json` file:
6
+ * "apply <source module> transpiled with this schema map, under my name". It
7
+ * needs no `.control`, no `pgpm.plan`, and no deploy/revert/verify scripts —
8
+ * workspace discovery synthesizes a module entry from the spec, so the proxy
9
+ * participates in dependency resolution by name exactly like a regular
10
+ * module. At deploy/verify/revert time the engine transpiles the source
11
+ * module in memory and runs the normal migration path against the result;
12
+ * transpiled code is never committed.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.APPLY_SPEC_FILE = void 0;
16
+ /** File name of the apply spec inside a proxy module directory. */
17
+ exports.APPLY_SPEC_FILE = 'pgpm.apply.json';
@@ -0,0 +1,35 @@
1
+ import { CreateBundleOptions, MigrationBundle } from '@pgpmjs/bundle';
2
+ /**
3
+ * Artifact file name for a module's stored bundle: it lives beside the packaged
4
+ * `sql/<name>--<version>.sql` as `sql/<name>--<version>.bundle.tar.gz`.
5
+ */
6
+ export declare function bundleArtifactFileName(name: string, version: string): string;
7
+ /**
8
+ * Locate a module's stored bundle artifact, if it has one.
9
+ *
10
+ * Prefers the version declared in the module's `.control` file, then falls back
11
+ * to any single `sql/*.bundle.tar.gz` present — so a module packaged at a
12
+ * different version still gets the fast path instead of silently degrading.
13
+ * Returns `null` when the module ships no artifact.
14
+ */
15
+ export declare function resolveBundleArtifactPath(moduleDir: string): string | null;
16
+ /**
17
+ * Build the executable form of a module's bundle: the content-addressed AST
18
+ * snapshot plus, per change, the deploy SQL with transaction / `CREATE
19
+ * EXTENSION` statements stripped (`cleanSql`).
20
+ *
21
+ * That pre-computation is the whole point of the artifact — the parse/deparse
22
+ * work moves from every deploy to the one-time package step, while the digests
23
+ * still cover the exact bytes that will be executed.
24
+ */
25
+ export declare function buildExecutableBundle(moduleDir: string, options?: CreateBundleOptions): Promise<MigrationBundle>;
26
+ /**
27
+ * Emit a module's stored bundle artifact into `sql/`, returning its path.
28
+ */
29
+ export declare function writeBundleArtifact(moduleDir: string, version: string, options?: CreateBundleOptions): Promise<string>;
30
+ /**
31
+ * Read a module's stored bundle artifact, or `null` when it has none or the
32
+ * archive cannot be read. Never throws — a broken artifact must degrade to the
33
+ * normal packaging path, not fail the deploy.
34
+ */
35
+ export declare function readBundleArtifact(moduleDir: string): MigrationBundle | null;
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bundleArtifactFileName = bundleArtifactFileName;
4
+ exports.resolveBundleArtifactPath = resolveBundleArtifactPath;
5
+ exports.buildExecutableBundle = buildExecutableBundle;
6
+ exports.writeBundleArtifact = writeBundleArtifact;
7
+ exports.readBundleArtifact = readBundleArtifact;
8
+ const fs_1 = require("fs");
9
+ const path_1 = require("path");
10
+ const files_1 = require("@pgpmjs/ast/files");
11
+ const bundle_1 = require("@pgpmjs/bundle");
12
+ const clean_1 = require("../migrate/clean");
13
+ /**
14
+ * Artifact file name for a module's stored bundle: it lives beside the packaged
15
+ * `sql/<name>--<version>.sql` as `sql/<name>--<version>.bundle.tar.gz`.
16
+ */
17
+ function bundleArtifactFileName(name, version) {
18
+ return `${name}--${version}${bundle_1.BUNDLE_ARCHIVE_EXTENSION}`;
19
+ }
20
+ /**
21
+ * Locate a module's stored bundle artifact, if it has one.
22
+ *
23
+ * Prefers the version declared in the module's `.control` file, then falls back
24
+ * to any single `sql/*.bundle.tar.gz` present — so a module packaged at a
25
+ * different version still gets the fast path instead of silently degrading.
26
+ * Returns `null` when the module ships no artifact.
27
+ */
28
+ function resolveBundleArtifactPath(moduleDir) {
29
+ const sqlDir = (0, path_1.join)(moduleDir, 'sql');
30
+ if (!(0, fs_1.existsSync)(sqlDir))
31
+ return null;
32
+ let name;
33
+ try {
34
+ name = (0, files_1.getExtensionName)(moduleDir);
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ const controlPath = (0, path_1.join)(moduleDir, `${name}.control`);
40
+ if ((0, fs_1.existsSync)(controlPath)) {
41
+ const { version } = (0, files_1.parseControlContent)((0, fs_1.readFileSync)(controlPath, 'utf-8'));
42
+ if (version) {
43
+ const versioned = (0, path_1.join)(sqlDir, bundleArtifactFileName(name, version));
44
+ if ((0, fs_1.existsSync)(versioned))
45
+ return versioned;
46
+ }
47
+ }
48
+ const candidates = (0, fs_1.readdirSync)(sqlDir).filter(file => file.startsWith(`${name}--`) && file.endsWith(bundle_1.BUNDLE_ARCHIVE_EXTENSION));
49
+ if (candidates.length !== 1)
50
+ return null;
51
+ return (0, path_1.join)(sqlDir, candidates[0]);
52
+ }
53
+ /**
54
+ * Build the executable form of a module's bundle: the content-addressed AST
55
+ * snapshot plus, per change, the deploy SQL with transaction / `CREATE
56
+ * EXTENSION` statements stripped (`cleanSql`).
57
+ *
58
+ * That pre-computation is the whole point of the artifact — the parse/deparse
59
+ * work moves from every deploy to the one-time package step, while the digests
60
+ * still cover the exact bytes that will be executed.
61
+ */
62
+ async function buildExecutableBundle(moduleDir, options) {
63
+ const bundle = (0, bundle_1.bundleFromModule)(moduleDir, options);
64
+ const execSql = {};
65
+ for (const change of bundle.changes) {
66
+ if (!change.deploy)
67
+ continue;
68
+ execSql[change.name] = await (0, clean_1.cleanSql)(change.deploy.sql, false, '$EOFCODE$');
69
+ }
70
+ return (0, bundle_1.withExecutableSql)(bundle, execSql);
71
+ }
72
+ /**
73
+ * Emit a module's stored bundle artifact into `sql/`, returning its path.
74
+ */
75
+ async function writeBundleArtifact(moduleDir, version, options) {
76
+ const bundle = await buildExecutableBundle(moduleDir, options);
77
+ const outPath = (0, path_1.join)(moduleDir, 'sql', bundleArtifactFileName(bundle.manifest.name, version));
78
+ (0, bundle_1.writeBundleArchiveFile)(bundle, outPath);
79
+ return outPath;
80
+ }
81
+ /**
82
+ * Read a module's stored bundle artifact, or `null` when it has none or the
83
+ * archive cannot be read. Never throws — a broken artifact must degrade to the
84
+ * normal packaging path, not fail the deploy.
85
+ */
86
+ function readBundleArtifact(moduleDir) {
87
+ const artifactPath = resolveBundleArtifactPath(moduleDir);
88
+ if (!artifactPath)
89
+ return null;
90
+ try {
91
+ return (0, bundle_1.readBundleArchiveFile)(artifactPath);
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
@@ -0,0 +1,71 @@
1
+ import { BundleChange } from '@pgpmjs/bundle';
2
+ import { PgConfig } from 'pg-env';
3
+ /**
4
+ * Why the one-shot path could not be used, so the caller falls back to the
5
+ * per-change migration path.
6
+ */
7
+ export type BundledDeploySkipReason = 'no-executable-sql' | 'unsupported-hash-method';
8
+ /** How the executable bundle for a module was obtained. */
9
+ export type BundleSource = 'artifact' | 'packaged';
10
+ export interface BundledDeployResult {
11
+ /** Module (package) name from the bundle manifest. */
12
+ name: string;
13
+ /** Where the executable bundle came from. */
14
+ source: BundleSource;
15
+ /** Changes executed and recorded on this run, in deploy order. */
16
+ deployed: string[];
17
+ /** Changes already present in the ledger with a matching hash. */
18
+ skipped: string[];
19
+ }
20
+ export interface BundledDeployOptions {
21
+ config: PgConfig;
22
+ /** Target database (defaults to `config.database`). */
23
+ database?: string;
24
+ /** Record ledger rows without executing the DDL. */
25
+ logOnly?: boolean;
26
+ /** Wrap execution + ledger writes in one transaction. Default true. */
27
+ useTransaction?: boolean;
28
+ /**
29
+ * Ledger hash scheme in force. The bundle's per-change digests are sha256 of
30
+ * the deploy script's bytes, i.e. identical to `PgpmMigrate`'s `content`
31
+ * scheme; the `ast` scheme is not reproducible here, so it is reported as a
32
+ * skip and the caller falls back.
33
+ */
34
+ hashMethod?: 'content' | 'ast';
35
+ }
36
+ /** Thrown when the ledger already holds a change with different content. */
37
+ export declare class BundledDeployConflictError extends Error {
38
+ readonly packageName: string;
39
+ readonly changeName: string;
40
+ constructor(packageName: string, changeName: string);
41
+ }
42
+ /**
43
+ * The ledger hash for a change: the sha256 of the deploy script bytes.
44
+ *
45
+ * This is deliberately the same value `PgpmMigrate.calculateScriptHash` derives
46
+ * from `deploy/<change>.sql` under the default `content` hash method, so a
47
+ * change hashed at package time and the hash recorded/compared at deploy time
48
+ * agree — the one-shot and per-change paths are interchangeable and idempotent
49
+ * against each other.
50
+ */
51
+ export declare function ledgerHashForChange(change: BundleChange): string | null;
52
+ /**
53
+ * Deploy a module in one shot **and** record the migration ledger — the `fast`
54
+ * deploy strategy.
55
+ *
56
+ * Pending changes' deploy SQL is concatenated and executed as a single
57
+ * statement (the speed of the old fast path), then one `pgpm_migrate.changes`
58
+ * row per change is bulk-inserted with the deploy script's own sha256 as
59
+ * `script_hash` — one round-trip, instead of an `isDeployed` + `readScript` +
60
+ * `CALL pgpm_migrate.deploy` per change. Because the ledger is written, the
61
+ * path is idempotent and resumable: re-deploys skip changes whose stored hash
62
+ * matches, and a same-name/different-content change raises
63
+ * {@link BundledDeployConflictError} exactly as the per-change path does.
64
+ *
65
+ * Returns a {@link BundledDeploySkipReason} (rather than throwing) when it
66
+ * cannot honour the requested semantics, so the caller falls back to the
67
+ * per-change migration path. Genuine SQL failures still throw.
68
+ */
69
+ export declare function deployModuleFast(moduleDir: string, options: BundledDeployOptions): Promise<BundledDeployResult | BundledDeploySkipReason>;
70
+ /** True when the value is a real deploy result rather than a skip reason. */
71
+ export declare function isBundledDeployResult(value: BundledDeployResult | BundledDeploySkipReason): value is BundledDeployResult;
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BundledDeployConflictError = void 0;
4
+ exports.ledgerHashForChange = ledgerHashForChange;
5
+ exports.deployModuleFast = deployModuleFast;
6
+ exports.isBundledDeployResult = isBundledDeployResult;
7
+ const logger_1 = require("@pgpmjs/logger");
8
+ const bundle_1 = require("@pgpmjs/bundle");
9
+ const pg_cache_1 = require("pg-cache");
10
+ const client_1 = require("../migrate/client");
11
+ const artifact_1 = require("./artifact");
12
+ const log = new logger_1.Logger('deploy-fast');
13
+ /** Thrown when the ledger already holds a change with different content. */
14
+ class BundledDeployConflictError extends Error {
15
+ packageName;
16
+ changeName;
17
+ constructor(packageName, changeName) {
18
+ super(`Change ${changeName} already deployed in package ${packageName} with different content`);
19
+ this.packageName = packageName;
20
+ this.changeName = changeName;
21
+ this.name = 'BundledDeployConflictError';
22
+ }
23
+ }
24
+ exports.BundledDeployConflictError = BundledDeployConflictError;
25
+ /**
26
+ * The ledger hash for a change: the sha256 of the deploy script bytes.
27
+ *
28
+ * This is deliberately the same value `PgpmMigrate.calculateScriptHash` derives
29
+ * from `deploy/<change>.sql` under the default `content` hash method, so a
30
+ * change hashed at package time and the hash recorded/compared at deploy time
31
+ * agree — the one-shot and per-change paths are interchangeable and idempotent
32
+ * against each other.
33
+ */
34
+ function ledgerHashForChange(change) {
35
+ return change.deploy?.digest ?? null;
36
+ }
37
+ /**
38
+ * Load a module's executable bundle: its stored artifact when one verifies,
39
+ * otherwise built from `deploy/` on the fly.
40
+ *
41
+ * The artifact is purely an optimization — it removes the read+parse work, not
42
+ * a capability. Whatever the source, the resulting bundle is byte-identical in
43
+ * what it executes and in the hashes it records, so a module without an
44
+ * artifact deploys exactly the same way, just slower.
45
+ */
46
+ async function loadExecutableBundle(moduleDir) {
47
+ const stored = (0, artifact_1.readBundleArtifact)(moduleDir);
48
+ if (stored) {
49
+ const issues = (0, bundle_1.verifyBundle)(stored);
50
+ if (issues.length === 0) {
51
+ return { bundle: stored, source: 'artifact' };
52
+ }
53
+ log.warn(`⚠️ Bundle artifact for ${stored.manifest.name} failed verification ` +
54
+ `(${issues.map(i => i.kind).join(', ')}); rebuilding from deploy/.`);
55
+ }
56
+ return { bundle: await (0, artifact_1.buildExecutableBundle)(moduleDir), source: 'packaged' };
57
+ }
58
+ /**
59
+ * Deploy a module in one shot **and** record the migration ledger — the `fast`
60
+ * deploy strategy.
61
+ *
62
+ * Pending changes' deploy SQL is concatenated and executed as a single
63
+ * statement (the speed of the old fast path), then one `pgpm_migrate.changes`
64
+ * row per change is bulk-inserted with the deploy script's own sha256 as
65
+ * `script_hash` — one round-trip, instead of an `isDeployed` + `readScript` +
66
+ * `CALL pgpm_migrate.deploy` per change. Because the ledger is written, the
67
+ * path is idempotent and resumable: re-deploys skip changes whose stored hash
68
+ * matches, and a same-name/different-content change raises
69
+ * {@link BundledDeployConflictError} exactly as the per-change path does.
70
+ *
71
+ * Returns a {@link BundledDeploySkipReason} (rather than throwing) when it
72
+ * cannot honour the requested semantics, so the caller falls back to the
73
+ * per-change migration path. Genuine SQL failures still throw.
74
+ */
75
+ async function deployModuleFast(moduleDir, options) {
76
+ if ((options.hashMethod ?? 'content') !== 'content') {
77
+ return 'unsupported-hash-method';
78
+ }
79
+ const { bundle, source } = await loadExecutableBundle(moduleDir);
80
+ const executable = bundle.changes.filter(change => change.deploy);
81
+ if (executable.length === 0 || executable.some(change => !change.exec)) {
82
+ return 'no-executable-sql';
83
+ }
84
+ const packageName = bundle.manifest.name;
85
+ const config = options.database
86
+ ? { ...options.config, database: options.database }
87
+ : options.config;
88
+ // Ensures the pgpm_migrate schema/procedures exist, exactly as the
89
+ // per-change deploy path does.
90
+ await new client_1.PgpmMigrate(config, { hashMethod: 'content' }).initialize();
91
+ const pool = (0, pg_cache_1.getPgPool)(config);
92
+ const existing = await pool.query('SELECT change_name, script_hash FROM pgpm_migrate.changes WHERE package = $1', [packageName]);
93
+ const deployedHashes = new Map(existing.rows.map(row => [row.change_name, row.script_hash]));
94
+ const pending = [];
95
+ const skipped = [];
96
+ for (const change of executable) {
97
+ const hash = ledgerHashForChange(change);
98
+ const recorded = deployedHashes.get(change.name);
99
+ if (recorded === undefined) {
100
+ pending.push(change);
101
+ continue;
102
+ }
103
+ if (recorded !== hash) {
104
+ throw new BundledDeployConflictError(packageName, change.name);
105
+ }
106
+ skipped.push(change.name);
107
+ }
108
+ if (pending.length === 0) {
109
+ log.info(`⚡ ${packageName}: all ${skipped.length} change(s) already deployed.`);
110
+ return { name: packageName, source, deployed: [], skipped };
111
+ }
112
+ const changeNames = pending.map(change => change.name);
113
+ const hashes = pending.map(change => ledgerHashForChange(change));
114
+ const depChanges = [];
115
+ const depRequires = [];
116
+ for (const change of pending) {
117
+ for (const dep of new Set(change.dependencies)) {
118
+ depChanges.push(change.name);
119
+ depRequires.push(dep.includes(':') ? dep : `${packageName}:${dep}`);
120
+ }
121
+ }
122
+ const deploySql = pending.map(change => change.exec.sql).join('\n');
123
+ const client = await pool.connect();
124
+ const useTransaction = options.useTransaction ?? true;
125
+ try {
126
+ if (useTransaction)
127
+ await client.query('BEGIN');
128
+ if (!options.logOnly) {
129
+ await client.query(deploySql);
130
+ }
131
+ await client.query('INSERT INTO pgpm_migrate.packages (package) VALUES ($1) ON CONFLICT (package) DO NOTHING', [packageName]);
132
+ await client.query(`WITH input AS (
133
+ SELECT change_name, script_hash
134
+ FROM unnest($2::text[], $3::text[]) AS t(change_name, script_hash)
135
+ ), inserted AS (
136
+ INSERT INTO pgpm_migrate.changes (change_id, change_name, package, script_hash)
137
+ SELECT encode(sha256(($1 || change_name || script_hash)::bytea), 'hex'),
138
+ change_name, $1, script_hash
139
+ FROM input
140
+ ON CONFLICT (package, change_name) DO NOTHING
141
+ RETURNING change_id, change_name
142
+ ), deps AS (
143
+ INSERT INTO pgpm_migrate.dependencies (change_id, requires)
144
+ SELECT inserted.change_id, d.requires
145
+ FROM unnest($4::text[], $5::text[]) AS d(change_name, requires)
146
+ JOIN inserted ON inserted.change_name = d.change_name
147
+ ON CONFLICT DO NOTHING
148
+ RETURNING change_id
149
+ )
150
+ INSERT INTO pgpm_migrate.events (event_type, change_name, package)
151
+ SELECT 'deploy', change_name, $1 FROM inserted`, [packageName, changeNames, hashes, depChanges, depRequires]);
152
+ if (useTransaction)
153
+ await client.query('COMMIT');
154
+ }
155
+ catch (error) {
156
+ if (useTransaction) {
157
+ try {
158
+ await client.query('ROLLBACK');
159
+ }
160
+ catch {
161
+ // the connection is already unusable; surface the original error
162
+ }
163
+ }
164
+ throw error;
165
+ }
166
+ finally {
167
+ client.release();
168
+ }
169
+ log.success(`⚡ ${packageName}: ${changeNames.length} change(s) ${options.logOnly ? 'logged' : 'deployed'} ` +
170
+ `from ${source === 'artifact' ? 'bundle artifact' : 'packaged deploy/'} ` +
171
+ `(${skipped.length} already deployed).`);
172
+ return { name: packageName, source, deployed: changeNames, skipped };
173
+ }
174
+ /** True when the value is a real deploy result rather than a skip reason. */
175
+ function isBundledDeployResult(value) {
176
+ return typeof value !== 'string';
177
+ }
package/bundle/index.d.ts CHANGED
@@ -11,3 +11,5 @@
11
11
  export * from '@pgpmjs/bundle';
12
12
  export * from './apply';
13
13
  export * from './apply-envelope';
14
+ export * from './artifact';
15
+ export * from './deploy-bundled';
package/bundle/index.js CHANGED
@@ -31,3 +31,7 @@ __exportStar(require("@pgpmjs/bundle"), exports);
31
31
  __exportStar(require("./apply"), exports);
32
32
  // applyEnvelope: schema apply + data/fixtures part replay for a BundleEnvelope.
33
33
  __exportStar(require("./apply-envelope"), exports);
34
+ // The stored `sql/<name>--<version>.bundle.tar.gz` artifact: emit + resolve.
35
+ __exportStar(require("./artifact"), exports);
36
+ // The bundle-backed deploy fast path (one-shot execute + ledger).
37
+ __exportStar(require("./deploy-bundled"), exports);
@@ -97,6 +97,14 @@ export declare class PgpmPackage {
97
97
  * Handles naming collisions by preferring the shortest path.
98
98
  */
99
99
  listModules(): ModuleMap;
100
+ /**
101
+ * Discover apply-spec proxy modules (`pgpm.apply.json`) and synthesize
102
+ * module-map entries for them so they participate in dependency resolution
103
+ * by name like any other module. A proxy needs no `.control` or `pgpm.plan`;
104
+ * its `requires` default to the source module's requires. Control-file
105
+ * modules win name collisions.
106
+ */
107
+ private addApplyModules;
100
108
  getModuleMap(): ModuleMap;
101
109
  getAvailableModules(): Promise<string[]>;
102
110
  getModuleProject(name: string): PgpmPackage;
@@ -271,6 +279,18 @@ export declare class PgpmPackage {
271
279
  resolved: string[];
272
280
  external: string[];
273
281
  }>;
282
+ /**
283
+ * Dependency closure for a named module. Proxy (apply-spec) modules have no
284
+ * pgpm.plan, so they resolve straight off the workspace module map instead
285
+ * of instantiating a module-rooted package.
286
+ */
287
+ private getModuleExtensionsFor;
288
+ /**
289
+ * Directory the migration engine should run against for a named module:
290
+ * the module's own directory, or — for proxy (apply-spec) modules — the
291
+ * materialized transpiled instance.
292
+ */
293
+ private getEffectiveModulePathFor;
274
294
  private parsePackageTarget;
275
295
  deploy(opts: PgpmOptions, target?: string, recursive?: boolean): Promise<void>;
276
296
  /**