@pgpmjs/core 7.17.2 → 7.18.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/apply/materialize.js +23 -21
- package/bundle/artifact.d.ts +12 -0
- package/bundle/artifact.js +30 -0
- package/bundle/deploy-bundled.js +9 -3
- package/esm/apply/materialize.js +25 -23
- package/esm/bundle/artifact.js +29 -0
- package/esm/bundle/deploy-bundled.js +10 -4
- package/package.json +4 -4
package/apply/materialize.js
CHANGED
|
@@ -35,11 +35,13 @@ const schemaNameLiteralPass = (content, schemaMapping) => {
|
|
|
35
35
|
function makeSubsystemStripper(source, spec, instanceName) {
|
|
36
36
|
const selector = { schemas: spec.exclude.schemas };
|
|
37
37
|
const rebinds = (0, transform_1.buildSchemaRouter)({ schemaMap: spec.schemas, routes: spec.route });
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
// One parse per deploy script; the analysis runs over all programs together
|
|
39
|
+
// (cross-change references into the subsystem are caught by construction)
|
|
40
|
+
// and returns per-change drop/strip/prune decisions from the same pass.
|
|
41
|
+
const programs = source.changes
|
|
42
|
+
.filter(c => c.deploy)
|
|
43
|
+
.map(c => [c.name, (0, transform_1.parseSqlProgram)(c.deploy.sql)]);
|
|
44
|
+
const analysis = (0, slice_1.excludeSubsystemPrograms)(programs, selector, { rebinds });
|
|
43
45
|
if (analysis.unsatisfied.length > 0) {
|
|
44
46
|
const detail = [
|
|
45
47
|
...new Set(analysis.unsatisfied.map(u => `${u.object.schema}.${u.object.name}${u.fk ? ' (foreign key target)' : ''}`))
|
|
@@ -49,26 +51,26 @@ function makeSubsystemStripper(source, spec, instanceName) {
|
|
|
49
51
|
`${detail} with no route/rebind target. Add "route" entries substituting each ` +
|
|
50
52
|
`referenced object, or keep the subsystem.`);
|
|
51
53
|
}
|
|
52
|
-
// A change
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
54
|
+
// A fully-excluded change is dropped *as a change* — removed from the plan
|
|
55
|
+
// and deploy order rather than emitted as an empty script (empty scripts
|
|
56
|
+
// collide on the deploy ledger's script-hash uniqueness). Its verify/revert
|
|
57
|
+
// scripts target dropped objects the classifier can't always see (bare
|
|
58
|
+
// DROPs, catalog probes), which is exactly why the whole change is dropped,
|
|
59
|
+
// not just blanked.
|
|
58
60
|
const excludedChanges = new Set();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
61
|
+
const strippedDeploys = new Map();
|
|
62
|
+
for (const [name, program] of programs) {
|
|
63
|
+
const exclusion = analysis.programs.get(name);
|
|
64
|
+
if (exclusion.fullyExcluded)
|
|
65
|
+
excludedChanges.add(name);
|
|
66
|
+
else
|
|
67
|
+
strippedDeploys.set(program.source, exclusion.sql);
|
|
67
68
|
}
|
|
68
69
|
// Surviving changes may still contain stray subsystem statements (a change
|
|
69
70
|
// that both creates a survivor and touches the excluded subsystem); strip
|
|
70
|
-
// those in place.
|
|
71
|
-
|
|
71
|
+
// those in place. Verify/revert scripts are separate sources, stripped on
|
|
72
|
+
// demand with the same selector.
|
|
73
|
+
const strip = (sql) => strippedDeploys.get(sql) ?? (0, slice_1.stripSubsystemSql)(sql, selector).sql;
|
|
72
74
|
return { strip, excludedChanges };
|
|
73
75
|
}
|
|
74
76
|
/**
|
package/bundle/artifact.d.ts
CHANGED
|
@@ -27,6 +27,18 @@ export declare function buildExecutableBundle(moduleDir: string, options?: Creat
|
|
|
27
27
|
* Emit a module's stored bundle artifact into `sql/`, returning its path.
|
|
28
28
|
*/
|
|
29
29
|
export declare function writeBundleArtifact(moduleDir: string, version: string, options?: CreateBundleOptions): Promise<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Whether a stored bundle still describes what is on disk.
|
|
32
|
+
*
|
|
33
|
+
* `verifyBundle` only proves an artifact is internally consistent — it cannot
|
|
34
|
+
* tell that `deploy/` moved on since the artifact was written. A committed
|
|
35
|
+
* artifact plus a rebased branch is exactly that case, and using it would
|
|
36
|
+
* deploy the stale SQL while recording hashes for it, silently.
|
|
37
|
+
*
|
|
38
|
+
* The check is deliberately I/O-only (read + sha256, no parse), so it costs a
|
|
39
|
+
* fraction of the parse/deparse work the artifact exists to avoid.
|
|
40
|
+
*/
|
|
41
|
+
export declare function bundleMatchesModule(moduleDir: string, bundle: MigrationBundle): boolean;
|
|
30
42
|
/**
|
|
31
43
|
* Read a module's stored bundle artifact, or `null` when it has none or the
|
|
32
44
|
* archive cannot be read. Never throws — a broken artifact must degrade to the
|
package/bundle/artifact.js
CHANGED
|
@@ -4,9 +4,11 @@ exports.bundleArtifactFileName = bundleArtifactFileName;
|
|
|
4
4
|
exports.resolveBundleArtifactPath = resolveBundleArtifactPath;
|
|
5
5
|
exports.buildExecutableBundle = buildExecutableBundle;
|
|
6
6
|
exports.writeBundleArtifact = writeBundleArtifact;
|
|
7
|
+
exports.bundleMatchesModule = bundleMatchesModule;
|
|
7
8
|
exports.readBundleArtifact = readBundleArtifact;
|
|
8
9
|
const fs_1 = require("fs");
|
|
9
10
|
const path_1 = require("path");
|
|
11
|
+
const ast_1 = require("@pgpmjs/ast");
|
|
10
12
|
const files_1 = require("@pgpmjs/ast/files");
|
|
11
13
|
const bundle_1 = require("@pgpmjs/bundle");
|
|
12
14
|
const clean_1 = require("../migrate/clean");
|
|
@@ -78,6 +80,34 @@ async function writeBundleArtifact(moduleDir, version, options) {
|
|
|
78
80
|
(0, bundle_1.writeBundleArchiveFile)(bundle, outPath);
|
|
79
81
|
return outPath;
|
|
80
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Whether a stored bundle still describes what is on disk.
|
|
85
|
+
*
|
|
86
|
+
* `verifyBundle` only proves an artifact is internally consistent — it cannot
|
|
87
|
+
* tell that `deploy/` moved on since the artifact was written. A committed
|
|
88
|
+
* artifact plus a rebased branch is exactly that case, and using it would
|
|
89
|
+
* deploy the stale SQL while recording hashes for it, silently.
|
|
90
|
+
*
|
|
91
|
+
* The check is deliberately I/O-only (read + sha256, no parse), so it costs a
|
|
92
|
+
* fraction of the parse/deparse work the artifact exists to avoid.
|
|
93
|
+
*/
|
|
94
|
+
function bundleMatchesModule(moduleDir, bundle) {
|
|
95
|
+
const planPath = (0, path_1.join)(moduleDir, 'pgpm.plan');
|
|
96
|
+
if (!(0, fs_1.existsSync)(planPath))
|
|
97
|
+
return false;
|
|
98
|
+
if ((0, fs_1.readFileSync)(planPath, 'utf-8') !== bundle.plan)
|
|
99
|
+
return false;
|
|
100
|
+
for (const change of bundle.changes) {
|
|
101
|
+
if (!change.deploy)
|
|
102
|
+
continue;
|
|
103
|
+
const deployPath = (0, path_1.join)(moduleDir, 'deploy', `${change.name}.sql`);
|
|
104
|
+
if (!(0, fs_1.existsSync)(deployPath))
|
|
105
|
+
return false;
|
|
106
|
+
if ((0, ast_1.hashString)((0, fs_1.readFileSync)(deployPath, 'utf-8')) !== change.deploy.digest)
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
81
111
|
/**
|
|
82
112
|
* Read a module's stored bundle artifact, or `null` when it has none or the
|
|
83
113
|
* archive cannot be read. Never throws — a broken artifact must degrade to the
|
package/bundle/deploy-bundled.js
CHANGED
|
@@ -47,11 +47,17 @@ async function loadExecutableBundle(moduleDir) {
|
|
|
47
47
|
const stored = (0, artifact_1.readBundleArtifact)(moduleDir);
|
|
48
48
|
if (stored) {
|
|
49
49
|
const issues = (0, bundle_1.verifyBundle)(stored);
|
|
50
|
-
if (issues.length
|
|
50
|
+
if (issues.length > 0) {
|
|
51
|
+
log.warn(`⚠️ Bundle artifact for ${stored.manifest.name} failed verification ` +
|
|
52
|
+
`(${issues.map(i => i.kind).join(', ')}); rebuilding from deploy/.`);
|
|
53
|
+
}
|
|
54
|
+
else if (!(0, artifact_1.bundleMatchesModule)(moduleDir, stored)) {
|
|
55
|
+
log.warn(`⚠️ Bundle artifact for ${stored.manifest.name} is stale (deploy/ has ` +
|
|
56
|
+
`changed since it was packaged); rebuilding from deploy/.`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
51
59
|
return { bundle: stored, source: 'artifact' };
|
|
52
60
|
}
|
|
53
|
-
log.warn(`⚠️ Bundle artifact for ${stored.manifest.name} failed verification ` +
|
|
54
|
-
`(${issues.map(i => i.kind).join(', ')}); rebuilding from deploy/.`);
|
|
55
61
|
}
|
|
56
62
|
return { bundle: await (0, artifact_1.buildExecutableBundle)(moduleDir), source: 'packaged' };
|
|
57
63
|
}
|
package/esm/apply/materialize.js
CHANGED
|
@@ -2,8 +2,8 @@ import { mkdtempSync } from 'fs';
|
|
|
2
2
|
import { tmpdir } from 'os';
|
|
3
3
|
import { join, resolve } from 'path';
|
|
4
4
|
import { bundleFromModule, materializeBundle, transpileBundle, verifyBundle } from '@pgpmjs/bundle';
|
|
5
|
-
import { buildSchemaRouter, loadModule, makeSchemaTranspiler } from '@pgpmjs/transform';
|
|
6
|
-
import {
|
|
5
|
+
import { buildSchemaRouter, loadModule, makeSchemaTranspiler, parseSqlProgram } from '@pgpmjs/transform';
|
|
6
|
+
import { excludeSubsystemPrograms, stripSubsystemSql } from '@pgpmjs/slice';
|
|
7
7
|
import { hasApplySpec, readApplySpec } from './apply-spec';
|
|
8
8
|
import { loadWorkspaceRoutingProfile, resolveEffectiveApplySpec } from './profile';
|
|
9
9
|
import { isReuseSpec, materializeReuseModule, resolveSharedModuleName } from './reuse';
|
|
@@ -30,11 +30,13 @@ const schemaNameLiteralPass = (content, schemaMapping) => {
|
|
|
30
30
|
function makeSubsystemStripper(source, spec, instanceName) {
|
|
31
31
|
const selector = { schemas: spec.exclude.schemas };
|
|
32
32
|
const rebinds = buildSchemaRouter({ schemaMap: spec.schemas, routes: spec.route });
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
// One parse per deploy script; the analysis runs over all programs together
|
|
34
|
+
// (cross-change references into the subsystem are caught by construction)
|
|
35
|
+
// and returns per-change drop/strip/prune decisions from the same pass.
|
|
36
|
+
const programs = source.changes
|
|
37
|
+
.filter(c => c.deploy)
|
|
38
|
+
.map(c => [c.name, parseSqlProgram(c.deploy.sql)]);
|
|
39
|
+
const analysis = excludeSubsystemPrograms(programs, selector, { rebinds });
|
|
38
40
|
if (analysis.unsatisfied.length > 0) {
|
|
39
41
|
const detail = [
|
|
40
42
|
...new Set(analysis.unsatisfied.map(u => `${u.object.schema}.${u.object.name}${u.fk ? ' (foreign key target)' : ''}`))
|
|
@@ -44,26 +46,26 @@ function makeSubsystemStripper(source, spec, instanceName) {
|
|
|
44
46
|
`${detail} with no route/rebind target. Add "route" entries substituting each ` +
|
|
45
47
|
`referenced object, or keep the subsystem.`);
|
|
46
48
|
}
|
|
47
|
-
// A change
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
49
|
+
// A fully-excluded change is dropped *as a change* — removed from the plan
|
|
50
|
+
// and deploy order rather than emitted as an empty script (empty scripts
|
|
51
|
+
// collide on the deploy ledger's script-hash uniqueness). Its verify/revert
|
|
52
|
+
// scripts target dropped objects the classifier can't always see (bare
|
|
53
|
+
// DROPs, catalog probes), which is exactly why the whole change is dropped,
|
|
54
|
+
// not just blanked.
|
|
53
55
|
const excludedChanges = new Set();
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
56
|
+
const strippedDeploys = new Map();
|
|
57
|
+
for (const [name, program] of programs) {
|
|
58
|
+
const exclusion = analysis.programs.get(name);
|
|
59
|
+
if (exclusion.fullyExcluded)
|
|
60
|
+
excludedChanges.add(name);
|
|
61
|
+
else
|
|
62
|
+
strippedDeploys.set(program.source, exclusion.sql);
|
|
62
63
|
}
|
|
63
64
|
// Surviving changes may still contain stray subsystem statements (a change
|
|
64
65
|
// that both creates a survivor and touches the excluded subsystem); strip
|
|
65
|
-
// those in place.
|
|
66
|
-
|
|
66
|
+
// those in place. Verify/revert scripts are separate sources, stripped on
|
|
67
|
+
// demand with the same selector.
|
|
68
|
+
const strip = (sql) => strippedDeploys.get(sql) ?? stripSubsystemSql(sql, selector).sql;
|
|
67
69
|
return { strip, excludedChanges };
|
|
68
70
|
}
|
|
69
71
|
/**
|
package/esm/bundle/artifact.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
+
import { hashString } from '@pgpmjs/ast';
|
|
3
4
|
import { getExtensionName, parseControlContent } from '@pgpmjs/ast/files';
|
|
4
5
|
import { BUNDLE_ARCHIVE_EXTENSION, bundleFromModule, readBundleArchiveFile, withExecutableSql, writeBundleArchiveFile } from '@pgpmjs/bundle';
|
|
5
6
|
import { cleanSql } from '../migrate/clean';
|
|
@@ -71,6 +72,34 @@ export async function writeBundleArtifact(moduleDir, version, options) {
|
|
|
71
72
|
writeBundleArchiveFile(bundle, outPath);
|
|
72
73
|
return outPath;
|
|
73
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether a stored bundle still describes what is on disk.
|
|
77
|
+
*
|
|
78
|
+
* `verifyBundle` only proves an artifact is internally consistent — it cannot
|
|
79
|
+
* tell that `deploy/` moved on since the artifact was written. A committed
|
|
80
|
+
* artifact plus a rebased branch is exactly that case, and using it would
|
|
81
|
+
* deploy the stale SQL while recording hashes for it, silently.
|
|
82
|
+
*
|
|
83
|
+
* The check is deliberately I/O-only (read + sha256, no parse), so it costs a
|
|
84
|
+
* fraction of the parse/deparse work the artifact exists to avoid.
|
|
85
|
+
*/
|
|
86
|
+
export function bundleMatchesModule(moduleDir, bundle) {
|
|
87
|
+
const planPath = join(moduleDir, 'pgpm.plan');
|
|
88
|
+
if (!existsSync(planPath))
|
|
89
|
+
return false;
|
|
90
|
+
if (readFileSync(planPath, 'utf-8') !== bundle.plan)
|
|
91
|
+
return false;
|
|
92
|
+
for (const change of bundle.changes) {
|
|
93
|
+
if (!change.deploy)
|
|
94
|
+
continue;
|
|
95
|
+
const deployPath = join(moduleDir, 'deploy', `${change.name}.sql`);
|
|
96
|
+
if (!existsSync(deployPath))
|
|
97
|
+
return false;
|
|
98
|
+
if (hashString(readFileSync(deployPath, 'utf-8')) !== change.deploy.digest)
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
74
103
|
/**
|
|
75
104
|
* Read a module's stored bundle artifact, or `null` when it has none or the
|
|
76
105
|
* archive cannot be read. Never throws — a broken artifact must degrade to the
|
|
@@ -2,7 +2,7 @@ import { Logger } from '@pgpmjs/logger';
|
|
|
2
2
|
import { verifyBundle } from '@pgpmjs/bundle';
|
|
3
3
|
import { getPgPool } from 'pg-cache';
|
|
4
4
|
import { PgpmMigrate } from '../migrate/client';
|
|
5
|
-
import { buildExecutableBundle, readBundleArtifact } from './artifact';
|
|
5
|
+
import { bundleMatchesModule, buildExecutableBundle, readBundleArtifact } from './artifact';
|
|
6
6
|
const log = new Logger('deploy-fast');
|
|
7
7
|
/** Thrown when the ledger already holds a change with different content. */
|
|
8
8
|
export class BundledDeployConflictError extends Error {
|
|
@@ -40,11 +40,17 @@ async function loadExecutableBundle(moduleDir) {
|
|
|
40
40
|
const stored = readBundleArtifact(moduleDir);
|
|
41
41
|
if (stored) {
|
|
42
42
|
const issues = verifyBundle(stored);
|
|
43
|
-
if (issues.length
|
|
43
|
+
if (issues.length > 0) {
|
|
44
|
+
log.warn(`⚠️ Bundle artifact for ${stored.manifest.name} failed verification ` +
|
|
45
|
+
`(${issues.map(i => i.kind).join(', ')}); rebuilding from deploy/.`);
|
|
46
|
+
}
|
|
47
|
+
else if (!bundleMatchesModule(moduleDir, stored)) {
|
|
48
|
+
log.warn(`⚠️ Bundle artifact for ${stored.manifest.name} is stale (deploy/ has ` +
|
|
49
|
+
`changed since it was packaged); rebuilding from deploy/.`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
44
52
|
return { bundle: stored, source: 'artifact' };
|
|
45
53
|
}
|
|
46
|
-
log.warn(`⚠️ Bundle artifact for ${stored.manifest.name} failed verification ` +
|
|
47
|
-
`(${issues.map(i => i.kind).join(', ')}); rebuilding from deploy/.`);
|
|
48
54
|
}
|
|
49
55
|
return { bundle: await buildExecutableBundle(moduleDir), source: 'packaged' };
|
|
50
56
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgpmjs/core",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.18.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "PGPM Package and Migration Tools",
|
|
6
6
|
"main": "index.js",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"@pgpmjs/env": "^2.39.2",
|
|
54
54
|
"@pgpmjs/logger": "^2.22.1",
|
|
55
55
|
"@pgpmjs/server-utils": "^3.23.4",
|
|
56
|
-
"@pgpmjs/slice": "^0.
|
|
57
|
-
"@pgpmjs/transform": "^0.
|
|
56
|
+
"@pgpmjs/slice": "^0.12.0",
|
|
57
|
+
"@pgpmjs/transform": "^0.13.0",
|
|
58
58
|
"@pgpmjs/types": "^2.48.1",
|
|
59
59
|
"csv-to-pg": "^4.8.1",
|
|
60
60
|
"genomic": "^5.6.2",
|
|
@@ -67,5 +67,5 @@
|
|
|
67
67
|
"pgsql-parser": "^18.1.1",
|
|
68
68
|
"yanse": "^0.2.1"
|
|
69
69
|
},
|
|
70
|
-
"gitHead": "
|
|
70
|
+
"gitHead": "04b34ce4b5efcebfd8e49d64d196e651288d97f3"
|
|
71
71
|
}
|