@pgpmjs/core 4.13.3 → 4.14.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/README.md CHANGED
@@ -22,6 +22,37 @@ pgpm Core is the main package for pgpm, providing tools for database migrations,
22
22
  - Reading and writing SQL scripts
23
23
  - Resolving dependencies between migrations
24
24
 
25
+ ## Configuration
26
+
27
+ ### Error Output Configuration
28
+
29
+ When migrations fail, pgpm provides detailed error output including query history. For large deployments with many migrations, this output can be verbose. The following environment variables control error output formatting:
30
+
31
+ | Variable | Default | Description |
32
+ |----------|---------|-------------|
33
+ | `PGPM_ERROR_QUERY_HISTORY_LIMIT` | `30` | Maximum number of queries to show in error output. Earlier queries are omitted with a count. |
34
+ | `PGPM_ERROR_MAX_LENGTH` | `10000` | Maximum characters for error output. Output exceeding this is truncated. |
35
+ | `PGPM_ERROR_VERBOSE` | `false` | Set to `true` to disable all limiting and show full error output. |
36
+
37
+ #### Smart Query Collapsing
38
+
39
+ Error output automatically collapses consecutive identical queries (like repeated `pgpm_migrate.deploy` calls) into a summary showing:
40
+ - The range of query numbers (e.g., "2-57")
41
+ - The total count (e.g., "56 calls")
42
+ - For deploy calls: the first and last change names for context
43
+
44
+ Example collapsed output:
45
+ ```
46
+ Query history for this transaction:
47
+ 1. BEGIN
48
+ 2-57. CALL pgpm_migrate.deploy($1::TEXT, ...) (56 calls)
49
+ First: schemas/metaschema_public/tables/extension/table
50
+ Last: schemas/metaschema_modules_public/tables/permissions_module/table
51
+ 58. ROLLBACK
52
+ ```
53
+
54
+ To see full uncompressed output, set `PGPM_ERROR_VERBOSE=true`.
55
+
25
56
  ---
26
57
 
27
58
  ## Education and Tutorials
