@supabase/pg-delta 1.0.0-alpha.1 → 1.0.0-alpha.3
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/core/catalog.model.d.ts +2 -2
- package/dist/core/catalog.model.js +29 -29
- package/dist/core/context.d.ts +3 -3
- package/dist/core/context.js +7 -10
- package/dist/core/depend.d.ts +2 -2
- package/dist/core/depend.js +8 -7
- package/dist/core/integrations/supabase.js +2 -0
- package/dist/core/objects/aggregate/aggregate.model.d.ts +2 -2
- package/dist/core/objects/aggregate/aggregate.model.js +7 -9
- package/dist/core/objects/collation/collation.model.d.ts +2 -2
- package/dist/core/objects/collation/collation.model.js +29 -28
- package/dist/core/objects/domain/domain.model.d.ts +2 -2
- package/dist/core/objects/domain/domain.model.js +8 -10
- package/dist/core/objects/event-trigger/event-trigger.model.d.ts +2 -2
- package/dist/core/objects/event-trigger/event-trigger.model.js +7 -9
- package/dist/core/objects/extension/extension.model.d.ts +2 -2
- package/dist/core/objects/extension/extension.model.js +8 -10
- package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.d.ts +2 -2
- package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.js +20 -22
- package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.d.ts +2 -2
- package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.js +20 -22
- package/dist/core/objects/foreign-data-wrapper/server/server.model.d.ts +2 -2
- package/dist/core/objects/foreign-data-wrapper/server/server.model.js +20 -22
- package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.d.ts +2 -2
- package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.js +20 -22
- package/dist/core/objects/index/index.model.d.ts +4 -4
- package/dist/core/objects/index/index.model.js +9 -11
- package/dist/core/objects/language/language.model.js +5 -7
- package/dist/core/objects/materialized-view/materialized-view.model.d.ts +2 -2
- package/dist/core/objects/materialized-view/materialized-view.model.js +8 -10
- package/dist/core/objects/procedure/procedure.model.d.ts +2 -2
- package/dist/core/objects/procedure/procedure.model.js +8 -10
- package/dist/core/objects/publication/publication.model.d.ts +2 -2
- package/dist/core/objects/publication/publication.model.js +7 -9
- package/dist/core/objects/rls-policy/rls-policy.model.d.ts +2 -2
- package/dist/core/objects/rls-policy/rls-policy.model.js +8 -10
- package/dist/core/objects/role/role.model.d.ts +2 -2
- package/dist/core/objects/role/role.model.js +28 -28
- package/dist/core/objects/rule/rule.model.d.ts +2 -2
- package/dist/core/objects/rule/rule.model.js +7 -9
- package/dist/core/objects/schema/schema.model.d.ts +2 -2
- package/dist/core/objects/schema/schema.model.js +8 -10
- package/dist/core/objects/sequence/sequence.model.d.ts +2 -2
- package/dist/core/objects/sequence/sequence.model.js +8 -10
- package/dist/core/objects/subscription/subscription.model.d.ts +2 -2
- package/dist/core/objects/subscription/subscription.model.js +25 -20
- package/dist/core/objects/table/table.model.d.ts +2 -2
- package/dist/core/objects/table/table.model.js +8 -10
- package/dist/core/objects/trigger/trigger.model.d.ts +2 -2
- package/dist/core/objects/trigger/trigger.model.js +8 -10
- package/dist/core/objects/type/composite-type/composite-type.model.d.ts +2 -2
- package/dist/core/objects/type/composite-type/composite-type.model.js +8 -10
- package/dist/core/objects/type/enum/enum.model.d.ts +2 -2
- package/dist/core/objects/type/enum/enum.model.js +22 -24
- package/dist/core/objects/type/range/range.model.d.ts +2 -2
- package/dist/core/objects/type/range/range.model.js +7 -9
- package/dist/core/objects/view/view.model.d.ts +2 -2
- package/dist/core/objects/view/view.model.js +8 -10
- package/dist/core/plan/apply.d.ts +2 -2
- package/dist/core/plan/apply.js +50 -16
- package/dist/core/plan/create.d.ts +2 -2
- package/dist/core/plan/create.js +84 -38
- package/dist/core/postgres-config.d.ts +18 -3
- package/dist/core/postgres-config.js +105 -41
- package/package.json +4 -2
package/dist/core/plan/create.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* Plan creation - the main entry point for creating migration plans.
|
|
3
3
|
*/
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
|
-
import
|
|
5
|
+
import { escapeIdentifier } from "pg";
|
|
6
6
|
import { diffCatalogs } from "../catalog.diff.js";
|
|
7
7
|
import { extractCatalog } from "../catalog.model.js";
|
|
8
8
|
import { buildPlanScopeFingerprint, hashStableIds } from "../fingerprint.js";
|
|
9
9
|
import { compileFilterDSL, } from "../integrations/filter/dsl.js";
|
|
10
10
|
import { compileSerializeDSL, } from "../integrations/serialize/dsl.js";
|
|
11
|
-
import {
|
|
11
|
+
import { createPool } from "../postgres-config.js";
|
|
12
12
|
import { sortChanges } from "../sort/sort-changes.js";
|
|
13
13
|
import { classifyChangesRisk } from "./risk.js";
|
|
14
14
|
/**
|
|
@@ -39,10 +39,6 @@ async function parseSslConfig(url, connectionType) {
|
|
|
39
39
|
sslmode === "prefer" ||
|
|
40
40
|
sslmode === "verify-ca" ||
|
|
41
41
|
sslmode === "verify-full") {
|
|
42
|
-
const rejectUnauthorized = sslmode === "verify-ca" || sslmode === "verify-full";
|
|
43
|
-
const ssl = {
|
|
44
|
-
rejectUnauthorized,
|
|
45
|
-
};
|
|
46
42
|
// Helper function to get certificate value: query param (file path) takes precedence over env var (content)
|
|
47
43
|
const getCertValue = async (queryParam, envVarName) => {
|
|
48
44
|
// Prefer query parameter (file path)
|
|
@@ -58,15 +54,35 @@ async function parseSslConfig(url, connectionType) {
|
|
|
58
54
|
const envValue = process.env[envVarName];
|
|
59
55
|
return envValue || undefined;
|
|
60
56
|
};
|
|
61
|
-
// Get CA certificate (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
57
|
+
// Get CA certificate value (needed for verify-ca, verify-full, and libpq compatibility with require/prefer)
|
|
58
|
+
const caEnvVar = connectionType === "source"
|
|
59
|
+
? "PGDELTA_SOURCE_SSLROOTCERT"
|
|
60
|
+
: "PGDELTA_TARGET_SSLROOTCERT";
|
|
61
|
+
const caValue = await getCertValue(sslrootcert, caEnvVar);
|
|
62
|
+
// Determine if we should verify the CA chain
|
|
63
|
+
// - verify-ca and verify-full: always verify CA
|
|
64
|
+
// - require/prefer with CA cert provided: verify CA (libpq backward compatibility)
|
|
65
|
+
// From PostgreSQL docs: "if a root CA file exists, the behavior of sslmode=require
|
|
66
|
+
// will be the same as that of verify-ca"
|
|
67
|
+
const hasExplicitVerification = sslmode === "verify-ca" || sslmode === "verify-full";
|
|
68
|
+
const hasLibpqCompatibility = (sslmode === "require" || sslmode === "prefer") && caValue !== undefined;
|
|
69
|
+
const shouldVerifyCa = hasExplicitVerification || hasLibpqCompatibility;
|
|
70
|
+
// Determine if we should verify hostname
|
|
71
|
+
// - verify-full: verify both CA and hostname
|
|
72
|
+
// - verify-ca: verify CA only (skip hostname)
|
|
73
|
+
// - require/prefer with CA (libpq compat): behaves like verify-ca (skip hostname)
|
|
74
|
+
const shouldVerifyHostname = sslmode === "verify-full";
|
|
75
|
+
const ssl = {
|
|
76
|
+
rejectUnauthorized: shouldVerifyCa,
|
|
77
|
+
};
|
|
78
|
+
// Add CA certificate if verifying
|
|
79
|
+
if (shouldVerifyCa && caValue) {
|
|
80
|
+
ssl.ca = caValue;
|
|
81
|
+
}
|
|
82
|
+
// For verify-ca and libpq compatibility mode: skip hostname verification
|
|
83
|
+
// This matches PostgreSQL semantics where verify-ca only checks the CA chain
|
|
84
|
+
if (shouldVerifyCa && !shouldVerifyHostname) {
|
|
85
|
+
ssl.checkServerIdentity = () => undefined;
|
|
70
86
|
}
|
|
71
87
|
// Get client certificate (optional, for mutual TLS)
|
|
72
88
|
const certEnvVar = connectionType === "source"
|
|
@@ -94,35 +110,65 @@ async function parseSslConfig(url, connectionType) {
|
|
|
94
110
|
return { cleanedUrl };
|
|
95
111
|
}
|
|
96
112
|
export async function createPlan(source, target, options = {}) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
let sourcePool;
|
|
114
|
+
let targetPool;
|
|
115
|
+
let shouldCloseSource = false;
|
|
116
|
+
let shouldCloseTarget = false;
|
|
117
|
+
// Suppress expected shutdown errors from idle pool connections (57P01 = admin_shutdown)
|
|
118
|
+
const onError = (err) => {
|
|
119
|
+
if (err.code !== "57P01") {
|
|
120
|
+
console.error("Pool error:", err);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
if (typeof source === "string") {
|
|
124
|
+
const sslConfig = await parseSslConfig(source, "source");
|
|
125
|
+
sourcePool = createPool(sslConfig.cleanedUrl, {
|
|
126
|
+
...(sslConfig.ssl ? { ssl: sslConfig.ssl } : {}),
|
|
127
|
+
onError,
|
|
128
|
+
onConnect: async (client) => {
|
|
129
|
+
// Force fully qualified names in catalog queries
|
|
130
|
+
await client.query("SET search_path = ''");
|
|
131
|
+
if (options.role) {
|
|
132
|
+
await client.query(`SET ROLE ${escapeIdentifier(options.role)}`);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
shouldCloseSource = true;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
sourcePool = source;
|
|
140
|
+
}
|
|
141
|
+
if (typeof target === "string") {
|
|
142
|
+
const sslConfig = await parseSslConfig(target, "target");
|
|
143
|
+
targetPool = createPool(sslConfig.cleanedUrl, {
|
|
144
|
+
...(sslConfig.ssl ? { ssl: sslConfig.ssl } : {}),
|
|
145
|
+
onError,
|
|
146
|
+
onConnect: async (client) => {
|
|
147
|
+
// Force fully qualified names in catalog queries
|
|
148
|
+
await client.query("SET search_path = ''");
|
|
149
|
+
if (options.role) {
|
|
150
|
+
await client.query(`SET ROLE ${escapeIdentifier(options.role)}`);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
shouldCloseTarget = true;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
targetPool = target;
|
|
158
|
+
}
|
|
113
159
|
try {
|
|
114
160
|
const [fromCatalog, toCatalog] = await Promise.all([
|
|
115
|
-
extractCatalog(
|
|
116
|
-
extractCatalog(
|
|
161
|
+
extractCatalog(sourcePool),
|
|
162
|
+
extractCatalog(targetPool),
|
|
117
163
|
]);
|
|
118
164
|
return buildPlanForCatalogs(fromCatalog, toCatalog, options);
|
|
119
165
|
}
|
|
120
166
|
finally {
|
|
121
167
|
const closers = [];
|
|
122
|
-
if (
|
|
123
|
-
closers.push(
|
|
124
|
-
if (
|
|
125
|
-
closers.push(
|
|
168
|
+
if (shouldCloseSource)
|
|
169
|
+
closers.push(sourcePool.end());
|
|
170
|
+
if (shouldCloseTarget)
|
|
171
|
+
closers.push(targetPool.end());
|
|
126
172
|
if (closers.length) {
|
|
127
173
|
await Promise.all(closers);
|
|
128
174
|
}
|
|
@@ -208,7 +254,7 @@ function buildPlan(ctx, changes, options, filterDSL, serializeDSL, integration)
|
|
|
208
254
|
function generateStatements(changes, options) {
|
|
209
255
|
const statements = [];
|
|
210
256
|
if (options?.role) {
|
|
211
|
-
statements.push(`SET ROLE
|
|
257
|
+
statements.push(`SET ROLE ${escapeIdentifier(options.role)}`);
|
|
212
258
|
}
|
|
213
259
|
if (hasRoutineChanges(changes)) {
|
|
214
260
|
statements.push("SET check_function_bodies = false");
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PostgreSQL connection configuration with custom type handlers.
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
4
|
+
import type { PoolClient, PoolConfig } from "pg";
|
|
5
|
+
import { Pool } from "pg";
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
+
* Options for creating a Pool with event listeners.
|
|
7
8
|
*/
|
|
8
|
-
|
|
9
|
+
interface CreatePoolOptions extends Partial<PoolConfig> {
|
|
10
|
+
/** Called when a new client connects to the pool */
|
|
11
|
+
onConnect?: (client: PoolClient) => void | Promise<void>;
|
|
12
|
+
/** Called when an idle client emits an error */
|
|
13
|
+
onError?: (err: Error, client: PoolClient) => void;
|
|
14
|
+
/** Called when a client is acquired from the pool */
|
|
15
|
+
onAcquire?: (client: PoolClient) => void;
|
|
16
|
+
/** Called when a client is removed from the pool */
|
|
17
|
+
onRemove?: (client: PoolClient) => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create a Pool with custom type handlers and optional event listeners.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createPool(connectionString: string, options?: CreatePoolOptions): Pool;
|
|
23
|
+
export {};
|
|
@@ -1,46 +1,110 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PostgreSQL connection configuration with custom type handlers.
|
|
3
3
|
*/
|
|
4
|
+
import { Pool, types } from "pg";
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Array Parser
|
|
7
|
+
// ============================================================================
|
|
4
8
|
/**
|
|
5
|
-
*
|
|
9
|
+
* Parse PostgreSQL array string into JavaScript array.
|
|
10
|
+
* Handles: {val1,val2}, {NULL,val}, {"quoted,val"}, nested arrays.
|
|
6
11
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
12
|
+
function parseArray(value, parseElement = (v) => v) {
|
|
13
|
+
if (!value || value === "{}")
|
|
14
|
+
return [];
|
|
15
|
+
// Remove outer braces
|
|
16
|
+
const inner = value.slice(1, -1);
|
|
17
|
+
if (inner === "")
|
|
18
|
+
return [];
|
|
19
|
+
const result = [];
|
|
20
|
+
let current = "";
|
|
21
|
+
let inQuotes = false;
|
|
22
|
+
let depth = 0;
|
|
23
|
+
for (let i = 0; i < inner.length; i++) {
|
|
24
|
+
const char = inner[i];
|
|
25
|
+
if (char === '"' && inner[i - 1] !== "\\") {
|
|
26
|
+
inQuotes = !inQuotes;
|
|
27
|
+
current += char;
|
|
28
|
+
}
|
|
29
|
+
else if (char === "{" && !inQuotes) {
|
|
30
|
+
depth++;
|
|
31
|
+
current += char;
|
|
32
|
+
}
|
|
33
|
+
else if (char === "}" && !inQuotes) {
|
|
34
|
+
depth--;
|
|
35
|
+
current += char;
|
|
36
|
+
}
|
|
37
|
+
else if (char === "," && !inQuotes && depth === 0) {
|
|
38
|
+
result.push(parseElement(current));
|
|
39
|
+
current = "";
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
current += char;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (current !== "") {
|
|
46
|
+
result.push(parseElement(current));
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parse element, handling NULL, quoted strings, and unquoted values.
|
|
52
|
+
*/
|
|
53
|
+
function parseStringElement(val) {
|
|
54
|
+
if (val === "NULL")
|
|
55
|
+
return null;
|
|
56
|
+
if (val.startsWith('"') && val.endsWith('"')) {
|
|
57
|
+
// Unescape quoted string
|
|
58
|
+
return val.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
59
|
+
}
|
|
60
|
+
return val;
|
|
61
|
+
}
|
|
62
|
+
function parseIntElement(val) {
|
|
63
|
+
if (val === "NULL")
|
|
64
|
+
return null;
|
|
65
|
+
return Number.parseInt(val, 10);
|
|
66
|
+
}
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Type Parsers
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// int2vector: "1 2 3" -> [1, 2, 3]
|
|
71
|
+
// @ts-expect-error - pg types expects TypeId but raw OID numbers work fine
|
|
72
|
+
types.setTypeParser(22, (val) => {
|
|
73
|
+
if (!val || val === "")
|
|
74
|
+
return [];
|
|
75
|
+
return val
|
|
76
|
+
.split(" ")
|
|
77
|
+
.map(Number)
|
|
78
|
+
.filter((n) => !Number.isNaN(n));
|
|
79
|
+
});
|
|
80
|
+
// bigint: string -> BigInt
|
|
81
|
+
types.setTypeParser(20, (val) => BigInt(val));
|
|
82
|
+
// PostgreSQL array types
|
|
83
|
+
// @ts-expect-error - pg types expects TypeId but raw OID numbers work fine
|
|
84
|
+
types.setTypeParser(1002, (val) => parseArray(val, parseStringElement)); // "char"[]
|
|
85
|
+
// @ts-expect-error - pg types expects TypeId but raw OID numbers work fine
|
|
86
|
+
types.setTypeParser(1009, (val) => parseArray(val, parseStringElement)); // text[]
|
|
87
|
+
// @ts-expect-error - pg types expects TypeId but raw OID numbers work fine
|
|
88
|
+
types.setTypeParser(1015, (val) => parseArray(val, parseStringElement)); // varchar[]
|
|
89
|
+
// @ts-expect-error - pg types expects TypeId but raw OID numbers work fine
|
|
90
|
+
types.setTypeParser(1005, (val) => parseArray(val, parseIntElement)); // int2[]
|
|
91
|
+
// @ts-expect-error - pg types expects TypeId but raw OID numbers work fine
|
|
92
|
+
types.setTypeParser(1007, (val) => parseArray(val, parseIntElement)); // int4[]
|
|
93
|
+
// @ts-expect-error - pg types expects TypeId but raw OID numbers work fine
|
|
94
|
+
types.setTypeParser(1016, (val) => parseArray(val, parseIntElement)); // int8[]
|
|
95
|
+
/**
|
|
96
|
+
* Create a Pool with custom type handlers and optional event listeners.
|
|
97
|
+
*/
|
|
98
|
+
export function createPool(connectionString, options) {
|
|
99
|
+
const { onConnect, onError, onAcquire, onRemove, ...config } = options ?? {};
|
|
100
|
+
const pool = new Pool({ connectionString, ...config });
|
|
101
|
+
if (onConnect)
|
|
102
|
+
pool.on("connect", onConnect);
|
|
103
|
+
if (onError)
|
|
104
|
+
pool.on("error", onError);
|
|
105
|
+
if (onAcquire)
|
|
106
|
+
pool.on("acquire", onAcquire);
|
|
107
|
+
if (onRemove)
|
|
108
|
+
pool.on("remove", onRemove);
|
|
109
|
+
return pool;
|
|
110
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supabase/pg-delta",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.3",
|
|
4
4
|
"description": "PostgreSQL migrations made easy",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -37,13 +37,15 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@stricli/core": "^1.2.4",
|
|
40
|
+
"@ts-safeql/sql-tag": "^0.2.0",
|
|
40
41
|
"chalk": "^5.6.2",
|
|
41
42
|
"debug": "^4.3.7",
|
|
42
|
-
"
|
|
43
|
+
"pg": "^8.17.2",
|
|
43
44
|
"zod": "^4.2.1"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@biomejs/biome": "2.3.10",
|
|
48
|
+
"@types/pg": "^8.11.10",
|
|
47
49
|
"@changesets/cli": "^2.29.8",
|
|
48
50
|
"@tsconfig/node-ts": "^23.6.2",
|
|
49
51
|
"@tsconfig/node24": "^24.0.3",
|