turbine-orm 0.55.0 → 0.57.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/README.md +16 -1
- package/dist/cjs/cli/index.d.ts +3 -1
- package/dist/cjs/cli/index.js +341 -14
- package/dist/cjs/client.d.ts +16 -1
- package/dist/cjs/client.js +7 -39
- package/dist/cjs/dialect.d.ts +9 -0
- package/dist/cjs/index-stats.d.ts +46 -0
- package/dist/cjs/index-stats.js +42 -1
- package/dist/cjs/plan-divergence.d.ts +511 -0
- package/dist/cjs/plan-divergence.js +790 -0
- package/dist/cjs/powql.d.ts +11 -0
- package/dist/cjs/powql.js +22 -0
- package/dist/cjs/prisma-compat.d.ts +32 -1
- package/dist/cjs/prisma-compat.js +297 -41
- package/dist/cjs/query/builder.d.ts +45 -0
- package/dist/cjs/query/builder.js +90 -17
- package/dist/cjs/query/deferred.d.ts +9 -0
- package/dist/cjs/query/index.d.ts +2 -0
- package/dist/cjs/query/index.js +18 -1
- package/dist/cjs/query/option-surface.d.ts +100 -0
- package/dist/cjs/query/option-surface.js +214 -0
- package/dist/cjs/query/types.d.ts +140 -0
- package/dist/cjs/query/utils.d.ts +30 -0
- package/dist/cjs/query/utils.js +67 -3
- package/dist/cjs/query/warn-registry.d.ts +8 -0
- package/dist/cjs/query/warn-registry.js +8 -0
- package/dist/cli/index.d.ts +3 -1
- package/dist/cli/index.js +341 -14
- package/dist/client.d.ts +16 -1
- package/dist/client.js +8 -40
- package/dist/dialect.d.ts +9 -0
- package/dist/index-stats.d.ts +46 -0
- package/dist/index-stats.js +42 -1
- package/dist/plan-divergence.d.ts +511 -0
- package/dist/plan-divergence.js +783 -0
- package/dist/powql.d.ts +11 -0
- package/dist/powql.js +22 -0
- package/dist/prisma-compat.d.ts +32 -1
- package/dist/prisma-compat.js +297 -41
- package/dist/query/builder.d.ts +45 -0
- package/dist/query/builder.js +90 -17
- package/dist/query/deferred.d.ts +9 -0
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +1 -0
- package/dist/query/option-surface.d.ts +100 -0
- package/dist/query/option-surface.js +209 -0
- package/dist/query/types.d.ts +140 -0
- package/dist/query/utils.d.ts +30 -0
- package/dist/query/utils.js +66 -3
- package/dist/query/warn-registry.d.ts +8 -0
- package/dist/query/warn-registry.js +8 -0
- package/package.json +1 -1
package/dist/cjs/client.js
CHANGED
|
@@ -148,50 +148,14 @@ const CONFIG_KEY_SET = new Set(Object.keys(TURBINE_CONFIG_KEYS));
|
|
|
148
148
|
* engine factories' first argument. Same story as `schema`.
|
|
149
149
|
*/
|
|
150
150
|
const NON_CONFIG_KEYS = new Set(['queryInterfaceFactory', 'schema', 'url']);
|
|
151
|
-
/** camelCase name → its lowercased words (`logQueryParams` → log, query, params). */
|
|
152
|
-
function camelWords(name) {
|
|
153
|
-
return name
|
|
154
|
-
.split(/(?=[A-Z])/)
|
|
155
|
-
.map((w) => w.toLowerCase())
|
|
156
|
-
.filter(Boolean);
|
|
157
|
-
}
|
|
158
151
|
/**
|
|
159
152
|
* The real config key `key` most likely meant, or null when nothing is close.
|
|
160
153
|
*
|
|
161
|
-
* {@link
|
|
162
|
-
*
|
|
163
|
-
* distance, which covers typos but not the miss this warning exists for: a
|
|
164
|
-
* guessed name that omits a whole word. `logParams` is five edits from
|
|
165
|
-
* `logQueryParams`, past the bound, yet it names the same words in the same
|
|
166
|
-
* order, so a second pass accepts a candidate whose camelCase words CONTAIN the
|
|
167
|
-
* guess's words in order, preferring the one that adds fewest words.
|
|
154
|
+
* {@link suggestKey} is shared with the prisma-compat query-option warner, so
|
|
155
|
+
* both diagnostics rank near-misses identically.
|
|
168
156
|
*/
|
|
169
157
|
function suggestConfigKey(key) {
|
|
170
|
-
|
|
171
|
-
if (direct)
|
|
172
|
-
return direct;
|
|
173
|
-
const wanted = camelWords(key);
|
|
174
|
-
if (wanted.length < 2)
|
|
175
|
-
return null;
|
|
176
|
-
let best = null;
|
|
177
|
-
let bestExtra = Number.POSITIVE_INFINITY;
|
|
178
|
-
for (const candidate of CONFIG_KEY_SET) {
|
|
179
|
-
const words = camelWords(candidate);
|
|
180
|
-
if (words.length <= wanted.length)
|
|
181
|
-
continue;
|
|
182
|
-
let i = 0;
|
|
183
|
-
for (const w of words)
|
|
184
|
-
if (w === wanted[i])
|
|
185
|
-
i++;
|
|
186
|
-
if (i !== wanted.length)
|
|
187
|
-
continue;
|
|
188
|
-
const extra = words.length - wanted.length;
|
|
189
|
-
if (extra < bestExtra) {
|
|
190
|
-
bestExtra = extra;
|
|
191
|
-
best = candidate;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return best;
|
|
158
|
+
return (0, utils_js_1.suggestKey)(key, CONFIG_KEY_SET);
|
|
195
159
|
}
|
|
196
160
|
/**
|
|
197
161
|
* Dev-mode notice for a key on the config object that is not part of the config
|
|
@@ -658,6 +622,10 @@ class TurbineClient {
|
|
|
658
622
|
jsonEncoding: config.jsonEncoding,
|
|
659
623
|
globalFilters: config.globalFilters,
|
|
660
624
|
preparedStatements: envDisablePrepared ? false : (config.preparedStatements ?? !config.pool),
|
|
625
|
+
// Forwarded so a per-query `forceCustomPlan` can refuse the combination
|
|
626
|
+
// this client's own connections would silently defeat, see the
|
|
627
|
+
// `planCacheMode` note on QueryInterfaceOptions.
|
|
628
|
+
planCacheMode,
|
|
661
629
|
sqlCache: config.sqlCache ?? true,
|
|
662
630
|
sqlCacheSize: config.sqlCacheSize,
|
|
663
631
|
dialect: config.dialect,
|
package/dist/cjs/dialect.d.ts
CHANGED
|
@@ -375,6 +375,15 @@ export interface Dialect {
|
|
|
375
375
|
* setting (`SET plan_cache_mode = auto | force_custom_plan |
|
|
376
376
|
* force_generic_plan`). Gates the opt-in `planCacheMode` client option.
|
|
377
377
|
*
|
|
378
|
+
* It ALSO gates the per-query `forceCustomPlan` read option, which uses a
|
|
379
|
+
* different mechanism (it withholds the prepared-statement name, so the
|
|
380
|
+
* driver re-parses the statement on every execution and the counter that
|
|
381
|
+
* generic-plan promotion depends on never reaches its threshold) but
|
|
382
|
+
* asks the identical capability question: does this engine have a PostgreSQL
|
|
383
|
+
* plan cache whose generic-plan promotion is worth pinning? An engine that
|
|
384
|
+
* answers no cannot honour either option, so both refuse on the same flag
|
|
385
|
+
* rather than on two flags that could never disagree.
|
|
386
|
+
*
|
|
378
387
|
* A capability flag rather than a `dialect.name === 'postgresql'` test, for
|
|
379
388
|
* the same reason every other refusal here is one: the setting is a property
|
|
380
389
|
* of the PostgreSQL PLAN CACHE, not of the SQL string, so a
|
|
@@ -86,6 +86,12 @@ export interface TableStats {
|
|
|
86
86
|
table: string;
|
|
87
87
|
/** pg_class.reltuples. 0 or -1 means never-analyzed → treated as UNKNOWN (null rows). */
|
|
88
88
|
reltuples: number;
|
|
89
|
+
/**
|
|
90
|
+
* pg_class.relpages. The SIZE input the plan-divergence crossover is computed
|
|
91
|
+
* from (a plan boundary tracks pages relative to the LIMIT, not rows).
|
|
92
|
+
* Absent when the pg_class read degraded; 0 means never analyzed.
|
|
93
|
+
*/
|
|
94
|
+
relpages?: number;
|
|
89
95
|
/** pg_stat_user_tables.n_live_tup, a cross-check for reltuples. */
|
|
90
96
|
nLiveTup?: number;
|
|
91
97
|
nTupIns?: number;
|
|
@@ -100,6 +106,13 @@ export interface TableStats {
|
|
|
100
106
|
tableSizeBytes?: number;
|
|
101
107
|
/** Count of indexes already on the table (pg_index). */
|
|
102
108
|
existingIndexCount?: number;
|
|
109
|
+
/**
|
|
110
|
+
* The later of pg_stat_user_tables.last_analyze / last_autoanalyze: when the
|
|
111
|
+
* planner's column statistics were last refreshed. This, NOT stats_reset, is
|
|
112
|
+
* the freshness that matters for anything read out of pg_stats. Null when
|
|
113
|
+
* never analyzed; absent when the pg_stat read degraded.
|
|
114
|
+
*/
|
|
115
|
+
lastAnalyze?: Date | null;
|
|
103
116
|
}
|
|
104
117
|
/**
|
|
105
118
|
* Placeholder for an index column that is an EXPRESSION, not a plain column
|
|
@@ -133,6 +146,27 @@ export interface IndexStat {
|
|
|
133
146
|
/** pg_relation_size of the index heap, bytes. Size reclaimed by a drop. */
|
|
134
147
|
sizeBytes?: number;
|
|
135
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* The value distribution of one column, read from pg_stats with
|
|
151
|
+
* `inherited = false`. Consumed by the plan-divergence advisor, which needs to
|
|
152
|
+
* reproduce the planner's OWN estimates rather than approximate them.
|
|
153
|
+
*/
|
|
154
|
+
export interface ColumnDistribution {
|
|
155
|
+
table: string;
|
|
156
|
+
column: string;
|
|
157
|
+
/**
|
|
158
|
+
* pg_stats.n_distinct, RAW: a positive value is a count, a negative value is a
|
|
159
|
+
* fraction of the row count. Kept undecoded so the consumer decodes it exactly
|
|
160
|
+
* the way the planner does. 0 means the column was never analyzed.
|
|
161
|
+
*/
|
|
162
|
+
nDistinct: number;
|
|
163
|
+
/** pg_stats.correlation, signed. NULL for types with no ordering. */
|
|
164
|
+
correlation: number | null;
|
|
165
|
+
/** pg_stats.most_common_freqs, or null when the column has no MCV list. */
|
|
166
|
+
mostCommonFreqs: number[] | null;
|
|
167
|
+
/** cardinality(most_common_vals): how many values the MCV list actually covers. */
|
|
168
|
+
mcvCount: number;
|
|
169
|
+
}
|
|
136
170
|
/**
|
|
137
171
|
* A point-in-time read of the statistics the triage needs. Every part is
|
|
138
172
|
* optional at the field level so the pure scorer degrades honestly.
|
|
@@ -150,6 +184,13 @@ export interface StatsSnapshot {
|
|
|
150
184
|
indexes: IndexStat[];
|
|
151
185
|
/** null_frac per probed column, keyed `table.column`. */
|
|
152
186
|
nullFrac: Record<string, number>;
|
|
187
|
+
/**
|
|
188
|
+
* Value distribution per candidate column, keyed `table.column`. Optional at
|
|
189
|
+
* the snapshot level: a caller that never asked for distribution statistics
|
|
190
|
+
* (or whose pg_stats read degraded) leaves it absent, and the plan-divergence
|
|
191
|
+
* advisor reports that as a suppressed candidate rather than scoring a guess.
|
|
192
|
+
*/
|
|
193
|
+
columnStats?: Record<string, ColumnDistribution>;
|
|
153
194
|
/** Per-signal degradation notices (privileges, catalog gaps, timeouts). */
|
|
154
195
|
notices: string[];
|
|
155
196
|
}
|
|
@@ -347,6 +388,11 @@ export interface CollectSnapshotOptions {
|
|
|
347
388
|
tables: string[];
|
|
348
389
|
/** Columns to read null_frac for (single-column probes). */
|
|
349
390
|
columns: ProbedColumn[];
|
|
391
|
+
/**
|
|
392
|
+
* Columns to read the full value distribution for (n_distinct, correlation,
|
|
393
|
+
* MCV frequencies). One extra pg_stats query; empty skips it entirely.
|
|
394
|
+
*/
|
|
395
|
+
distributionColumns?: ProbedColumn[];
|
|
350
396
|
/** statement_timeout for each catalog read. Default 5000ms. */
|
|
351
397
|
statementTimeoutMs?: number;
|
|
352
398
|
}
|
package/dist/cjs/index-stats.js
CHANGED
|
@@ -148,6 +148,7 @@ function emptyStatsSnapshot(notices = []) {
|
|
|
148
148
|
tables: {},
|
|
149
149
|
indexes: [],
|
|
150
150
|
nullFrac: {},
|
|
151
|
+
columnStats: {},
|
|
151
152
|
notices,
|
|
152
153
|
};
|
|
153
154
|
}
|
|
@@ -590,12 +591,14 @@ async function collectStatsSnapshot(options) {
|
|
|
590
591
|
}
|
|
591
592
|
}
|
|
592
593
|
// --- table stats (pg_stat_user_tables) ---------------------------------
|
|
593
|
-
const statRows = await run('pg_stat_user_tables', `SELECT relname, n_tup_ins, n_tup_upd, n_tup_del, n_tup_hot_upd, seq_scan, seq_tup_read, n_live_tup
|
|
594
|
+
const statRows = await run('pg_stat_user_tables', `SELECT relname, n_tup_ins, n_tup_upd, n_tup_del, n_tup_hot_upd, seq_scan, seq_tup_read, n_live_tup,
|
|
595
|
+
greatest(last_analyze, last_autoanalyze) AS last_analyze
|
|
594
596
|
FROM pg_stat_user_tables
|
|
595
597
|
WHERE schemaname = $1 AND relname = ANY($2)`, [options.schema, options.tables]);
|
|
596
598
|
// --- class size + existing index count (pg_class) ----------------------
|
|
597
599
|
const classRows = await run('pg_class size', `SELECT c.relname,
|
|
598
600
|
c.reltuples::bigint::text AS reltuples,
|
|
601
|
+
c.relpages::bigint::text AS relpages,
|
|
599
602
|
pg_total_relation_size(c.oid)::text AS total_size,
|
|
600
603
|
pg_relation_size(c.oid)::text AS table_size,
|
|
601
604
|
(SELECT count(*) FROM pg_index i WHERE i.indrelid = c.oid)::text AS index_count
|
|
@@ -618,6 +621,7 @@ async function collectStatsSnapshot(options) {
|
|
|
618
621
|
for (const row of classRows) {
|
|
619
622
|
const s = ensure(row.relname);
|
|
620
623
|
s.reltuples = Number(row.reltuples);
|
|
624
|
+
s.relpages = Number(row.relpages);
|
|
621
625
|
s.totalSizeBytes = Number(row.total_size);
|
|
622
626
|
s.tableSizeBytes = Number(row.table_size);
|
|
623
627
|
s.existingIndexCount = Number(row.index_count);
|
|
@@ -634,6 +638,7 @@ async function collectStatsSnapshot(options) {
|
|
|
634
638
|
s.seqScan = Number(row.seq_scan);
|
|
635
639
|
s.seqTupRead = Number(row.seq_tup_read);
|
|
636
640
|
s.nLiveTup = Number(row.n_live_tup);
|
|
641
|
+
s.lastAnalyze = row.last_analyze == null ? null : new Date(row.last_analyze);
|
|
637
642
|
}
|
|
638
643
|
}
|
|
639
644
|
// --- invalid + all indexes (whole schema, for invalid detection) -------
|
|
@@ -697,6 +702,42 @@ async function collectStatsSnapshot(options) {
|
|
|
697
702
|
}
|
|
698
703
|
}
|
|
699
704
|
}
|
|
705
|
+
// --- value distribution for divergence candidates (pg_stats) -----------
|
|
706
|
+
const distCols = options.distributionColumns ?? [];
|
|
707
|
+
if (distCols.length > 0) {
|
|
708
|
+
// `inherited = false` is required: on a partitioned parent the inherited
|
|
709
|
+
// row describes the whole tree, and a per-partition plan is not chosen
|
|
710
|
+
// from it. most_common_vals is an anyarray, so its cardinality is read
|
|
711
|
+
// through a text[] cast (array_length on anyarray cannot resolve a type).
|
|
712
|
+
const distRows = await run('pg_stats.distribution', `SELECT s.tablename, s.attname,
|
|
713
|
+
s.n_distinct::text AS n_distinct,
|
|
714
|
+
s.correlation::text AS correlation,
|
|
715
|
+
s.most_common_freqs,
|
|
716
|
+
coalesce(array_length(s.most_common_vals::text::text[], 1), 0)::text AS mcv_count
|
|
717
|
+
FROM pg_stats s
|
|
718
|
+
JOIN unnest($2::text[], $3::text[]) AS probe(t, c)
|
|
719
|
+
ON probe.t = s.tablename AND probe.c = s.attname
|
|
720
|
+
WHERE s.schemaname = $1 AND s.inherited = false`, [options.schema, distCols.map((c) => c.table), distCols.map((c) => c.column)]);
|
|
721
|
+
if (distRows) {
|
|
722
|
+
const byKey = snapshot.columnStats ?? {};
|
|
723
|
+
for (const row of distRows) {
|
|
724
|
+
byKey[`${row.tablename}.${row.attname}`] = {
|
|
725
|
+
table: row.tablename,
|
|
726
|
+
column: row.attname,
|
|
727
|
+
nDistinct: Number(row.n_distinct),
|
|
728
|
+
correlation: row.correlation == null ? null : Number(row.correlation),
|
|
729
|
+
mostCommonFreqs: row.most_common_freqs == null ? null : row.most_common_freqs.map(Number),
|
|
730
|
+
mcvCount: row.mcv_count == null ? 0 : Number(row.mcv_count),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
snapshot.columnStats = byKey;
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
// The read failed (privileges, catalog gap). Leave columnStats absent so
|
|
737
|
+
// the advisor suppresses every candidate instead of scoring zeros.
|
|
738
|
+
snapshot.columnStats = undefined;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
700
741
|
}
|
|
701
742
|
finally {
|
|
702
743
|
try {
|