@@ -0,0 +1,169 @@
1
+ import { getEnvVars } from '@pgpmjs/env';
2
+ import { pgpmDefaults } from '@pgpmjs/types';
3
+ /**
4
+ * Get error output configuration from environment variables with defaults.
5
+ * Uses centralized env var parsing from @pgpmjs/env and defaults from @pgpmjs/types.
6
+ */
7
+ export const getErrorOutputConfig = () => {
8
+ const envVars = getEnvVars();
9
+ const defaults = pgpmDefaults.errorOutput;
10
+ return {
11
+ queryHistoryLimit: envVars.errorOutput?.queryHistoryLimit ?? defaults.queryHistoryLimit,
12
+ maxLength: envVars.errorOutput?.maxLength ?? defaults.maxLength,
13
+ verbose: envVars.errorOutput?.verbose ?? defaults.verbose,
14
+ };
15
+ };
16
+ const errorConfig = getErrorOutputConfig();
17
+ /**
18
+ * Extract a friendly name from pgpm_migrate.deploy params for better error context
19
+ */
20
+ function extractDeployChangeName(params) {
21
+ if (!params || params.length < 2)
22
+ return null;
23
+ // params[1] is the change name (e.g., "schemas/metaschema_public/tables/extension/table")
24
+ return typeof params[1] === 'string' ? params[1] : null;
25
+ }
26
+ /**
27
+ * Group consecutive queries by their query template (ignoring params)
28
+ */
29
+ function groupConsecutiveQueries(history) {
30
+ if (history.length === 0)
31
+ return [];
32
+ const groups = [];
33
+ let currentGroup = {
34
+ query: history[0].query,
35
+ startIndex: 0,
36
+ endIndex: 0,
37
+ count: 1,
38
+ entries: [history[0]]
39
+ };
40
+ for (let i = 1; i < history.length; i++) {
41
+ const entry = history[i];
42
+ if (entry.query === currentGroup.query) {
43
+ // Same query template, extend the group
44
+ currentGroup.endIndex = i;
45
+ currentGroup.count++;
46
+ currentGroup.entries.push(entry);
47
+ }
48
+ else {
49
+ // Different query, start a new group
50
+ groups.push(currentGroup);
51
+ currentGroup = {
52
+ query: entry.query,
53
+ startIndex: i,
54
+ endIndex: i,
55
+ count: 1,
56
+ entries: [entry]
57
+ };
58
+ }
59
+ }
60
+ groups.push(currentGroup);
61
+ return groups;
62
+ }
63
+ /**
64
+ * Format a single query entry for display
65
+ */
66
+ function formatQueryEntry(entry, index) {
67
+ const duration = entry.duration ? ` (${entry.duration}ms)` : '';
68
+ const params = entry.params && entry.params.length > 0
69
+ ? ` with params: ${JSON.stringify(entry.params.slice(0, 2))}${entry.params.length > 2 ? '...' : ''}`
70
+ : '';
71
+ return ` ${index + 1}. ${entry.query.split('\n')[0].trim()}${params}${duration}`;
72
+ }
73
+ /**
74
+ * Format a group of queries for display, collapsing repetitive queries
75
+ */
76
+ function formatQueryGroup(group) {
77
+ const lines = [];
78
+ const queryPreview = group.query.split('\n')[0].trim();
79
+ if (group.count === 1) {
80
+ // Single query, format normally
81
+ lines.push(formatQueryEntry(group.entries[0], group.startIndex));
82
+ }
83
+ else {
84
+ // Multiple consecutive identical queries, collapse them
85
+ const isPgpmDeploy = queryPreview.includes('pgpm_migrate.deploy');
86
+ // Show range and count
87
+ lines.push(` ${group.startIndex + 1}-${group.endIndex + 1}. ${queryPreview} (${group.count} calls)`);
88
+ // For pgpm_migrate.deploy, show first and last change names for context
89
+ if (isPgpmDeploy) {
90
+ const firstChange = extractDeployChangeName(group.entries[0].params);
91
+ const lastChange = extractDeployChangeName(group.entries[group.entries.length - 1].params);
92
+ if (firstChange) {
93
+ lines.push(` First: ${firstChange}`);
94
+ }
95
+ if (lastChange && lastChange !== firstChange) {
96
+ lines.push(` Last: ${lastChange}`);
97
+ }
98
+ }
99
+ else {
100
+ // For other queries, show first and last params
101
+ const firstParams = group.entries[0].params;
102
+ const lastParams = group.entries[group.entries.length - 1].params;
103
+ if (firstParams && firstParams.length > 0) {
104
+ lines.push(` First params: ${JSON.stringify(firstParams.slice(0, 2))}${firstParams.length > 2 ? '...' : ''}`);
105
+ }
106
+ if (lastParams && lastParams.length > 0 && JSON.stringify(lastParams) !== JSON.stringify(firstParams)) {
107
+ lines.push(` Last params: ${JSON.stringify(lastParams.slice(0, 2))}${lastParams.length > 2 ? '...' : ''}`);
108
+ }
109
+ }
110
+ }
111
+ return lines;
112
+ }
113
+ /**
114
+ * Format query history with smart collapsing and limiting
115
+ */
116
+ export function formatQueryHistory(history) {
117
+ if (history.length === 0)
118
+ return [];
119
+ // In verbose mode, show everything without collapsing
120
+ if (errorConfig.verbose) {
121
+ return history.map((entry, index) => formatQueryEntry(entry, index));
122
+ }
123
+ // Group consecutive identical queries
124
+ const groups = groupConsecutiveQueries(history);
125
+ // Apply limit - keep last N queries worth of groups
126
+ let totalQueries = 0;
127
+ let startGroupIndex = groups.length;
128
+ for (let i = groups.length - 1; i >= 0; i--) {
129
+ totalQueries += groups[i].count;
130
+ if (totalQueries > errorConfig.queryHistoryLimit) {
131
+ startGroupIndex = i + 1;
132
+ break;
133
+ }
134
+ startGroupIndex = i;
135
+ }
136
+ const lines = [];
137
+ // If we're truncating, show how many were omitted
138
+ if (startGroupIndex > 0) {
139
+ let omittedCount = 0;
140
+ for (let i = 0; i < startGroupIndex; i++) {
141
+ omittedCount += groups[i].count;
142
+ }
143
+ lines.push(` ... (${omittedCount} earlier queries omitted, set PGPM_ERROR_VERBOSE=true to see all)`);
144
+ }
145
+ // Format the remaining groups
146
+ for (let i = startGroupIndex; i < groups.length; i++) {
147
+ lines.push(...formatQueryGroup(groups[i]));
148
+ }
149
+ return lines;
150
+ }
151
+ /**
152
+ * Truncate error output if it exceeds the max length
153
+ */
154
+ export function truncateErrorOutput(lines) {
155
+ if (errorConfig.verbose)
156
+ return lines;
157
+ const joined = lines.join('\n');
158
+ if (joined.length <= errorConfig.maxLength)
159
+ return lines;
160
+ // Truncate and add notice
161
+ const truncated = joined.slice(0, errorConfig.maxLength);
162
+ const lastNewline = truncated.lastIndexOf('\n');
163
+ const cleanTruncated = lastNewline > 0 ? truncated.slice(0, lastNewline) : truncated;
164
+ return [
165
+ ...cleanTruncated.split('\n'),
166
+ `... (output truncated at ${errorConfig.maxLength} chars, total was ${joined.length} chars)`,
167
+ ` Set PGPM_ERROR_VERBOSE=true to see full output`
168
+ ];
169
+ }
@@ -1,5 +1,8 @@
1
1
  import { Logger } from '@pgpmjs/logger';
