pgterra 0.2.12 → 0.2.14

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 (2) hide show
  1. package/dist/index.js +137 -84
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -21475,6 +21475,7 @@ function parseCreateIndex(stmt) {
21475
21475
  const schema = stmt.relation?.schemaname || undefined;
21476
21476
  const indexParams = stmt.indexParams || [];
21477
21477
  const columns = [];
21478
+ let opclasses;
21478
21479
  let expression;
21479
21480
  if (indexParams.length === 1 && indexParams[0].IndexElem?.expr) {
21480
21481
  expression = import_pgsql_parser3.deparseSync([indexParams[0].IndexElem.expr]).trim();
@@ -21484,6 +21485,14 @@ function parseCreateIndex(stmt) {
21484
21485
  const colName = param.IndexElem.name;
21485
21486
  if (colName) {
21486
21487
  columns.push(colName);
21488
+ if (param.IndexElem.opclass && param.IndexElem.opclass.length > 0) {
21489
+ const opclassName = param.IndexElem.opclass.map((node) => node.String?.sval).filter(Boolean).join(".");
21490
+ if (opclassName) {
21491
+ if (!opclasses)
21492
+ opclasses = {};
21493
+ opclasses[colName] = opclassName;
21494
+ }
21495
+ }
21487
21496
  } else if (param.IndexElem.expr) {
21488
21497
  expression = import_pgsql_parser3.deparseSync([param.IndexElem.expr]).trim();
21489
21498
  break;
@@ -21553,6 +21562,7 @@ function parseCreateIndex(stmt) {
21553
21562
  tableName,
21554
21563
  schema,
21555
21564
  columns,
21565
+ opclasses,
21556
21566
  type,
21557
21567
  unique,
21558
21568
  concurrent,
@@ -22846,6 +22856,22 @@ class DatabaseInspector {
22846
22856
  -- Expression index - no simple column names
22847
22857
  ARRAY[]::text[]
22848
22858
  END as column_names,
22859
+ -- Get operator class names for each column (non-default only)
22860
+ CASE
22861
+ WHEN ix.indexprs IS NULL THEN
22862
+ ARRAY(
22863
+ SELECT
22864
+ CASE
22865
+ WHEN opc.opcdefault THEN NULL
22866
+ ELSE opc.opcname
22867
+ END
22868
+ FROM unnest(ix.indclass) WITH ORDINALITY AS u(opcoid, ord)
22869
+ JOIN pg_opclass opc ON opc.oid = u.opcoid
22870
+ ORDER BY u.ord
22871
+ )
22872
+ ELSE
22873
+ ARRAY[]::text[]
22874
+ END as opclass_names,
22849
22875
  CASE
22850
22876
  WHEN ix.indpred IS NOT NULL THEN
22851
22877
  regexp_replace(
@@ -22874,19 +22900,32 @@ class DatabaseInspector {
22874
22900
  )
22875
22901
  ORDER BY i.indexname
22876
22902
  `, [tableName, tableSchema]);
22877
- return result.rows.map((row) => ({
22878
- name: row.index_name,
22879
- tableName: row.table_name,
22880
- schema: row.table_schema,
22881
- columns: row.column_names || [],
22882
- type: this.mapPostgreSQLIndexType(row.access_method),
22883
- unique: row.is_unique,
22884
- concurrent: false,
22885
- where: row.where_clause || undefined,
22886
- expression: row.has_expressions ? row.expression_def : undefined,
22887
- storageParameters: this.parseStorageOptions(row.storage_options),
22888
- tablespace: row.tablespace_name || undefined
22889
- }));
22903
+ return result.rows.map((row) => {
22904
+ const columns = row.column_names || [];
22905
+ const opclassNames = row.opclass_names || [];
22906
+ let opclasses;
22907
+ for (let i = 0;i < columns.length; i++) {
22908
+ if (opclassNames[i]) {
22909
+ if (!opclasses)
22910
+ opclasses = {};
22911
+ opclasses[columns[i]] = opclassNames[i];
22912
+ }
22913
+ }
22914
+ return {
22915
+ name: row.index_name,
22916
+ tableName: row.table_name,
22917
+ schema: row.table_schema,
22918
+ columns,
22919
+ opclasses,
22920
+ type: this.mapPostgreSQLIndexType(row.access_method),
22921
+ unique: row.is_unique,
22922
+ concurrent: false,
22923
+ where: row.where_clause || undefined,
22924
+ expression: row.has_expressions ? row.expression_def : undefined,
22925
+ storageParameters: this.parseStorageOptions(row.storage_options),
22926
+ tablespace: row.tablespace_name || undefined
22927
+ };
22928
+ });
22890
22929
  }
22891
22930
  parseStorageOptions(reloptions) {
22892
22931
  if (!reloptions || !Array.isArray(reloptions) || reloptions.length === 0) {
@@ -23688,6 +23727,30 @@ function normalizeDefault(value) {
23688
23727
  normalized = normalized.replace(/::[a-z_]+(\s+[a-z_]+)*(\([^)]*\))?$/i, "");
23689
23728
  return normalized.trim();
23690
23729
  }
23730
+ function normalizeExpression(expr) {
23731
+ let normalized = expr.replace(/\s+/g, " ").trim().replace(/::"?[a-z_]+"?(\([^)]*\))?/gi, "").replace(/\(([a-z_][a-z0-9_]*)\)/gi, "$1");
23732
+ while (/^\(.*\)$/.test(normalized)) {
23733
+ const inner = normalized.slice(1, -1);
23734
+ let depth = 0;
23735
+ let balanced = true;
23736
+ for (const char of inner) {
23737
+ if (char === "(")
23738
+ depth++;
23739
+ if (char === ")")
23740
+ depth--;
23741
+ if (depth < 0) {
23742
+ balanced = false;
23743
+ break;
23744
+ }
23745
+ }
23746
+ if (balanced && depth === 0) {
23747
+ normalized = inner.trim();
23748
+ } else {
23749
+ break;
23750
+ }
23751
+ }
23752
+ return normalized.replace(/\s+/g, " ").trim();
23753
+ }
23691
23754
  function columnsAreDifferent(desired, current) {
23692
23755
  const normalizedDesiredType = normalizeType(desired.type);
23693
23756
  const normalizedCurrentType = normalizeType(current.type);
@@ -23718,7 +23781,7 @@ function columnsAreDifferent(desired, current) {
23718
23781
  if (!desired.generated || !current.generated) {
23719
23782
  return true;
23720
23783
  }
23721
- if (desired.generated.always !== current.generated.always || desired.generated.stored !== current.generated.stored || desired.generated.expression !== current.generated.expression) {
23784
+ if (desired.generated.always !== current.generated.always || desired.generated.stored !== current.generated.stored || normalizeExpression(desired.generated.expression) !== normalizeExpression(current.generated.expression)) {
23722
23785
  return true;
23723
23786
  }
23724
23787
  }
@@ -24419,7 +24482,7 @@ class SchemaDiffer {
24419
24482
  }
24420
24483
  generateColumnModificationStatements(tableName, desiredColumn, currentColumn) {
24421
24484
  const statements = [];
24422
- const generatedChanging = (desiredColumn.generated || currentColumn.generated) && (!desiredColumn.generated || !currentColumn.generated || desiredColumn.generated.expression !== currentColumn.generated.expression || desiredColumn.generated.always !== currentColumn.generated.always || desiredColumn.generated.stored !== currentColumn.generated.stored);
24485
+ const generatedChanging = (desiredColumn.generated || currentColumn.generated) && (!desiredColumn.generated || !currentColumn.generated || normalizeExpression(desiredColumn.generated.expression) !== normalizeExpression(currentColumn.generated.expression) || desiredColumn.generated.always !== currentColumn.generated.always || desiredColumn.generated.stored !== currentColumn.generated.stored);
24423
24486
  if (generatedChanging) {
24424
24487
  const dropSql = new SQLBuilder().p("ALTER TABLE").p(tableName).p("DROP COLUMN").ident(desiredColumn.name).p(";").build();
24425
24488
  statements.push(dropSql);
@@ -24624,6 +24687,16 @@ class SchemaDiffer {
24624
24687
  return false;
24625
24688
  if (index1.tablespace !== index2.tablespace)
24626
24689
  return false;
24690
+ const opclasses1 = index1.opclasses || {};
24691
+ const opclasses2 = index2.opclasses || {};
24692
+ const opKeys1 = Object.keys(opclasses1);
24693
+ const opKeys2 = Object.keys(opclasses2);
24694
+ if (opKeys1.length !== opKeys2.length)
24695
+ return false;
24696
+ for (const key of opKeys1) {
24697
+ if (opclasses1[key] !== opclasses2[key])
24698
+ return false;
24699
+ }
24627
24700
  const params1 = index1.storageParameters || {};
24628
24701
  const params2 = index2.storageParameters || {};
24629
24702
  const keys1 = Object.keys(params1);
@@ -24669,7 +24742,11 @@ class SchemaDiffer {
24669
24742
  if (index.expression) {
24670
24743
  builder.p(`(${index.expression})`);
24671
24744
  } else {
24672
- const quotedColumns = index.columns.map((col) => `"${col.replace(/"/g, '""')}"`).join(", ");
24745
+ const quotedColumns = index.columns.map((col) => {
24746
+ const quoted = `"${col.replace(/"/g, '""')}"`;
24747
+ const opclass = index.opclasses?.[col];
24748
+ return opclass ? `${quoted} ${opclass}` : quoted;
24749
+ }).join(", ");
24673
24750
  builder.p(`(${quotedColumns})`);
24674
24751
  }
24675
24752
  if (index.where) {
@@ -24699,9 +24776,9 @@ class SchemaDiffer {
24699
24776
  }
24700
24777
  generateCheckConstraintStatements(tableName, desiredConstraints, currentConstraints) {
24701
24778
  const statements = [];
24702
- const normalizeExpression = (expr) => expr.replace(/\s+/g, " ").trim();
24703
- const currentMap = new Map(currentConstraints.map((c) => [normalizeExpression(c.expression), c]));
24704
- const desiredMap = new Map(desiredConstraints.map((c) => [normalizeExpression(c.expression), c]));
24779
+ const normalizeExpression2 = (expr) => expr.replace(/\s+/g, " ").trim();
24780
+ const currentMap = new Map(currentConstraints.map((c) => [normalizeExpression2(c.expression), c]));
24781
+ const desiredMap = new Map(desiredConstraints.map((c) => [normalizeExpression2(c.expression), c]));
24705
24782
  for (const [key, constraint] of currentMap) {
24706
24783
  if (!desiredMap.has(key)) {
24707
24784
  if (constraint.name) {
@@ -24822,7 +24899,7 @@ class SchemaDiffer {
24822
24899
  return alterations;
24823
24900
  }
24824
24901
  collectColumnModificationAlterations(desiredColumn, currentColumn, alterations) {
24825
- const generatedChanging = (desiredColumn.generated || currentColumn.generated) && (!desiredColumn.generated || !currentColumn.generated || desiredColumn.generated.expression !== currentColumn.generated.expression || desiredColumn.generated.always !== currentColumn.generated.always || desiredColumn.generated.stored !== currentColumn.generated.stored);
24902
+ const generatedChanging = (desiredColumn.generated || currentColumn.generated) && (!desiredColumn.generated || !currentColumn.generated || normalizeExpression(desiredColumn.generated.expression) !== normalizeExpression(currentColumn.generated.expression) || desiredColumn.generated.always !== currentColumn.generated.always || desiredColumn.generated.stored !== currentColumn.generated.stored);
24826
24903
  if (generatedChanging) {
24827
24904
  alterations.push({ type: "drop_column", columnName: desiredColumn.name });
24828
24905
  alterations.push({ type: "add_column", column: desiredColumn });
@@ -24880,9 +24957,9 @@ class SchemaDiffer {
24880
24957
  }
24881
24958
  }
24882
24959
  collectCheckConstraintAlterations(desiredConstraints, currentConstraints, alterations) {
24883
- const normalizeExpression = (expr) => expr.replace(/\s+/g, " ").trim();
24884
- const currentMap = new Map(currentConstraints.map((c) => [normalizeExpression(c.expression), c]));
24885
- const desiredMap = new Map(desiredConstraints.map((c) => [normalizeExpression(c.expression), c]));
24960
+ const normalizeExpression2 = (expr) => expr.replace(/\s+/g, " ").trim();
24961
+ const currentMap = new Map(currentConstraints.map((c) => [normalizeExpression2(c.expression), c]));
24962
+ const desiredMap = new Map(desiredConstraints.map((c) => [normalizeExpression2(c.expression), c]));
24886
24963
  for (const [key, constraint] of currentMap) {
24887
24964
  if (!desiredMap.has(key) && constraint.name) {
24888
24965
  alterations.push({
@@ -25119,27 +25196,32 @@ class MigrationExecutor {
25119
25196
  spinner.stopAndPersist({ symbol: "✔", text: `Applied concurrent change (${elapsed}s)` });
25120
25197
  } catch (error2) {
25121
25198
  spinner.stopAndPersist({ symbol: "✗", text: "Failed to apply concurrent change" });
25122
- throw error2;
25199
+ if (error2 && typeof error2 === "object" && "code" in error2) {
25200
+ const pgError = error2;
25201
+ throw new MigrationError(pgError.message || "Concurrent statement failed", statement, {
25202
+ code: pgError.code,
25203
+ detail: pgError.detail,
25204
+ hint: pgError.hint,
25205
+ position: pgError.position
25206
+ });
25207
+ }
25208
+ throw new MigrationError(error2 instanceof Error ? error2.message : String(error2), statement);
25123
25209
  }
25124
25210
  }
25125
25211
  }
25126
25212
  } catch (error2) {
25213
+ if (error2 instanceof MigrationError) {
25214
+ throw error2;
25215
+ }
25127
25216
  if (error2 && typeof error2 === "object" && "code" in error2) {
25128
25217
  const pgError = error2;
25129
- let failedStatement;
25130
- if (pgError.message) {
25131
- failedStatement = undefined;
25132
- }
25133
- throw new MigrationError(pgError.message || "Database migration failed", failedStatement, {
25218
+ throw new MigrationError(pgError.message || "Database migration failed", undefined, {
25134
25219
  code: pgError.code,
25135
25220
  detail: pgError.detail,
25136
25221
  hint: pgError.hint,
25137
25222
  position: pgError.position
25138
25223
  });
25139
25224
  }
25140
- if (error2 instanceof MigrationError) {
25141
- throw error2;
25142
- }
25143
25225
  throw new MigrationError(error2 instanceof Error ? error2.message : String(error2));
25144
25226
  }
25145
25227
  }
@@ -25209,18 +25291,13 @@ class EnumHandler {
25209
25291
  }
25210
25292
  return { transactional, concurrent };
25211
25293
  }
25212
- async generateRemovalStatements(desiredEnums, currentEnums, client, schemas) {
25294
+ generateRemovalStatements(desiredEnums, currentEnums) {
25213
25295
  const statements = [];
25214
25296
  const desiredEnumNames = new Set(desiredEnums.map((e) => e.name));
25215
25297
  for (const currentEnum of currentEnums) {
25216
25298
  if (!desiredEnumNames.has(currentEnum.name)) {
25217
- const isUsed = await this.isTypeUsed(currentEnum.name, client, schemas);
25218
- if (!isUsed) {
25219
- statements.push(generateDropTypeSQL(currentEnum.name, currentEnum.schema));
25220
- Logger.info(`Dropping unused ENUM type '${currentEnum.name}'`);
25221
- } else {
25222
- Logger.warning(`ENUM type '${currentEnum.name}' is not in schema but is still referenced by table columns. ` + `Cannot auto-drop. Remove column references first.`);
25223
- }
25299
+ statements.push(generateDropTypeSQL(currentEnum.name, currentEnum.schema));
25300
+ Logger.info(`Dropping ENUM type '${currentEnum.name}'`);
25224
25301
  }
25225
25302
  }
25226
25303
  return statements;
@@ -25258,14 +25335,6 @@ class EnumHandler {
25258
25335
  }
25259
25336
  return statements;
25260
25337
  }
25261
- async isTypeUsed(enumName, client, schemas) {
25262
- const result = await client.query(`
25263
- SELECT COUNT(*) as usage_count
25264
- FROM information_schema.columns
25265
- WHERE udt_name = $1 AND table_schema = ANY($2::text[])
25266
- `, [enumName, schemas]);
25267
- return parseInt(result.rows[0].usage_count) > 0;
25268
- }
25269
25338
  }
25270
25339
  // src/core/schema/handlers/extension-handler.ts
25271
25340
  class ExtensionHandler {
@@ -25604,15 +25673,25 @@ class SchemaService {
25604
25673
  return;
25605
25674
  }
25606
25675
  }
25607
- await this.executeStatements(client, sequenceStatements, autoApprove);
25608
- await this.executor.executePlan(client, plan, autoApprove);
25609
- await this.executeEnumRemovals(client, desiredEnums, currentEnums, schemas);
25610
- await this.executeStatements(client, functionStatements, autoApprove);
25611
- await this.executeStatements(client, procedureStatements, autoApprove);
25612
- await this.executeStatements(client, viewStatements, autoApprove);
25613
- await this.executeStatements(client, triggerStatements, autoApprove);
25614
- await this.executeStatements(client, commentStatements, autoApprove);
25615
- await this.executeStatements(client, extensionDropStatements, autoApprove);
25676
+ const enumRemovalStatements = this.enumHandler.generateRemovalStatements(desiredEnums, currentEnums);
25677
+ const combinedPlan = {
25678
+ transactional: [
25679
+ ...sequenceStatements,
25680
+ ...plan.transactional,
25681
+ ...plan.deferred,
25682
+ ...enumRemovalStatements,
25683
+ ...functionStatements,
25684
+ ...procedureStatements,
25685
+ ...viewStatements,
25686
+ ...triggerStatements,
25687
+ ...commentStatements,
25688
+ ...extensionDropStatements
25689
+ ],
25690
+ concurrent: plan.concurrent,
25691
+ deferred: [],
25692
+ hasChanges: true
25693
+ };
25694
+ await this.executor.executePlan(client, combinedPlan, autoApprove);
25616
25695
  } finally {
25617
25696
  if (lockOptions && !dryRun) {
25618
25697
  await this.databaseService.releaseAdvisoryLock(client, lockOptions.lockName);
@@ -25645,32 +25724,6 @@ class SchemaService {
25645
25724
  return await this.parser.parseSchemaFile(input);
25646
25725
  }
25647
25726
  }
25648
- async executeStatements(client, statements, autoApprove) {
25649
- if (statements.length === 0)
25650
- return;
25651
- await this.executor.executePlan(client, {
25652
- transactional: statements,
25653
- concurrent: [],
25654
- deferred: [],
25655
- hasChanges: true
25656
- }, autoApprove);
25657
- }
25658
- async executeEnumRemovals(client, desiredEnums, currentEnums, schemas) {
25659
- const statements = await this.enumHandler.generateRemovalStatements(desiredEnums, currentEnums, client, schemas);
25660
- for (const statement of statements) {
25661
- try {
25662
- await client.query(statement);
25663
- } catch (error2) {
25664
- if (error2.code === "2BP01") {
25665
- const match = statement.match(/DROP TYPE\s+(?:"?(\w+)"?\.)?"?(\w+)"?/i);
25666
- const typeName = match ? match[1] ? `${match[1]}.${match[2]}` : match[2] : "unknown";
25667
- Logger.warning(`Could not drop ENUM '${typeName}': now in use (concurrent change). Will retry next migration.`);
25668
- } else {
25669
- throw error2;
25670
- }
25671
- }
25672
- }
25673
- }
25674
25727
  validateSchemaReferences(managedSchemas, tables, enums, views, functions, procedures, triggers, sequences) {
25675
25728
  const errors = [];
25676
25729
  const checkSchema = (objType, objName, objSchema) => {
@@ -25811,7 +25864,7 @@ async function applyCommand(options, config6) {
25811
25864
  // package.json
25812
25865
  var package_default = {
25813
25866
  name: "pgterra",
25814
- version: "0.2.12",
25867
+ version: "0.2.14",
25815
25868
  description: "Declarative schema management for Postgres",
25816
25869
  keywords: [
25817
25870
  "postgres",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgterra",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "Declarative schema management for Postgres",
5
5
  "keywords": [
6
6
  "postgres",