@query-doctor/core 0.0.3

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 ADDED
@@ -0,0 +1,1833 @@
1
+ 'use client'
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
10
+ var __export = (target, all) => {
11
+ for (var name in all)
12
+ __defProp(target, name, { get: all[name], enumerable: true });
13
+ };
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") {
16
+ for (let key of __getOwnPropNames(from))
17
+ if (!__hasOwnProp.call(to, key) && key !== except)
18
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
23
+ // If the importer is in node compatibility mode or this is not an ESM
24
+ // file that has been converted to a CommonJS file using a Babel-
25
+ // compatible transform (i.e. "__esModule" has not been set), then set
26
+ // "default" to the CommonJS "module.exports" for node compatibility.
27
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
28
+ mod
29
+ ));
30
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
31
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
32
+
33
+ // src/index.ts
34
+ var index_exports = {};
35
+ __export(index_exports, {
36
+ Analyzer: () => Analyzer,
37
+ ExportedStats: () => ExportedStats,
38
+ ExportedStatsColumns: () => ExportedStatsColumns,
39
+ ExportedStatsIndex: () => ExportedStatsIndex,
40
+ ExportedStatsStatistics: () => ExportedStatsStatistics,
41
+ ExportedStatsV1: () => ExportedStatsV1,
42
+ IndexOptimizer: () => IndexOptimizer,
43
+ PROCEED: () => PROCEED,
44
+ PostgresQueryBuilder: () => PostgresQueryBuilder,
45
+ PostgresVersion: () => PostgresVersion,
46
+ SKIP: () => SKIP,
47
+ Statistics: () => Statistics,
48
+ StatisticsMode: () => StatisticsMode,
49
+ StatisticsSource: () => StatisticsSource,
50
+ ignoredIdentifier: () => ignoredIdentifier,
51
+ parseNudges: () => parseNudges,
52
+ permuteWithFeedback: () => permuteWithFeedback
53
+ });
54
+ module.exports = __toCommonJS(index_exports);
55
+
56
+ // src/sql/analyzer.ts
57
+ var import_colorette = require("colorette");
58
+
59
+ // src/sql/walker.ts
60
+ var import_pgsql_deparser = require("pgsql-deparser");
61
+
62
+ // src/sql/nudges.ts
63
+ function is(node, kind) {
64
+ return kind in node;
65
+ }
66
+ function isANode(node) {
67
+ if (typeof node !== "object" || node === null) {
68
+ return false;
69
+ }
70
+ const keys = Object.keys(node);
71
+ return keys.length === 1 && /^[A-Z]/.test(keys[0]);
72
+ }
73
+ function parseNudges(node, stack) {
74
+ const nudges = [];
75
+ if (is(node, "A_Star")) {
76
+ nudges.push({
77
+ kind: "AVOID_SELECT_STAR",
78
+ severity: "INFO",
79
+ message: "Avoid using SELECT *"
80
+ });
81
+ }
82
+ if (is(node, "FuncCall")) {
83
+ const inWhereClause = stack.some((item) => item === "whereClause");
84
+ if (inWhereClause && node.FuncCall.args) {
85
+ const hasColumnRef = containsColumnRef(node.FuncCall.args);
86
+ if (hasColumnRef) {
87
+ nudges.push({
88
+ kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
89
+ severity: "WARNING",
90
+ message: "Avoid using functions on columns in WHERE clause"
91
+ });
92
+ }
93
+ }
94
+ }
95
+ if (is(node, "SelectStmt")) {
96
+ const isSubquery = stack.some(
97
+ (item) => item === "RangeSubselect" || item === "SubLink" || item === "CommonTableExpr"
98
+ );
99
+ if (!isSubquery) {
100
+ const hasFromClause = node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 0;
101
+ if (hasFromClause) {
102
+ const hasActualTables = node.SelectStmt.fromClause.some((fromItem) => {
103
+ return is(fromItem, "RangeVar") || is(fromItem, "JoinExpr") && hasActualTablesInJoin(fromItem);
104
+ });
105
+ if (hasActualTables) {
106
+ if (!node.SelectStmt.whereClause) {
107
+ nudges.push({
108
+ kind: "MISSING_WHERE_CLAUSE",
109
+ severity: "INFO",
110
+ message: "Missing WHERE clause"
111
+ });
112
+ }
113
+ if (!node.SelectStmt.limitCount) {
114
+ nudges.push({
115
+ kind: "MISSING_LIMIT_CLAUSE",
116
+ severity: "INFO",
117
+ message: "Missing LIMIT clause"
118
+ });
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+ if (is(node, "A_Expr")) {
125
+ const isEqualityOp = node.A_Expr.kind === "AEXPR_OP" && node.A_Expr.name && node.A_Expr.name.length > 0 && is(node.A_Expr.name[0], "String") && (node.A_Expr.name[0].String.sval === "=" || node.A_Expr.name[0].String.sval === "!=" || node.A_Expr.name[0].String.sval === "<>");
126
+ if (isEqualityOp) {
127
+ const leftIsNull = isNullConstant(node.A_Expr.lexpr);
128
+ const rightIsNull = isNullConstant(node.A_Expr.rexpr);
129
+ if (leftIsNull || rightIsNull) {
130
+ nudges.push({
131
+ kind: "USE_IS_NULL_NOT_EQUALS",
132
+ severity: "WARNING",
133
+ message: "Use IS NULL instead of = or != or <> for NULL comparisons"
134
+ });
135
+ }
136
+ }
137
+ const isLikeOp = node.A_Expr.kind === "AEXPR_LIKE" || node.A_Expr.kind === "AEXPR_ILIKE";
138
+ if (isLikeOp && node.A_Expr.rexpr) {
139
+ const patternString = getStringConstantValue(node.A_Expr.rexpr);
140
+ if (patternString && patternString.startsWith("%")) {
141
+ nudges.push({
142
+ kind: "AVOID_LEADING_WILDCARD_LIKE",
143
+ severity: "WARNING",
144
+ message: "Avoid using LIKE with leading wildcards"
145
+ });
146
+ }
147
+ }
148
+ }
149
+ if (is(node, "SelectStmt") && node.SelectStmt.distinctClause) {
150
+ nudges.push({
151
+ kind: "AVOID_DISTINCT_WITHOUT_REASON",
152
+ severity: "WARNING",
153
+ message: "Avoid using DISTINCT without a reason"
154
+ });
155
+ }
156
+ if (is(node, "JoinExpr")) {
157
+ if (!node.JoinExpr.quals) {
158
+ nudges.push({
159
+ kind: "MISSING_JOIN_CONDITION",
160
+ severity: "WARNING",
161
+ message: "Missing JOIN condition"
162
+ });
163
+ }
164
+ }
165
+ if (is(node, "SelectStmt") && node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 1) {
166
+ const tableCount = node.SelectStmt.fromClause.filter(
167
+ (item) => is(item, "RangeVar")
168
+ ).length;
169
+ if (tableCount > 1) {
170
+ nudges.push({
171
+ kind: "MISSING_JOIN_CONDITION",
172
+ severity: "WARNING",
173
+ message: "Missing JOIN condition"
174
+ });
175
+ }
176
+ }
177
+ if (is(node, "BoolExpr") && node.BoolExpr.boolop === "OR_EXPR") {
178
+ const orCount = countBoolOrConditions(node);
179
+ if (orCount >= 3) {
180
+ nudges.push({
181
+ kind: "CONSIDER_IN_INSTEAD_OF_MANY_ORS",
182
+ severity: "WARNING",
183
+ message: "Consider using IN instead of many ORs"
184
+ });
185
+ }
186
+ }
187
+ return nudges;
188
+ }
189
+ function containsColumnRef(args) {
190
+ for (const arg of args) {
191
+ if (hasColumnRefInNode(arg)) {
192
+ return true;
193
+ }
194
+ }
195
+ return false;
196
+ }
197
+ function hasColumnRefInNode(node) {
198
+ if (isANode(node) && is(node, "ColumnRef")) {
199
+ return true;
200
+ }
201
+ if (typeof node !== "object" || node === null) {
202
+ return false;
203
+ }
204
+ if (Array.isArray(node)) {
205
+ return node.some((item) => hasColumnRefInNode(item));
206
+ }
207
+ if (isANode(node)) {
208
+ const keys = Object.keys(node);
209
+ return hasColumnRefInNode(node[keys[0]]);
210
+ }
211
+ for (const child of Object.values(node)) {
212
+ if (hasColumnRefInNode(child)) {
213
+ return true;
214
+ }
215
+ }
216
+ return false;
217
+ }
218
+ function hasActualTablesInJoin(joinExpr) {
219
+ if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, "RangeVar")) {
220
+ return true;
221
+ }
222
+ if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, "JoinExpr")) {
223
+ if (hasActualTablesInJoin(joinExpr.JoinExpr.larg)) {
224
+ return true;
225
+ }
226
+ }
227
+ if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, "RangeVar")) {
228
+ return true;
229
+ }
230
+ if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, "JoinExpr")) {
231
+ if (hasActualTablesInJoin(joinExpr.JoinExpr.rarg)) {
232
+ return true;
233
+ }
234
+ }
235
+ return false;
236
+ }
237
+ function isNullConstant(node) {
238
+ if (!node || typeof node !== "object") {
239
+ return false;
240
+ }
241
+ if (isANode(node) && is(node, "A_Const")) {
242
+ return node.A_Const.isnull !== void 0;
243
+ }
244
+ return false;
245
+ }
246
+ function getStringConstantValue(node) {
247
+ if (!node || typeof node !== "object") {
248
+ return null;
249
+ }
250
+ if (isANode(node) && is(node, "A_Const") && node.A_Const.sval) {
251
+ return node.A_Const.sval.sval || null;
252
+ }
253
+ return null;
254
+ }
255
+ function countBoolOrConditions(node) {
256
+ if (node.BoolExpr.boolop !== "OR_EXPR" || !node.BoolExpr.args) {
257
+ return 1;
258
+ }
259
+ let count = 0;
260
+ for (const arg of node.BoolExpr.args) {
261
+ if (isANode(arg) && is(arg, "BoolExpr") && arg.BoolExpr.boolop === "OR_EXPR") {
262
+ count += countBoolOrConditions(arg);
263
+ } else {
264
+ count += 1;
265
+ }
266
+ }
267
+ return count;
268
+ }
269
+
270
+ // src/sql/walker.ts
271
+ var Walker = class _Walker {
272
+ constructor(query) {
273
+ this.query = query;
274
+ __publicField(this, "tableMappings", /* @__PURE__ */ new Map());
275
+ __publicField(this, "tempTables", /* @__PURE__ */ new Set());
276
+ __publicField(this, "highlights", []);
277
+ __publicField(this, "indexRepresentations", /* @__PURE__ */ new Set());
278
+ __publicField(this, "indexesToCheck", []);
279
+ __publicField(this, "highlightPositions", /* @__PURE__ */ new Set());
280
+ // used for tallying the amount of times we see stuff so
281
+ // we have a better idea of what to start off the algorithm with
282
+ __publicField(this, "seenReferences", /* @__PURE__ */ new Map());
283
+ __publicField(this, "shadowedAliases", []);
284
+ __publicField(this, "nudges", []);
285
+ }
286
+ walk(root) {
287
+ this.tableMappings = /* @__PURE__ */ new Map();
288
+ this.tempTables = /* @__PURE__ */ new Set();
289
+ this.highlights = [];
290
+ this.indexRepresentations = /* @__PURE__ */ new Set();
291
+ this.indexesToCheck = [];
292
+ this.highlightPositions = /* @__PURE__ */ new Set();
293
+ this.seenReferences = /* @__PURE__ */ new Map();
294
+ this.shadowedAliases = [];
295
+ this.nudges = [];
296
+ _Walker.traverse(root, [], (node, stack) => {
297
+ const nodeNudges = parseNudges(node, stack);
298
+ this.nudges = [...this.nudges, ...nodeNudges];
299
+ if (is2(node, "CommonTableExpr")) {
300
+ if (node.CommonTableExpr.ctename) {
301
+ this.tempTables.add(node.CommonTableExpr.ctename);
302
+ }
303
+ }
304
+ if (is2(node, "RangeSubselect")) {
305
+ if (node.RangeSubselect.alias?.aliasname) {
306
+ this.tempTables.add(node.RangeSubselect.alias.aliasname);
307
+ }
308
+ }
309
+ if (is2(node, "NullTest")) {
310
+ if (node.NullTest.arg && node.NullTest.nulltesttype && is2(node.NullTest.arg, "ColumnRef")) {
311
+ this.add(node.NullTest.arg, {
312
+ where: { nulltest: node.NullTest.nulltesttype }
313
+ });
314
+ }
315
+ }
316
+ if (is2(node, "RangeVar") && node.RangeVar.relname) {
317
+ this.tableMappings.set(node.RangeVar.relname, {
318
+ text: node.RangeVar.relname,
319
+ start: node.RangeVar.location,
320
+ quoted: false
321
+ });
322
+ if (node.RangeVar.alias?.aliasname) {
323
+ const aliasName = node.RangeVar.alias.aliasname;
324
+ const existingMapping = this.tableMappings.get(aliasName);
325
+ const part = {
326
+ text: node.RangeVar.relname,
327
+ start: node.RangeVar.location,
328
+ // what goes here? the text here doesn't _really_ exist.
329
+ // so it can't be quoted or not quoted.
330
+ // Does it even matter?
331
+ quoted: true,
332
+ alias: aliasName
333
+ };
334
+ if (existingMapping) {
335
+ console.warn(
336
+ `Ignoring alias ${aliasName} as it shadows an existing mapping. We currently do not support alias shadowing.`
337
+ );
338
+ this.shadowedAliases.push(part);
339
+ return;
340
+ }
341
+ this.tableMappings.set(aliasName, part);
342
+ }
343
+ }
344
+ if (is2(node, "SortBy")) {
345
+ if (node.SortBy.node && is2(node.SortBy.node, "ColumnRef")) {
346
+ this.add(node.SortBy.node, {
347
+ sort: {
348
+ dir: node.SortBy.sortby_dir ?? "SORTBY_DEFAULT",
349
+ nulls: node.SortBy.sortby_nulls ?? "SORTBY_NULLS_DEFAULT"
350
+ }
351
+ });
352
+ }
353
+ }
354
+ if (is2(node, "JoinExpr") && node.JoinExpr.quals) {
355
+ if (is2(node.JoinExpr.quals, "A_Expr")) {
356
+ if (node.JoinExpr.quals.A_Expr.lexpr && is2(node.JoinExpr.quals.A_Expr.lexpr, "ColumnRef")) {
357
+ this.add(node.JoinExpr.quals.A_Expr.lexpr);
358
+ }
359
+ if (node.JoinExpr.quals.A_Expr.rexpr && is2(node.JoinExpr.quals.A_Expr.rexpr, "ColumnRef")) {
360
+ this.add(node.JoinExpr.quals.A_Expr.rexpr);
361
+ }
362
+ }
363
+ }
364
+ if (is2(node, "ColumnRef")) {
365
+ for (let i = 0; i < stack.length; i++) {
366
+ const inReturningList = stack[i] === "returningList" && stack[i + 1] === "ResTarget" && stack[i + 2] === "val" && stack[i + 3] === "ColumnRef";
367
+ if (inReturningList) {
368
+ this.add(node, { ignored: true });
369
+ return;
370
+ }
371
+ if (
372
+ // stack[i] === "SelectStmt" &&
373
+ stack[i + 1] === "targetList" && stack[i + 2] === "ResTarget" && stack[i + 3] === "val" && stack[i + 4] === "ColumnRef"
374
+ ) {
375
+ this.add(node, { ignored: true });
376
+ return;
377
+ }
378
+ if (stack[i] === "FuncCall" && stack[i + 1] === "args") {
379
+ this.add(node, { ignored: true });
380
+ return;
381
+ }
382
+ }
383
+ this.add(node);
384
+ }
385
+ });
386
+ return {
387
+ highlights: this.highlights,
388
+ indexRepresentations: this.indexRepresentations,
389
+ indexesToCheck: this.indexesToCheck,
390
+ shadowedAliases: this.shadowedAliases,
391
+ tempTables: this.tempTables,
392
+ tableMappings: this.tableMappings,
393
+ nudges: this.nudges
394
+ };
395
+ }
396
+ add(node, options) {
397
+ if (!node.ColumnRef.location) {
398
+ console.error(`Node did not have a location. Skipping`, node);
399
+ return;
400
+ }
401
+ if (!node.ColumnRef.fields) {
402
+ console.error(node);
403
+ throw new Error("Column reference must have fields");
404
+ }
405
+ let ignored = options?.ignored ?? false;
406
+ let runningLength = node.ColumnRef.location;
407
+ const parts = node.ColumnRef.fields.map(
408
+ (field, i, length) => {
409
+ if (!is2(field, "String") || !field.String.sval) {
410
+ const out = (0, import_pgsql_deparser.deparseSync)(field);
411
+ ignored = true;
412
+ return {
413
+ quoted: out.startsWith('"'),
414
+ text: out,
415
+ start: runningLength
416
+ };
417
+ }
418
+ const start = runningLength;
419
+ const size = field.String.sval?.length ?? 0;
420
+ let quoted = false;
421
+ if (node.ColumnRef.location !== void 0) {
422
+ const boundary = this.query[runningLength];
423
+ if (boundary === '"') {
424
+ quoted = true;
425
+ }
426
+ }
427
+ const isLastIteration = i === length.length - 1;
428
+ runningLength += size + (isLastIteration ? 0 : 1) + (quoted ? 2 : 0);
429
+ return {
430
+ text: field.String.sval,
431
+ start,
432
+ quoted
433
+ };
434
+ }
435
+ );
436
+ const end = runningLength;
437
+ if (this.highlightPositions.has(node.ColumnRef.location)) {
438
+ return;
439
+ }
440
+ this.highlightPositions.add(node.ColumnRef.location);
441
+ const highlighted = `${this.query.slice(node.ColumnRef.location, end)}`;
442
+ const seen = this.seenReferences.get(highlighted);
443
+ if (!ignored) {
444
+ this.seenReferences.set(highlighted, (seen ?? 0) + 1);
445
+ }
446
+ const ref = {
447
+ frequency: seen ?? 1,
448
+ representation: highlighted,
449
+ parts,
450
+ ignored: ignored ?? false,
451
+ position: {
452
+ start: node.ColumnRef.location,
453
+ end
454
+ }
455
+ };
456
+ if (options?.sort) {
457
+ ref.sort = options.sort;
458
+ }
459
+ if (options?.where) {
460
+ ref.where = options.where;
461
+ }
462
+ this.highlights.push(ref);
463
+ }
464
+ static traverse(node, stack, callback) {
465
+ if (isANode2(node)) {
466
+ callback(node, [...stack, getNodeKind(node)]);
467
+ }
468
+ if (typeof node !== "object" || node === null) {
469
+ return;
470
+ }
471
+ if (Array.isArray(node)) {
472
+ for (const item of node) {
473
+ if (isANode2(item)) {
474
+ _Walker.traverse(item, stack, callback);
475
+ }
476
+ }
477
+ } else if (isANode2(node)) {
478
+ const keys = Object.keys(node);
479
+ _Walker.traverse(node[keys[0]], [...stack, getNodeKind(node)], callback);
480
+ } else {
481
+ for (const [key, child] of Object.entries(node)) {
482
+ _Walker.traverse(child, [...stack, key], callback);
483
+ }
484
+ }
485
+ }
486
+ };
487
+ function is2(node, kind) {
488
+ return kind in node;
489
+ }
490
+ function getNodeKind(node) {
491
+ const keys = Object.keys(node);
492
+ return keys[0];
493
+ }
494
+ function isANode2(node) {
495
+ if (typeof node !== "object" || node === null) {
496
+ return false;
497
+ }
498
+ const keys = Object.keys(node);
499
+ return keys.length === 1 && /^[A-Z]/.test(keys[0]);
500
+ }
501
+
502
+ // src/sql/analyzer.ts
503
+ var ignoredIdentifier = "__qd_placeholder";
504
+ var Analyzer = class {
505
+ constructor(parser) {
506
+ this.parser = parser;
507
+ }
508
+ async analyze(query, formattedQuery) {
509
+ const ast = await this.parser(query);
510
+ if (!ast.stmts) {
511
+ throw new Error(
512
+ "Query did not have any statements. This should probably never happen?"
513
+ );
514
+ }
515
+ const stmt = ast.stmts[0].stmt;
516
+ if (!stmt) {
517
+ throw new Error(
518
+ "Query did not have any statements. This should probably never happen?"
519
+ );
520
+ }
521
+ const walker = new Walker(query);
522
+ const {
523
+ highlights,
524
+ indexRepresentations,
525
+ indexesToCheck,
526
+ shadowedAliases,
527
+ tempTables,
528
+ tableMappings,
529
+ nudges
530
+ } = walker.walk(stmt);
531
+ const sortedHighlights = highlights.sort(
532
+ (a, b) => b.position.end - a.position.end
533
+ );
534
+ let currQuery = query;
535
+ for (const highlight of sortedHighlights) {
536
+ const parts = this.resolveTableAliases(highlight.parts, tableMappings);
537
+ if (parts.length === 0) {
538
+ console.error(highlight);
539
+ throw new Error("Highlight must have at least one part");
540
+ }
541
+ let color;
542
+ let skip = false;
543
+ if (highlight.ignored) {
544
+ color = (x) => (0, import_colorette.dim)((0, import_colorette.strikethrough)(x));
545
+ skip = true;
546
+ } else if (parts.length === 2 && tempTables.has(parts[0].text) && // sometimes temp tables are aliased as existing tables
547
+ // we don't want to ignore them if they are
548
+ !tableMappings.has(parts[0].text)) {
549
+ color = import_colorette.blue;
550
+ skip = true;
551
+ } else {
552
+ color = import_colorette.bgMagentaBright;
553
+ }
554
+ const queryRepr = highlight.representation;
555
+ const queryBeforeMatch = currQuery.slice(0, highlight.position.start);
556
+ const queryAfterToken = currQuery.slice(highlight.position.end);
557
+ currQuery = `${queryBeforeMatch}${color(queryRepr)}${this.colorizeKeywords(
558
+ queryAfterToken,
559
+ color
560
+ )}`;
561
+ if (indexRepresentations.has(queryRepr)) {
562
+ skip = true;
563
+ }
564
+ if (!skip) {
565
+ indexesToCheck.push(highlight);
566
+ indexRepresentations.add(queryRepr);
567
+ }
568
+ }
569
+ const referencedTables = [];
570
+ for (const value of tableMappings.values()) {
571
+ if (!value.alias) {
572
+ referencedTables.push(value.text);
573
+ }
574
+ }
575
+ const { tags, queryWithoutTags } = this.extractSqlcommenter(query);
576
+ const formattedQueryWithoutTags = formattedQuery ? this.extractSqlcommenter(formattedQuery).queryWithoutTags : void 0;
577
+ return {
578
+ indexesToCheck,
579
+ ansiHighlightedQuery: currQuery,
580
+ referencedTables,
581
+ shadowedAliases,
582
+ tags,
583
+ queryWithoutTags,
584
+ formattedQueryWithoutTags,
585
+ nudges
586
+ };
587
+ }
588
+ deriveIndexes(tables, discovered) {
589
+ const allIndexes = [];
590
+ const seenIndexes = /* @__PURE__ */ new Set();
591
+ function addIndex(index) {
592
+ const key = `"${index.schema}":"${index.table}":"${index.column}"`;
593
+ if (seenIndexes.has(key)) {
594
+ return;
595
+ }
596
+ seenIndexes.add(key);
597
+ allIndexes.push(index);
598
+ }
599
+ for (const colReference of discovered) {
600
+ const partsCount = colReference.parts.length;
601
+ const columnOnlyReference = partsCount === 1;
602
+ const tableReference = partsCount === 2;
603
+ const fullReference = partsCount === 3;
604
+ if (columnOnlyReference) {
605
+ const [column] = colReference.parts;
606
+ const referencedColumn = this.normalize(column);
607
+ const matchingTables = tables.filter((table) => {
608
+ return table.columns?.some((column2) => {
609
+ return column2.columnName === referencedColumn;
610
+ }) ?? false;
611
+ });
612
+ for (const table of matchingTables) {
613
+ const index = {
614
+ schema: table.schemaName,
615
+ table: table.tableName,
616
+ column: referencedColumn
617
+ };
618
+ if (colReference.sort) {
619
+ index.sort = colReference.sort;
620
+ }
621
+ if (colReference.where) {
622
+ index.where = colReference.where;
623
+ }
624
+ addIndex(index);
625
+ }
626
+ } else if (tableReference) {
627
+ const [table, column] = colReference.parts;
628
+ const referencedTable = this.normalize(table);
629
+ const referencedColumn = this.normalize(column);
630
+ const matchingTable = tables.find((table2) => {
631
+ const hasMatchingColumn = table2.columns?.some((column2) => {
632
+ return column2.columnName === referencedColumn;
633
+ }) ?? false;
634
+ return table2.tableName === referencedTable && hasMatchingColumn;
635
+ });
636
+ if (matchingTable) {
637
+ const index = {
638
+ schema: matchingTable.schemaName,
639
+ table: referencedTable,
640
+ column: referencedColumn
641
+ };
642
+ if (colReference.sort) {
643
+ index.sort = colReference.sort;
644
+ }
645
+ if (colReference.where) {
646
+ index.where = colReference.where;
647
+ }
648
+ addIndex(index);
649
+ }
650
+ } else if (fullReference) {
651
+ const [schema, table, column] = colReference.parts;
652
+ const referencedSchema = this.normalize(schema);
653
+ const referencedTable = this.normalize(table);
654
+ const referencedColumn = this.normalize(column);
655
+ const index = {
656
+ schema: referencedSchema,
657
+ table: referencedTable,
658
+ column: referencedColumn
659
+ };
660
+ if (colReference.sort) {
661
+ index.sort = colReference.sort;
662
+ }
663
+ if (colReference.where) {
664
+ index.where = colReference.where;
665
+ }
666
+ addIndex(index);
667
+ } else {
668
+ console.error(
669
+ "Column reference has too many parts. The query is malformed",
670
+ colReference
671
+ );
672
+ continue;
673
+ }
674
+ }
675
+ return allIndexes;
676
+ }
677
+ colorizeKeywords(query, color) {
678
+ return query.replace(
679
+ // eh? This kinda sucks
680
+ /(^\s+)(asc|desc)?(\s+(nulls first|nulls last))?/i,
681
+ (_, pre, dir, spaceNulls, nulls) => {
682
+ return `${pre}${dir ? color(dir) : ""}${nulls ? spaceNulls.replace(nulls, color(nulls)) : ""}`;
683
+ }
684
+ ).replace(/(^\s+)(is (null|not null))/i, (_, pre, nulltest) => {
685
+ return `${pre}${color(nulltest)}`;
686
+ });
687
+ }
688
+ /**
689
+ * Resolves aliases such as `a.b` to `x.b` if `a` is a known
690
+ * alias to a table called x.
691
+ *
692
+ * Ignores all other combination of parts such as `a.b.c`
693
+ */
694
+ resolveTableAliases(parts, tableMappings) {
695
+ if (parts.length !== 2) {
696
+ return parts;
697
+ }
698
+ const tablePart = parts[0];
699
+ const mapping = tableMappings.get(tablePart.text);
700
+ if (mapping) {
701
+ parts[0] = mapping;
702
+ }
703
+ return parts;
704
+ }
705
+ normalize(columnReference) {
706
+ return columnReference.quoted ? columnReference.text : (
707
+ // postgres automatically lowercases column names if not quoted
708
+ columnReference.text.toLowerCase()
709
+ );
710
+ }
711
+ extractSqlcommenter(query) {
712
+ const trimmedQuery = query.trimEnd();
713
+ const startPosition = trimmedQuery.lastIndexOf("/*");
714
+ const endPosition = trimmedQuery.lastIndexOf("*/");
715
+ if (startPosition === -1 || endPosition === -1) {
716
+ return { tags: [], queryWithoutTags: trimmedQuery };
717
+ }
718
+ const queryWithoutTags = trimmedQuery.slice(0, startPosition);
719
+ const tagString = trimmedQuery.slice(startPosition + 2, endPosition);
720
+ if (!tagString || typeof tagString !== "string") {
721
+ return { tags: [], queryWithoutTags };
722
+ }
723
+ const tags = [];
724
+ for (const match of tagString.split(",")) {
725
+ const [key, value] = match.split("=");
726
+ if (!key || !value) {
727
+ console.warn(`Invalid sqlcommenter tag: ${match}. Ignoring`);
728
+ continue;
729
+ }
730
+ try {
731
+ let sliceStart = 0;
732
+ if (value.startsWith("'")) {
733
+ sliceStart = 1;
734
+ }
735
+ let sliceEnd = value.length;
736
+ if (value.endsWith("'")) {
737
+ sliceEnd -= 1;
738
+ }
739
+ const decoded = decodeURIComponent(value.slice(sliceStart, sliceEnd));
740
+ tags.push({ key: key.trim(), value: decoded });
741
+ } catch (err) {
742
+ console.error(err);
743
+ }
744
+ }
745
+ return { tags, queryWithoutTags };
746
+ }
747
+ };
748
+
749
+ // src/sql/database.ts
750
+ var import_zod = require("zod");
751
+ var PostgresVersion = import_zod.z.string().brand("PostgresVersion");
752
+
753
+ // src/optimizer/genalgo.ts
754
+ var import_colorette2 = require("colorette");
755
+
756
+ // src/sql/builder.ts
757
+ var PostgresQueryBuilder = class _PostgresQueryBuilder {
758
+ constructor(query) {
759
+ this.query = query;
760
+ __publicField(this, "commands", {});
761
+ __publicField(this, "isIntrospection", false);
762
+ __publicField(this, "explainFlags", []);
763
+ __publicField(this, "_preamble", 0);
764
+ }
765
+ get preamble() {
766
+ return this._preamble;
767
+ }
768
+ static createIndex(definition, name) {
769
+ if (name) {
770
+ return new _PostgresQueryBuilder(
771
+ `create index "${name}" on ${definition};`
772
+ );
773
+ }
774
+ return new _PostgresQueryBuilder(`create index on ${definition};`);
775
+ }
776
+ enable(command, value = true) {
777
+ const commandString = `enable_${command}`;
778
+ if (value) {
779
+ this.commands[commandString] = "on";
780
+ } else {
781
+ this.commands[commandString] = "off";
782
+ }
783
+ return this;
784
+ }
785
+ withQuery(query) {
786
+ this.query = query;
787
+ return this;
788
+ }
789
+ introspect() {
790
+ this.isIntrospection = true;
791
+ return this;
792
+ }
793
+ explain(flags) {
794
+ this.explainFlags = flags;
795
+ return this;
796
+ }
797
+ build() {
798
+ let commands = this.generateSetCommands();
799
+ commands += this.generateExplain().query;
800
+ if (this.isIntrospection) {
801
+ commands += " -- @qd_introspection";
802
+ }
803
+ return commands;
804
+ }
805
+ /** Return the "set a=b" parts of the command in the query separate from the explain select ... part */
806
+ buildParts() {
807
+ const commands = this.generateSetCommands();
808
+ const explain = this.generateExplain();
809
+ this._preamble = explain.preamble;
810
+ if (this.isIntrospection) {
811
+ explain.query += " -- @qd_introspection";
812
+ }
813
+ return { commands, query: explain.query };
814
+ }
815
+ generateSetCommands() {
816
+ let commands = "";
817
+ for (const key in this.commands) {
818
+ const value = this.commands[key];
819
+ commands += `set local ${key}=${value};
820
+ `;
821
+ }
822
+ return commands;
823
+ }
824
+ generateExplain() {
825
+ let query = "";
826
+ if (this.explainFlags.length > 0) {
827
+ query += `explain (${this.explainFlags.join(", ")}) `;
828
+ }
829
+ const semicolon = this.query.endsWith(";") ? "" : ";";
830
+ const preamble = query.length;
831
+ query += `${this.query}${semicolon}`;
832
+ return { query, preamble };
833
+ }
834
+ };
835
+
836
+ // src/optimizer/genalgo.ts
837
+ var _IndexOptimizer = class _IndexOptimizer {
838
+ constructor(db, statistics, existingIndexes, config = {}) {
839
+ this.db = db;
840
+ this.statistics = statistics;
841
+ this.existingIndexes = existingIndexes;
842
+ this.config = config;
843
+ }
844
+ async run(builder, indexes) {
845
+ const baseExplain = await this.runWithoutIndexes(builder);
846
+ const baseCost = Number(baseExplain.Plan["Total Cost"]);
847
+ if (baseCost === 0) {
848
+ return {
849
+ kind: "zero_cost_plan",
850
+ explainPlan: baseExplain
851
+ };
852
+ }
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
+ });
915
+ }
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
+ );
923
+ }
924
+ });
925
+ const finalCost = Number(finalExplain.Plan["Total Cost"]);
926
+ if (this.config.debug) {
927
+ console.dir(finalExplain, { depth: null });
928
+ }
929
+ const deltaPercentage = (baseCost - finalCost) / baseCost * 100;
930
+ if (finalCost < baseCost) {
931
+ console.log(
932
+ ` \u{1F389}\u{1F389}\u{1F389} ${(0, import_colorette2.green)(`+${deltaPercentage.toFixed(2).padStart(5, "0")}%`)}`
933
+ );
934
+ } else if (finalCost > baseCost) {
935
+ console.log(
936
+ `${(0, import_colorette2.red)(
937
+ `-${Math.abs(deltaPercentage).toFixed(2).padStart(5, "0")}%`
938
+ )} ${(0, import_colorette2.gray)("If there's a better index, we haven't tried it")}`
939
+ );
940
+ }
941
+ const { newIndexes, existingIndexes: existingIndexesUsedByQuery } = this.findUsedIndexes(finalExplain.Plan);
942
+ return {
943
+ kind: "ok",
944
+ baseCost,
945
+ finalCost,
946
+ newIndexes,
947
+ existingIndexes: existingIndexesUsedByQuery,
948
+ triedIndexes,
949
+ baseExplainPlan: baseExplain,
950
+ explainPlan: finalExplain
951
+ };
952
+ }
953
+ async runWithoutIndexes(builder) {
954
+ return await this.testQueryWithStats(builder, async (tx) => {
955
+ await this.dropExistingIndexes(tx);
956
+ });
957
+ }
958
+ /**
959
+ * Postgres has a limit of 63 characters for index names.
960
+ * So we use this to make sure we don't derive it from a list of columns that can
961
+ * overflow that limit.
962
+ */
963
+ indexName() {
964
+ return _IndexOptimizer.prefix + Math.random().toString(36).substring(2, 16);
965
+ }
966
+ // TODO: this doesn't belong in the optimizer
967
+ indexAlreadyExists(table, columns) {
968
+ return this.existingIndexes.find(
969
+ (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
+ );
971
+ }
972
+ toDefinition(permuted) {
973
+ const make = (col, order, where, keyword) => {
974
+ const baseColumn = `"${permuted.schema}"."${permuted.table}"(${permuted.columns.map((c) => {
975
+ const direction = c.sort && this.sortDirection(c.sort);
976
+ const nulls = c.sort && this.nullsOrder(c.sort);
977
+ let sort = col(`"${c.column}"`);
978
+ if (direction) {
979
+ sort += ` ${order(direction)}`;
980
+ }
981
+ if (nulls) {
982
+ sort += ` ${order(nulls)}`;
983
+ }
984
+ return sort;
985
+ }).join(", ")})`;
986
+ return baseColumn;
987
+ };
988
+ const id = (a) => a;
989
+ const raw = make(id, id, id, id);
990
+ const colored = make(import_colorette2.green, import_colorette2.yellow, import_colorette2.magenta, import_colorette2.blue);
991
+ return { raw, colored };
992
+ }
993
+ /**
994
+ * Drop indexes that can be dropped (non-primary keys)
995
+ */
996
+ async dropExistingIndexes(tx) {
997
+ for (const index of this.existingIndexes) {
998
+ if (index.is_primary) {
999
+ continue;
1000
+ }
1001
+ await tx.exec(
1002
+ `drop index if exists ${index.schema_name}.${index.index_name} cascade`
1003
+ );
1004
+ }
1005
+ }
1006
+ whereClause(c, col, keyword) {
1007
+ if (!c.where) {
1008
+ return "";
1009
+ }
1010
+ if (c.where.nulltest === "IS_NULL") {
1011
+ return `${col(`"${c.column}"`)} is ${keyword("null")}`;
1012
+ }
1013
+ if (c.where.nulltest === "IS_NOT_NULL") {
1014
+ return `${col(`"${c.column}"`)} is not ${keyword("null")}`;
1015
+ }
1016
+ return "";
1017
+ }
1018
+ nullsOrder(s) {
1019
+ if (!s.nulls) {
1020
+ return "";
1021
+ }
1022
+ switch (s.nulls) {
1023
+ case "SORTBY_NULLS_FIRST":
1024
+ return "nulls first";
1025
+ case "SORTBY_NULLS_LAST":
1026
+ return "nulls last";
1027
+ case "SORTBY_NULLS_DEFAULT":
1028
+ default:
1029
+ return "";
1030
+ }
1031
+ }
1032
+ sortDirection(s) {
1033
+ if (!s.dir) {
1034
+ return "";
1035
+ }
1036
+ switch (s.dir) {
1037
+ case "SORTBY_DESC":
1038
+ return "desc";
1039
+ case "SORTBY_ASC":
1040
+ return "asc";
1041
+ case "SORTBY_DEFAULT":
1042
+ // god help us if we ever run into this
1043
+ case "SORTBY_USING":
1044
+ default:
1045
+ return "";
1046
+ }
1047
+ }
1048
+ async testQueryWithStats(builder, f, options) {
1049
+ try {
1050
+ await this.db.transaction(async (tx) => {
1051
+ await f?.(tx);
1052
+ await this.statistics.restoreStats(tx);
1053
+ const flags = ["format json", "trace"];
1054
+ if (options && !options.genericPlan) {
1055
+ flags.push("analyze");
1056
+ } else {
1057
+ flags.push("generic_plan");
1058
+ }
1059
+ const { commands, query } = builder.explain(flags).buildParts();
1060
+ await tx.exec(commands);
1061
+ const result = await tx.exec(
1062
+ query,
1063
+ options?.params
1064
+ );
1065
+ const explain = result[0]["QUERY PLAN"][0];
1066
+ throw new RollbackError(explain);
1067
+ });
1068
+ } catch (error) {
1069
+ if (error instanceof RollbackError) {
1070
+ return error.value;
1071
+ }
1072
+ throw error;
1073
+ }
1074
+ throw new Error("Unreachable");
1075
+ }
1076
+ tableColumnIndexCandidates(indexes) {
1077
+ const tableColumns = /* @__PURE__ */ new Map();
1078
+ for (const index of indexes) {
1079
+ const existing = tableColumns.get(`${index.schema}.${index.table}`);
1080
+ if (existing) {
1081
+ existing.columns.push(index);
1082
+ } else {
1083
+ tableColumns.set(`${index.schema}.${index.table}`, {
1084
+ table: index.table,
1085
+ schema: index.schema,
1086
+ columns: [index]
1087
+ });
1088
+ }
1089
+ }
1090
+ return tableColumns;
1091
+ }
1092
+ findUsedIndexes(explain) {
1093
+ const newIndexes = /* @__PURE__ */ new Set();
1094
+ const existingIndexes = /* @__PURE__ */ new Set();
1095
+ function go(plan) {
1096
+ const indexName = plan["Index Name"];
1097
+ if (indexName) {
1098
+ if (indexName.startsWith(_IndexOptimizer.prefix)) {
1099
+ newIndexes.add(indexName);
1100
+ } else {
1101
+ existingIndexes.add(indexName);
1102
+ }
1103
+ }
1104
+ if (plan.Plans) {
1105
+ for (const p of plan.Plans) {
1106
+ go(p);
1107
+ }
1108
+ }
1109
+ }
1110
+ go(explain);
1111
+ return {
1112
+ newIndexes,
1113
+ existingIndexes
1114
+ };
1115
+ }
1116
+ };
1117
+ __publicField(_IndexOptimizer, "prefix", "__qd_");
1118
+ var IndexOptimizer = _IndexOptimizer;
1119
+ var RollbackError = class {
1120
+ constructor(value) {
1121
+ this.value = value;
1122
+ }
1123
+ };
1124
+ var PROCEED = Symbol("PROCEED");
1125
+ var SKIP = Symbol("SKIP");
1126
+ function* permuteWithFeedback(arr) {
1127
+ function* helper(path, rest) {
1128
+ let i = 0;
1129
+ while (i < rest.length) {
1130
+ const nextPath = [...path, rest[i]];
1131
+ const nextRest = [...rest.slice(0, i), ...rest.slice(i + 1)];
1132
+ const input = yield nextPath;
1133
+ if (input === PROCEED) {
1134
+ yield* helper(nextPath, nextRest);
1135
+ }
1136
+ i++;
1137
+ }
1138
+ }
1139
+ yield* helper([], arr);
1140
+ }
1141
+
1142
+ // src/optimizer/statistics.ts
1143
+ var import_colorette3 = require("colorette");
1144
+ var import_dedent = __toESM(require("dedent"), 1);
1145
+ var import_zod2 = require("zod");
1146
+ var StatisticsSource = import_zod2.z.union([
1147
+ import_zod2.z.object({
1148
+ kind: import_zod2.z.literal("path"),
1149
+ path: import_zod2.z.string().min(1)
1150
+ }),
1151
+ import_zod2.z.object({
1152
+ kind: import_zod2.z.literal("inline")
1153
+ })
1154
+ ]);
1155
+ var ExportedStatsStatistics = import_zod2.z.object({
1156
+ stawidth: import_zod2.z.number(),
1157
+ stainherit: import_zod2.z.boolean().default(false),
1158
+ // 0 representing unknown
1159
+ stadistinct: import_zod2.z.number(),
1160
+ // this has no "nullable" state
1161
+ stanullfrac: import_zod2.z.number(),
1162
+ stakind1: import_zod2.z.number().min(0),
1163
+ stakind2: import_zod2.z.number().min(0),
1164
+ stakind3: import_zod2.z.number().min(0),
1165
+ stakind4: import_zod2.z.number().min(0),
1166
+ stakind5: import_zod2.z.number().min(0),
1167
+ staop1: import_zod2.z.string(),
1168
+ staop2: import_zod2.z.string(),
1169
+ staop3: import_zod2.z.string(),
1170
+ staop4: import_zod2.z.string(),
1171
+ staop5: import_zod2.z.string(),
1172
+ stacoll1: import_zod2.z.string(),
1173
+ stacoll2: import_zod2.z.string(),
1174
+ stacoll3: import_zod2.z.string(),
1175
+ stacoll4: import_zod2.z.string(),
1176
+ stacoll5: import_zod2.z.string(),
1177
+ stanumbers1: import_zod2.z.array(import_zod2.z.number()).nullable(),
1178
+ stanumbers2: import_zod2.z.array(import_zod2.z.number()).nullable(),
1179
+ stanumbers3: import_zod2.z.array(import_zod2.z.number()).nullable(),
1180
+ stanumbers4: import_zod2.z.array(import_zod2.z.number()).nullable(),
1181
+ stanumbers5: import_zod2.z.array(import_zod2.z.number()).nullable(),
1182
+ // theoretically... this could only be strings and numbers
1183
+ // but we don't have a crystal ball
1184
+ stavalues1: import_zod2.z.array(import_zod2.z.any()).nullable(),
1185
+ stavalues2: import_zod2.z.array(import_zod2.z.any()).nullable(),
1186
+ stavalues3: import_zod2.z.array(import_zod2.z.any()).nullable(),
1187
+ stavalues4: import_zod2.z.array(import_zod2.z.any()).nullable(),
1188
+ stavalues5: import_zod2.z.array(import_zod2.z.any()).nullable()
1189
+ });
1190
+ var ExportedStatsColumns = import_zod2.z.object({
1191
+ 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()
1199
+ });
1200
+ var ExportedStatsIndex = import_zod2.z.object({
1201
+ indexName: import_zod2.z.string(),
1202
+ relpages: import_zod2.z.number(),
1203
+ reltuples: import_zod2.z.number(),
1204
+ relallvisible: import_zod2.z.number(),
1205
+ relallfrozen: import_zod2.z.number().optional()
1206
+ });
1207
+ var ExportedStatsV1 = import_zod2.z.object({
1208
+ tableName: import_zod2.z.string(),
1209
+ schemaName: import_zod2.z.string(),
1210
+ // can be negative
1211
+ relpages: import_zod2.z.number(),
1212
+ // can be negative
1213
+ reltuples: import_zod2.z.number(),
1214
+ relallvisible: import_zod2.z.number(),
1215
+ // only postgres 18+
1216
+ relallfrozen: import_zod2.z.number().optional(),
1217
+ columns: import_zod2.z.array(ExportedStatsColumns).nullable(),
1218
+ indexes: import_zod2.z.array(ExportedStatsIndex)
1219
+ });
1220
+ var ExportedStats = import_zod2.z.union([ExportedStatsV1]);
1221
+ var StatisticsMode = import_zod2.z.discriminatedUnion("kind", [
1222
+ import_zod2.z.object({
1223
+ kind: import_zod2.z.literal("fromAssumption"),
1224
+ reltuples: import_zod2.z.number().min(0),
1225
+ relpages: import_zod2.z.number().min(0)
1226
+ }),
1227
+ import_zod2.z.object({
1228
+ kind: import_zod2.z.literal("fromStatisticsExport"),
1229
+ stats: import_zod2.z.array(ExportedStats),
1230
+ source: StatisticsSource
1231
+ })
1232
+ ]);
1233
+ var DEFAULT_RELTUPLES = 1e4;
1234
+ var DEFAULT_RELPAGES = 1;
1235
+ var _Statistics = class _Statistics {
1236
+ constructor(db, postgresVersion, ownMetadata, statsMode) {
1237
+ this.db = db;
1238
+ this.postgresVersion = postgresVersion;
1239
+ this.ownMetadata = ownMetadata;
1240
+ __publicField(this, "mode");
1241
+ __publicField(this, "exportedMetadata");
1242
+ if (statsMode) {
1243
+ this.mode = statsMode;
1244
+ if (statsMode.kind === "fromStatisticsExport") {
1245
+ this.exportedMetadata = statsMode.stats;
1246
+ }
1247
+ } else {
1248
+ this.mode = _Statistics.defaultStatsMode;
1249
+ }
1250
+ }
1251
+ static statsModeFromAssumption({
1252
+ reltuples,
1253
+ relpages
1254
+ }) {
1255
+ return {
1256
+ kind: "fromAssumption",
1257
+ reltuples,
1258
+ relpages
1259
+ };
1260
+ }
1261
+ /**
1262
+ * Create a statistic mode from stats exported from another database
1263
+ **/
1264
+ static statsModeFromExport(stats) {
1265
+ return {
1266
+ kind: "fromStatisticsExport",
1267
+ source: { kind: "inline" },
1268
+ stats
1269
+ };
1270
+ }
1271
+ static async fromPostgres(db, statsMode) {
1272
+ const version = await db.serverNum();
1273
+ const ownStats = await _Statistics.dumpStats(db, version, "full");
1274
+ return new _Statistics(db, version, ownStats, statsMode);
1275
+ }
1276
+ restoreStats(tx) {
1277
+ return this.restoreStats17(tx);
1278
+ }
1279
+ /**
1280
+ * We have to cast stavaluesN to the correct type
1281
+ * This derives that type for us so it can be used in `array_in`
1282
+ */
1283
+ stavalueKind(values) {
1284
+ if (!values || values.length === 0) {
1285
+ return null;
1286
+ }
1287
+ const [elem] = values;
1288
+ if (typeof elem === "number") {
1289
+ return "real";
1290
+ } else if (typeof elem === "boolean") {
1291
+ return "boolean";
1292
+ }
1293
+ return "text";
1294
+ }
1295
+ async restoreStats17(tx) {
1296
+ const warnings = {
1297
+ tablesNotInExports: [],
1298
+ tablesNotInTest: [],
1299
+ tableNotAnalyzed: [],
1300
+ statsMissing: []
1301
+ };
1302
+ const processedTables = /* @__PURE__ */ new Set();
1303
+ let columnStatsUpdatePromise;
1304
+ const columnStatsValues = [];
1305
+ if (this.exportedMetadata) {
1306
+ for (const table of this.ownMetadata) {
1307
+ const targetTable = this.exportedMetadata.find(
1308
+ (m) => m.tableName === table.tableName && m.schemaName === table.schemaName
1309
+ );
1310
+ if (!targetTable?.columns) {
1311
+ continue;
1312
+ }
1313
+ for (const column of targetTable.columns) {
1314
+ const { stats } = column;
1315
+ if (!stats) {
1316
+ continue;
1317
+ }
1318
+ columnStatsValues.push({
1319
+ schema_name: table.schemaName,
1320
+ table_name: table.tableName,
1321
+ column_name: column.columnName,
1322
+ stainherit: stats.stainherit ?? false,
1323
+ stanullfrac: stats.stanullfrac,
1324
+ stawidth: stats.stawidth,
1325
+ stadistinct: stats.stadistinct,
1326
+ stakind1: stats.stakind1,
1327
+ stakind2: stats.stakind2,
1328
+ stakind3: stats.stakind3,
1329
+ stakind4: stats.stakind4,
1330
+ stakind5: stats.stakind5,
1331
+ staop1: stats.staop1,
1332
+ staop2: stats.staop2,
1333
+ staop3: stats.staop3,
1334
+ staop4: stats.staop4,
1335
+ staop5: stats.staop5,
1336
+ stacoll1: stats.stacoll1,
1337
+ stacoll2: stats.stacoll2,
1338
+ stacoll3: stats.stacoll3,
1339
+ stacoll4: stats.stacoll4,
1340
+ stacoll5: stats.stacoll5,
1341
+ stanumbers1: stats.stanumbers1,
1342
+ stanumbers2: stats.stanumbers2,
1343
+ stanumbers3: stats.stanumbers3,
1344
+ stanumbers4: stats.stanumbers4,
1345
+ stanumbers5: stats.stanumbers5,
1346
+ stavalues1: stats.stavalues1,
1347
+ stavalues2: stats.stavalues2,
1348
+ stavalues3: stats.stavalues3,
1349
+ stavalues4: stats.stavalues4,
1350
+ stavalues5: stats.stavalues5,
1351
+ _value_type1: this.stavalueKind(stats.stavalues1),
1352
+ _value_type2: this.stavalueKind(stats.stavalues2),
1353
+ _value_type3: this.stavalueKind(stats.stavalues3),
1354
+ _value_type4: this.stavalueKind(stats.stavalues4),
1355
+ _value_type5: this.stavalueKind(stats.stavalues5)
1356
+ });
1357
+ }
1358
+ }
1359
+ const sql = import_dedent.default`
1360
+ WITH input AS (
1361
+ SELECT
1362
+ c.oid AS starelid,
1363
+ a.attnum AS staattnum,
1364
+ v.stainherit,
1365
+ v.stanullfrac,
1366
+ v.stawidth,
1367
+ v.stadistinct,
1368
+ v.stakind1,
1369
+ v.stakind2,
1370
+ v.stakind3,
1371
+ v.stakind4,
1372
+ v.stakind5,
1373
+ v.staop1,
1374
+ v.staop2,
1375
+ v.staop3,
1376
+ v.staop4,
1377
+ v.staop5,
1378
+ v.stacoll1,
1379
+ v.stacoll2,
1380
+ v.stacoll3,
1381
+ v.stacoll4,
1382
+ v.stacoll5,
1383
+ v.stanumbers1,
1384
+ v.stanumbers2,
1385
+ v.stanumbers3,
1386
+ v.stanumbers4,
1387
+ v.stanumbers5,
1388
+ v.stavalues1,
1389
+ v.stavalues2,
1390
+ v.stavalues3,
1391
+ v.stavalues4,
1392
+ v.stavalues5,
1393
+ _value_type1,
1394
+ _value_type2,
1395
+ _value_type3,
1396
+ _value_type4,
1397
+ _value_type5
1398
+ FROM jsonb_to_recordset($1::jsonb) AS v(
1399
+ schema_name text,
1400
+ table_name text,
1401
+ column_name text,
1402
+ stainherit boolean,
1403
+ stanullfrac real,
1404
+ stawidth integer,
1405
+ stadistinct real,
1406
+ stakind1 real,
1407
+ stakind2 real,
1408
+ stakind3 real,
1409
+ stakind4 real,
1410
+ stakind5 real,
1411
+ staop1 oid,
1412
+ staop2 oid,
1413
+ staop3 oid,
1414
+ staop4 oid,
1415
+ staop5 oid,
1416
+ stacoll1 oid,
1417
+ stacoll2 oid,
1418
+ stacoll3 oid,
1419
+ stacoll4 oid,
1420
+ stacoll5 oid,
1421
+ stanumbers1 real[],
1422
+ stanumbers2 real[],
1423
+ stanumbers3 real[],
1424
+ stanumbers4 real[],
1425
+ stanumbers5 real[],
1426
+ stavalues1 text[],
1427
+ stavalues2 text[],
1428
+ stavalues3 text[],
1429
+ stavalues4 text[],
1430
+ stavalues5 text[],
1431
+ _value_type1 text,
1432
+ _value_type2 text,
1433
+ _value_type3 text,
1434
+ _value_type4 text,
1435
+ _value_type5 text
1436
+ )
1437
+ JOIN pg_class c ON c.relname = v.table_name
1438
+ JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = v.schema_name
1439
+ JOIN pg_attribute a ON a.attrelid = c.oid AND a.attname = v.column_name
1440
+ ),
1441
+ updated AS (
1442
+ UPDATE pg_statistic s
1443
+ SET
1444
+ stanullfrac = i.stanullfrac,
1445
+ stawidth = i.stawidth,
1446
+ stadistinct = i.stadistinct,
1447
+ stakind1 = i.stakind1,
1448
+ stakind2 = i.stakind2,
1449
+ stakind3 = i.stakind3,
1450
+ stakind4 = i.stakind4,
1451
+ stakind5 = i.stakind5,
1452
+ staop1 = i.staop1,
1453
+ staop2 = i.staop2,
1454
+ staop3 = i.staop3,
1455
+ staop4 = i.staop4,
1456
+ staop5 = i.staop5,
1457
+ stacoll1 = i.stacoll1,
1458
+ stacoll2 = i.stacoll2,
1459
+ stacoll3 = i.stacoll3,
1460
+ stacoll4 = i.stacoll4,
1461
+ stacoll5 = i.stacoll5,
1462
+ stanumbers1 = i.stanumbers1,
1463
+ stanumbers2 = i.stanumbers2,
1464
+ stanumbers3 = i.stanumbers3,
1465
+ stanumbers4 = i.stanumbers4,
1466
+ stanumbers5 = i.stanumbers5,
1467
+ stavalues1 = case
1468
+ when i.stavalues1 is null then null
1469
+ else array_in(i.stavalues1::text::cstring, i._value_type1::regtype::oid, -1)
1470
+ end,
1471
+ stavalues2 = case
1472
+ when i.stavalues2 is null then null
1473
+ else array_in(i.stavalues2::text::cstring, i._value_type2::regtype::oid, -1)
1474
+ end,
1475
+ stavalues3 = case
1476
+ when i.stavalues3 is null then null
1477
+ else array_in(i.stavalues3::text::cstring, i._value_type3::regtype::oid, -1)
1478
+ end,
1479
+ stavalues4 = case
1480
+ when i.stavalues4 is null then null
1481
+ else array_in(i.stavalues4::text::cstring, i._value_type4::regtype::oid, -1)
1482
+ end,
1483
+ stavalues5 = case
1484
+ when i.stavalues5 is null then null
1485
+ else array_in(i.stavalues5::text::cstring, i._value_type5::regtype::oid, -1)
1486
+ end
1487
+ -- stavalues1 = i.stavalues1,
1488
+ -- stavalues2 = i.stavalues2,
1489
+ -- stavalues3 = i.stavalues3,
1490
+ -- stavalues4 = i.stavalues4,
1491
+ -- stavalues5 = i.stavalues5
1492
+ FROM input i
1493
+ WHERE s.starelid = i.starelid AND s.staattnum = i.staattnum AND s.stainherit = i.stainherit
1494
+ RETURNING s.starelid, s.staattnum, s.stainherit, s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5
1495
+ ),
1496
+ inserted as (
1497
+ INSERT INTO pg_statistic (
1498
+ starelid, staattnum, stainherit,
1499
+ stanullfrac, stawidth, stadistinct,
1500
+ stakind1, stakind2, stakind3, stakind4, stakind5,
1501
+ staop1, staop2, staop3, staop4, staop5,
1502
+ stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
1503
+ stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
1504
+ stavalues1, stavalues2, stavalues3, stavalues4, stavalues5
1505
+ )
1506
+ SELECT
1507
+ i.starelid, i.staattnum, i.stainherit,
1508
+ i.stanullfrac, i.stawidth, i.stadistinct,
1509
+ i.stakind1, i.stakind2, i.stakind3, i.stakind4, i.stakind5,
1510
+ i.staop1, i.staop2, i.staop3, i.staop4, i.staop5,
1511
+ i.stacoll1, i.stacoll2, i.stacoll3, i.stacoll4, i.stacoll5,
1512
+ i.stanumbers1, i.stanumbers2, i.stanumbers3, i.stanumbers4, i.stanumbers5,
1513
+ -- i.stavalues1, i.stavalues2, i.stavalues3, i.stavalues4, i.stavalues5,
1514
+ case
1515
+ when i.stavalues1 is null then null
1516
+ else array_in(i.stavalues1::text::cstring, i._value_type1::regtype::oid, -1)
1517
+ end,
1518
+ case
1519
+ when i.stavalues2 is null then null
1520
+ else array_in(i.stavalues2::text::cstring, i._value_type2::regtype::oid, -1)
1521
+ end,
1522
+ case
1523
+ when i.stavalues3 is null then null
1524
+ else array_in(i.stavalues3::text::cstring, i._value_type3::regtype::oid, -1)
1525
+ end,
1526
+ case
1527
+ when i.stavalues4 is null then null
1528
+ else array_in(i.stavalues4::text::cstring, i._value_type4::regtype::oid, -1)
1529
+ end,
1530
+ case
1531
+ when i.stavalues5 is null then null
1532
+ else array_in(i.stavalues5::text::cstring, i._value_type5::regtype::oid, -1)
1533
+ end
1534
+ -- i._value_type1, i._value_type2, i._value_type3, i._value_type4, i._value_type5
1535
+ FROM input i
1536
+ LEFT JOIN updated u
1537
+ ON i.starelid = u.starelid AND i.staattnum = u.staattnum AND i.stainherit = u.stainherit
1538
+ WHERE u.starelid IS NULL
1539
+ returning starelid, staattnum, stainherit, stakind1, stakind2, stakind3, stakind4, stakind5
1540
+ )
1541
+ select * from updated union all (select * from inserted); -- @qd_introspection`;
1542
+ columnStatsUpdatePromise = tx.exec(sql, [columnStatsValues]).catch((err) => {
1543
+ console.error("Something wrong wrong updating column stats");
1544
+ console.error(err);
1545
+ throw err;
1546
+ });
1547
+ }
1548
+ const reltuplesValues = [];
1549
+ for (const table of this.ownMetadata) {
1550
+ if (!table.columns) {
1551
+ continue;
1552
+ }
1553
+ processedTables.add(`${table.schemaName}.${table.tableName}`);
1554
+ let targetTable;
1555
+ if (this.exportedMetadata) {
1556
+ targetTable = this.exportedMetadata.find(
1557
+ (m) => m.tableName === table.tableName && m.schemaName === table.schemaName
1558
+ );
1559
+ }
1560
+ let reltuples;
1561
+ let relpages;
1562
+ let relallvisible = 0;
1563
+ let relallfrozen;
1564
+ if (targetTable) {
1565
+ reltuples = targetTable.reltuples;
1566
+ relpages = targetTable.relpages;
1567
+ relallvisible = targetTable.relallvisible;
1568
+ relallfrozen = targetTable.relallfrozen;
1569
+ } else if (this.mode.kind === "fromAssumption") {
1570
+ reltuples = this.mode.reltuples;
1571
+ relpages = this.mode.relpages;
1572
+ } else {
1573
+ warnings.tablesNotInExports.push(
1574
+ `${table.schemaName}.${table.tableName}`
1575
+ );
1576
+ reltuples = DEFAULT_RELTUPLES;
1577
+ relpages = DEFAULT_RELPAGES;
1578
+ }
1579
+ reltuplesValues.push({
1580
+ relname: table.tableName,
1581
+ schema_name: table.schemaName,
1582
+ reltuples,
1583
+ relpages,
1584
+ relallfrozen,
1585
+ relallvisible
1586
+ });
1587
+ if (targetTable && targetTable.indexes) {
1588
+ for (const index of targetTable.indexes) {
1589
+ reltuplesValues.push({
1590
+ relname: index.indexName,
1591
+ schema_name: targetTable.schemaName,
1592
+ reltuples: index.reltuples,
1593
+ relpages: index.relpages,
1594
+ relallfrozen: index.relallfrozen,
1595
+ relallvisible: index.relallvisible
1596
+ });
1597
+ }
1598
+ }
1599
+ }
1600
+ const reltuplesQuery = import_dedent.default`
1601
+ update pg_class p
1602
+ set reltuples = v.reltuples,
1603
+ relpages = v.relpages,
1604
+ -- relallfrozen = case when v.relallfrozen is null then p.relallfrozen else v.relallfrozen end,
1605
+ relallvisible = case when v.relallvisible is null then p.relallvisible else v.relallvisible end
1606
+ from jsonb_to_recordset($1::jsonb)
1607
+ as v(reltuples real, relpages integer, relallfrozen integer, relallvisible integer, relname text, schema_name text)
1608
+ where p.relname = v.relname
1609
+ and p.relnamespace = (select oid from pg_namespace where nspname = v.schema_name)
1610
+ returning p.relname, p.relnamespace, p.reltuples, p.relpages;
1611
+ `;
1612
+ const reltuplesPromise = tx.exec(reltuplesQuery, [reltuplesValues]).catch((err) => {
1613
+ console.error("Something went wrong updating reltuples/relpages");
1614
+ console.error(err);
1615
+ return err;
1616
+ });
1617
+ if (this.exportedMetadata) {
1618
+ for (const table of this.exportedMetadata) {
1619
+ const tableExists = processedTables.has(
1620
+ `${table.schemaName}.${table.tableName}`
1621
+ );
1622
+ if (tableExists && table.reltuples === -1) {
1623
+ console.warn(
1624
+ `Table ${table.tableName} has reltuples -1. Your production database is probably not analyzed properly`
1625
+ );
1626
+ warnings.tableNotAnalyzed.push(
1627
+ `${table.schemaName}.${table.tableName}`
1628
+ );
1629
+ }
1630
+ if (tableExists) {
1631
+ continue;
1632
+ }
1633
+ warnings.tablesNotInTest.push(`${table.schemaName}.${table.tableName}`);
1634
+ }
1635
+ }
1636
+ const [statsUpdates, reltuplesUpdates] = await Promise.all([
1637
+ columnStatsUpdatePromise,
1638
+ reltuplesPromise
1639
+ ]);
1640
+ const updatedColumnsProperly = statsUpdates ? statsUpdates.length === columnStatsValues.length : true;
1641
+ if (!updatedColumnsProperly) {
1642
+ console.error(`Did not update expected column stats`);
1643
+ }
1644
+ if (reltuplesUpdates.length !== reltuplesValues.length) {
1645
+ console.error(`Did not update expected reltuples/relpages`);
1646
+ }
1647
+ return warnings;
1648
+ }
1649
+ static async dumpStats(db, postgresVersion, kind) {
1650
+ const fullDump = kind === "full";
1651
+ console.log(`dumping stats for postgres ${(0, import_colorette3.gray)(postgresVersion)}`);
1652
+ const stats = await db.exec(
1653
+ `
1654
+ WITH table_columns AS (
1655
+ SELECT
1656
+ c.table_name,
1657
+ c.table_schema,
1658
+ cl.reltuples,
1659
+ cl.relpages,
1660
+ cl.relallvisible,
1661
+ -- cl.relallfrozen,
1662
+ n.nspname AS schema_name,
1663
+ json_agg(
1664
+ json_build_object(
1665
+ '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
+ 'stats', (
1673
+ SELECT json_build_object(
1674
+ 'starelid', s.starelid,
1675
+ 'staattnum', s.staattnum,
1676
+ 'stanullfrac', s.stanullfrac,
1677
+ 'stawidth', s.stawidth,
1678
+ 'stadistinct', s.stadistinct,
1679
+ 'stakind1', s.stakind1, 'staop1', s.staop1, 'stacoll1', s.stacoll1, 'stanumbers1', s.stanumbers1,
1680
+ 'stakind2', s.stakind2, 'staop2', s.staop2, 'stacoll2', s.stacoll2, 'stanumbers2', s.stanumbers2,
1681
+ 'stakind3', s.stakind3, 'staop3', s.staop3, 'stacoll3', s.stacoll3, 'stanumbers3', s.stanumbers3,
1682
+ 'stakind4', s.stakind4, 'staop4', s.staop4, 'stacoll4', s.stacoll4, 'stanumbers4', s.stanumbers4,
1683
+ 'stakind5', s.stakind5, 'staop5', s.staop5, 'stacoll5', s.stacoll5, 'stanumbers5', s.stanumbers5,
1684
+ 'stavalues1', CASE WHEN $1 THEN s.stavalues1 ELSE NULL END,
1685
+ 'stavalues2', CASE WHEN $1 THEN s.stavalues2 ELSE NULL END,
1686
+ 'stavalues3', CASE WHEN $1 THEN s.stavalues3 ELSE NULL END,
1687
+ 'stavalues4', CASE WHEN $1 THEN s.stavalues4 ELSE NULL END,
1688
+ 'stavalues5', CASE WHEN $1 THEN s.stavalues5 ELSE NULL END
1689
+ )
1690
+ FROM pg_statistic s
1691
+ WHERE s.starelid = a.attrelid AND s.staattnum = a.attnum
1692
+ )
1693
+ )
1694
+ ORDER BY c.ordinal_position
1695
+ ) AS columns
1696
+ FROM information_schema.columns c
1697
+ JOIN pg_attribute a
1698
+ ON a.attrelid = (quote_ident(c.table_schema) || '.' || quote_ident(c.table_name))::regclass
1699
+ AND a.attname = c.column_name
1700
+ JOIN pg_class cl
1701
+ ON cl.oid = a.attrelid
1702
+ JOIN pg_namespace n
1703
+ ON n.oid = cl.relnamespace
1704
+ WHERE c.table_name NOT LIKE 'pg_%'
1705
+ AND n.nspname <> 'information_schema'
1706
+ AND c.table_name NOT IN ('pg_stat_statements', 'pg_stat_statements_info')
1707
+ GROUP BY c.table_name, c.table_schema, cl.reltuples, cl.relpages, cl.relallvisible, n.nspname
1708
+ ),
1709
+ table_indexes AS (
1710
+ SELECT
1711
+ t.relname AS table_name,
1712
+ json_agg(
1713
+ json_build_object(
1714
+ 'indexName', i.relname,
1715
+ 'reltuples', i.reltuples,
1716
+ 'relpages', i.relpages,
1717
+ 'relallvisible', i.relallvisible
1718
+ -- 'relallfrozen', i.relallfrozen
1719
+ )
1720
+ ) AS indexes
1721
+ FROM pg_class t
1722
+ JOIN pg_index ix ON ix.indrelid = t.oid
1723
+ JOIN pg_class i ON i.oid = ix.indexrelid
1724
+ JOIN pg_namespace n ON n.oid = t.relnamespace
1725
+ WHERE t.relname NOT LIKE 'pg_%'
1726
+ AND n.nspname <> 'information_schema'
1727
+ GROUP BY t.relname
1728
+ )
1729
+ SELECT json_agg(
1730
+ json_build_object(
1731
+ 'tableName', tc.table_name,
1732
+ 'schemaName', tc.table_schema,
1733
+ 'reltuples', tc.reltuples,
1734
+ 'relpages', tc.relpages,
1735
+ 'relallvisible', tc.relallvisible,
1736
+ -- 'relallfrozen', tc.relallfrozen,
1737
+ 'columns', tc.columns,
1738
+ 'indexes', COALESCE(ti.indexes, '[]'::json)
1739
+ )
1740
+ )
1741
+ FROM table_columns tc
1742
+ LEFT JOIN table_indexes ti
1743
+ ON ti.table_name = tc.table_name;
1744
+ `,
1745
+ [fullDump]
1746
+ );
1747
+ return stats[0].json_agg;
1748
+ }
1749
+ /**
1750
+ * Returns all indexes in the database.
1751
+ * ONLY handles regular btree indexes
1752
+ */
1753
+ async getExistingIndexes() {
1754
+ const indexes = await this.db.exec(`
1755
+ WITH partitioned_tables AS (
1756
+ SELECT
1757
+ inhparent::regclass AS parent_table,
1758
+ inhrelid::regclass AS partition_table
1759
+ FROM
1760
+ pg_inherits
1761
+ )
1762
+ SELECT
1763
+ n.nspname AS schema_name,
1764
+ COALESCE(pt.parent_table::text, t.relname) AS table_name,
1765
+ i.relname AS index_name,
1766
+ ix.indisprimary as is_primary,
1767
+ am.amname AS index_type,
1768
+ array_agg(
1769
+ CASE
1770
+ -- Handle regular columns
1771
+ WHEN a.attname IS NOT NULL THEN
1772
+ json_build_object('name', a.attname, 'order',
1773
+ CASE
1774
+ WHEN (indoption[array_position(ix.indkey, a.attnum)] & 1) = 1 THEN 'DESC'
1775
+ ELSE 'ASC'
1776
+ END)
1777
+ -- Handle expressions
1778
+ ELSE
1779
+ json_build_object('name', pg_get_expr((ix.indexprs)::pg_node_tree, t.oid), 'order',
1780
+ CASE
1781
+ WHEN (indoption[array_position(ix.indkey, k.attnum)] & 1) = 1 THEN 'DESC'
1782
+ ELSE 'ASC'
1783
+ END)
1784
+ END
1785
+ ORDER BY array_position(ix.indkey, k.attnum)
1786
+ ) AS index_columns
1787
+ FROM
1788
+ pg_class t
1789
+ LEFT JOIN partitioned_tables pt ON t.oid = pt.partition_table
1790
+ JOIN pg_index ix ON t.oid = ix.indrelid
1791
+ JOIN pg_class i ON i.oid = ix.indexrelid
1792
+ JOIN pg_am am ON i.relam = am.oid
1793
+ LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY k(attnum, ordinality) ON true
1794
+ LEFT JOIN pg_attribute a ON a.attnum = k.attnum AND a.attrelid = t.oid
1795
+ JOIN pg_namespace n ON t.relnamespace = n.oid
1796
+ WHERE
1797
+ n.nspname = 'public'
1798
+ GROUP BY
1799
+ n.nspname, COALESCE(pt.parent_table::text, t.relname), i.relname, am.amname, ix.indisprimary
1800
+ ORDER BY
1801
+ COALESCE(pt.parent_table::text, t.relname), i.relname; -- @qd_introspection
1802
+ `);
1803
+ return indexes;
1804
+ }
1805
+ };
1806
+ // preventing accidental internal mutations
1807
+ __publicField(_Statistics, "defaultStatsMode", Object.freeze({
1808
+ kind: "fromAssumption",
1809
+ reltuples: DEFAULT_RELTUPLES,
1810
+ relpages: DEFAULT_RELPAGES
1811
+ }));
1812
+ var Statistics = _Statistics;
1813
+ // Annotate the CommonJS export names for ESM import in node:
1814
+ 0 && (module.exports = {
1815
+ Analyzer,
1816
+ ExportedStats,
1817
+ ExportedStatsColumns,
1818
+ ExportedStatsIndex,
1819
+ ExportedStatsStatistics,
1820
+ ExportedStatsV1,
1821
+ IndexOptimizer,
1822
+ PROCEED,
1823
+ PostgresQueryBuilder,
1824
+ PostgresVersion,
1825
+ SKIP,
1826
+ Statistics,
1827
+ StatisticsMode,
1828
+ StatisticsSource,
1829
+ ignoredIdentifier,
1830
+ parseNudges,
1831
+ permuteWithFeedback
1832
+ });
1833
+ //# sourceMappingURL=index.cjs.map