2
2
  import { extractPgErrorFields, formatPgErrorFields } from '@pgpmjs/types';
3
+ import { formatQueryHistory, truncateErrorOutput } from './errors';
4
+ // Re-export error formatting functions for backward compatibility
5
+ export { formatQueryHistory, truncateErrorOutput } from './errors';
3
6
  const log = new Logger('migrate:transaction');
4
7
  /**
5
8
  * Execute a function within a transaction context
@@ -60,16 +63,11 @@ export async function withTransaction(pool, options, fn) {
60
63
  errorLines.push(...fieldLines);
61
64
  }
62
65
  }
63
- // Log query history for debugging
66
+ // Log query history for debugging (with smart collapsing and limiting)
64
67
  if (queryHistory.length > 0) {
65
68
  errorLines.push('Query history for this transaction:');
66
- queryHistory.forEach((entry, index) => {
67
- const duration = entry.duration ? ` (${entry.duration}ms)` : '';
68
- const params = entry.params && entry.params.length > 0
69
- ? ` with params: ${JSON.stringify(entry.params.slice(0, 2))}${entry.params.length > 2 ? '...' : ''}`
70
- : '';
71
- errorLines.push(` ${index + 1}. ${entry.query.split('\n')[0].trim()}${params}${duration}`);
72
- });
69
+ const historyLines = formatQueryHistory(queryHistory);
70
+ errorLines.push(...historyLines);
73
71
  }
74
72
  // For transaction aborted errors, provide additional context
75
73
  if (error.code === '25P02') {
@@ -77,8 +75,9 @@ export async function withTransaction(pool, options, fn) {
77
75
  errorLines.push(' This usually means a previous command in the transaction failed.');
78
76
  errorLines.push(' Check the query history above to identify the failing command.');
79
77
  }
80
- // Log the consolidated error message
81
- log.error(errorLines.join('\n'));
78
+ // Apply total output length limit and log the consolidated error message
79
+ const finalLines = truncateErrorOutput(errorLines);
80
+ log.error(finalLines.join('\n'));
82
81
  throw error;
83
82
  }
84
83
  finally {
@@ -0,0 +1,18 @@
1
+ import { QueryHistoryEntry } from './transaction';
2
+ /**
3
+ * Get error output configuration from environment variables with defaults.
4
+ * Uses centralized env var parsing from @pgpmjs/env and defaults from @pgpmjs/types.
5
+ */
6
+ export declare const getErrorOutputConfig: () => {
7
+ queryHistoryLimit: number;
8
+ maxLength: number;
9
+ verbose: boolean;
10
+ };
11
+ /**
12
+ * Format query history with smart collapsing and limiting
13
+ */
14
+ export declare function formatQueryHistory(history: QueryHistoryEntry[]): string[];
15
+ /**
16
+ * Truncate error output if it exceeds the max length
17
+ */
18
+ export declare function truncateErrorOutput(lines: string[]): string[];
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getErrorOutputConfig = void 0;
4
+ exports.formatQueryHistory = formatQueryHistory;
5
+ exports.truncateErrorOutput = truncateErrorOutput;
6
+ const env_1 = require("@pgpmjs/env");
7
+ const types_1 = require("@pgpmjs/types");
8
+ /**
9
+ * Get error output configuration from environment variables with defaults.
10
+ * Uses centralized env var parsing from @pgpmjs/env and defaults from @pgpmjs/types.
11
+ */
12
+ const getErrorOutputConfig = () => {
13
+ const envVars = (0, env_1.getEnvVars)();
14
+ const defaults = types_1.pgpmDefaults.errorOutput;
15
+ return {
16
+ queryHistoryLimit: envVars.errorOutput?.queryHistoryLimit ?? defaults.queryHistoryLimit,
17
+ maxLength: envVars.errorOutput?.maxLength ?? defaults.maxLength,
18
+ verbose: envVars.errorOutput?.verbose ?? defaults.verbose,
19
+ };
20
+ };
21
+ exports.getErrorOutputConfig = getErrorOutputConfig;
22
+ const errorConfig = (0, exports.getErrorOutputConfig)();
23
+ /**
24
+ * Extract a friendly name from pgpm_migrate.deploy params for better error context
25
+ */
26
+ function extractDeployChangeName(params) {
27
+ if (!params || params.length < 2)
28
+ return null;
29
+ // params[1] is the change name (e.g., "schemas/metaschema_public/tables/extension/table")
30
+ return typeof params[1] === 'string' ? params[1] : null;
31
+ }
32
+ /**
33
+ * Group consecutive queries by their query template (ignoring params)
34
+ */
35
+ function groupConsecutiveQueries(history) {
36
+ if (history.length === 0)
37
+ return [];
38
+ const groups = [];
39
+ let currentGroup = {
40
+ query: history[0].query,
41
+ startIndex: 0,
42
+ endIndex: 0,
43
+ count: 1,
44
+ entries: [history[0]]
45
+ };
46
+ for (let i = 1; i < history.length; i++) {
47
+ const entry = history[i];
48
+ if (entry.query === currentGroup.query) {
49
+ // Same query template, extend the group
50
+ currentGroup.endIndex = i;
51
+ currentGroup.count++;
52
+ currentGroup.entries.push(entry);
53
+ }
54
+ else {
55
+ // Different query, start a new group
56
+ groups.push(currentGroup);
57
+ currentGroup = {
58
+ query: entry.query,
59
+ startIndex: i,
60
+ endIndex: i,
61
+ count: 1,
62
+ entries: [entry]
63
+ };
64
+ }
65
+ }
66
+ groups.push(currentGroup);
67
+ return groups;
68
+ }
69
+ /**
70
+ * Format a single query entry for display
71
+ */
72
+ function formatQueryEntry(entry, index) {
73
+ const duration = entry.duration ? ` (${entry.duration}ms)` : '';
74
+ const params = entry.params && entry.params.length > 0
75
+ ? ` with params: ${JSON.stringify(entry.params.slice(0, 2))}${entry.params.length > 2 ? '...' : ''}`
76
+ : '';
77
+ return ` ${index + 1}. ${entry.query.split('\n')[0].trim()}${params}${duration}`;
78
+ }
79
+ /**
80
+ * Format a group of queries for display, collapsing repetitive queries
81
+ */
82
+ function formatQueryGroup(group) {
83
+ const lines = [];
84
+ const queryPreview = group.query.split('\n')[0].trim();
85
+ if (group.count === 1) {
86
+ // Single query, format normally
87
+ lines.push(formatQueryEntry(group.entries[0], group.startIndex));
88
+ }
89
+ else {
90
+ // Multiple consecutive identical queries, collapse them
91
+ const isPgpmDeploy = queryPreview.includes('pgpm_migrate.deploy');
92
+ // Show range and count
93
+ lines.push(` ${group.startIndex + 1}-${group.endIndex + 1}. ${queryPreview} (${group.count} calls)`);
94
+ // For pgpm_migrate.deploy, show first and last change names for context
95
+ if (isPgpmDeploy) {
96
+ const firstChange = extractDeployChangeName(group.entries[0].params);
97
+ const lastChange = extractDeployChangeName(group.entries[group.entries.length - 1].params);
98
+ if (firstChange) {
99
+ lines.push(` First: ${firstChange}`);
100
+ }
101
+ if (lastChange && lastChange !== firstChange) {
102
+ lines.push(` Last: ${lastChange}`);
103
+ }
104
+ }
105
+ else {
106
+ // For other queries, show first and last params
107
+ const firstParams = group.entries[0].params;
108
+ const lastParams = group.entries[group.entries.length - 1].params;
109
+ if (firstParams && firstParams.length > 0) {
110
+ lines.push(` First params: ${JSON.stringify(firstParams.slice(0, 2))}${firstParams.length > 2 ? '...' : ''}`);
111
+ }
112
+ if (lastParams && lastParams.length > 0 && JSON.stringify(lastParams) !== JSON.stringify(firstParams)) {
113
+ lines.push(` Last params: ${JSON.stringify(lastParams.slice(0, 2))}${lastParams.length > 2 ? '...' : ''}`);
114
+ }
115
+ }
116
+ }
117
+ return lines;
118
+ }
119
+ /**
120
+ * Format query history with smart collapsing and limiting
121
+ */
122
+ function formatQueryHistory(history) {
123
+ if (history.length === 0)
124
+ return [];
125
+ // In verbose mode, show everything without collapsing
126
+ if (errorConfig.verbose) {
127
+ return history.map((entry, index) => formatQueryEntry(entry, index));
128
+ }
129
+ // Group consecutive identical queries
130
+ const groups = groupConsecutiveQueries(history);
131
+ // Apply limit - keep last N queries worth of groups
132
+ let totalQueries = 0;
133
+ let startGroupIndex = groups.length;
134
+ for (let i = groups.length - 1; i >= 0; i--) {
135
+ totalQueries += groups[i].count;
136
+ if (totalQueries > errorConfig.queryHistoryLimit) {
137
+ startGroupIndex = i + 1;
138
+ break;
139
+ }
140
+ startGroupIndex = i;
141
+ }
142
+ const lines = [];
143
+ // If we're truncating, show how many were omitted
144
+ if (startGroupIndex > 0) {
145
+ let omittedCount = 0;
146
+ for (let i = 0; i < startGroupIndex; i++) {
147
+ omittedCount += groups[i].count;
148
+ }
149
+ lines.push(` ... (${omittedCount} earlier queries omitted, set PGPM_ERROR_VERBOSE=true to see all)`);
150
+ }
151
+ // Format the remaining groups
152
+ for (let i = startGroupIndex; i < groups.length; i++) {
153
+ lines.push(...formatQueryGroup(groups[i]));
154
+ }
155
+ return lines;
156
+ }
157
+ /**
158
+ * Truncate error output if it exceeds the max length
159
+ */
160
+ function truncateErrorOutput(lines) {
161
+ if (errorConfig.verbose)
162
+ return lines;
163
+ const joined = lines.join('\n');
164
+ if (joined.length <= errorConfig.maxLength)
165
+ return lines;
166
+ // Truncate and add notice
167
+ const truncated = joined.slice(0, errorConfig.maxLength);
168
+ const lastNewline = truncated.lastIndexOf('\n');
169
+ const cleanTruncated = lastNewline > 0 ? truncated.slice(0, lastNewline) : truncated;
170
+ return [
171
+ ...cleanTruncated.split('\n'),
172
+ `... (output truncated at ${errorConfig.maxLength} chars, total was ${joined.length} chars)`,
173
+ ` Set PGPM_ERROR_VERBOSE=true to see full output`
174
+ ];
175
+ }
@@ -1,4 +1,5 @@
1
1
  import { Pool, PoolClient } from 'pg';
