prisma-sql 1.75.9 → 1.75.11
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/collect-planner-stats.cjs +2 -0
- package/dist/collect-planner-stats.cjs.map +1 -1
- package/dist/collect-planner-stats.js +2 -0
- package/dist/collect-planner-stats.js.map +1 -1
- package/dist/generator.cjs +13 -599
- package/dist/generator.cjs.map +1 -1
- package/dist/generator.js +13 -599
- package/dist/generator.js.map +1 -1
- package/package.json +5 -4
package/dist/generator.js
CHANGED
|
@@ -68,7 +68,7 @@ var require_package = __commonJS({
|
|
|
68
68
|
"package.json"(exports$1, module) {
|
|
69
69
|
module.exports = {
|
|
70
70
|
name: "prisma-sql",
|
|
71
|
-
version: "1.75.
|
|
71
|
+
version: "1.75.11",
|
|
72
72
|
description: "Convert Prisma queries to optimized SQL with type safety. 2-7x faster than Prisma Client.",
|
|
73
73
|
main: "dist/index.cjs",
|
|
74
74
|
module: "dist/index.js",
|
|
@@ -137,12 +137,14 @@ var require_package = __commonJS({
|
|
|
137
137
|
"@dee-wan/schema-parser": "1.4.0",
|
|
138
138
|
"@prisma/generator-helper": "^7.4.0",
|
|
139
139
|
"@prisma/internals": "^7.4.0",
|
|
140
|
+
dotenv: "^17.3.1",
|
|
140
141
|
postgres: "^3.4.8"
|
|
141
142
|
},
|
|
142
143
|
devDependencies: {
|
|
143
144
|
"@faker-js/faker": "^10.2.0",
|
|
144
145
|
"@prisma/adapter-better-sqlite3": "^7.4.0",
|
|
145
146
|
"@prisma/adapter-pg": "^7.4.0",
|
|
147
|
+
"@prisma/client": "7.4.0",
|
|
146
148
|
"@semantic-release/changelog": "^6.0.3",
|
|
147
149
|
"@semantic-release/git": "^10.0.1",
|
|
148
150
|
"@types/better-sqlite3": "^7.6.13",
|
|
@@ -151,12 +153,11 @@ var require_package = __commonJS({
|
|
|
151
153
|
"better-sqlite3": "^12.6.2",
|
|
152
154
|
"drizzle-kit": "^0.31.8",
|
|
153
155
|
"drizzle-orm": "^0.45.1",
|
|
156
|
+
prisma: "7.4.0",
|
|
154
157
|
tsup: "^8.5.1",
|
|
155
158
|
tsx: "^4.21.0",
|
|
156
159
|
typescript: "^5.9.3",
|
|
157
|
-
vitest: "^4.0.18"
|
|
158
|
-
"@prisma/client": "7.4.0",
|
|
159
|
-
prisma: "7.4.0"
|
|
160
|
+
vitest: "^4.0.18"
|
|
160
161
|
},
|
|
161
162
|
engines: {
|
|
162
163
|
node: ">=16.0.0"
|
|
@@ -7242,38 +7243,6 @@ function generateSQL2(directive) {
|
|
|
7242
7243
|
}
|
|
7243
7244
|
|
|
7244
7245
|
// src/utils/pure-utils.ts
|
|
7245
|
-
function toNumberOrZero(v) {
|
|
7246
|
-
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
7247
|
-
if (typeof v === "bigint") return Number(v);
|
|
7248
|
-
if (typeof v === "string" && v.trim() !== "") {
|
|
7249
|
-
const n = Number(v);
|
|
7250
|
-
if (Number.isFinite(n)) return n;
|
|
7251
|
-
}
|
|
7252
|
-
return 0;
|
|
7253
|
-
}
|
|
7254
|
-
function clampStatsMonotonic(avg, p95, p99, max, coverage) {
|
|
7255
|
-
const safeAvg = Math.max(1, avg);
|
|
7256
|
-
const safeP95 = Math.max(safeAvg, p95);
|
|
7257
|
-
const safeP99 = Math.max(safeP95, p99);
|
|
7258
|
-
const safeMax = Math.max(safeP99, max);
|
|
7259
|
-
const safeCoverage = Math.max(0, Math.min(1, coverage));
|
|
7260
|
-
return {
|
|
7261
|
-
avg: safeAvg,
|
|
7262
|
-
p95: safeP95,
|
|
7263
|
-
p99: safeP99,
|
|
7264
|
-
max: safeMax,
|
|
7265
|
-
coverage: safeCoverage
|
|
7266
|
-
};
|
|
7267
|
-
}
|
|
7268
|
-
function normalizeStats(row) {
|
|
7269
|
-
return clampStatsMonotonic(
|
|
7270
|
-
toNumberOrZero(row.avg),
|
|
7271
|
-
toNumberOrZero(row.p95),
|
|
7272
|
-
toNumberOrZero(row.p99),
|
|
7273
|
-
toNumberOrZero(row.max),
|
|
7274
|
-
toNumberOrZero(row.coverage)
|
|
7275
|
-
);
|
|
7276
|
-
}
|
|
7277
7246
|
function stableJson(value) {
|
|
7278
7247
|
return JSON.stringify(
|
|
7279
7248
|
value,
|
|
@@ -7310,486 +7279,6 @@ function countTotalQueries(queries) {
|
|
|
7310
7279
|
}
|
|
7311
7280
|
|
|
7312
7281
|
// src/cardinality-planner.ts
|
|
7313
|
-
function quoteIdent(dialect, ident) {
|
|
7314
|
-
return `"${ident.replace(/"/g, '""')}"`;
|
|
7315
|
-
}
|
|
7316
|
-
function createDatabaseExecutor(options) {
|
|
7317
|
-
return __async(this, null, function* () {
|
|
7318
|
-
const { databaseUrl, dialect, connectTimeoutMs = 3e4 } = options;
|
|
7319
|
-
if (dialect === "postgres") {
|
|
7320
|
-
const postgres = yield import('postgres');
|
|
7321
|
-
const sql = postgres.default(databaseUrl, {
|
|
7322
|
-
connect_timeout: Math.ceil(connectTimeoutMs / 1e3),
|
|
7323
|
-
max: 1
|
|
7324
|
-
});
|
|
7325
|
-
return {
|
|
7326
|
-
executor: {
|
|
7327
|
-
query: (q, params) => __async(null, null, function* () {
|
|
7328
|
-
return yield sql.unsafe(q, params != null ? params : []);
|
|
7329
|
-
})
|
|
7330
|
-
},
|
|
7331
|
-
cleanup: () => __async(null, null, function* () {
|
|
7332
|
-
yield sql.end();
|
|
7333
|
-
})
|
|
7334
|
-
};
|
|
7335
|
-
}
|
|
7336
|
-
throw new Error(`createDatabaseExecutor does not support dialect: ${dialect}`);
|
|
7337
|
-
});
|
|
7338
|
-
}
|
|
7339
|
-
function extractMeasurableOneToManyEdges(datamodel) {
|
|
7340
|
-
const modelByName = new Map(datamodel.models.map((m) => [m.name, m]));
|
|
7341
|
-
const edges = [];
|
|
7342
|
-
for (const parent of datamodel.models) {
|
|
7343
|
-
const pkFields = parent.fields.filter((f) => f.isId);
|
|
7344
|
-
if (pkFields.length === 0) continue;
|
|
7345
|
-
pkFields.map((f) => f.dbName || f.name);
|
|
7346
|
-
const parentTable = parent.dbName || parent.name;
|
|
7347
|
-
for (const f of parent.fields) {
|
|
7348
|
-
if (!f.relationName) continue;
|
|
7349
|
-
if (!f.isList) continue;
|
|
7350
|
-
const child = modelByName.get(f.type);
|
|
7351
|
-
if (!child) continue;
|
|
7352
|
-
const childRelField = child.fields.find(
|
|
7353
|
-
(cf) => cf.relationName === f.relationName && cf.type === parent.name
|
|
7354
|
-
);
|
|
7355
|
-
if (!childRelField) continue;
|
|
7356
|
-
const fkFieldNames = childRelField.relationFromFields || [];
|
|
7357
|
-
if (fkFieldNames.length === 0) continue;
|
|
7358
|
-
const fkFields = fkFieldNames.map((name) => {
|
|
7359
|
-
const fld = child.fields.find((x) => x.name === name);
|
|
7360
|
-
return fld ? fld.dbName || fld.name : name;
|
|
7361
|
-
});
|
|
7362
|
-
const refFieldNames = childRelField.relationToFields || [];
|
|
7363
|
-
if (refFieldNames.length === 0) continue;
|
|
7364
|
-
const references = refFieldNames.map((name) => {
|
|
7365
|
-
const fld = parent.fields.find((x) => x.name === name);
|
|
7366
|
-
return fld ? fld.dbName || fld.name : name;
|
|
7367
|
-
});
|
|
7368
|
-
if (fkFields.length !== references.length) continue;
|
|
7369
|
-
const childTable = child.dbName || child.name;
|
|
7370
|
-
edges.push({
|
|
7371
|
-
parentModel: parent.name,
|
|
7372
|
-
relName: f.name,
|
|
7373
|
-
childModel: child.name,
|
|
7374
|
-
parentTable,
|
|
7375
|
-
childTable,
|
|
7376
|
-
parentPkColumns: references,
|
|
7377
|
-
childFkColumns: fkFields,
|
|
7378
|
-
isMany: true
|
|
7379
|
-
});
|
|
7380
|
-
}
|
|
7381
|
-
}
|
|
7382
|
-
return edges;
|
|
7383
|
-
}
|
|
7384
|
-
function buildPostgresStatsSql(edge) {
|
|
7385
|
-
const childTable = quoteIdent("postgres", edge.childTable);
|
|
7386
|
-
const parentTable = quoteIdent("postgres", edge.parentTable);
|
|
7387
|
-
const groupCols = edge.childFkColumns.map((c) => quoteIdent("postgres", c)).join(", ");
|
|
7388
|
-
return `
|
|
7389
|
-
WITH counts AS (
|
|
7390
|
-
SELECT ${groupCols}, COUNT(*) AS cnt
|
|
7391
|
-
FROM ${childTable}
|
|
7392
|
-
GROUP BY ${groupCols}
|
|
7393
|
-
),
|
|
7394
|
-
total_parents AS (
|
|
7395
|
-
SELECT COUNT(*) AS total FROM ${parentTable}
|
|
7396
|
-
)
|
|
7397
|
-
SELECT
|
|
7398
|
-
AVG(cnt)::float AS avg,
|
|
7399
|
-
MAX(cnt)::int AS max,
|
|
7400
|
-
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY cnt)::float AS p95,
|
|
7401
|
-
PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY cnt)::float AS p99,
|
|
7402
|
-
(SELECT COUNT(*) FROM counts)::float / GREATEST(1, (SELECT total FROM total_parents)) AS coverage
|
|
7403
|
-
FROM counts
|
|
7404
|
-
`.trim();
|
|
7405
|
-
}
|
|
7406
|
-
function buildSqliteStatsSql(edge) {
|
|
7407
|
-
const childTable = quoteIdent("sqlite", edge.childTable);
|
|
7408
|
-
const parentTable = quoteIdent("sqlite", edge.parentTable);
|
|
7409
|
-
const groupCols = edge.childFkColumns.map((c) => quoteIdent("sqlite", c)).join(", ");
|
|
7410
|
-
return `
|
|
7411
|
-
WITH counts AS (
|
|
7412
|
-
SELECT ${groupCols}, COUNT(*) AS cnt
|
|
7413
|
-
FROM ${childTable}
|
|
7414
|
-
GROUP BY ${groupCols}
|
|
7415
|
-
),
|
|
7416
|
-
n AS (
|
|
7417
|
-
SELECT COUNT(*) AS total FROM counts
|
|
7418
|
-
),
|
|
7419
|
-
parent_n AS (
|
|
7420
|
-
SELECT COUNT(*) AS total FROM ${parentTable}
|
|
7421
|
-
),
|
|
7422
|
-
ordered AS (
|
|
7423
|
-
SELECT cnt
|
|
7424
|
-
FROM counts
|
|
7425
|
-
ORDER BY cnt
|
|
7426
|
-
)
|
|
7427
|
-
SELECT
|
|
7428
|
-
(SELECT AVG(cnt) FROM counts) AS avg,
|
|
7429
|
-
(SELECT MAX(cnt) FROM counts) AS max,
|
|
7430
|
-
(
|
|
7431
|
-
SELECT cnt
|
|
7432
|
-
FROM ordered
|
|
7433
|
-
LIMIT 1
|
|
7434
|
-
OFFSET (
|
|
7435
|
-
SELECT
|
|
7436
|
-
CASE
|
|
7437
|
-
WHEN total <= 1 THEN 0
|
|
7438
|
-
ELSE CAST((0.95 * (total - 1)) AS INT)
|
|
7439
|
-
END
|
|
7440
|
-
FROM n
|
|
7441
|
-
)
|
|
7442
|
-
) AS p95,
|
|
7443
|
-
(
|
|
7444
|
-
SELECT cnt
|
|
7445
|
-
FROM ordered
|
|
7446
|
-
LIMIT 1
|
|
7447
|
-
OFFSET (
|
|
7448
|
-
SELECT
|
|
7449
|
-
CASE
|
|
7450
|
-
WHEN total <= 1 THEN 0
|
|
7451
|
-
ELSE CAST((0.99 * (total - 1)) AS INT)
|
|
7452
|
-
END
|
|
7453
|
-
FROM n
|
|
7454
|
-
)
|
|
7455
|
-
) AS p99,
|
|
7456
|
-
CAST((SELECT total FROM n) AS FLOAT) / MAX(1, (SELECT total FROM parent_n)) AS coverage
|
|
7457
|
-
`.trim();
|
|
7458
|
-
}
|
|
7459
|
-
function buildFanoutStatsSql(dialect, edge) {
|
|
7460
|
-
return dialect === "postgres" ? buildPostgresStatsSql(edge) : buildSqliteStatsSql(edge);
|
|
7461
|
-
}
|
|
7462
|
-
function findLargestTable(params) {
|
|
7463
|
-
return __async(this, null, function* () {
|
|
7464
|
-
var _a3;
|
|
7465
|
-
const { executor, dialect, datamodel } = params;
|
|
7466
|
-
let best = null;
|
|
7467
|
-
for (const model of datamodel.models) {
|
|
7468
|
-
const table = quoteIdent(dialect, model.dbName || model.name);
|
|
7469
|
-
try {
|
|
7470
|
-
const rows = yield executor.query(`SELECT COUNT(*) AS cnt FROM ${table}`);
|
|
7471
|
-
const count = toNumberOrZero((_a3 = rows[0]) == null ? void 0 : _a3.cnt);
|
|
7472
|
-
if (!best || count > best.rowCount) {
|
|
7473
|
-
best = { tableName: table, rowCount: count };
|
|
7474
|
-
}
|
|
7475
|
-
} catch (_) {
|
|
7476
|
-
}
|
|
7477
|
-
}
|
|
7478
|
-
return best;
|
|
7479
|
-
});
|
|
7480
|
-
}
|
|
7481
|
-
function measureRoundtripCost(params) {
|
|
7482
|
-
return __async(this, null, function* () {
|
|
7483
|
-
var _a3, _b;
|
|
7484
|
-
const { executor, dialect, datamodel } = params;
|
|
7485
|
-
const WARMUP = 5;
|
|
7486
|
-
const SAMPLES = 15;
|
|
7487
|
-
for (let i = 0; i < WARMUP; i++) {
|
|
7488
|
-
yield executor.query("SELECT 1");
|
|
7489
|
-
}
|
|
7490
|
-
const roundtripTimes = [];
|
|
7491
|
-
for (let i = 0; i < SAMPLES; i++) {
|
|
7492
|
-
const start = performance.now();
|
|
7493
|
-
yield executor.query("SELECT 1");
|
|
7494
|
-
roundtripTimes.push(performance.now() - start);
|
|
7495
|
-
}
|
|
7496
|
-
roundtripTimes.sort((a, b) => a - b);
|
|
7497
|
-
const medianRoundtrip = roundtripTimes[Math.floor(SAMPLES / 2)];
|
|
7498
|
-
console.log(
|
|
7499
|
-
` [roundtrip] SELECT 1 times (ms): min=${roundtripTimes[0].toFixed(3)} median=${medianRoundtrip.toFixed(3)} max=${roundtripTimes[SAMPLES - 1].toFixed(3)}`
|
|
7500
|
-
);
|
|
7501
|
-
const largest = yield findLargestTable({ executor, dialect, datamodel });
|
|
7502
|
-
if (!largest || largest.rowCount < 50) {
|
|
7503
|
-
console.log(
|
|
7504
|
-
` [roundtrip] Largest table: ${(_a3 = largest == null ? void 0 : largest.tableName) != null ? _a3 : "none"} (${(_b = largest == null ? void 0 : largest.rowCount) != null ? _b : 0} rows) \u2014 too small, using default 50`
|
|
7505
|
-
);
|
|
7506
|
-
return 50;
|
|
7507
|
-
}
|
|
7508
|
-
console.log(
|
|
7509
|
-
` [roundtrip] Using table ${largest.tableName} (${largest.rowCount} rows)`
|
|
7510
|
-
);
|
|
7511
|
-
return estimateFromQueryPairRatio({
|
|
7512
|
-
executor,
|
|
7513
|
-
tableName: largest.tableName,
|
|
7514
|
-
tableRowCount: largest.rowCount
|
|
7515
|
-
});
|
|
7516
|
-
});
|
|
7517
|
-
}
|
|
7518
|
-
function estimateFromQueryPairRatio(params) {
|
|
7519
|
-
return __async(this, null, function* () {
|
|
7520
|
-
const { executor, tableName, tableRowCount } = params;
|
|
7521
|
-
const WARMUP = 5;
|
|
7522
|
-
const SAMPLES = 10;
|
|
7523
|
-
const smallLimit = 1;
|
|
7524
|
-
const largeLimit = Math.min(1e3, tableRowCount);
|
|
7525
|
-
for (let i = 0; i < WARMUP; i++) {
|
|
7526
|
-
yield executor.query(`SELECT * FROM ${tableName} LIMIT ${largeLimit}`);
|
|
7527
|
-
}
|
|
7528
|
-
const smallTimes = [];
|
|
7529
|
-
for (let i = 0; i < SAMPLES; i++) {
|
|
7530
|
-
const start = performance.now();
|
|
7531
|
-
yield executor.query(`SELECT * FROM ${tableName} LIMIT ${smallLimit}`);
|
|
7532
|
-
smallTimes.push(performance.now() - start);
|
|
7533
|
-
}
|
|
7534
|
-
smallTimes.sort((a, b) => a - b);
|
|
7535
|
-
const medianSmall = smallTimes[Math.floor(SAMPLES / 2)];
|
|
7536
|
-
const largeTimes = [];
|
|
7537
|
-
let actualLargeRows = 0;
|
|
7538
|
-
for (let i = 0; i < SAMPLES; i++) {
|
|
7539
|
-
const start = performance.now();
|
|
7540
|
-
const rows = yield executor.query(
|
|
7541
|
-
`SELECT * FROM ${tableName} LIMIT ${largeLimit}`
|
|
7542
|
-
);
|
|
7543
|
-
largeTimes.push(performance.now() - start);
|
|
7544
|
-
actualLargeRows = rows.length;
|
|
7545
|
-
}
|
|
7546
|
-
largeTimes.sort((a, b) => a - b);
|
|
7547
|
-
const medianLarge = largeTimes[Math.floor(SAMPLES / 2)];
|
|
7548
|
-
const rowDiff = actualLargeRows - smallLimit;
|
|
7549
|
-
const timeDiff = medianLarge - medianSmall;
|
|
7550
|
-
console.log(
|
|
7551
|
-
` [roundtrip] LIMIT ${smallLimit}: median=${medianSmall.toFixed(3)}ms`
|
|
7552
|
-
);
|
|
7553
|
-
console.log(
|
|
7554
|
-
` [roundtrip] LIMIT ${largeLimit} (got ${actualLargeRows}): median=${medianLarge.toFixed(3)}ms`
|
|
7555
|
-
);
|
|
7556
|
-
console.log(
|
|
7557
|
-
` [roundtrip] Time diff: ${timeDiff.toFixed(3)}ms for ${rowDiff} rows`
|
|
7558
|
-
);
|
|
7559
|
-
if (rowDiff < 50 || timeDiff <= 0.05) {
|
|
7560
|
-
console.log(
|
|
7561
|
-
` [roundtrip] Insufficient signal (need \u226550 row diff and >0.05ms time diff), defaulting to 50`
|
|
7562
|
-
);
|
|
7563
|
-
return 50;
|
|
7564
|
-
}
|
|
7565
|
-
const perRow = timeDiff / rowDiff;
|
|
7566
|
-
const sequentialTimes = [];
|
|
7567
|
-
for (let i = 0; i < SAMPLES; i++) {
|
|
7568
|
-
const start = performance.now();
|
|
7569
|
-
yield executor.query(`SELECT * FROM ${tableName} LIMIT ${smallLimit}`);
|
|
7570
|
-
yield executor.query(`SELECT * FROM ${tableName} LIMIT ${smallLimit}`);
|
|
7571
|
-
yield executor.query(`SELECT * FROM ${tableName} LIMIT ${smallLimit}`);
|
|
7572
|
-
sequentialTimes.push(performance.now() - start);
|
|
7573
|
-
}
|
|
7574
|
-
sequentialTimes.sort((a, b) => a - b);
|
|
7575
|
-
const median3Sequential = sequentialTimes[Math.floor(SAMPLES / 2)];
|
|
7576
|
-
const marginalQueryCost = (median3Sequential - medianSmall) / 2;
|
|
7577
|
-
console.log(
|
|
7578
|
-
` [roundtrip] 3x sequential LIMIT 1: median=${median3Sequential.toFixed(3)}ms`
|
|
7579
|
-
);
|
|
7580
|
-
console.log(` [roundtrip] Single query: ${medianSmall.toFixed(3)}ms`);
|
|
7581
|
-
console.log(
|
|
7582
|
-
` [roundtrip] Marginal query cost: ${marginalQueryCost.toFixed(3)}ms`
|
|
7583
|
-
);
|
|
7584
|
-
console.log(` [roundtrip] Per-row cost: ${perRow.toFixed(4)}ms`);
|
|
7585
|
-
const equivalent = Math.round(marginalQueryCost / perRow);
|
|
7586
|
-
console.log(` [roundtrip] Raw equivalent: ${equivalent} rows`);
|
|
7587
|
-
const clamped = Math.max(10, Math.min(500, equivalent));
|
|
7588
|
-
console.log(` [roundtrip] Final (clamped): ${clamped} rows`);
|
|
7589
|
-
return clamped;
|
|
7590
|
-
});
|
|
7591
|
-
}
|
|
7592
|
-
function measureJsonOverhead(params) {
|
|
7593
|
-
return __async(this, null, function* () {
|
|
7594
|
-
const { executor, tableName, tableRowCount } = params;
|
|
7595
|
-
const WARMUP = 3;
|
|
7596
|
-
const SAMPLES = 10;
|
|
7597
|
-
const limit = Math.min(500, tableRowCount);
|
|
7598
|
-
const rawSql = `SELECT * FROM ${tableName} LIMIT ${limit}`;
|
|
7599
|
-
const colsResult = yield executor.query(
|
|
7600
|
-
`SELECT column_name FROM information_schema.columns WHERE table_name = ${tableName.replace(/"/g, "'")} LIMIT 10`
|
|
7601
|
-
);
|
|
7602
|
-
let aggSql;
|
|
7603
|
-
if (colsResult.length >= 3) {
|
|
7604
|
-
const cols = colsResult.slice(0, 6).map((r) => `"${r.column_name}"`);
|
|
7605
|
-
const aggExprs = cols.map((c) => `array_agg(${c})`).join(", ");
|
|
7606
|
-
const groupCol = cols[0];
|
|
7607
|
-
aggSql = `SELECT ${groupCol}, ${aggExprs} FROM ${tableName} GROUP BY ${groupCol} LIMIT ${limit}`;
|
|
7608
|
-
} else {
|
|
7609
|
-
aggSql = `SELECT json_agg(t) FROM (SELECT * FROM ${tableName} LIMIT ${limit}) t`;
|
|
7610
|
-
}
|
|
7611
|
-
for (let i = 0; i < WARMUP; i++) {
|
|
7612
|
-
yield executor.query(rawSql);
|
|
7613
|
-
yield executor.query(aggSql);
|
|
7614
|
-
}
|
|
7615
|
-
const rawTimes = [];
|
|
7616
|
-
for (let i = 0; i < SAMPLES; i++) {
|
|
7617
|
-
const start = performance.now();
|
|
7618
|
-
yield executor.query(rawSql);
|
|
7619
|
-
rawTimes.push(performance.now() - start);
|
|
7620
|
-
}
|
|
7621
|
-
rawTimes.sort((a, b) => a - b);
|
|
7622
|
-
const medianRaw = rawTimes[Math.floor(SAMPLES / 2)];
|
|
7623
|
-
const aggTimes = [];
|
|
7624
|
-
for (let i = 0; i < SAMPLES; i++) {
|
|
7625
|
-
const start = performance.now();
|
|
7626
|
-
yield executor.query(aggSql);
|
|
7627
|
-
aggTimes.push(performance.now() - start);
|
|
7628
|
-
}
|
|
7629
|
-
aggTimes.sort((a, b) => a - b);
|
|
7630
|
-
const medianAgg = aggTimes[Math.floor(SAMPLES / 2)];
|
|
7631
|
-
const factor = medianRaw > 0.01 ? medianAgg / medianRaw : 3;
|
|
7632
|
-
console.log(` [json] Raw ${limit} rows: ${medianRaw.toFixed(3)}ms`);
|
|
7633
|
-
console.log(` [json] array_agg grouped: ${medianAgg.toFixed(3)}ms`);
|
|
7634
|
-
console.log(` [json] Overhead factor: ${factor.toFixed(2)}x`);
|
|
7635
|
-
return Math.max(1.5, Math.min(8, factor));
|
|
7636
|
-
});
|
|
7637
|
-
}
|
|
7638
|
-
function collectPostgresStatsFromCatalog(params) {
|
|
7639
|
-
return __async(this, null, function* () {
|
|
7640
|
-
const { executor, datamodel } = params;
|
|
7641
|
-
const edges = extractMeasurableOneToManyEdges(datamodel);
|
|
7642
|
-
const out = {};
|
|
7643
|
-
const tablesToAnalyze = /* @__PURE__ */ new Set();
|
|
7644
|
-
for (const edge of edges) {
|
|
7645
|
-
tablesToAnalyze.add(edge.parentTable);
|
|
7646
|
-
tablesToAnalyze.add(edge.childTable);
|
|
7647
|
-
}
|
|
7648
|
-
for (const table of tablesToAnalyze) {
|
|
7649
|
-
try {
|
|
7650
|
-
yield executor.query(`ANALYZE ${quoteIdent("postgres", table)}`);
|
|
7651
|
-
} catch (_) {
|
|
7652
|
-
}
|
|
7653
|
-
}
|
|
7654
|
-
const tableStatsQuery = `
|
|
7655
|
-
SELECT
|
|
7656
|
-
c.relname as table_name,
|
|
7657
|
-
c.reltuples::bigint as row_count
|
|
7658
|
-
FROM pg_class c
|
|
7659
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
7660
|
-
WHERE c.relkind = 'r'
|
|
7661
|
-
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
7662
|
-
`;
|
|
7663
|
-
const tableStats = yield executor.query(tableStatsQuery, []);
|
|
7664
|
-
const rowCounts = /* @__PURE__ */ new Map();
|
|
7665
|
-
for (const row of tableStats) {
|
|
7666
|
-
const tableName = String(row.table_name);
|
|
7667
|
-
const count = toNumberOrZero(row.row_count);
|
|
7668
|
-
rowCounts.set(tableName, count);
|
|
7669
|
-
}
|
|
7670
|
-
for (const edge of edges) {
|
|
7671
|
-
const parentRows = rowCounts.get(edge.parentTable) || 0;
|
|
7672
|
-
const childRows = rowCounts.get(edge.childTable) || 0;
|
|
7673
|
-
if (parentRows === 0 || childRows === 0) {
|
|
7674
|
-
if (!out[edge.parentModel]) out[edge.parentModel] = {};
|
|
7675
|
-
out[edge.parentModel][edge.relName] = {
|
|
7676
|
-
avg: 1,
|
|
7677
|
-
p95: 1,
|
|
7678
|
-
p99: 1,
|
|
7679
|
-
max: 1,
|
|
7680
|
-
coverage: 0
|
|
7681
|
-
};
|
|
7682
|
-
continue;
|
|
7683
|
-
}
|
|
7684
|
-
const fkColumn = edge.childFkColumns[0];
|
|
7685
|
-
const statsQuery = `
|
|
7686
|
-
SELECT
|
|
7687
|
-
s.n_distinct,
|
|
7688
|
-
s.correlation,
|
|
7689
|
-
(s.most_common_freqs)[1] as max_freq
|
|
7690
|
-
FROM pg_stats s
|
|
7691
|
-
WHERE s.tablename = $1
|
|
7692
|
-
AND s.attname = $2
|
|
7693
|
-
AND s.schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
7694
|
-
`;
|
|
7695
|
-
const statsRows = yield executor.query(statsQuery, [
|
|
7696
|
-
edge.childTable,
|
|
7697
|
-
fkColumn
|
|
7698
|
-
]);
|
|
7699
|
-
let avg;
|
|
7700
|
-
let p95;
|
|
7701
|
-
let p99;
|
|
7702
|
-
let max;
|
|
7703
|
-
let coverage;
|
|
7704
|
-
if (statsRows.length > 0) {
|
|
7705
|
-
const stats = statsRows[0];
|
|
7706
|
-
const nDistinct = toNumberOrZero(stats.n_distinct);
|
|
7707
|
-
const correlation = stats.correlation !== null ? Number(stats.correlation) : 0;
|
|
7708
|
-
const maxFreq = stats.max_freq !== null ? Number(stats.max_freq) : null;
|
|
7709
|
-
const distinctCount = nDistinct < 0 ? Math.abs(nDistinct) * childRows : nDistinct > 0 ? nDistinct : parentRows;
|
|
7710
|
-
avg = distinctCount > 0 ? childRows / distinctCount : childRows / parentRows;
|
|
7711
|
-
coverage = Math.min(1, distinctCount / parentRows);
|
|
7712
|
-
const skewFactor = Math.abs(correlation) > 0.5 ? 2.5 : 1.5;
|
|
7713
|
-
p95 = avg * skewFactor;
|
|
7714
|
-
p99 = avg * (skewFactor * 1.3);
|
|
7715
|
-
max = maxFreq ? Math.ceil(childRows * maxFreq) : Math.ceil(p99 * 1.5);
|
|
7716
|
-
} else {
|
|
7717
|
-
avg = childRows / parentRows;
|
|
7718
|
-
coverage = 1;
|
|
7719
|
-
p95 = avg * 2;
|
|
7720
|
-
p99 = avg * 3;
|
|
7721
|
-
max = avg * 5;
|
|
7722
|
-
}
|
|
7723
|
-
if (!out[edge.parentModel]) out[edge.parentModel] = {};
|
|
7724
|
-
out[edge.parentModel][edge.relName] = clampStatsMonotonic(
|
|
7725
|
-
Math.ceil(avg),
|
|
7726
|
-
Math.ceil(p95),
|
|
7727
|
-
Math.ceil(p99),
|
|
7728
|
-
Math.ceil(max),
|
|
7729
|
-
coverage
|
|
7730
|
-
);
|
|
7731
|
-
}
|
|
7732
|
-
return out;
|
|
7733
|
-
});
|
|
7734
|
-
}
|
|
7735
|
-
function collectPreciseCardinalities(params) {
|
|
7736
|
-
return __async(this, null, function* () {
|
|
7737
|
-
const { executor, datamodel, dialect } = params;
|
|
7738
|
-
const edges = extractMeasurableOneToManyEdges(datamodel);
|
|
7739
|
-
const out = {};
|
|
7740
|
-
for (const edge of edges) {
|
|
7741
|
-
const sql = buildFanoutStatsSql(dialect, edge);
|
|
7742
|
-
const rows = yield executor.query(sql, []);
|
|
7743
|
-
const row = rows[0] || {};
|
|
7744
|
-
const stats = normalizeStats(row);
|
|
7745
|
-
if (!out[edge.parentModel]) out[edge.parentModel] = {};
|
|
7746
|
-
out[edge.parentModel][edge.relName] = stats;
|
|
7747
|
-
}
|
|
7748
|
-
return out;
|
|
7749
|
-
});
|
|
7750
|
-
}
|
|
7751
|
-
function collectRelationCardinalities(params) {
|
|
7752
|
-
return __async(this, null, function* () {
|
|
7753
|
-
const { executor, datamodel, dialect, mode = "precise" } = params;
|
|
7754
|
-
if (dialect === "postgres" && mode === "fast") {
|
|
7755
|
-
const stats = yield collectPostgresStatsFromCatalog({ executor, datamodel });
|
|
7756
|
-
let allTrivial = true;
|
|
7757
|
-
for (const model of Object.values(stats)) {
|
|
7758
|
-
for (const rel of Object.values(model)) {
|
|
7759
|
-
if (rel.avg > 1 || rel.coverage > 0.5) {
|
|
7760
|
-
allTrivial = false;
|
|
7761
|
-
break;
|
|
7762
|
-
}
|
|
7763
|
-
}
|
|
7764
|
-
if (!allTrivial) break;
|
|
7765
|
-
}
|
|
7766
|
-
if (allTrivial && Object.keys(stats).length > 0) {
|
|
7767
|
-
console.warn("\u26A0 Catalog stats look stale, falling back to precise mode");
|
|
7768
|
-
return collectPreciseCardinalities({ executor, datamodel, dialect });
|
|
7769
|
-
}
|
|
7770
|
-
return stats;
|
|
7771
|
-
}
|
|
7772
|
-
return collectPreciseCardinalities({ executor, datamodel, dialect });
|
|
7773
|
-
});
|
|
7774
|
-
}
|
|
7775
|
-
function collectPlannerArtifacts(params) {
|
|
7776
|
-
return __async(this, null, function* () {
|
|
7777
|
-
const { executor, datamodel, dialect, mode } = params;
|
|
7778
|
-
const largest = yield findLargestTable({ executor, dialect, datamodel });
|
|
7779
|
-
const [relationStats, roundtripRowEquivalent, jsonRowFactor] = yield Promise.all([
|
|
7780
|
-
collectRelationCardinalities({ executor, datamodel, dialect, mode }),
|
|
7781
|
-
measureRoundtripCost({ executor, dialect, datamodel }),
|
|
7782
|
-
largest && largest.rowCount >= 50 && dialect === "postgres" ? measureJsonOverhead({
|
|
7783
|
-
executor,
|
|
7784
|
-
tableName: largest.tableName,
|
|
7785
|
-
tableRowCount: largest.rowCount
|
|
7786
|
-
}) : Promise.resolve(1.5)
|
|
7787
|
-
]);
|
|
7788
|
-
console.log(` Roundtrip cost: ~${roundtripRowEquivalent} row equivalents`);
|
|
7789
|
-
console.log(` JSON overhead factor: ${jsonRowFactor.toFixed(2)}x`);
|
|
7790
|
-
return { relationStats, roundtripRowEquivalent, jsonRowFactor };
|
|
7791
|
-
});
|
|
7792
|
-
}
|
|
7793
7282
|
function emitPlannerGeneratedModule(artifacts) {
|
|
7794
7283
|
return [
|
|
7795
7284
|
`export const RELATION_STATS = ${stableJson(artifacts.relationStats)} as const`,
|
|
@@ -7804,7 +7293,6 @@ function emitPlannerGeneratedModule(artifacts) {
|
|
|
7804
7293
|
}
|
|
7805
7294
|
|
|
7806
7295
|
// src/code-emitter.ts
|
|
7807
|
-
var DB_CONNECT_TIMEOUT_MS = 5e3;
|
|
7808
7296
|
function extractEnumMappings(datamodel) {
|
|
7809
7297
|
const mappings = {};
|
|
7810
7298
|
const fieldTypes = {};
|
|
@@ -7877,8 +7365,8 @@ function processAllModelDirectives(directiveResults, config) {
|
|
|
7877
7365
|
}
|
|
7878
7366
|
function generateClient(options) {
|
|
7879
7367
|
return __async(this, null, function* () {
|
|
7880
|
-
var _a3;
|
|
7881
|
-
const { datamodel, outputDir, config
|
|
7368
|
+
var _a3, _b;
|
|
7369
|
+
const { datamodel, outputDir, config } = options;
|
|
7882
7370
|
const runtimeImportPath = (_a3 = options.runtimeImportPath) != null ? _a3 : "prisma-sql";
|
|
7883
7371
|
setGlobalDialect(config.dialect);
|
|
7884
7372
|
const models = convertDMMFToModels(datamodel);
|
|
@@ -7893,28 +7381,11 @@ function generateClient(options) {
|
|
|
7893
7381
|
);
|
|
7894
7382
|
const absoluteOutputDir = resolve(process.cwd(), outputDir);
|
|
7895
7383
|
yield mkdir(absoluteOutputDir, { recursive: true });
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
"\u23ED Skipping planner stats collection (PRISMA_SQL_SKIP_PLANNER)"
|
|
7902
|
-
);
|
|
7903
|
-
} else {
|
|
7904
|
-
plannerArtifacts = yield collectPlannerWithTimeout(
|
|
7905
|
-
options,
|
|
7906
|
-
config,
|
|
7907
|
-
datasourceUrl
|
|
7908
|
-
);
|
|
7909
|
-
}
|
|
7910
|
-
}
|
|
7911
|
-
if (!plannerArtifacts) {
|
|
7912
|
-
plannerArtifacts = {
|
|
7913
|
-
relationStats: {},
|
|
7914
|
-
roundtripRowEquivalent: 73,
|
|
7915
|
-
jsonRowFactor: 1.5
|
|
7916
|
-
};
|
|
7917
|
-
}
|
|
7384
|
+
const plannerArtifacts = (_b = options.plannerArtifacts) != null ? _b : {
|
|
7385
|
+
relationStats: {},
|
|
7386
|
+
roundtripRowEquivalent: 73,
|
|
7387
|
+
jsonRowFactor: 1.5
|
|
7388
|
+
};
|
|
7918
7389
|
const plannerCode = emitPlannerGeneratedModule(plannerArtifacts);
|
|
7919
7390
|
const plannerPath = join(absoluteOutputDir, "planner.generated.ts");
|
|
7920
7391
|
yield writeFile(plannerPath, plannerCode);
|
|
@@ -7937,61 +7408,6 @@ function generateClient(options) {
|
|
|
7937
7408
|
console.log(`\u2713 Output: ${outputPath}`);
|
|
7938
7409
|
});
|
|
7939
7410
|
}
|
|
7940
|
-
var PLANNER_TOTAL_TIMEOUT_MS = 15e3;
|
|
7941
|
-
function collectPlannerWithTimeout(options, config, datasourceUrl) {
|
|
7942
|
-
return __async(this, null, function* () {
|
|
7943
|
-
const timeoutMs = Number(process.env.PRISMA_SQL_PLANNER_TIMEOUT_MS) || PLANNER_TOTAL_TIMEOUT_MS;
|
|
7944
|
-
let cleanup;
|
|
7945
|
-
let settled = false;
|
|
7946
|
-
const work = () => __async(null, null, function* () {
|
|
7947
|
-
let executor = options.executor;
|
|
7948
|
-
if (!executor && datasourceUrl) {
|
|
7949
|
-
const dbConn = yield createDatabaseExecutor({
|
|
7950
|
-
databaseUrl: datasourceUrl,
|
|
7951
|
-
dialect: config.dialect,
|
|
7952
|
-
connectTimeoutMs: DB_CONNECT_TIMEOUT_MS
|
|
7953
|
-
});
|
|
7954
|
-
executor = dbConn.executor;
|
|
7955
|
-
cleanup = dbConn.cleanup;
|
|
7956
|
-
}
|
|
7957
|
-
if (!executor) return void 0;
|
|
7958
|
-
console.log("\u{1F4CA} Collecting relation cardinalities and roundtrip cost...");
|
|
7959
|
-
return yield collectPlannerArtifacts({
|
|
7960
|
-
executor,
|
|
7961
|
-
datamodel: options.datamodel,
|
|
7962
|
-
dialect: config.dialect
|
|
7963
|
-
});
|
|
7964
|
-
});
|
|
7965
|
-
const timeout = new Promise((resolve3) => {
|
|
7966
|
-
const id = setTimeout(() => {
|
|
7967
|
-
settled = true;
|
|
7968
|
-
console.warn(
|
|
7969
|
-
`\u26A0 Planner stats collection timed out after ${timeoutMs}ms, using defaults`
|
|
7970
|
-
);
|
|
7971
|
-
resolve3(void 0);
|
|
7972
|
-
}, timeoutMs);
|
|
7973
|
-
if (typeof id === "object" && "unref" in id) id.unref();
|
|
7974
|
-
});
|
|
7975
|
-
try {
|
|
7976
|
-
const result = yield Promise.race([work(), timeout]);
|
|
7977
|
-
if (settled) return void 0;
|
|
7978
|
-
return result;
|
|
7979
|
-
} catch (error) {
|
|
7980
|
-
if (!settled) {
|
|
7981
|
-
console.warn(
|
|
7982
|
-
"\u26A0 Failed to collect planner stats:",
|
|
7983
|
-
error instanceof Error ? error.message : error
|
|
7984
|
-
);
|
|
7985
|
-
}
|
|
7986
|
-
return void 0;
|
|
7987
|
-
} finally {
|
|
7988
|
-
if (cleanup) {
|
|
7989
|
-
yield cleanup().catch(() => {
|
|
7990
|
-
});
|
|
7991
|
-
}
|
|
7992
|
-
}
|
|
7993
|
-
});
|
|
7994
|
-
}
|
|
7995
7411
|
function generateImports(runtimeImportPath) {
|
|
7996
7412
|
return `import {
|
|
7997
7413
|
buildSQL,
|
|
@@ -9076,9 +8492,7 @@ generatorHandler({
|
|
|
9076
8492
|
yield generateClient({
|
|
9077
8493
|
datamodel: dmmf.datamodel,
|
|
9078
8494
|
outputDir,
|
|
9079
|
-
config
|
|
9080
|
-
datasourceUrl
|
|
9081
|
-
});
|
|
8495
|
+
config});
|
|
9082
8496
|
console.info("\u2713 Generated SQL client successfully");
|
|
9083
8497
|
});
|
|
9084
8498
|
}
|