@query-doctor/core 0.0.3 → 0.0.4

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 (42) hide show
  1. package/dist/index.cjs +222 -97
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +218 -97
  6. package/dist/index.js.map +1 -1
  7. package/dist/optimizer/genalgo.d.ts +9 -5
  8. package/dist/optimizer/genalgo.d.ts.map +1 -1
  9. package/dist/optimizer/genalgo.js +304 -0
  10. package/dist/optimizer/statistics.d.ts +1 -24
  11. package/dist/optimizer/statistics.d.ts.map +1 -1
  12. package/dist/optimizer/statistics.js +700 -0
  13. package/dist/package.json +25 -0
  14. package/dist/sql/analyzer.d.ts +3 -3
  15. package/dist/sql/analyzer.js +270 -0
  16. package/dist/sql/analyzer.test.d.ts +2 -0
  17. package/dist/sql/analyzer.test.d.ts.map +1 -0
  18. package/dist/sql/analyzer_test.js +584 -0
  19. package/dist/sql/builder.js +77 -0
  20. package/dist/sql/database.d.ts +5 -0
  21. package/dist/sql/database.d.ts.map +1 -1
  22. package/dist/sql/database.js +20 -0
  23. package/dist/sql/indexes.d.ts +8 -0
  24. package/dist/sql/indexes.d.ts.map +1 -0
  25. package/dist/sql/indexes.js +12 -0
  26. package/dist/sql/nudges.js +241 -0
  27. package/dist/sql/permutations.test.d.ts +2 -0
  28. package/dist/sql/permutations.test.d.ts.map +1 -0
  29. package/dist/sql/permutations_test.js +53 -0
  30. package/dist/sql/pg-identifier.d.ts +9 -0
  31. package/dist/sql/pg-identifier.d.ts.map +1 -0
  32. package/dist/sql/pg-identifier.test.d.ts +2 -0
  33. package/dist/sql/pg-identifier.test.d.ts.map +1 -0
  34. package/dist/sql/walker.d.ts +2 -2
  35. package/dist/sql/walker.js +295 -0
  36. package/package.json +2 -2
  37. package/dist/index.mjs +0 -24297
  38. package/dist/index.mjs.map +0 -1
  39. package/dist/sql/schema_dump.d.ts +0 -132
  40. package/dist/sql/schema_dump.d.ts.map +0 -1
  41. package/dist/sql/trace.d.ts +0 -1
  42. package/dist/sql/trace.d.ts.map +0 -1
package/dist/index.cjs CHANGED
@@ -41,13 +41,17 @@ __export(index_exports, {
41
41
  ExportedStatsV1: () => ExportedStatsV1,
42
42
  IndexOptimizer: () => IndexOptimizer,
43
43
  PROCEED: () => PROCEED,
44
+ PgIdentifier: () => PgIdentifier,
44
45
  PostgresQueryBuilder: () => PostgresQueryBuilder,
45
46
  PostgresVersion: () => PostgresVersion,
46
47
  SKIP: () => SKIP,
47
48
  Statistics: () => Statistics,
48
49
  StatisticsMode: () => StatisticsMode,
49
50
  StatisticsSource: () => StatisticsSource,
51
+ dropIndex: () => dropIndex,
50
52
  ignoredIdentifier: () => ignoredIdentifier,
53
+ isIndexProbablyDroppable: () => isIndexProbablyDroppable,
54
+ isIndexSupported: () => isIndexSupported,
51
55
  parseNudges: () => parseNudges,
52
56
  permuteWithFeedback: () => permuteWithFeedback
53
57
  });
