@query-doctor/core 0.3.0 → 0.4.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/dist/index.cjs CHANGED
@@ -392,6 +392,18 @@ var Walker = class _Walker {
392
392
  }
393
393
  }
394
394
  }
395
+ if (is2(node, "A_Expr") && node.A_Expr.kind === "AEXPR_OP") {
396
+ const opName = node.A_Expr.name?.[0] && is2(node.A_Expr.name[0], "String") && node.A_Expr.name[0].String.sval;
397
+ if (opName && (opName === "@>" || opName === "?" || opName === "?|" || opName === "?&")) {
398
+ const jsonbOperator = opName;
399
+ if (node.A_Expr.lexpr && is2(node.A_Expr.lexpr, "ColumnRef")) {
400
+ this.add(node.A_Expr.lexpr, { jsonbOperator });
401
+ }
402
+ if (node.A_Expr.rexpr && is2(node.A_Expr.rexpr, "ColumnRef")) {
403
+ this.add(node.A_Expr.rexpr, { jsonbOperator });
404
+ }
405
+ }
406
+ }
395
407
  if (is2(node, "ColumnRef")) {
396
408
  for (let i = 0; i < stack.length; i++) {
397
409
  const inReturningList = stack[i] === "returningList" && stack[i + 1] === "ResTarget" && stack[i + 2] === "val" && stack[i + 3] === "ColumnRef";
@@ -490,6 +502,9 @@ var Walker = class _Walker {
490
502
  if (options?.where) {
491
503
  ref.where = options.where;
492
504
  }
505
+ if (options?.jsonbOperator) {
506
+ ref.jsonbOperator = options.jsonbOperator;
507
+ }
493
508
  this.highlights.push(ref);
494
509
  }
495
510
  static traverse(node, stack, callback) {
@@ -654,6 +669,9 @@ var Analyzer = class {
654
669
  if (colReference.where) {
655
670
  index.where = colReference.where;
656
671
  }
672
+ if (colReference.jsonbOperator) {
673
+ index.jsonbOperator = colReference.jsonbOperator;
674
+ }
657
675
  addIndex(index);
658
676
  }
659
677
  } else if (tableReference) {
@@ -675,6 +693,9 @@ var Analyzer = class {
675
693
  if (colReference.where) {
676
694
  index.where = colReference.where;
677
695
  }
696
+ if (colReference.jsonbOperator) {
697
+ index.jsonbOperator = colReference.jsonbOperator;
698
+ }
678
699
  addIndex(index);
679
700
  }
680
701
  } else if (fullReference) {
@@ -693,6 +714,9 @@ var Analyzer = class {
693
714
  if (colReference.where) {
694
715
  index.where = colReference.where;
695
716
  }
717
+ if (colReference.jsonbOperator) {
718
+ index.jsonbOperator = colReference.jsonbOperator;
719
+ }
696
720
  addIndex(index);
697
721
  } else {
698
722
  console.error(
@@ -815,7 +839,7 @@ async function dropIndex(tx, index) {
815
839
 
816
840
  // src/sql/indexes.ts
817
841
  function isIndexSupported(index) {
818
- return index.index_type === "btree";
842
+ return index.index_type === "btree" || index.index_type === "gin";
819
843
  }
820
844
  function isIndexProbablyDroppable(index) {
821
845
  return !index.is_primary && !index.is_unique;
@@ -1297,8 +1321,10 @@ var _IndexOptimizer = class _IndexOptimizer {
1297
1321
  * Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]
1298
1322
  **/
1299
1323
  indexesToCreate(rootCandidates) {
1300
- const permutedIndexes = this.groupPotentialIndexColumnsByTable(rootCandidates);
1324
+ const btreeCandidates = rootCandidates.filter((c) => !c.jsonbOperator);
1325
+ const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);
1301
1326
  const nextStage = [];
1327
+ const permutedIndexes = this.groupPotentialIndexColumnsByTable(btreeCandidates);
1302
1328
  for (const permutation of permutedIndexes.values()) {
1303
1329
  const { table: rawTable, schema: rawSchema, columns } = permutation;
1304
1330
  const permutations = permutationsWithDescendingLength(columns);
@@ -1323,6 +1349,41 @@ var _IndexOptimizer = class _IndexOptimizer {
1323
1349
  });
1324
1350
  }
1325
1351
  }
1352
+ const ginGroups = this.groupGinCandidatesByColumn(ginCandidates);
1353
+ for (const group of ginGroups.values()) {
1354
+ const { schema: rawSchema, table: rawTable, column, operators } = group;
1355
+ const schema = PgIdentifier.fromString(rawSchema);
1356
+ const table = PgIdentifier.fromString(rawTable);
1357
+ const needsKeyExistence = operators.some(
1358
+ (op) => op === "?" || op === "?|" || op === "?&"
1359
+ );
1360
+ const opclass = needsKeyExistence ? void 0 : "jsonb_path_ops";
1361
+ const existingGin = this.ginIndexAlreadyExists(table.toString(), column);
1362
+ if (existingGin) {
1363
+ continue;
1364
+ }
1365
+ const indexName = this.indexName();
1366
+ const candidate = {
1367
+ schema: rawSchema,
1368
+ table: rawTable,
1369
+ column
1370
+ };
1371
+ const definition = this.toGinDefinition({
1372
+ table,
1373
+ schema,
1374
+ column: PgIdentifier.fromString(column),
1375
+ opclass
1376
+ });
1377
+ nextStage.push({
1378
+ name: indexName,
1379
+ schema: schema.toString(),
1380
+ table: table.toString(),
1381
+ columns: [candidate],
1382
+ definition,
1383
+ indexMethod: "gin",
1384
+ opclass
1385
+ });
1386
+ }
1326
1387
  return nextStage;
1327
1388
  }
1328
1389
  toDefinition({
@@ -1357,6 +1418,47 @@ var _IndexOptimizer = class _IndexOptimizer {
1357
1418
  const colored = make(import_colorette2.green, import_colorette2.yellow, import_colorette2.magenta, import_colorette2.blue);
1358
1419
  return { raw, colored };
1359
1420
  }
1421
+ toGinDefinition({
1422
+ schema,
1423
+ table,
1424
+ column,
1425
+ opclass
1426
+ }) {
1427
+ let fullyQualifiedTable;
1428
+ if (schema.toString() === "public") {
1429
+ fullyQualifiedTable = table;
1430
+ } else {
1431
+ fullyQualifiedTable = PgIdentifier.fromParts(schema, table);
1432
+ }
1433
+ const opclassSuffix = opclass ? ` ${opclass}` : "";
1434
+ return `${fullyQualifiedTable} using gin (${column}${opclassSuffix})`;
1435
+ }
1436
+ groupGinCandidatesByColumn(candidates) {
1437
+ const groups = /* @__PURE__ */ new Map();
1438
+ for (const c of candidates) {
1439
+ if (!c.jsonbOperator) continue;
1440
+ const key = `${c.schema}.${c.table}.${c.column}`;
1441
+ const existing = groups.get(key);
1442
+ if (existing) {
1443
+ if (!existing.operators.includes(c.jsonbOperator)) {
1444
+ existing.operators.push(c.jsonbOperator);
1445
+ }
1446
+ } else {
1447
+ groups.set(key, {
1448
+ schema: c.schema,
1449
+ table: c.table,
1450
+ column: c.column,
1451
+ operators: [c.jsonbOperator]
1452
+ });
1453
+ }
1454
+ }
1455
+ return groups;
1456
+ }
1457
+ ginIndexAlreadyExists(table, column) {
1458
+ return this.existingIndexes.find(
1459
+ (index) => index.index_type === "gin" && index.table_name === table && index.index_columns.some((c) => c.name === column)
1460
+ );
1461
+ }
1360
1462
  /**
1361
1463
  * Drop indexes that can be dropped. Ignore the ones that can't
1362
1464
  */
@@ -1673,6 +1775,21 @@ var _Statistics = class _Statistics {
1673
1775
  }
1674
1776
  return "text";
1675
1777
  }
1778
+ /**
1779
+ * PostgreSQL's anyarray columns in pg_statistic can hold arrays of arrays
1780
+ * for columns with array types (e.g. text[], int4[]). These create
1781
+ * multidimensional arrays that can be "ragged" (sub-arrays with different
1782
+ * lengths). jsonb_to_recordset can't reconstruct ragged multidimensional
1783
+ * arrays from JSON, so we need to drop these values.
1784
+ */
1785
+ static safeStavalues(values) {
1786
+ if (!values || values.length === 0) return values;
1787
+ if (values.some((v) => Array.isArray(v))) {
1788
+ console.warn("Discarding ragged multidimensional stavalues array");
1789
+ return null;
1790
+ }
1791
+ return values;
1792
+ }
1676
1793
  async restoreStats17(tx) {
1677
1794
  const warnings = {
1678
1795
  tablesNotInExports: [],
@@ -1724,16 +1841,26 @@ var _Statistics = class _Statistics {
1724
1841
  stanumbers3: stats.stanumbers3,
1725
1842
  stanumbers4: stats.stanumbers4,
1726
1843
  stanumbers5: stats.stanumbers5,
1727
- stavalues1: stats.stavalues1,
1728
- stavalues2: stats.stavalues2,
1729
- stavalues3: stats.stavalues3,
1730
- stavalues4: stats.stavalues4,
1731
- stavalues5: stats.stavalues5,
1732
- _value_type1: this.stavalueKind(stats.stavalues1),
1733
- _value_type2: this.stavalueKind(stats.stavalues2),
1734
- _value_type3: this.stavalueKind(stats.stavalues3),
1735
- _value_type4: this.stavalueKind(stats.stavalues4),
1736
- _value_type5: this.stavalueKind(stats.stavalues5)
1844
+ stavalues1: _Statistics.safeStavalues(stats.stavalues1),
1845
+ stavalues2: _Statistics.safeStavalues(stats.stavalues2),
1846
+ stavalues3: _Statistics.safeStavalues(stats.stavalues3),
1847
+ stavalues4: _Statistics.safeStavalues(stats.stavalues4),
1848
+ stavalues5: _Statistics.safeStavalues(stats.stavalues5),
1849
+ _value_type1: this.stavalueKind(
1850
+ _Statistics.safeStavalues(stats.stavalues1)
1851
+ ),
1852
+ _value_type2: this.stavalueKind(
1853
+ _Statistics.safeStavalues(stats.stavalues2)
1854
+ ),
1855
+ _value_type3: this.stavalueKind(
1856
+ _Statistics.safeStavalues(stats.stavalues3)
1857
+ ),
1858
+ _value_type4: this.stavalueKind(
1859
+ _Statistics.safeStavalues(stats.stavalues4)
1860
+ ),
1861
+ _value_type5: this.stavalueKind(
1862
+ _Statistics.safeStavalues(stats.stavalues5)
1863
+ )
1737
1864
  });
1738
1865
  }
1739
1866
  }
@@ -2149,14 +2276,16 @@ var _Statistics = class _Statistics {
2149
2276
  CASE
2150
2277
  WHEN (indoption[array_position(ix.indkey, a.attnum)] & 1) = 1 THEN 'DESC'
2151
2278
  ELSE 'ASC'
2152
- END)
2279
+ END,
2280
+ 'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
2153
2281
  -- Handle expressions
2154
2282
  ELSE
2155
2283
  json_build_object('name', pg_get_expr((ix.indexprs)::pg_node_tree, t.oid), 'order',
2156
2284
  CASE
2157
2285
  WHEN (indoption[array_position(ix.indkey, k.attnum)] & 1) = 1 THEN 'DESC'
2158
2286
  ELSE 'ASC'
2159
- END)
2287
+ END,
2288
+ 'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
2160
2289
  END
2161
2290
  ORDER BY array_position(ix.indkey, k.attnum)
2162
2291
  ) AS index_columns
@@ -2168,6 +2297,7 @@ var _Statistics = class _Statistics {
2168
2297
  JOIN pg_am am ON i.relam = am.oid
2169
2298
  LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY k(attnum, ordinality) ON true
2170
2299
  LEFT JOIN pg_attribute a ON a.attnum = k.attnum AND a.attrelid = t.oid
2300
+ LEFT JOIN pg_opclass opc ON opc.oid = ix.indclass[k.ordinality - 1]
2171
2301
  JOIN pg_namespace n ON t.relnamespace = n.oid
2172
2302
  WHERE
2173
2303
  n.nspname not like 'pg_%' and