@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.js CHANGED
@@ -343,6 +343,18 @@ var Walker = class _Walker {
343
343
  }
344
344
  }
345
345
  }
346
+ if (is2(node, "A_Expr") && node.A_Expr.kind === "AEXPR_OP") {
347
+ const opName = node.A_Expr.name?.[0] && is2(node.A_Expr.name[0], "String") && node.A_Expr.name[0].String.sval;
348
+ if (opName && (opName === "@>" || opName === "?" || opName === "?|" || opName === "?&")) {
349
+ const jsonbOperator = opName;
350
+ if (node.A_Expr.lexpr && is2(node.A_Expr.lexpr, "ColumnRef")) {
351
+ this.add(node.A_Expr.lexpr, { jsonbOperator });
352
+ }
353
+ if (node.A_Expr.rexpr && is2(node.A_Expr.rexpr, "ColumnRef")) {
354
+ this.add(node.A_Expr.rexpr, { jsonbOperator });
355
+ }
356
+ }
357
+ }
346
358
  if (is2(node, "ColumnRef")) {
347
359
  for (let i = 0; i < stack.length; i++) {
348
360
  const inReturningList = stack[i] === "returningList" && stack[i + 1] === "ResTarget" && stack[i + 2] === "val" && stack[i + 3] === "ColumnRef";
@@ -441,6 +453,9 @@ var Walker = class _Walker {
441
453
  if (options?.where) {
442
454
  ref.where = options.where;
443
455
  }
456
+ if (options?.jsonbOperator) {
457
+ ref.jsonbOperator = options.jsonbOperator;
458
+ }
444
459
  this.highlights.push(ref);
445
460
  }
446
461
  static traverse(node, stack, callback) {
@@ -605,6 +620,9 @@ var Analyzer = class {
605
620
  if (colReference.where) {
606
621
  index.where = colReference.where;
607
622
  }
623
+ if (colReference.jsonbOperator) {
624
+ index.jsonbOperator = colReference.jsonbOperator;
625
+ }
608
626
  addIndex(index);
609
627
  }
610
628
  } else if (tableReference) {
@@ -626,6 +644,9 @@ var Analyzer = class {
626
644
  if (colReference.where) {
627
645
  index.where = colReference.where;
628
646
  }
647
+ if (colReference.jsonbOperator) {
648
+ index.jsonbOperator = colReference.jsonbOperator;
649
+ }
629
650
  addIndex(index);
630
651
  }
631
652
  } else if (fullReference) {
@@ -644,6 +665,9 @@ var Analyzer = class {
644
665
  if (colReference.where) {
645
666
  index.where = colReference.where;
646
667
  }
668
+ if (colReference.jsonbOperator) {
669
+ index.jsonbOperator = colReference.jsonbOperator;
670
+ }
647
671
  addIndex(index);
648
672
  } else {
649
673
  console.error(
@@ -766,7 +790,7 @@ async function dropIndex(tx, index) {
766
790
 
767
791
  // src/sql/indexes.ts
768
792
  function isIndexSupported(index) {
769
- return index.index_type === "btree";
793
+ return index.index_type === "btree" || index.index_type === "gin";
770
794
  }
771
795
  function isIndexProbablyDroppable(index) {
772
796
  return !index.is_primary && !index.is_unique;
@@ -1248,8 +1272,10 @@ var _IndexOptimizer = class _IndexOptimizer {
1248
1272
  * Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]
1249
1273
  **/
1250
1274
  indexesToCreate(rootCandidates) {
1251
- const permutedIndexes = this.groupPotentialIndexColumnsByTable(rootCandidates);
1275
+ const btreeCandidates = rootCandidates.filter((c) => !c.jsonbOperator);
1276
+ const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);
1252
1277
  const nextStage = [];
1278
+ const permutedIndexes = this.groupPotentialIndexColumnsByTable(btreeCandidates);
1253
1279
  for (const permutation of permutedIndexes.values()) {
1254
1280
  const { table: rawTable, schema: rawSchema, columns } = permutation;
1255
1281
  const permutations = permutationsWithDescendingLength(columns);
@@ -1274,6 +1300,41 @@ var _IndexOptimizer = class _IndexOptimizer {
1274
1300
  });
1275
1301
  }
1276
1302
  }
1303
+ const ginGroups = this.groupGinCandidatesByColumn(ginCandidates);
1304
+ for (const group of ginGroups.values()) {
1305
+ const { schema: rawSchema, table: rawTable, column, operators } = group;
1306
+ const schema = PgIdentifier.fromString(rawSchema);
1307
+ const table = PgIdentifier.fromString(rawTable);
1308
+ const needsKeyExistence = operators.some(
1309
+ (op) => op === "?" || op === "?|" || op === "?&"
1310
+ );
1311
+ const opclass = needsKeyExistence ? void 0 : "jsonb_path_ops";
1312
+ const existingGin = this.ginIndexAlreadyExists(table.toString(), column);
1313
+ if (existingGin) {
1314
+ continue;
1315
+ }
1316
+ const indexName = this.indexName();
1317
+ const candidate = {
1318
+ schema: rawSchema,
1319
+ table: rawTable,
1320
+ column
1321
+ };
1322
+ const definition = this.toGinDefinition({
1323
+ table,
1324
+ schema,
1325
+ column: PgIdentifier.fromString(column),
1326
+ opclass
1327
+ });
1328
+ nextStage.push({
1329
+ name: indexName,
1330
+ schema: schema.toString(),
1331
+ table: table.toString(),
1332
+ columns: [candidate],
1333
+ definition,
1334
+ indexMethod: "gin",
1335
+ opclass
1336
+ });
1337
+ }
1277
1338
  return nextStage;
1278
1339
  }
1279
1340
  toDefinition({
@@ -1308,6 +1369,47 @@ var _IndexOptimizer = class _IndexOptimizer {
1308
1369
  const colored = make(green, yellow, magenta, blue2);
1309
1370
  return { raw, colored };
1310
1371
  }
1372
+ toGinDefinition({
1373
+ schema,
1374
+ table,
1375
+ column,
1376
+ opclass
1377
+ }) {
1378
+ let fullyQualifiedTable;
1379
+ if (schema.toString() === "public") {
1380
+ fullyQualifiedTable = table;
1381
+ } else {
1382
+ fullyQualifiedTable = PgIdentifier.fromParts(schema, table);
1383
+ }
1384
+ const opclassSuffix = opclass ? ` ${opclass}` : "";
1385
+ return `${fullyQualifiedTable} using gin (${column}${opclassSuffix})`;
1386
+ }
1387
+ groupGinCandidatesByColumn(candidates) {
1388
+ const groups = /* @__PURE__ */ new Map();
1389
+ for (const c of candidates) {
1390
+ if (!c.jsonbOperator) continue;
1391
+ const key = `${c.schema}.${c.table}.${c.column}`;
1392
+ const existing = groups.get(key);
1393
+ if (existing) {
1394
+ if (!existing.operators.includes(c.jsonbOperator)) {
1395
+ existing.operators.push(c.jsonbOperator);
1396
+ }
1397
+ } else {
1398
+ groups.set(key, {
1399
+ schema: c.schema,
1400
+ table: c.table,
1401
+ column: c.column,
1402
+ operators: [c.jsonbOperator]
1403
+ });
1404
+ }
1405
+ }
1406
+ return groups;
1407
+ }
1408
+ ginIndexAlreadyExists(table, column) {
1409
+ return this.existingIndexes.find(
1410
+ (index) => index.index_type === "gin" && index.table_name === table && index.index_columns.some((c) => c.name === column)
1411
+ );
1412
+ }
1311
1413
  /**
1312
1414
  * Drop indexes that can be dropped. Ignore the ones that can't
1313
1415
  */
@@ -1624,6 +1726,21 @@ var _Statistics = class _Statistics {
1624
1726
  }
1625
1727
  return "text";
1626
1728
  }
1729
+ /**
1730
+ * PostgreSQL's anyarray columns in pg_statistic can hold arrays of arrays
1731
+ * for columns with array types (e.g. text[], int4[]). These create
1732
+ * multidimensional arrays that can be "ragged" (sub-arrays with different
1733
+ * lengths). jsonb_to_recordset can't reconstruct ragged multidimensional
1734
+ * arrays from JSON, so we need to drop these values.
1735
+ */
1736
+ static safeStavalues(values) {
1737
+ if (!values || values.length === 0) return values;
1738
+ if (values.some((v) => Array.isArray(v))) {
1739
+ console.warn("Discarding ragged multidimensional stavalues array");
1740
+ return null;
1741
+ }
1742
+ return values;
1743
+ }
1627
1744
  async restoreStats17(tx) {
1628
1745
  const warnings = {
1629
1746
  tablesNotInExports: [],
@@ -1675,16 +1792,26 @@ var _Statistics = class _Statistics {
1675
1792
  stanumbers3: stats.stanumbers3,
1676
1793
  stanumbers4: stats.stanumbers4,
1677
1794
  stanumbers5: stats.stanumbers5,
1678
- stavalues1: stats.stavalues1,
1679
- stavalues2: stats.stavalues2,
1680
- stavalues3: stats.stavalues3,
1681
- stavalues4: stats.stavalues4,
1682
- stavalues5: stats.stavalues5,
1683
- _value_type1: this.stavalueKind(stats.stavalues1),
1684
- _value_type2: this.stavalueKind(stats.stavalues2),
1685
- _value_type3: this.stavalueKind(stats.stavalues3),
1686
- _value_type4: this.stavalueKind(stats.stavalues4),
1687
- _value_type5: this.stavalueKind(stats.stavalues5)
1795
+ stavalues1: _Statistics.safeStavalues(stats.stavalues1),
1796
+ stavalues2: _Statistics.safeStavalues(stats.stavalues2),
1797
+ stavalues3: _Statistics.safeStavalues(stats.stavalues3),
1798
+ stavalues4: _Statistics.safeStavalues(stats.stavalues4),
1799
+ stavalues5: _Statistics.safeStavalues(stats.stavalues5),
1800
+ _value_type1: this.stavalueKind(
1801
+ _Statistics.safeStavalues(stats.stavalues1)
1802
+ ),
1803
+ _value_type2: this.stavalueKind(
1804
+ _Statistics.safeStavalues(stats.stavalues2)
1805
+ ),
1806
+ _value_type3: this.stavalueKind(
1807
+ _Statistics.safeStavalues(stats.stavalues3)
1808
+ ),
1809
+ _value_type4: this.stavalueKind(
1810
+ _Statistics.safeStavalues(stats.stavalues4)
1811
+ ),
1812
+ _value_type5: this.stavalueKind(
1813
+ _Statistics.safeStavalues(stats.stavalues5)
1814
+ )
1688
1815
  });
1689
1816
  }
1690
1817
  }
@@ -2100,14 +2227,16 @@ var _Statistics = class _Statistics {
2100
2227
  CASE
2101
2228
  WHEN (indoption[array_position(ix.indkey, a.attnum)] & 1) = 1 THEN 'DESC'
2102
2229
  ELSE 'ASC'
2103
- END)
2230
+ END,
2231
+ 'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
2104
2232
  -- Handle expressions
2105
2233
  ELSE
2106
2234
  json_build_object('name', pg_get_expr((ix.indexprs)::pg_node_tree, t.oid), 'order',
2107
2235
  CASE
2108
2236
  WHEN (indoption[array_position(ix.indkey, k.attnum)] & 1) = 1 THEN 'DESC'
2109
2237
  ELSE 'ASC'
2110
- END)
2238
+ END,
2239
+ 'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
2111
2240
  END
2112
2241
  ORDER BY array_position(ix.indkey, k.attnum)
2113
2242
  ) AS index_columns
@@ -2119,6 +2248,7 @@ var _Statistics = class _Statistics {
2119
2248
  JOIN pg_am am ON i.relam = am.oid
2120
2249
  LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY k(attnum, ordinality) ON true
2121
2250
  LEFT JOIN pg_attribute a ON a.attnum = k.attnum AND a.attrelid = t.oid
2251
+ LEFT JOIN pg_opclass opc ON opc.oid = ix.indclass[k.ordinality - 1]
2122
2252
  JOIN pg_namespace n ON t.relnamespace = n.oid
2123
2253
  WHERE
2124
2254
  n.nspname not like 'pg_%' and