@@ -749,9 +753,26 @@ var Analyzer = class {
749
753
  // src/sql/database.ts
750
754
  var import_zod = require("zod");
751
755
  var PostgresVersion = import_zod.z.string().brand("PostgresVersion");
756
+ async function dropIndex(tx, index) {
757
+ try {
758
+ await tx.exec(`
759
+ savepoint idx_drop;
760
+ drop index if exists ${index} cascade;
761
+ `);
762
+ return true;
763
+ } catch (error) {
764
+ await tx.exec(`rollback to idx_drop`);
765
+ return false;
766
+ }
767
+ }
752
768
 
753
- // src/optimizer/genalgo.ts
754
- var import_colorette2 = require("colorette");
769
+ // src/sql/indexes.ts
770
+ function isIndexSupported(index) {
771
+ return index.index_type === "btree";
772
+ }
773
+ function isIndexProbablyDroppable(index) {
774
+ return !index.is_primary && !index.is_unique;
775
+ }
755
776
 
756
777
  // src/sql/builder.ts
757
778
  var PostgresQueryBuilder = class _PostgresQueryBuilder {
@@ -833,7 +854,141 @@ var PostgresQueryBuilder = class _PostgresQueryBuilder {
833
854
  }
834
855
  };
835
856
 
857
+ // src/sql/pg-identifier.ts
858
+ var _PgIdentifier = class _PgIdentifier {
859
+ constructor(value, quoted) {
860
+ this.value = value;
861
+ this.quoted = quoted;
862
+ }
863
+ static fromString(identifier) {
864
+ const identifierRegex = /^[a-z_][a-zA-Z0-9_]*$/;
865
+ const match = identifier.match(/^"(.+)"$/);
866
+ if (match) {
867
+ return new _PgIdentifier(match[1], true);
868
+ }
869
+ const quoted = !identifierRegex.test(identifier) || this.reservedKeywords.has(identifier.toLowerCase());
870
+ return new _PgIdentifier(identifier, quoted);
871
+ }
872
+ toString() {
873
+ if (this.quoted) {
874
+ return `"${this.value.replace(/"/g, '""')}"`;
875
+ }
876
+ return this.value;
877
+ }
878
+ };
879
+ __publicField(_PgIdentifier, "reservedKeywords", /* @__PURE__ */ new Set([
880
+ "abort",
881
+ "analyze",
882
+ "binary",
883
+ "cluster",
884
+ "constraint",
885
+ "copy",
886
+ "do",
887
+ "explain",
888
+ "extend",
889
+ "listen",
890
+ "load",
891
+ "lock",
892
+ "move",
893
+ "new",
894
+ "none",
895
+ "notify",
896
+ "offset",
897
+ "reset",
898
+ "setof",
899
+ "show",
900
+ "unlisten",
901
+ "until",
902
+ "vacuum",
903
+ "verbose",
904
+ "all",
905
+ "any",
906
+ "asc",
907
+ "between",
908
+ "bit",
909
+ "both",
910
+ "case",
911
+ "cast",
912
+ "char",
913
+ "character",
914
+ "check",
915
+ "coalesce",
916
+ "collate",
917
+ "column",
918
+ "constraint",
919
+ "cross",
920
+ "current",
921
+ "current_date",
922
+ "current_time",
923
+ "current_timestamp",
924
+ "current_user",
925
+ "dec",
926
+ "decimal",
927
+ "default",
928
+ "desc",
929
+ "distinct",
930
+ "else",
931
+ "end",
932
+ "except",
933
+ "exists",
934
+ "extract",
935
+ "false",
936
+ "float",
937
+ "for",
938
+ "foreign",
939
+ "from",
940
+ "full",
941
+ "global",
942
+ "group",
943
+ "having",
944
+ "in",
945
+ "inner",
946
+ "intersect",
947
+ "into",
948
+ "is",
949
+ "join",
950
+ "leading",
951
+ "left",
952
+ "like",
953
+ "local",
954
+ "natural",
955
+ "nchar",
956
+ "not",
957
+ "null",
958
+ "nullif",
959
+ "numeric",
960
+ "on",
961
+ "or",
962
+ "order",
963
+ "outer",
964
+ "overlaps",
965
+ "position",
966
+ "precision",
967
+ "primary",
968
+ "public",
969
+ "references",
970
+ "right",
971
+ "select",
972
+ "session_user",
973
+ "some",
974
+ "substring",
975
+ "table",
976
+ "then",
977
+ "to",
978
+ "transaction",
979
+ "trim",
980
+ "true",
981
+ "union",
982
+ "unique",
983
+ "user",
984
+ "varchar",
985
+ "when",
986
+ "where"
987
+ ]));
988
+ var PgIdentifier = _PgIdentifier;
989
+
836
990
  // src/optimizer/genalgo.ts
991
+ var import_colorette2 = require("colorette");
837
992
  var _IndexOptimizer = class _IndexOptimizer {
838
993
  constructor(db, statistics, existingIndexes, config = {}) {
839
994
  this.db = db;
@@ -841,8 +996,12 @@ var _IndexOptimizer = class _IndexOptimizer {
841
996
  this.existingIndexes = existingIndexes;
842
997
  this.config = config;
843
998
  }
844
- async run(builder, indexes) {
845
- const baseExplain = await this.runWithoutIndexes(builder);
999
+ async run(builder, indexes, beforeQuery) {
1000
+ const baseExplain = await this.testQueryWithStats(builder, async (tx) => {
1001
+ if (beforeQuery) {
1002
+ await beforeQuery(tx);
1003
+ }
1004
+ });
846
1005
  const baseCost = Number(baseExplain.Plan["Total Cost"]);
847
1006
  if (baseCost === 0) {
848
1007
  return {
@@ -850,76 +1009,17 @@ var _IndexOptimizer = class _IndexOptimizer {
850
1009
  explainPlan: baseExplain
851
1010
  };
852
1011
  }
853
- console.log("Base cost with current indexes", baseCost);
854
- const permutedIndexes = this.tableColumnIndexCandidates(indexes);
855
- const nextStage = [];
856
- const triedIndexes = /* @__PURE__ */ new Map();
857
- for (const { table, schema, columns } of permutedIndexes.values()) {
858
- const permutations = permuteWithFeedback(columns);
859
- let iter = permutations.next(PROCEED);
860
- const previousCost = baseCost;
861
- while (!iter.done) {
862
- const columns2 = iter.value;
863
- const existingIndex = this.indexAlreadyExists(table, columns2);
864
- if (existingIndex) {
865
- console.log(` <${(0, import_colorette2.gray)("skip")}> ${(0, import_colorette2.gray)(existingIndex.index_name)}`);
866
- iter = permutations.next(PROCEED);
867
- continue;
868
- }
869
- let indexDefinition = "?";
870
- const indexName = this.indexName();
871
- const { raw, colored } = this.toDefinition({
872
- columns: columns2,
873
- schema,
874
- table
875
- });
876
- const shortenedSchema = schema === "public" ? "" : `"${schema}".`;
877
- const indexDefinitionClean = `${shortenedSchema}"${table}"(${columns2.map((c) => `"${c.column}"`).join(", ")})`;
878
- indexDefinition = colored;
879
- const query = PostgresQueryBuilder.createIndex(
880
- raw,
881
- indexName
882
- ).introspect();
883
- triedIndexes.set(indexName, {
884
- schema,
885
- table,
886
- columns: columns2,
887
- definition: indexDefinitionClean
888
- });
889
- const explain = await this.testQueryWithStats(builder, async (sql) => {
890
- await sql.exec(query.build());
891
- });
892
- const explainCost = Number(explain.Plan["Total Cost"]);
893
- const costDeltaPercentage = (previousCost - explainCost) / previousCost * 100;
894
- if (previousCost > explainCost) {
895
- console.log(
896
- `${(0, import_colorette2.green)(
897
- `+${costDeltaPercentage.toFixed(2).padStart(5, "0")}%`
898
- )} ${indexDefinition} `
899
- );
900
- iter = permutations.next(PROCEED);
901
- } else {
902
- console.log(
903
- `${previousCost === explainCost ? ` ${(0, import_colorette2.gray)("00.00%")}` : `${(0, import_colorette2.red)(
904
- `-${Math.abs(costDeltaPercentage).toFixed(2).padStart(5, "0")}%`
905
- )}`} ${indexDefinition}`
906
- );
907
- iter = permutations.next(PROCEED);
908
- }
909
- nextStage.push({
910
- name: indexName,
911
- schema,
912
- table,
913
- columns: columns2
914
- });
1012
+ const toCreate = this.indexesToCreate(indexes);
1013
+ const finalExplain = await this.testQueryWithStats(builder, async (tx) => {
1014
+ if (beforeQuery) {
1015
+ await beforeQuery(tx);
915
1016
  }
916
- }
917
- const finalExplain = await this.testQueryWithStats(builder, async (sql) => {
918
- for (const permutation of nextStage) {
919
- const indexName = permutation.name;
920
- await sql.exec(
921
- `create index "${indexName}" on ${this.toDefinition(permutation).raw}; -- @qd_introspection`
922
- );
1017
+ for (const permutation of toCreate) {
1018
+ const createIndex = PostgresQueryBuilder.createIndex(
1019
+ this.toDefinition(permutation).raw,
1020
+ permutation.name
1021
+ ).introspect().build();
1022
+ await tx.exec(createIndex);
923
1023
  }
924
1024
  });
925
1025
  const finalCost = Number(finalExplain.Plan["Total Cost"]);
@@ -938,14 +1038,15 @@ var _IndexOptimizer = class _IndexOptimizer {
938
1038
  )} ${(0, import_colorette2.gray)("If there's a better index, we haven't tried it")}`
939
1039
  );
940
1040
  }
941
- const { newIndexes, existingIndexes: existingIndexesUsedByQuery } = this.findUsedIndexes(finalExplain.Plan);
1041
+ const baseIndexes = this.findUsedIndexes(baseExplain.Plan);
1042
+ const finalIndexes = this.findUsedIndexes(finalExplain.Plan);
942
1043
  return {
943
1044
  kind: "ok",
944
1045
  baseCost,
945
1046
  finalCost,
946
- newIndexes,
947
- existingIndexes: existingIndexesUsedByQuery,
948
- triedIndexes,
1047
+ newIndexes: finalIndexes.newIndexes,
1048
+ existingIndexes: baseIndexes.existingIndexes,
1049
+ triedIndexes: new Map(toCreate.map((index) => [index.name, index])),
949
1050
  baseExplainPlan: baseExplain,
950
1051
  explainPlan: finalExplain
951
1052
  };
@@ -969,6 +1070,37 @@ var _IndexOptimizer = class _IndexOptimizer {
969
1070
  (index) => index.index_type === "btree" && index.table_name === table && index.index_columns.length === columns.length && index.index_columns.every((c, i) => columns[i].column === c.name)
970
1071
  );
971
1072
  }
1073
+ /**
1074
+ * Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]
1075
+ **/
1076
+ indexesToCreate(rootCandidates) {
1077
+ const permutedIndexes = this.tableColumnIndexCandidates(rootCandidates);
1078
+ const nextStage = [];
1079
+ for (const { table, schema, columns } of permutedIndexes.values()) {
1080
+ const permutations = permuteWithFeedback(columns);
1081
+ let iter = permutations.next(PROCEED);
1082
+ while (!iter.done) {
1083
+ const columns2 = iter.value;
1084
+ const existingIndex = this.indexAlreadyExists(table, columns2);
1085
+ if (existingIndex) {
1086
+ iter = permutations.next(PROCEED);
1087
+ continue;
1088
+ }
1089
+ const indexName = this.indexName();
1090
+ const shortenedSchema = schema === "public" ? "" : `"${schema}".`;
1091
+ const indexDefinitionClean = `${shortenedSchema}"${table}"(${columns2.map((c) => `"${c.column}"`).join(", ")})`;
1092
+ iter = permutations.next(PROCEED);
1093
+ nextStage.push({
1094
+ name: indexName,
1095
+ schema,
1096
+ table,
1097
+ columns: columns2,
1098
+ definition: indexDefinitionClean
1099
+ });
1100
+ }
1101
+ }
1102
+ return nextStage;
1103
+ }
972
1104
  toDefinition(permuted) {
973
1105
  const make = (col, order, where, keyword) => {
974
1106
  const baseColumn = `"${permuted.schema}"."${permuted.table}"(${permuted.columns.map((c) => {
@@ -991,16 +1123,15 @@ var _IndexOptimizer = class _IndexOptimizer {
991
1123
  return { raw, colored };
992
1124
  }
993
1125
  /**
994
- * Drop indexes that can be dropped (non-primary keys)
1126
+ * Drop indexes that can be dropped. Ignore the ones that can't
995
1127
  */
996
1128
  async dropExistingIndexes(tx) {
997
1129
  for (const index of this.existingIndexes) {
998
- if (index.is_primary) {
1130
+ if (!isIndexProbablyDroppable(index)) {
999
1131
  continue;
1000
1132
  }
1001
- await tx.exec(
1002
- `drop index if exists ${index.schema_name}.${index.index_name} cascade`
1003
- );
1133
+ const indexName = `${index.schema_name}.${index.index_name}`;
1134
+ await dropIndex(tx, indexName);
1004
1135
  }
1005
1136
  }
1006
1137
  whereClause(c, col, keyword) {
@@ -1189,13 +1320,7 @@ var ExportedStatsStatistics = import_zod2.z.object({
1189
1320
  });
1190
1321
  var ExportedStatsColumns = import_zod2.z.object({
1191
1322
  columnName: import_zod2.z.string(),
1192
- stats: ExportedStatsStatistics.nullable(),
1193
- dataType: import_zod2.z.string(),
1194
- isNullable: import_zod2.z.boolean(),
1195
- numericScale: import_zod2.z.number().nullable(),
1196
- columnDefault: import_zod2.z.string().nullable(),
1197
- numericPrecision: import_zod2.z.number().nullable(),
1198
- characterMaximumLength: import_zod2.z.number().nullable()
1323
+ stats: ExportedStatsStatistics.nullable()
1199
1324
  });
1200
1325
  var ExportedStatsIndex = import_zod2.z.object({
1201
1326
  indexName: import_zod2.z.string(),
@@ -1663,12 +1788,6 @@ var _Statistics = class _Statistics {
1663
1788
  json_agg(
1664
1789
  json_build_object(
1665
1790
  'columnName', c.column_name,
1666
- 'dataType', c.data_type,
1667
- 'isNullable', (c.is_nullable = 'YES')::boolean,
1668
- 'characterMaximumLength', c.character_maximum_length,
1669
- 'numericPrecision', c.numeric_precision,
1670
- 'numericScale', c.numeric_scale,
1671
- 'columnDefault', c.column_default,
1672
1791
  'stats', (
1673
1792
  SELECT json_build_object(
1674
1793
  'starelid', s.starelid,
@@ -1764,6 +1883,7 @@ var _Statistics = class _Statistics {
1764
1883
  COALESCE(pt.parent_table::text, t.relname) AS table_name,
1765
1884
  i.relname AS index_name,
1766
1885
  ix.indisprimary as is_primary,
1886
+ ix.indisunique as is_unique,
1767
1887
  am.amname AS index_type,
1768
1888
  array_agg(
1769
1889
  CASE
@@ -1794,9 +1914,10 @@ var _Statistics = class _Statistics {
1794
1914
  LEFT JOIN pg_attribute a ON a.attnum = k.attnum AND a.attrelid = t.oid
1795
1915
  JOIN pg_namespace n ON t.relnamespace = n.oid
1796
1916
  WHERE
1797
- n.nspname = 'public'
1917
+ n.nspname not like 'pg_%' and
1918
+ n.nspname <> 'information_schema'
1798
1919
  GROUP BY
1799
- n.nspname, COALESCE(pt.parent_table::text, t.relname), i.relname, am.amname, ix.indisprimary
1920
+ n.nspname, COALESCE(pt.parent_table::text, t.relname), i.relname, am.amname, ix.indisprimary, ix.indisunique
1800
1921
  ORDER BY
1801
1922
  COALESCE(pt.parent_table::text, t.relname), i.relname; -- @qd_introspection
1802
1923
  `);
@@ -1820,13 +1941,17 @@ var Statistics = _Statistics;
1820
1941
  ExportedStatsV1,
1821
1942
  IndexOptimizer,
1822
1943
  PROCEED,
1944
+ PgIdentifier,
1823
1945
  PostgresQueryBuilder,
1824
1946
  PostgresVersion,
1825
1947
  SKIP,
1826
1948
  Statistics,
1827
1949
  StatisticsMode,
1828
1950
  StatisticsSource,
1951
+ dropIndex,
1829
1952
  ignoredIdentifier,
1953
+ isIndexProbablyDroppable,
1954
+ isIndexSupported,
1830
1955
  parseNudges,
1831
1956
  permuteWithFeedback
1832
1957
  });