2
+ export { formatQueryHistory, truncateErrorOutput } from './errors';
2
3
  export interface TransactionOptions {
3
4
  useTransaction: boolean;
4
5
  }
@@ -1,9 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.truncateErrorOutput = exports.formatQueryHistory = void 0;
3
4
  exports.withTransaction = withTransaction;
4
5
  exports.executeQuery = executeQuery;
5
6
  const logger_1 = require("@pgpmjs/logger");
6
7
  const types_1 = require("@pgpmjs/types");
8
+ const errors_1 = require("./errors");
9
+ // Re-export error formatting functions for backward compatibility
10
+ var errors_2 = require("./errors");
11
+ Object.defineProperty(exports, "formatQueryHistory", { enumerable: true, get: function () { return errors_2.formatQueryHistory; } });
12
+ Object.defineProperty(exports, "truncateErrorOutput", { enumerable: true, get: function () { return errors_2.truncateErrorOutput; } });
7
13
  const log = new logger_1.Logger('migrate:transaction');
8
14
  /**
9
15
  * Execute a function within a transaction context
@@ -64,16 +70,11 @@ async function withTransaction(pool, options, fn) {
64
70
  errorLines.push(...fieldLines);
65
71
  }
66
72
  }
67
- // Log query history for debugging
73
+ // Log query history for debugging (with smart collapsing and limiting)
68
74
  if (queryHistory.length > 0) {
69
75
  errorLines.push('Query history for this transaction:');
70
- queryHistory.forEach((entry, index) => {
71
- const duration = entry.duration ? ` (${entry.duration}ms)` : '';
72
- const params = entry.params && entry.params.length > 0
73
- ? ` with params: ${JSON.stringify(entry.params.slice(0, 2))}${entry.params.length > 2 ? '...' : ''}`
74
- : '';
75
- errorLines.push(` ${index + 1}. ${entry.query.split('\n')[0].trim()}${params}${duration}`);
76
- });
76
+ const historyLines = (0, errors_1.formatQueryHistory)(queryHistory);
77
+ errorLines.push(...historyLines);
77
78
  }
78
79
  // For transaction aborted errors, provide additional context
79
80
  if (error.code === '25P02') {
@@ -81,8 +82,9 @@ async function withTransaction(pool, options, fn) {
81
82
  errorLines.push(' This usually means a previous command in the transaction failed.');
82
83
  errorLines.push(' Check the query history above to identify the failing command.');
83
84
  }
84
- // Log the consolidated error message
85
- log.error(errorLines.join('\n'));
85
+ // Apply total output length limit and log the consolidated error message
86
+ const finalLines = (0, errors_1.truncateErrorOutput)(errorLines);
87
+ log.error(finalLines.join('\n'));
86
88
  throw error;
87
89
  }
88
90
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pgpmjs/core",
3
- "version": "4.13.3",
3
+ "version": "4.14.0",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "PGPM Package and Migration Tools",
6
6
  "main": "index.js",
@@ -48,21 +48,21 @@
48
48
  "makage": "^0.1.10"
49
49
  },
50
50
  "dependencies": {
51
- "@pgpmjs/env": "^2.9.3",
51
+ "@pgpmjs/env": "^2.9.4",
52
52
  "@pgpmjs/logger": "^1.3.7",
53
- "@pgpmjs/server-utils": "^2.8.14",
54
- "@pgpmjs/types": "^2.14.0",
53
+ "@pgpmjs/server-utils": "^2.8.15",
54
+ "@pgpmjs/types": "^2.14.1",
55
55
  "csv-to-pg": "^3.4.1",
56
56
  "genomic": "^5.2.3",
57
57
  "glob": "^13.0.0",
58
58
  "komoji": "^0.7.14",
59
59
  "parse-package-name": "^1.0.0",
60
60
  "pg": "^8.16.3",
61
- "pg-cache": "^1.6.14",
61
+ "pg-cache": "^1.6.15",
62
62
  "pg-env": "^1.2.5",
63
63
  "pgsql-deparser": "^17.17.2",
64
64
  "pgsql-parser": "^17.9.11",
65
65
  "yanse": "^0.1.11"
66
66
  },
67
- "gitHead": "c1708066d429d86bbc1ca3aed778ff51779c8efc"
67
+ "gitHead": "cb4af2cf6c23dad24cd951c232d3e2006b81aa3d"
68
68
  }