@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.
Files changed (65) hide show
  1. package/dist/core/catalog.model.d.ts +2 -2
  2. package/dist/core/catalog.model.js +29 -29
  3. package/dist/core/context.d.ts +3 -3
  4. package/dist/core/context.js +7 -10
  5. package/dist/core/depend.d.ts +2 -2
  6. package/dist/core/depend.js +8 -7
  7. package/dist/core/integrations/supabase.js +2 -0
  8. package/dist/core/objects/aggregate/aggregate.model.d.ts +2 -2
  9. package/dist/core/objects/aggregate/aggregate.model.js +7 -9
  10. package/dist/core/objects/collation/collation.model.d.ts +2 -2
  11. package/dist/core/objects/collation/collation.model.js +29 -28
  12. package/dist/core/objects/domain/domain.model.d.ts +2 -2
  13. package/dist/core/objects/domain/domain.model.js +8 -10
  14. package/dist/core/objects/event-trigger/event-trigger.model.d.ts +2 -2
  15. package/dist/core/objects/event-trigger/event-trigger.model.js +7 -9
  16. package/dist/core/objects/extension/extension.model.d.ts +2 -2
  17. package/dist/core/objects/extension/extension.model.js +8 -10
  18. package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.d.ts +2 -2
  19. package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.js +20 -22
  20. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.d.ts +2 -2
  21. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.js +20 -22
  22. package/dist/core/objects/foreign-data-wrapper/server/server.model.d.ts +2 -2
  23. package/dist/core/objects/foreign-data-wrapper/server/server.model.js +20 -22
  24. package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.d.ts +2 -2
  25. package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.js +20 -22
  26. package/dist/core/objects/index/index.model.d.ts +4 -4
  27. package/dist/core/objects/index/index.model.js +9 -11
  28. package/dist/core/objects/language/language.model.js +5 -7
  29. package/dist/core/objects/materialized-view/materialized-view.model.d.ts +2 -2
  30. package/dist/core/objects/materialized-view/materialized-view.model.js +8 -10
  31. package/dist/core/objects/procedure/procedure.model.d.ts +2 -2
  32. package/dist/core/objects/procedure/procedure.model.js +8 -10
  33. package/dist/core/objects/publication/publication.model.d.ts +2 -2
  34. package/dist/core/objects/publication/publication.model.js +7 -9
  35. package/dist/core/objects/rls-policy/rls-policy.model.d.ts +2 -2
  36. package/dist/core/objects/rls-policy/rls-policy.model.js +8 -10
  37. package/dist/core/objects/role/role.model.d.ts +2 -2
  38. package/dist/core/objects/role/role.model.js +28 -28
  39. package/dist/core/objects/rule/rule.model.d.ts +2 -2
  40. package/dist/core/objects/rule/rule.model.js +7 -9
  41. package/dist/core/objects/schema/schema.model.d.ts +2 -2
  42. package/dist/core/objects/schema/schema.model.js +8 -10
  43. package/dist/core/objects/sequence/sequence.model.d.ts +2 -2
  44. package/dist/core/objects/sequence/sequence.model.js +8 -10
  45. package/dist/core/objects/subscription/subscription.model.d.ts +2 -2
  46. package/dist/core/objects/subscription/subscription.model.js +25 -20
  47. package/dist/core/objects/table/table.model.d.ts +2 -2
  48. package/dist/core/objects/table/table.model.js +8 -10
  49. package/dist/core/objects/trigger/trigger.model.d.ts +2 -2
  50. package/dist/core/objects/trigger/trigger.model.js +8 -10
  51. package/dist/core/objects/type/composite-type/composite-type.model.d.ts +2 -2
  52. package/dist/core/objects/type/composite-type/composite-type.model.js +8 -10
  53. package/dist/core/objects/type/enum/enum.model.d.ts +2 -2
  54. package/dist/core/objects/type/enum/enum.model.js +22 -24
  55. package/dist/core/objects/type/range/range.model.d.ts +2 -2
  56. package/dist/core/objects/type/range/range.model.js +7 -9
  57. package/dist/core/objects/view/view.model.d.ts +2 -2
  58. package/dist/core/objects/view/view.model.js +8 -10
  59. package/dist/core/plan/apply.d.ts +2 -2
  60. package/dist/core/plan/apply.js +50 -16
  61. package/dist/core/plan/create.d.ts +2 -2
  62. package/dist/core/plan/create.js +84 -38
  63. package/dist/core/postgres-config.d.ts +18 -3
  64. package/dist/core/postgres-config.js +105 -41
  65. package/package.json +4 -2
