@pgpmjs/core 4.13.2 → 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 +31 -0
- package/esm/export/export-meta.js +39 -16
- package/esm/migrate/utils/errors.js +169 -0
- package/esm/migrate/utils/transaction.js +9 -10
- package/export/export-meta.js +39 -16
- package/migrate/utils/errors.d.ts +18 -0
- package/migrate/utils/errors.js +175 -0
- package/migrate/utils/transaction.d.ts +1 -0
- package/migrate/utils/transaction.js +12 -10
- package/package.json +6 -6
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
|
|
@@ -187,8 +187,8 @@ const config = {
|
|
|
187
187
|
fields: {
|
|
188
188
|
id: 'uuid',
|
|
189
189
|
database_id: 'uuid',
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
name: 'text',
|
|
191
|
+
code: 'text'
|
|
192
192
|
}
|
|
193
193
|
},
|
|
194
194
|
rls_function: {
|
|
@@ -197,8 +197,13 @@ const config = {
|
|
|
197
197
|
fields: {
|
|
198
198
|
id: 'uuid',
|
|
199
199
|
database_id: 'uuid',
|
|
200
|
-
|
|
201
|
-
name: 'text'
|
|
200
|
+
table_id: 'uuid',
|
|
201
|
+
name: 'text',
|
|
202
|
+
label: 'text',
|
|
203
|
+
description: 'text',
|
|
204
|
+
data: 'jsonb',
|
|
205
|
+
inline: 'boolean',
|
|
206
|
+
security: 'int'
|
|
202
207
|
}
|
|
203
208
|
},
|
|
204
209
|
limit_function: {
|
|
@@ -207,8 +212,12 @@ const config = {
|
|
|
207
212
|
fields: {
|
|
208
213
|
id: 'uuid',
|
|
209
214
|
database_id: 'uuid',
|
|
210
|
-
|
|
211
|
-
name: 'text'
|
|
215
|
+
table_id: 'uuid',
|
|
216
|
+
name: 'text',
|
|
217
|
+
label: 'text',
|
|
218
|
+
description: 'text',
|
|
219
|
+
data: 'jsonb',
|
|
220
|
+
security: 'int'
|
|
212
221
|
}
|
|
213
222
|
},
|
|
214
223
|
procedure: {
|
|
@@ -217,8 +226,12 @@ const config = {
|
|
|
217
226
|
fields: {
|
|
218
227
|
id: 'uuid',
|
|
219
228
|
database_id: 'uuid',
|
|
220
|
-
|
|
221
|
-
|
|
229
|
+
name: 'text',
|
|
230
|
+
argnames: 'text[]',
|
|
231
|
+
argtypes: 'text[]',
|
|
232
|
+
argdefaults: 'text[]',
|
|
233
|
+
lang_name: 'text',
|
|
234
|
+
definition: 'text'
|
|
222
235
|
}
|
|
223
236
|
},
|
|
224
237
|
foreign_key_constraint: {
|
|
@@ -229,11 +242,14 @@ const config = {
|
|
|
229
242
|
database_id: 'uuid',
|
|
230
243
|
table_id: 'uuid',
|
|
231
244
|
name: 'text',
|
|
245
|
+
description: 'text',
|
|
246
|
+
smart_tags: 'jsonb',
|
|
247
|
+
type: 'text',
|
|
232
248
|
field_ids: 'uuid[]',
|
|
233
249
|
ref_table_id: 'uuid',
|
|
234
250
|
ref_field_ids: 'uuid[]',
|
|
235
|
-
|
|
236
|
-
|
|
251
|
+
delete_action: 'text',
|
|
252
|
+
update_action: 'text'
|
|
237
253
|
}
|
|
238
254
|
},
|
|
239
255
|
primary_key_constraint: {
|
|
@@ -244,6 +260,7 @@ const config = {
|
|
|
244
260
|
database_id: 'uuid',
|
|
245
261
|
table_id: 'uuid',
|
|
246
262
|
name: 'text',
|
|
263
|
+
type: 'text',
|
|
247
264
|
field_ids: 'uuid[]'
|
|
248
265
|
}
|
|
249
266
|
},
|
|
@@ -255,6 +272,9 @@ const config = {
|
|
|
255
272
|
database_id: 'uuid',
|
|
256
273
|
table_id: 'uuid',
|
|
257
274
|
name: 'text',
|
|
275
|
+
description: 'text',
|
|
276
|
+
smart_tags: 'jsonb',
|
|
277
|
+
type: 'text',
|
|
258
278
|
field_ids: 'uuid[]'
|
|
259
279
|
}
|
|
260
280
|
},
|
|
@@ -266,7 +286,9 @@ const config = {
|
|
|
266
286
|
database_id: 'uuid',
|
|
267
287
|
table_id: 'uuid',
|
|
268
288
|
name: 'text',
|
|
269
|
-
|
|
289
|
+
type: 'text',
|
|
290
|
+
field_ids: 'uuid[]',
|
|
291
|
+
expr: 'jsonb'
|
|
270
292
|
}
|
|
271
293
|
},
|
|
272
294
|
full_text_search: {
|
|
@@ -276,9 +298,10 @@ const config = {
|
|
|
276
298
|
id: 'uuid',
|
|
277
299
|
database_id: 'uuid',
|
|
278
300
|
table_id: 'uuid',
|
|
279
|
-
|
|
301
|
+
field_id: 'uuid',
|
|
280
302
|
field_ids: 'uuid[]',
|
|
281
|
-
weights: 'text[]'
|
|
303
|
+
weights: 'text[]',
|
|
304
|
+
langs: 'text[]'
|
|
282
305
|
}
|
|
283
306
|
},
|
|
284
307
|
schema_grant: {
|
|
@@ -288,8 +311,7 @@ const config = {
|
|
|
288
311
|
id: 'uuid',
|
|
289
312
|
database_id: 'uuid',
|
|
290
313
|
schema_id: 'uuid',
|
|
291
|
-
|
|
292
|
-
privilege: 'text'
|
|
314
|
+
grantee_name: 'text'
|
|
293
315
|
}
|
|
294
316
|
},
|
|
295
317
|
table_grant: {
|
|
@@ -299,8 +321,9 @@ const config = {
|
|
|
299
321
|
id: 'uuid',
|
|
300
322
|
database_id: 'uuid',
|
|
301
323
|
table_id: 'uuid',
|
|
324
|
+
privilege: 'text',
|
|
302
325
|
role_name: 'text',
|
|
303
|
-
|
|
326
|
+
field_ids: 'uuid[]'
|
|
304
327
|
}
|
|
305
328
|
},
|
|
306
329
|
// =============================================================================
|
|
@@ -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
|
-
|
|
67
|
-
|
|
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
|
-
//
|
|
81
|
-
|
|
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 {
|
package/export/export-meta.js
CHANGED
|
@@ -190,8 +190,8 @@ const config = {
|
|
|
190
190
|
fields: {
|
|
191
191
|
id: 'uuid',
|
|
192
192
|
database_id: 'uuid',
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
name: 'text',
|
|
194
|
+
code: 'text'
|
|
195
195
|
}
|
|
196
196
|
},
|
|
197
197
|
rls_function: {
|
|
@@ -200,8 +200,13 @@ const config = {
|
|
|
200
200
|
fields: {
|
|
201
201
|
id: 'uuid',
|
|
202
202
|
database_id: 'uuid',
|
|
203
|
-
|
|
204
|
-
name: 'text'
|
|
203
|
+
table_id: 'uuid',
|
|
204
|
+
name: 'text',
|
|
205
|
+
label: 'text',
|
|
206
|
+
description: 'text',
|
|
207
|
+
data: 'jsonb',
|
|
208
|
+
inline: 'boolean',
|
|
209
|
+
security: 'int'
|
|
205
210
|
}
|
|
206
211
|
},
|
|
207
212
|
limit_function: {
|
|
@@ -210,8 +215,12 @@ const config = {
|
|
|
210
215
|
fields: {
|
|
211
216
|
id: 'uuid',
|
|
212
217
|
database_id: 'uuid',
|
|
213
|
-
|
|
214
|
-
name: 'text'
|
|
218
|
+
table_id: 'uuid',
|
|
219
|
+
name: 'text',
|
|
220
|
+
label: 'text',
|
|
221
|
+
description: 'text',
|
|
222
|
+
data: 'jsonb',
|
|
223
|
+
security: 'int'
|
|
215
224
|
}
|
|
216
225
|
},
|
|
217
226
|
procedure: {
|
|
@@ -220,8 +229,12 @@ const config = {
|
|
|
220
229
|
fields: {
|
|
221
230
|
id: 'uuid',
|
|
222
231
|
database_id: 'uuid',
|
|
223
|
-
|
|
224
|
-
|
|
232
|
+
name: 'text',
|
|
233
|
+
argnames: 'text[]',
|
|
234
|
+
argtypes: 'text[]',
|
|
235
|
+
argdefaults: 'text[]',
|
|
236
|
+
lang_name: 'text',
|
|
237
|
+
definition: 'text'
|
|
225
238
|
}
|
|
226
239
|
},
|
|
227
240
|
foreign_key_constraint: {
|
|
@@ -232,11 +245,14 @@ const config = {
|
|
|
232
245
|
database_id: 'uuid',
|
|
233
246
|
table_id: 'uuid',
|
|
234
247
|
name: 'text',
|
|
248
|
+
description: 'text',
|
|
249
|
+
smart_tags: 'jsonb',
|
|
250
|
+
type: 'text',
|
|
235
251
|
field_ids: 'uuid[]',
|
|
236
252
|
ref_table_id: 'uuid',
|
|
237
253
|
ref_field_ids: 'uuid[]',
|
|
238
|
-
|
|
239
|
-
|
|
254
|
+
delete_action: 'text',
|
|
255
|
+
update_action: 'text'
|
|
240
256
|
}
|
|
241
257
|
},
|
|
242
258
|
primary_key_constraint: {
|
|
@@ -247,6 +263,7 @@ const config = {
|
|
|
247
263
|
database_id: 'uuid',
|
|
248
264
|
table_id: 'uuid',
|
|
249
265
|
name: 'text',
|
|
266
|
+
type: 'text',
|
|
250
267
|
field_ids: 'uuid[]'
|
|
251
268
|
}
|
|
252
269
|
},
|
|
@@ -258,6 +275,9 @@ const config = {
|
|
|
258
275
|
database_id: 'uuid',
|
|
259
276
|
table_id: 'uuid',
|
|
260
277
|
name: 'text',
|
|
278
|
+
description: 'text',
|
|
279
|
+
smart_tags: 'jsonb',
|
|
280
|
+
type: 'text',
|
|
261
281
|
field_ids: 'uuid[]'
|
|
262
282
|
}
|
|
263
283
|
},
|
|
@@ -269,7 +289,9 @@ const config = {
|
|
|
269
289
|
database_id: 'uuid',
|
|
270
290
|
table_id: 'uuid',
|
|
271
291
|
name: 'text',
|
|
272
|
-
|
|
292
|
+
type: 'text',
|
|
293
|
+
field_ids: 'uuid[]',
|
|
294
|
+
expr: 'jsonb'
|
|
273
295
|
}
|
|
274
296
|
},
|
|
275
297
|
full_text_search: {
|
|
@@ -279,9 +301,10 @@ const config = {
|
|
|
279
301
|
id: 'uuid',
|
|
280
302
|
database_id: 'uuid',
|
|
281
303
|
table_id: 'uuid',
|
|
282
|
-
|
|
304
|
+
field_id: 'uuid',
|
|
283
305
|
field_ids: 'uuid[]',
|
|
284
|
-
weights: 'text[]'
|
|
306
|
+
weights: 'text[]',
|
|
307
|
+
langs: 'text[]'
|
|
285
308
|
}
|
|
286
309
|
},
|
|
287
310
|
schema_grant: {
|
|
@@ -291,8 +314,7 @@ const config = {
|
|
|
291
314
|
id: 'uuid',
|
|
292
315
|
database_id: 'uuid',
|
|
293
316
|
schema_id: 'uuid',
|
|
294
|
-
|
|
295
|
-
privilege: 'text'
|
|
317
|
+
grantee_name: 'text'
|
|
296
318
|
}
|
|
297
319
|
},
|
|
298
320
|
table_grant: {
|
|
@@ -302,8 +324,9 @@ const config = {
|
|
|
302
324
|
id: 'uuid',
|
|
303
325
|
database_id: 'uuid',
|
|
304
326
|
table_id: 'uuid',
|
|
327
|
+
privilege: 'text',
|
|
305
328
|
role_name: 'text',
|
|
306
|
-
|
|
329
|
+
field_ids: 'uuid[]'
|
|
307
330
|
}
|
|
308
331
|
},
|
|
309
332
|
// =============================================================================
|
|
@@ -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,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
|
-
|
|
71
|
-
|
|
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
|
-
//
|
|
85
|
-
|
|
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.
|
|
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.
|
|
51
|
+
"@pgpmjs/env": "^2.9.4",
|
|
52
52
|
"@pgpmjs/logger": "^1.3.7",
|
|
53
|
-
"@pgpmjs/server-utils": "^2.8.
|
|
54
|
-
"@pgpmjs/types": "^2.14.
|
|
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.
|
|
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": "
|
|
67
|
+
"gitHead": "cb4af2cf6c23dad24cd951c232d3e2006b81aa3d"
|
|
68
68
|
}
|