relq 1.0.109 → 1.0.111
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/cjs/cli/commands/push.cjs +3 -0
- package/dist/cjs/cli/utils/migration-generator.cjs +8 -0
- package/dist/cjs/cli/utils/schema-diff.cjs +39 -4
- package/dist/cjs/core/relq-base.cjs +41 -3
- package/dist/esm/cli/commands/push.js +3 -0
- package/dist/esm/cli/utils/migration-generator.js +8 -0
- package/dist/esm/cli/utils/schema-diff.js +39 -4
- package/dist/esm/core/relq-base.js +41 -3
- package/dist/index.d.ts +16 -0
- package/package.json +1 -1
|
@@ -282,6 +282,9 @@ async function runPush(config, projectRoot, opts = {}) {
|
|
|
282
282
|
const removed = e.changes.removed.length > 0 ? `-${e.changes.removed.join(',')}` : '';
|
|
283
283
|
console.log(` ${colors_1.colors.yellow('~')} ${colors_1.colors.bold(e.name)} ${colors_1.colors.muted(`(enum: ${[added, removed].filter(Boolean).join(' ')})`)}`);
|
|
284
284
|
}
|
|
285
|
+
for (const idx of comparison.modified.indexes) {
|
|
286
|
+
console.log(` ${colors_1.colors.yellow('~')} index ${colors_1.colors.bold(idx.newIndex.name)} ${colors_1.colors.muted(`(${idx.changes.join(', ')})`)}`);
|
|
287
|
+
}
|
|
285
288
|
for (const d of comparison.added.domains) {
|
|
286
289
|
console.log(` ${colors_1.colors.green('+')} ${colors_1.colors.bold(d.name)} ${colors_1.colors.muted('(new domain)')}`);
|
|
287
290
|
}
|
|
@@ -238,6 +238,14 @@ function generateMigrationFromComparison(comparison, options = {}) {
|
|
|
238
238
|
down.unshift(`DROP INDEX IF EXISTS "${index.name}";`);
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
|
+
for (const { table, oldIndex, newIndex } of comparison.modified.indexes) {
|
|
242
|
+
up.push(`DROP INDEX IF EXISTS "${oldIndex.name}";`);
|
|
243
|
+
up.push(generateCreateIndexFromParsed(table, newIndex));
|
|
244
|
+
if (includeDown) {
|
|
245
|
+
down.unshift(`DROP INDEX IF EXISTS "${newIndex.name}";`);
|
|
246
|
+
down.unshift(generateCreateIndexFromParsed(table, oldIndex));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
241
249
|
for (const view of comparison.added.views) {
|
|
242
250
|
const materialized = view.isMaterialized ? 'MATERIALIZED ' : '';
|
|
243
251
|
up.push(`CREATE ${materialized}VIEW "${view.name}" AS ${view.definition};`);
|
|
@@ -883,6 +883,7 @@ function compareSchemas(oldSchema, newSchema) {
|
|
|
883
883
|
modified: {
|
|
884
884
|
tables: [],
|
|
885
885
|
columns: [],
|
|
886
|
+
indexes: [],
|
|
886
887
|
enums: [],
|
|
887
888
|
},
|
|
888
889
|
hasChanges: false,
|
|
@@ -938,6 +939,7 @@ function compareSchemas(oldSchema, newSchema) {
|
|
|
938
939
|
result.renamed.functions.length > 0 ||
|
|
939
940
|
result.modified.tables.length > 0 ||
|
|
940
941
|
result.modified.columns.length > 0 ||
|
|
942
|
+
result.modified.indexes.length > 0 ||
|
|
941
943
|
result.modified.enums.length > 0;
|
|
942
944
|
return result;
|
|
943
945
|
}
|
|
@@ -1124,21 +1126,35 @@ function compareTableIndexes(oldTable, newTable, tableName, result) {
|
|
|
1124
1126
|
const processedNew = new Set();
|
|
1125
1127
|
for (const [trackingId, oldIdx] of oldByTrackingId) {
|
|
1126
1128
|
const newIdx = newByTrackingId.get(trackingId);
|
|
1127
|
-
if (newIdx
|
|
1129
|
+
if (!newIdx)
|
|
1130
|
+
continue;
|
|
1131
|
+
if (newIdx.name !== oldIdx.name) {
|
|
1128
1132
|
result.renamed.indexes.push({
|
|
1129
1133
|
table: tableName,
|
|
1130
1134
|
from: oldIdx.name,
|
|
1131
1135
|
to: newIdx.name,
|
|
1132
1136
|
trackingId,
|
|
1133
1137
|
});
|
|
1134
|
-
processedOld.add(oldIdx.name);
|
|
1135
|
-
processedNew.add(newIdx.name);
|
|
1136
1138
|
}
|
|
1139
|
+
const changes = compareIndexProperties(oldIdx, newIdx);
|
|
1140
|
+
if (changes.length > 0) {
|
|
1141
|
+
result.modified.indexes.push({ table: tableName, oldIndex: oldIdx, newIndex: newIdx, changes });
|
|
1142
|
+
}
|
|
1143
|
+
processedOld.add(oldIdx.name);
|
|
1144
|
+
processedNew.add(newIdx.name);
|
|
1137
1145
|
}
|
|
1138
1146
|
for (const [name, newIdx] of newByName) {
|
|
1139
1147
|
if (processedNew.has(name))
|
|
1140
1148
|
continue;
|
|
1141
|
-
|
|
1149
|
+
const oldIdx = oldByName.get(name);
|
|
1150
|
+
if (oldIdx) {
|
|
1151
|
+
const changes = compareIndexProperties(oldIdx, newIdx);
|
|
1152
|
+
if (changes.length > 0) {
|
|
1153
|
+
result.modified.indexes.push({ table: tableName, oldIndex: oldIdx, newIndex: newIdx, changes });
|
|
1154
|
+
}
|
|
1155
|
+
processedOld.add(name);
|
|
1156
|
+
}
|
|
1157
|
+
else {
|
|
1142
1158
|
result.added.indexes.push({ table: tableName, index: newIdx });
|
|
1143
1159
|
}
|
|
1144
1160
|
}
|
|
@@ -1150,6 +1166,25 @@ function compareTableIndexes(oldTable, newTable, tableName, result) {
|
|
|
1150
1166
|
}
|
|
1151
1167
|
}
|
|
1152
1168
|
}
|
|
1169
|
+
function compareIndexProperties(oldIdx, newIdx) {
|
|
1170
|
+
const changes = [];
|
|
1171
|
+
if (oldIdx.isUnique !== newIdx.isUnique) {
|
|
1172
|
+
changes.push(`unique: ${oldIdx.isUnique} → ${newIdx.isUnique}`);
|
|
1173
|
+
}
|
|
1174
|
+
if ((oldIdx.method || 'btree') !== (newIdx.method || 'btree')) {
|
|
1175
|
+
changes.push(`method: ${oldIdx.method || 'btree'} → ${newIdx.method || 'btree'}`);
|
|
1176
|
+
}
|
|
1177
|
+
if (oldIdx.columns.join(',') !== newIdx.columns.join(',')) {
|
|
1178
|
+
changes.push(`columns: ${oldIdx.columns.join(',')} → ${newIdx.columns.join(',')}`);
|
|
1179
|
+
}
|
|
1180
|
+
if ((oldIdx.whereClause || '') !== (newIdx.whereClause || '')) {
|
|
1181
|
+
changes.push(`where: changed`);
|
|
1182
|
+
}
|
|
1183
|
+
if ((oldIdx.includeColumns || []).join(',') !== (newIdx.includeColumns || []).join(',')) {
|
|
1184
|
+
changes.push(`include: changed`);
|
|
1185
|
+
}
|
|
1186
|
+
return changes;
|
|
1187
|
+
}
|
|
1153
1188
|
function compareEnums(oldEnums, newEnums, result) {
|
|
1154
1189
|
const oldByName = new Map(oldEnums.map(e => [e.name, e]));
|
|
1155
1190
|
const newByName = new Map(newEnums.map(e => [e.name, e]));
|
|
@@ -93,9 +93,32 @@ class RelqBase {
|
|
|
93
93
|
}
|
|
94
94
|
await this.ensureInitialized();
|
|
95
95
|
const startTime = performance.now();
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
const retryConfig = this.config.retry;
|
|
97
|
+
if (!retryConfig) {
|
|
98
|
+
const result = await this._query(sql);
|
|
99
|
+
return { result, duration: performance.now() - startTime };
|
|
100
|
+
}
|
|
101
|
+
const opts = typeof retryConfig === 'object'
|
|
102
|
+
? { maxRetries: retryConfig.maxRetries ?? 3, initialDelayMs: retryConfig.initialDelayMs ?? 250, maxDelayMs: retryConfig.maxDelayMs ?? 5000 }
|
|
103
|
+
: { maxRetries: 3, initialDelayMs: 250, maxDelayMs: 5000 };
|
|
104
|
+
let lastError;
|
|
105
|
+
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
|
|
106
|
+
try {
|
|
107
|
+
const result = await this._query(sql);
|
|
108
|
+
return { result, duration: performance.now() - startTime };
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
lastError = error;
|
|
112
|
+
if (attempt < opts.maxRetries && isTransientError(error)) {
|
|
113
|
+
const delay = Math.min(opts.initialDelayMs * 2 ** attempt, opts.maxDelayMs);
|
|
114
|
+
(0, helpers_1.debugLog)(this.config, `Transient error (attempt ${attempt + 1}/${opts.maxRetries}), retrying in ${delay}ms: ${error.code || error.message}`);
|
|
115
|
+
await sleep(delay);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
throw lastError;
|
|
99
122
|
}
|
|
100
123
|
buildMetadata(result, duration) {
|
|
101
124
|
return {
|
|
@@ -220,3 +243,18 @@ class RelqBase {
|
|
|
220
243
|
}
|
|
221
244
|
}
|
|
222
245
|
exports.RelqBase = RelqBase;
|
|
246
|
+
const TRANSIENT_CODES = new Set([
|
|
247
|
+
'ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ESERVFAIL',
|
|
248
|
+
'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'EHOSTUNREACH',
|
|
249
|
+
'CONNECTION_LOST', 'PROTOCOL_CONNECTION_LOST',
|
|
250
|
+
]);
|
|
251
|
+
function isTransientError(error) {
|
|
252
|
+
if (TRANSIENT_CODES.has(error.code))
|
|
253
|
+
return true;
|
|
254
|
+
if (error.cause && TRANSIENT_CODES.has(error.cause.code))
|
|
255
|
+
return true;
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
function sleep(ms) {
|
|
259
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
260
|
+
}
|
|
@@ -246,6 +246,9 @@ export async function runPush(config, projectRoot, opts = {}) {
|
|
|
246
246
|
const removed = e.changes.removed.length > 0 ? `-${e.changes.removed.join(',')}` : '';
|
|
247
247
|
console.log(` ${colors.yellow('~')} ${colors.bold(e.name)} ${colors.muted(`(enum: ${[added, removed].filter(Boolean).join(' ')})`)}`);
|
|
248
248
|
}
|
|
249
|
+
for (const idx of comparison.modified.indexes) {
|
|
250
|
+
console.log(` ${colors.yellow('~')} index ${colors.bold(idx.newIndex.name)} ${colors.muted(`(${idx.changes.join(', ')})`)}`);
|
|
251
|
+
}
|
|
249
252
|
for (const d of comparison.added.domains) {
|
|
250
253
|
console.log(` ${colors.green('+')} ${colors.bold(d.name)} ${colors.muted('(new domain)')}`);
|
|
251
254
|
}
|
|
@@ -196,6 +196,14 @@ export function generateMigrationFromComparison(comparison, options = {}) {
|
|
|
196
196
|
down.unshift(`DROP INDEX IF EXISTS "${index.name}";`);
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
|
+
for (const { table, oldIndex, newIndex } of comparison.modified.indexes) {
|
|
200
|
+
up.push(`DROP INDEX IF EXISTS "${oldIndex.name}";`);
|
|
201
|
+
up.push(generateCreateIndexFromParsed(table, newIndex));
|
|
202
|
+
if (includeDown) {
|
|
203
|
+
down.unshift(`DROP INDEX IF EXISTS "${newIndex.name}";`);
|
|
204
|
+
down.unshift(generateCreateIndexFromParsed(table, oldIndex));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
199
207
|
for (const view of comparison.added.views) {
|
|
200
208
|
const materialized = view.isMaterialized ? 'MATERIALIZED ' : '';
|
|
201
209
|
up.push(`CREATE ${materialized}VIEW "${view.name}" AS ${view.definition};`);
|
|
@@ -872,6 +872,7 @@ export function compareSchemas(oldSchema, newSchema) {
|
|
|
872
872
|
modified: {
|
|
873
873
|
tables: [],
|
|
874
874
|
columns: [],
|
|
875
|
+
indexes: [],
|
|
875
876
|
enums: [],
|
|
876
877
|
},
|
|
877
878
|
hasChanges: false,
|
|
@@ -927,6 +928,7 @@ export function compareSchemas(oldSchema, newSchema) {
|
|
|
927
928
|
result.renamed.functions.length > 0 ||
|
|
928
929
|
result.modified.tables.length > 0 ||
|
|
929
930
|
result.modified.columns.length > 0 ||
|
|
931
|
+
result.modified.indexes.length > 0 ||
|
|
930
932
|
result.modified.enums.length > 0;
|
|
931
933
|
return result;
|
|
932
934
|
}
|
|
@@ -1113,21 +1115,35 @@ function compareTableIndexes(oldTable, newTable, tableName, result) {
|
|
|
1113
1115
|
const processedNew = new Set();
|
|
1114
1116
|
for (const [trackingId, oldIdx] of oldByTrackingId) {
|
|
1115
1117
|
const newIdx = newByTrackingId.get(trackingId);
|
|
1116
|
-
if (newIdx
|
|
1118
|
+
if (!newIdx)
|
|
1119
|
+
continue;
|
|
1120
|
+
if (newIdx.name !== oldIdx.name) {
|
|
1117
1121
|
result.renamed.indexes.push({
|
|
1118
1122
|
table: tableName,
|
|
1119
1123
|
from: oldIdx.name,
|
|
1120
1124
|
to: newIdx.name,
|
|
1121
1125
|
trackingId,
|
|
1122
1126
|
});
|
|
1123
|
-
processedOld.add(oldIdx.name);
|
|
1124
|
-
processedNew.add(newIdx.name);
|
|
1125
1127
|
}
|
|
1128
|
+
const changes = compareIndexProperties(oldIdx, newIdx);
|
|
1129
|
+
if (changes.length > 0) {
|
|
1130
|
+
result.modified.indexes.push({ table: tableName, oldIndex: oldIdx, newIndex: newIdx, changes });
|
|
1131
|
+
}
|
|
1132
|
+
processedOld.add(oldIdx.name);
|
|
1133
|
+
processedNew.add(newIdx.name);
|
|
1126
1134
|
}
|
|
1127
1135
|
for (const [name, newIdx] of newByName) {
|
|
1128
1136
|
if (processedNew.has(name))
|
|
1129
1137
|
continue;
|
|
1130
|
-
|
|
1138
|
+
const oldIdx = oldByName.get(name);
|
|
1139
|
+
if (oldIdx) {
|
|
1140
|
+
const changes = compareIndexProperties(oldIdx, newIdx);
|
|
1141
|
+
if (changes.length > 0) {
|
|
1142
|
+
result.modified.indexes.push({ table: tableName, oldIndex: oldIdx, newIndex: newIdx, changes });
|
|
1143
|
+
}
|
|
1144
|
+
processedOld.add(name);
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1131
1147
|
result.added.indexes.push({ table: tableName, index: newIdx });
|
|
1132
1148
|
}
|
|
1133
1149
|
}
|
|
@@ -1139,6 +1155,25 @@ function compareTableIndexes(oldTable, newTable, tableName, result) {
|
|
|
1139
1155
|
}
|
|
1140
1156
|
}
|
|
1141
1157
|
}
|
|
1158
|
+
function compareIndexProperties(oldIdx, newIdx) {
|
|
1159
|
+
const changes = [];
|
|
1160
|
+
if (oldIdx.isUnique !== newIdx.isUnique) {
|
|
1161
|
+
changes.push(`unique: ${oldIdx.isUnique} → ${newIdx.isUnique}`);
|
|
1162
|
+
}
|
|
1163
|
+
if ((oldIdx.method || 'btree') !== (newIdx.method || 'btree')) {
|
|
1164
|
+
changes.push(`method: ${oldIdx.method || 'btree'} → ${newIdx.method || 'btree'}`);
|
|
1165
|
+
}
|
|
1166
|
+
if (oldIdx.columns.join(',') !== newIdx.columns.join(',')) {
|
|
1167
|
+
changes.push(`columns: ${oldIdx.columns.join(',')} → ${newIdx.columns.join(',')}`);
|
|
1168
|
+
}
|
|
1169
|
+
if ((oldIdx.whereClause || '') !== (newIdx.whereClause || '')) {
|
|
1170
|
+
changes.push(`where: changed`);
|
|
1171
|
+
}
|
|
1172
|
+
if ((oldIdx.includeColumns || []).join(',') !== (newIdx.includeColumns || []).join(',')) {
|
|
1173
|
+
changes.push(`include: changed`);
|
|
1174
|
+
}
|
|
1175
|
+
return changes;
|
|
1176
|
+
}
|
|
1142
1177
|
function compareEnums(oldEnums, newEnums, result) {
|
|
1143
1178
|
const oldByName = new Map(oldEnums.map(e => [e.name, e]));
|
|
1144
1179
|
const newByName = new Map(newEnums.map(e => [e.name, e]));
|
|
@@ -90,9 +90,32 @@ export class RelqBase {
|
|
|
90
90
|
}
|
|
91
91
|
await this.ensureInitialized();
|
|
92
92
|
const startTime = performance.now();
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
const retryConfig = this.config.retry;
|
|
94
|
+
if (!retryConfig) {
|
|
95
|
+
const result = await this._query(sql);
|
|
96
|
+
return { result, duration: performance.now() - startTime };
|
|
97
|
+
}
|
|
98
|
+
const opts = typeof retryConfig === 'object'
|
|
99
|
+
? { maxRetries: retryConfig.maxRetries ?? 3, initialDelayMs: retryConfig.initialDelayMs ?? 250, maxDelayMs: retryConfig.maxDelayMs ?? 5000 }
|
|
100
|
+
: { maxRetries: 3, initialDelayMs: 250, maxDelayMs: 5000 };
|
|
101
|
+
let lastError;
|
|
102
|
+
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
|
|
103
|
+
try {
|
|
104
|
+
const result = await this._query(sql);
|
|
105
|
+
return { result, duration: performance.now() - startTime };
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
lastError = error;
|
|
109
|
+
if (attempt < opts.maxRetries && isTransientError(error)) {
|
|
110
|
+
const delay = Math.min(opts.initialDelayMs * 2 ** attempt, opts.maxDelayMs);
|
|
111
|
+
debugLog(this.config, `Transient error (attempt ${attempt + 1}/${opts.maxRetries}), retrying in ${delay}ms: ${error.code || error.message}`);
|
|
112
|
+
await sleep(delay);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
throw lastError;
|
|
96
119
|
}
|
|
97
120
|
buildMetadata(result, duration) {
|
|
98
121
|
return {
|
|
@@ -216,3 +239,18 @@ export class RelqBase {
|
|
|
216
239
|
return this._isClosed;
|
|
217
240
|
}
|
|
218
241
|
}
|
|
242
|
+
const TRANSIENT_CODES = new Set([
|
|
243
|
+
'ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ESERVFAIL',
|
|
244
|
+
'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'EHOSTUNREACH',
|
|
245
|
+
'CONNECTION_LOST', 'PROTOCOL_CONNECTION_LOST',
|
|
246
|
+
]);
|
|
247
|
+
function isTransientError(error) {
|
|
248
|
+
if (TRANSIENT_CODES.has(error.code))
|
|
249
|
+
return true;
|
|
250
|
+
if (error.cause && TRANSIENT_CODES.has(error.cause.code))
|
|
251
|
+
return true;
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
function sleep(ms) {
|
|
255
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
256
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6760,6 +6760,22 @@ export interface RelqBaseConfig {
|
|
|
6760
6760
|
commitLimit?: number;
|
|
6761
6761
|
/** Schema relations for type-safe joins */
|
|
6762
6762
|
relations?: SchemaRelations | SchemaRelationsArray;
|
|
6763
|
+
/**
|
|
6764
|
+
* Retry transient query failures (DNS, connection reset, timeout).
|
|
6765
|
+
* Disabled by default. Set `true` for 3 retries, or pass options.
|
|
6766
|
+
*/
|
|
6767
|
+
retry?: boolean | RetryOptions;
|
|
6768
|
+
}
|
|
6769
|
+
/**
|
|
6770
|
+
* Configuration for automatic query retry on transient errors.
|
|
6771
|
+
*/
|
|
6772
|
+
export interface RetryOptions {
|
|
6773
|
+
/** Maximum number of retry attempts @default 3 */
|
|
6774
|
+
maxRetries?: number;
|
|
6775
|
+
/** Initial delay in ms before first retry (doubles each attempt) @default 250 */
|
|
6776
|
+
initialDelayMs?: number;
|
|
6777
|
+
/** Maximum delay in ms between retries @default 5000 */
|
|
6778
|
+
maxDelayMs?: number;
|
|
6763
6779
|
}
|
|
6764
6780
|
export interface PgConnectionFields {
|
|
6765
6781
|
/** Database host @default 'localhost' */
|