@@ -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 postgres from "postgres";
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 { postgresConfig } from "../postgres-config.js";
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 (required for verify-ca/verify-full)
62
- if (rejectUnauthorized) {
63
- const caEnvVar = connectionType === "source"
64
- ? "PGDELTA_SOURCE_SSLROOTCERT"
65
- : "PGDELTA_TARGET_SSLROOTCERT";
66
- const caValue = await getCertValue(sslrootcert, caEnvVar);
67
- if (caValue) {
68
- ssl.ca = caValue;
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
- const sourceSslConfig = typeof source === "string" ? await parseSslConfig(source, "source") : null;
98
- const targetSslConfig = typeof target === "string" ? await parseSslConfig(target, "target") : null;
99
- const sourceSql = typeof source === "string" && sourceSslConfig
100
- ? postgres(sourceSslConfig.cleanedUrl, {
101
- ...postgresConfig,
102
- ...(sourceSslConfig.ssl ? { ssl: sourceSslConfig.ssl } : {}),
103
- })
104
- : source;
105
- const targetSql = typeof target === "string" && targetSslConfig
106
- ? postgres(targetSslConfig.cleanedUrl, {
107
- ...postgresConfig,
108
- ...(targetSslConfig.ssl ? { ssl: targetSslConfig.ssl } : {}),
109
- })
110
- : target;
111
- const shouldCloseFrom = typeof source === "string";
112
- const shouldCloseTo = typeof target === "string";
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(sourceSql),
116
- extractCatalog(targetSql),
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 (shouldCloseFrom)
123
- closers.push(sourceSql.end());
124
- if (shouldCloseTo)
125
- closers.push(targetSql.end());
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 "${options.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 postgres from "postgres";
4
+ import type { PoolClient, PoolConfig } from "pg";
5
+ import { Pool } from "pg";
5
6
  /**
6
- * Custom type handler for specific corner cases.
7
+ * Options for creating a Pool with event listeners.
7
8
  */
8
- export declare const postgresConfig: postgres.Options<Record<string, postgres.PostgresType>>;
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
- * Custom type handler for specific corner cases.
9
+ * Parse PostgreSQL array string into JavaScript array.
10
+ * Handles: {val1,val2}, {NULL,val}, {"quoted,val"}, nested arrays.
6
11
  */
7
- export const postgresConfig = {
8
- types: {
9
- int2vector: {
10
- // The pg_types oid for int2vector (22 is the OID for int2vector)
11
- to: 22,
12
- // Array of pg_types oids to handle when parsing values coming from the db
13
- from: [22],
14
- // Parse int2vector from string format "1 2 3" to array [1, 2, 3]
15
- parse: (value) => {
16
- if (!value || value === "")
17
- return [];
18
- return value
19
- .split(" ")
20
- .map(Number)
21
- .filter((n) => !Number.isNaN(n));
22
- },
23
- // Serialize array back to int2vector format if needed
24
- serialize: (value) => {
25
- if (!Array.isArray(value))
26
- return "";
27
- return value.join(" ");
28
- },
29
- },
30
- // Handle bigint values from PostgreSQL
31
- bigint: {
32
- // The pg_types oid for bigint (20 is the OID for int8/bigint)
33
- to: 20,
34
- // Array of pg_types oids to handle when parsing values coming from the db
35
- from: [20],
36
- // Parse bigint string to JavaScript BigInt
37
- parse: (value) => {
38
- return BigInt(value);
39
- },
40
- // Serialize BigInt back to string for PostgreSQL
41
- serialize: (value) => {
42
- return value.toString();
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.1",
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
- "postgres": "^3.4.7",
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",