@vibeorm/sql 4.1.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ast.d.ts +221 -1
- package/dist/codecs.d.ts +2 -0
- package/dist/dialect.d.ts +118 -1
- package/dist/dialects/mysql-check-probe.d.ts +1 -0
- package/dist/dialects/mysql-generated-columns.d.ts +43 -0
- package/dist/dialects/mysql-index-expressions.d.ts +20 -0
- package/dist/dialects/mysql-row-rules.d.ts +8 -0
- package/dist/dialects/postgres-generated-columns.d.ts +30 -0
- package/dist/dialects/postgres-index-expressions.d.ts +10 -0
- package/dist/dialects/postgres-index-keys.d.ts +15 -0
- package/dist/dialects/postgres-row-rules.d.ts +25 -0
- package/dist/dialects/sqlite-generated-columns.d.ts +18 -0
- package/dist/dialects/sqlite-index-expressions.d.ts +23 -0
- package/dist/dialects/sqlite-row-rules.d.ts +8 -0
- package/dist/dialects/sqlite-table-definition.d.ts +25 -5
- package/dist/generated-columns.d.ts +18 -0
- package/dist/index-expressions.d.ts +61 -0
- package/dist/index.d.ts +19 -6
- package/dist/index.js +2358 -1079
- package/dist/index.js.map +32 -18
- package/dist/key-list.d.ts +83 -0
- package/dist/like.d.ts +17 -0
- package/dist/local-date.d.ts +9 -0
- package/dist/pg-array.d.ts +6 -0
- package/dist/predicate.d.ts +21 -7
- package/dist/query/index.js +59 -19
- package/dist/query/index.js.map +7 -7
- package/dist/query/state.d.ts +5 -0
- package/dist/query/types.d.ts +2 -0
- package/dist/row-rules.d.ts +57 -0
- package/dist/timestamp-identity.d.ts +8 -2
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/sql/src/timestamp-identity.ts
|
|
3
3
|
var identities = new WeakMap;
|
|
4
|
+
function canonicalKey(params) {
|
|
5
|
+
const fraction = /\.(\d+)/.exec(params.text)?.[1] ?? "";
|
|
6
|
+
const remainder = fraction.slice(3).replace(/0+$/, "");
|
|
7
|
+
const canonical = Date.prototype.toISOString.call(new Date(params.milliseconds)).replace(/(\.\d{3})Z$/, `$1${remainder}Z`);
|
|
8
|
+
return `date:${canonical}`;
|
|
9
|
+
}
|
|
10
|
+
function identityKey(identity) {
|
|
11
|
+
if (identity.key === undefined)
|
|
12
|
+
identity.key = canonicalKey({ milliseconds: identity.milliseconds, text: identity.text });
|
|
13
|
+
return identity.key;
|
|
14
|
+
}
|
|
4
15
|
function rememberTimestampIdentity(params) {
|
|
5
16
|
const { date, text } = params;
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
17
|
+
const milliseconds = Date.prototype.getTime.call(date);
|
|
18
|
+
if (!Number.isFinite(milliseconds))
|
|
19
|
+
Date.prototype.toISOString.call(date);
|
|
9
20
|
identities.set(date, {
|
|
10
|
-
milliseconds
|
|
21
|
+
milliseconds,
|
|
11
22
|
text,
|
|
12
|
-
|
|
13
|
-
key: params.key
|
|
23
|
+
numeric: params.numeric,
|
|
24
|
+
key: params.key,
|
|
14
25
|
kind: params.kind ?? "timestamp"
|
|
15
26
|
});
|
|
16
27
|
return date;
|
|
@@ -24,7 +35,8 @@ function identityOf(params) {
|
|
|
24
35
|
return stored;
|
|
25
36
|
}
|
|
26
37
|
function timestampIdentityKey(params) {
|
|
27
|
-
|
|
38
|
+
const identity = identityOf(params);
|
|
39
|
+
return identity !== undefined ? identityKey(identity) : `date:${Number.isFinite(Date.prototype.getTime.call(params.date)) ? Date.prototype.toISOString.call(params.date) : "invalid"}`;
|
|
28
40
|
}
|
|
29
41
|
function timestampIdentityText(params) {
|
|
30
42
|
const identity = identityOf(params);
|
|
@@ -44,7 +56,7 @@ function compareTimestampDates(params) {
|
|
|
44
56
|
return left?.numeric !== undefined && right?.numeric !== undefined ? rank(left.numeric, right.numeric) : null;
|
|
45
57
|
}
|
|
46
58
|
const clock = (identity) => {
|
|
47
|
-
const [, hours, minutes, seconds, offset] = identity.
|
|
59
|
+
const [, hours, minutes, seconds, offset] = identityKey(identity).split(":");
|
|
48
60
|
const [whole, fraction = ""] = seconds.split(".");
|
|
49
61
|
const local = BigInt(Number(hours) * 3600 + Number(minutes) * 60 + Number(whole)) * 1000000000n + BigInt(fraction.padEnd(9, "0"));
|
|
50
62
|
const zone = Number(offset);
|
|
@@ -142,39 +154,43 @@ function toPgArrayLiteral(params) {
|
|
|
142
154
|
}
|
|
143
155
|
function parsePgArrayLiteral(params) {
|
|
144
156
|
const literal = params.literal.startsWith("[") ? params.literal.replace(/^\[-?\d+:-?\d+\]=/, "") : params.literal;
|
|
145
|
-
if (literal.length < 2 ||
|
|
157
|
+
if (literal.length < 2 || literal.charCodeAt(0) !== 123 || literal.charCodeAt(literal.length - 1) !== 125)
|
|
146
158
|
return null;
|
|
147
|
-
const
|
|
148
|
-
if (
|
|
159
|
+
const end = literal.length - 1;
|
|
160
|
+
if (end === 1)
|
|
149
161
|
return [];
|
|
150
162
|
const out = [];
|
|
151
|
-
let index =
|
|
152
|
-
while (index <
|
|
153
|
-
if (
|
|
163
|
+
let index = 1;
|
|
164
|
+
while (index < end) {
|
|
165
|
+
if (literal.charCodeAt(index) === 34) {
|
|
154
166
|
let text = "";
|
|
155
167
|
index++;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
let run = index;
|
|
169
|
+
while (index < end) {
|
|
170
|
+
const code = literal.charCodeAt(index);
|
|
171
|
+
if (code === 92) {
|
|
172
|
+
text += literal.slice(run, index) + (index + 1 < end ? literal[index + 1] : "");
|
|
160
173
|
index += 2;
|
|
174
|
+
run = index;
|
|
161
175
|
continue;
|
|
162
176
|
}
|
|
163
|
-
|
|
164
|
-
if (char === '"')
|
|
177
|
+
if (code === 34)
|
|
165
178
|
break;
|
|
166
|
-
|
|
179
|
+
index++;
|
|
167
180
|
}
|
|
181
|
+
text += literal.slice(run, Math.min(index, end));
|
|
182
|
+
if (index < end)
|
|
183
|
+
index++;
|
|
168
184
|
out.push(text);
|
|
169
185
|
} else {
|
|
170
|
-
let
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
186
|
+
let stop = literal.indexOf(",", index);
|
|
187
|
+
if (stop === -1 || stop > end)
|
|
188
|
+
stop = end;
|
|
189
|
+
const text = literal.slice(index, stop);
|
|
175
190
|
out.push(text === "NULL" ? null : text);
|
|
191
|
+
index = stop;
|
|
176
192
|
}
|
|
177
|
-
if (
|
|
193
|
+
if (literal.charCodeAt(index) === 44)
|
|
178
194
|
index++;
|
|
179
195
|
}
|
|
180
196
|
return out;
|
|
@@ -235,7 +251,7 @@ function parseDimension(params) {
|
|
|
235
251
|
}
|
|
236
252
|
|
|
237
253
|
// packages/sql/src/codecs.ts
|
|
238
|
-
import { VibeError, invalidStoredData, timeOfDayType } from "@vibeorm/schema";
|
|
254
|
+
import { VibeError, invalidStoredData, isCanonicalLocalDate, timeOfDayType } from "@vibeorm/schema";
|
|
239
255
|
function enumMembershipCodec(params) {
|
|
240
256
|
const { model, field, enum: enumName } = params;
|
|
241
257
|
const members = new Set(params.members);
|
|
@@ -303,20 +319,33 @@ var ZONE_LESS_DATETIME = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9}
|
|
|
303
319
|
var PG_ZONED_DATETIME = /^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}(?::\d{2})?)(\.\d{1,9})?([+-]\d{2})(?::?(\d{2}))?$/;
|
|
304
320
|
function parseDateTimeText(params) {
|
|
305
321
|
const trimmed = params.text.trim();
|
|
322
|
+
const date = parseDateTimeInstant({ trimmed });
|
|
323
|
+
return date === undefined ? undefined : rememberTimestampIdentity({ date, text: trimmed });
|
|
324
|
+
}
|
|
325
|
+
function parseDateTimeInstant(params) {
|
|
326
|
+
const { trimmed } = params;
|
|
327
|
+
const length = trimmed.length;
|
|
328
|
+
if (length > 0 && trimmed.charCodeAt(length - 1) === 90 || (length === 25 || length === 29 && trimmed.charCodeAt(19) === 46) && trimmed.charCodeAt(10) === 84 && trimmed.charCodeAt(length - 3) === 58 && (trimmed.charCodeAt(length - 6) === 43 || trimmed.charCodeAt(length - 6) === 45)) {
|
|
329
|
+
const parsed = new Date(trimmed);
|
|
330
|
+
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
331
|
+
}
|
|
306
332
|
if (ZONE_LESS_DATETIME.test(trimmed)) {
|
|
307
333
|
const asUtc = new Date(`${trimmed.replace(" ", "T").replace(/(\.\d{3})\d+$/, "$1")}Z`);
|
|
308
334
|
if (!Number.isNaN(asUtc.getTime()))
|
|
309
|
-
return
|
|
335
|
+
return asUtc;
|
|
310
336
|
}
|
|
311
337
|
const zoned = PG_ZONED_DATETIME.exec(trimmed);
|
|
312
338
|
if (zoned !== null) {
|
|
313
339
|
const [, date, time, fraction, hours, minutes] = zoned;
|
|
314
340
|
const iso = new Date(`${date}T${time}${(fraction ?? "").slice(0, 4)}${hours}:${minutes ?? "00"}`);
|
|
315
341
|
if (!Number.isNaN(iso.getTime()))
|
|
316
|
-
return
|
|
342
|
+
return iso;
|
|
317
343
|
}
|
|
318
344
|
const parsed = new Date(trimmed);
|
|
319
|
-
return Number.isNaN(parsed.getTime()) ? undefined :
|
|
345
|
+
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
346
|
+
}
|
|
347
|
+
function invalidDecodedDateTime() {
|
|
348
|
+
throw new VibeError({ code: "VIBE_DATA_INVALID", message: "DateTime requires a finite timestamp; use an explicit extended timestamp decoder for infinity" });
|
|
320
349
|
}
|
|
321
350
|
var dateNativeCodec = {
|
|
322
351
|
encode: (value) => {
|
|
@@ -329,7 +358,7 @@ var dateNativeCodec = {
|
|
|
329
358
|
const date = value instanceof Date ? value : typeof value === "string" ? parseDateTimeText({ text: value }) : typeof value === "number" ? new Date(value) : undefined;
|
|
330
359
|
if (date !== undefined && Number.isFinite(date.getTime()))
|
|
331
360
|
return date;
|
|
332
|
-
|
|
361
|
+
return invalidDecodedDateTime();
|
|
333
362
|
}
|
|
334
363
|
};
|
|
335
364
|
var dateTextCodec = {
|
|
@@ -341,10 +370,16 @@ var dateTextCodec = {
|
|
|
341
370
|
throw new VibeError({ code: "VIBE_VALIDATION", message: "DateTime parameters require a valid finite timestamp" });
|
|
342
371
|
},
|
|
343
372
|
decode: (value) => {
|
|
373
|
+
if (typeof value === "string") {
|
|
374
|
+
const date = parseDateTimeInstant({ trimmed: value.trim() });
|
|
375
|
+
if (date === undefined || !Number.isFinite(date.getTime()))
|
|
376
|
+
return invalidDecodedDateTime();
|
|
377
|
+
return rememberTimestampIdentity({ date, text: value, key: `date:${value}`, kind: "sqlite" });
|
|
378
|
+
}
|
|
344
379
|
const date = dateNativeCodec.decode(value);
|
|
345
380
|
if (typeof value === "number")
|
|
346
381
|
return rememberTimestampIdentity({ date, text: date.toISOString(), numeric: value, key: `epoch:${value}`, kind: "sqlite" });
|
|
347
|
-
return
|
|
382
|
+
return date;
|
|
348
383
|
}
|
|
349
384
|
};
|
|
350
385
|
var mysqlDateCodec = {
|
|
@@ -416,6 +451,28 @@ function timeOfDayCodec(params) {
|
|
|
416
451
|
const type = timeOfDayType({ nativeType: params.nativeType });
|
|
417
452
|
return type === "timetz" ? timetzCodec : type === "time" ? timeCodec : undefined;
|
|
418
453
|
}
|
|
454
|
+
var localDateCodec = Object.freeze({
|
|
455
|
+
encode: (value) => {
|
|
456
|
+
const candidate = { value };
|
|
457
|
+
if (isCanonicalLocalDate(candidate))
|
|
458
|
+
return candidate.value;
|
|
459
|
+
throw new VibeError({
|
|
460
|
+
code: "VIBE_VALIDATION",
|
|
461
|
+
message: "A local date requires canonical YYYY-MM-DD text for a real date in years 0001\u20139999",
|
|
462
|
+
meta: { value }
|
|
463
|
+
});
|
|
464
|
+
},
|
|
465
|
+
decode: (value) => {
|
|
466
|
+
const candidate = { value };
|
|
467
|
+
if (isCanonicalLocalDate(candidate))
|
|
468
|
+
return candidate.value;
|
|
469
|
+
throw new VibeError({
|
|
470
|
+
code: "VIBE_DATA_INVALID",
|
|
471
|
+
message: "A local-date column returned noncanonical or out-of-domain calendar-day text",
|
|
472
|
+
meta: { value }
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
});
|
|
419
476
|
var booleanIntCodec = {
|
|
420
477
|
encode: (value) => typeof value === "boolean" ? value ? 1 : 0 : value,
|
|
421
478
|
decode: (value) => typeof value === "number" ? value !== 0 : value
|
|
@@ -1096,7 +1153,7 @@ function renderPostgresPhysicalCatalog(params) {
|
|
|
1096
1153
|
text = `SELECT table_name FROM information_schema.views WHERE table_schema = ${namespace} AND ${extensionOwnedFilter} ORDER BY table_name`;
|
|
1097
1154
|
break;
|
|
1098
1155
|
case "columns":
|
|
1099
|
-
text = `SELECT table_name, column_name, data_type, udt_name, udt_schema, is_nullable, is_identity, is_generated, collation_name, column_default, datetime_precision, character_maximum_length, numeric_precision, numeric_scale, ordinal_position, (SELECT format_type(a.atttypid, a.atttypmod) FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = ${namespace} AND c.relname = columns.table_name AND a.attname = columns.column_name
|
|
1156
|
+
text = `SELECT table_name, column_name, data_type, udt_name, udt_schema, is_nullable, is_identity, is_generated, collation_name, column_default, datetime_precision, character_maximum_length, numeric_precision, numeric_scale, ordinal_position, catalog.full_type, catalog.generated_kind, catalog.generation_expression FROM information_schema.columns AS columns LEFT JOIN LATERAL (SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) AS full_type, a.attgenerated::text AS generated_kind, CASE WHEN a.attgenerated <> '' THEN pg_catalog.pg_get_expr(d.adbin, d.adrelid, false) ELSE NULL END AS generation_expression FROM pg_catalog.pg_attribute a JOIN pg_catalog.pg_class c ON c.oid = a.attrelid JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum WHERE n.nspname = ${namespace} AND c.relname = columns.table_name AND a.attname = columns.column_name AND a.attnum > 0 AND NOT a.attisdropped) catalog ON true WHERE table_schema = ${namespace} ORDER BY table_name, ordinal_position`;
|
|
1100
1157
|
break;
|
|
1101
1158
|
case "constraints":
|
|
1102
1159
|
text = `SELECT c.conname AS constraint_name, CASE c.contype WHEN 'p' THEN 'PRIMARY KEY' ELSE 'UNIQUE' END AS constraint_type, cl.relname AS table_name, att.attname AS column_name FROM pg_catalog.pg_constraint c JOIN pg_catalog.pg_class cl ON cl.oid = c.conrelid JOIN pg_catalog.pg_namespace n ON n.oid = cl.relnamespace JOIN unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord) ON true JOIN pg_catalog.pg_attribute att ON att.attrelid = c.conrelid AND att.attnum = k.attnum WHERE c.contype IN ('p', 'u') AND n.nspname = ${namespace} ORDER BY cl.relname, c.conname, k.ord`;
|
|
@@ -1118,15 +1175,28 @@ function renderPostgresPhysicalCatalog(params) {
|
|
|
1118
1175
|
}
|
|
1119
1176
|
// packages/sql/src/dialects/postgres-index-keys.ts
|
|
1120
1177
|
import { unsupportedCapability as unsupportedCapability6 } from "@vibeorm/schema";
|
|
1178
|
+
function renderPostgresJitSuspend(params) {
|
|
1179
|
+
if (params.dialect.name !== "postgres")
|
|
1180
|
+
throw unsupportedCapability6({ dialect: params.dialect.name, feature: "indexOperatorClasses" });
|
|
1181
|
+
return { text: "SELECT setting.previous, pg_catalog.set_config('jit', 'off', true) AS current FROM (SELECT pg_catalog.current_setting('jit') AS previous OFFSET 0) setting", values: [], returning: "none" };
|
|
1182
|
+
}
|
|
1183
|
+
function renderPostgresJitRestore(params) {
|
|
1184
|
+
if (params.dialect.name !== "postgres")
|
|
1185
|
+
throw unsupportedCapability6({ dialect: params.dialect.name, feature: "indexOperatorClasses" });
|
|
1186
|
+
const collector = new ParamCollector(params.dialect);
|
|
1187
|
+
return { text: `SELECT pg_catalog.set_config('jit', ${collector.add(params.previous)}, true) AS current`, values: collector.values, returning: "none" };
|
|
1188
|
+
}
|
|
1121
1189
|
function renderPostgresIndexKeys(params) {
|
|
1122
1190
|
const { dialect } = params;
|
|
1123
1191
|
if (dialect.name !== "postgres")
|
|
1124
1192
|
throw unsupportedCapability6({ dialect: dialect.name, feature: "indexOperatorClasses" });
|
|
1125
1193
|
const collector = new ParamCollector(dialect);
|
|
1126
|
-
const scope = params.table !== undefined ? `n.nspname = ${collector.add(params.table.namespace)} AND c.relname = ${collector.add(params.table.name)}` : params.namespaces === undefined ? "n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg\\_%' ESCAPE '\\'" : params.namespaces.length === 0 ? "false" : `n.nspname IN (${[...new Set(params.namespaces)].map((namespace) => collector.add(namespace)).join(", ")})`;
|
|
1127
|
-
const defaultResolution = `LEFT JOIN LATERAL (WITH RECURSIVE input_types AS (SELECT t.oid, t.typbasetype, t.typtype, t.typelem, t.typcategory FROM pg_catalog.pg_type t WHERE t.oid = a.atttypid UNION ALL SELECT t.oid, t.typbasetype, t.typtype, t.typelem, t.typcategory FROM pg_catalog.pg_type t JOIN input_types previous ON t.oid = previous.typbasetype WHERE previous.typtype = 'd'), candidates AS (SELECT candidate.oid, CASE WHEN candidate.opcintype = input.oid THEN 0 WHEN class_input.typispreferred AND class_input.typcategory = input.typcategory THEN 1 ELSE 2 END AS priority FROM pg_catalog.pg_opclass candidate JOIN pg_catalog.pg_type class_input ON class_input.oid = candidate.opcintype CROSS JOIN input_types input WHERE input.typtype <> 'd' AND candidate.opcdefault AND candidate.opcmethod = idx.relam AND (candidate.opcintype = input.oid OR EXISTS (SELECT 1 FROM pg_catalog.pg_cast cast_entry WHERE cast_entry.castsource = input.oid AND cast_entry.casttarget = candidate.opcintype AND cast_entry.castmethod = 'b' AND cast_entry.castcontext = 'i') OR input.typtype = 'e' AND candidate.opcintype = 'pg_catalog.anyenum'::regtype OR input.typelem <> 0 AND candidate.opcintype = 'pg_catalog.anyarray'::regtype)) SELECT (array_agg(candidate.oid))[1] AS oid FROM candidates candidate WHERE candidate.priority = (SELECT min(priority) FROM candidates) HAVING count(*) = 1) resolved_default ON true`;
|
|
1194
|
+
const scope = params.table !== undefined ? `${params.expressionMetadata && params.table.namespace === "pg_temp" ? "n.oid = pg_catalog.pg_my_temp_schema()" : `n.nspname = ${collector.add(params.table.namespace)}`} AND c.relname = ${collector.add(params.table.name)}` : params.namespaces === undefined ? "n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg\\_%' ESCAPE '\\'" : params.namespaces.length === 0 ? "false" : `n.nspname IN (${[...new Set(params.namespaces)].map((namespace) => collector.add(namespace)).join(", ")})`;
|
|
1195
|
+
const defaultResolution = `LEFT JOIN LATERAL (WITH RECURSIVE input_types AS (SELECT t.oid, t.typbasetype, t.typtype, t.typelem, t.typcategory FROM pg_catalog.pg_type t WHERE t.oid = ${params.expressionMetadata ? "key_attribute.atttypid" : "a.atttypid"} UNION ALL SELECT t.oid, t.typbasetype, t.typtype, t.typelem, t.typcategory FROM pg_catalog.pg_type t JOIN input_types previous ON t.oid = previous.typbasetype WHERE previous.typtype = 'd'), candidates AS (SELECT candidate.oid, CASE WHEN candidate.opcintype = input.oid THEN 0 WHEN class_input.typispreferred AND class_input.typcategory = input.typcategory THEN 1 ELSE 2 END AS priority FROM pg_catalog.pg_opclass candidate JOIN pg_catalog.pg_type class_input ON class_input.oid = candidate.opcintype CROSS JOIN input_types input WHERE input.typtype <> 'd' AND candidate.opcdefault AND candidate.opcmethod = idx.relam AND (candidate.opcintype = input.oid OR EXISTS (SELECT 1 FROM pg_catalog.pg_cast cast_entry WHERE cast_entry.castsource = input.oid AND cast_entry.casttarget = candidate.opcintype AND cast_entry.castmethod = 'b' AND cast_entry.castcontext = 'i') OR input.typtype = 'e' AND candidate.opcintype = 'pg_catalog.anyenum'::regtype OR input.typelem <> 0 AND candidate.opcintype = 'pg_catalog.anyarray'::regtype)) SELECT (array_agg(candidate.oid))[1] AS oid FROM candidates candidate WHERE candidate.priority = (SELECT min(priority) FROM candidates) HAVING count(*) = 1) resolved_default ON true`;
|
|
1196
|
+
const expressionColumns = params.expressionMetadata ? `, CASE WHEN k.attnum = 0 THEN pg_catalog.pg_get_indexdef(i.indexrelid, k.ordinality::integer, false) ELSE NULL END AS expression, pg_catalog.pg_get_indexdef(i.indexrelid, 0, false) AS index_definition, pg_catalog.format_type(key_attribute.atttypid, key_attribute.atttypmod) AS result_type, (i.indoption[k.ordinality - 1] & 2) <> 0 AS nulls_first, ((i.indoption[k.ordinality - 1] & 2) <> 0) = ((i.indoption[k.ordinality - 1] & 1) <> 0) AS default_null_order, i.indcollation[k.ordinality - 1] AS collation_oid, CASE WHEN k.attnum = 0 THEN NULL ELSE i.indcollation[k.ordinality - 1] = a.attcollation END AS default_collation, i.indisvalid AS valid, i.indisready AS ready, i.indimmediate AS immediate, i.indnkeyatts AS key_count, i.indnatts AS attribute_count` : "";
|
|
1197
|
+
const expressionJoins = params.expressionMetadata ? ` JOIN pg_catalog.pg_attribute key_attribute ON key_attribute.attrelid = idx.oid AND key_attribute.attnum = k.ordinality` : "";
|
|
1128
1198
|
return {
|
|
1129
|
-
text: `SELECT n.nspname AS table_namespace, c.relname AS table_name, idx.relname AS index_name, am.amname AS method, i.indisunique AS unique, pg_catalog.pg_get_expr(i.indpred, i.indrelid, false) AS predicate, i.indnullsnotdistinct AS nulls_not_distinct, k.ordinality AS ordinal, a.attname AS column_name, (i.indoption[k.ordinality - 1] & 1) <> 0 AS descending, opn.nspname AS operator_namespace, op.opcname AS operator_name, COALESCE(op.oid = resolved_default.oid, false) AS exact_default FROM pg_catalog.pg_index i JOIN pg_catalog.pg_class c ON c.oid = i.indrelid JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace JOIN pg_catalog.pg_class idx ON idx.oid = i.indexrelid JOIN pg_catalog.pg_am am ON am.oid = idx.relam CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY k(attnum, ordinality) LEFT JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid AND a.attnum = k.attnum JOIN pg_catalog.pg_opclass op ON op.oid = i.indclass[k.ordinality - 1] JOIN pg_catalog.pg_namespace opn ON opn.oid = op.opcnamespace ${defaultResolution} WHERE ${scope} AND k.ordinality <= i.indnkeyatts ORDER BY n.nspname, c.relname, idx.relname, k.ordinality`,
|
|
1199
|
+
text: `SELECT n.nspname AS table_namespace, c.relname AS table_name, idx.relname AS index_name, am.amname AS method, i.indisunique AS unique, pg_catalog.pg_get_expr(i.indpred, i.indrelid, false) AS predicate, i.indnullsnotdistinct AS nulls_not_distinct, k.ordinality AS ordinal, a.attname AS column_name, (i.indoption[k.ordinality - 1] & 1) <> 0 AS descending, opn.nspname AS operator_namespace, op.opcname AS operator_name, COALESCE(op.oid = resolved_default.oid, false) AS exact_default${expressionColumns} FROM pg_catalog.pg_index i JOIN pg_catalog.pg_class c ON c.oid = i.indrelid JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace JOIN pg_catalog.pg_class idx ON idx.oid = i.indexrelid JOIN pg_catalog.pg_am am ON am.oid = idx.relam CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY k(attnum, ordinality) LEFT JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid AND a.attnum = k.attnum${expressionJoins} JOIN pg_catalog.pg_opclass op ON op.oid = i.indclass[k.ordinality - 1] JOIN pg_catalog.pg_namespace opn ON opn.oid = op.opcnamespace ${defaultResolution} WHERE ${scope} AND k.ordinality <= i.indnkeyatts ORDER BY n.nspname, c.relname, idx.relname, k.ordinality`,
|
|
1130
1200
|
values: collector.values,
|
|
1131
1201
|
returning: "none"
|
|
1132
1202
|
};
|
|
@@ -1149,1004 +1219,1524 @@ function renderIndexColumns(params) {
|
|
|
1149
1219
|
return `${dialect.quoteIdent({ name: column.name })}${ops}${column.desc ? " DESC" : ""}`;
|
|
1150
1220
|
});
|
|
1151
1221
|
}
|
|
1152
|
-
// packages/sql/src/
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
text: `WITH owned_tables AS (${owned})
|
|
1171
|
-
SELECT * FROM (SELECT ${pg ? "fk.conname" : "fk.CONSTRAINT_NAME"} AS constraint_name, ${sourceNamespace} AS source_namespace, ${sourceTable} AS source_table, ${targetNamespace} AS target_namespace, ${targetTable} AS target_table, ${isOwned({ namespace: sourceNamespace, table: sourceTable })} AS source_owned, ${isOwned({ namespace: targetNamespace, table: targetTable })} AS target_owned FROM ${from}) dependencies
|
|
1172
|
-
WHERE source_owned OR target_owned
|
|
1173
|
-
ORDER BY source_namespace, source_table, constraint_name`,
|
|
1174
|
-
values: collector.values,
|
|
1175
|
-
returning: "none"
|
|
1222
|
+
// packages/sql/src/index-expressions.ts
|
|
1223
|
+
import { VibeError as VibeError11, indexKeyDefinitionKey, indexKeyTerms, isExpressionIndex, unsupportedCapability as unsupportedCapability12 } from "@vibeorm/schema";
|
|
1224
|
+
|
|
1225
|
+
// packages/sql/src/predicate.ts
|
|
1226
|
+
import { VibeError as VibeError7, assertPredicateCapabilities, fieldTypeIdentity, isCanonicalLocalDate as isCanonicalLocalDate2, isEnumType, isLocalDateField, unsupportedCapability as unsupportedCapability8 } from "@vibeorm/schema";
|
|
1227
|
+
function renderPredicate(params) {
|
|
1228
|
+
assertPredicateCapabilities({ predicate: params.predicate, dialect: params.dialect.name, usage: params.usage, model: params.model });
|
|
1229
|
+
if (params.usage === "exclusionPredicate" && !params.dialect.capabilities.exclusionConstraints)
|
|
1230
|
+
throw unsupportedCapability8({ dialect: params.dialect.name, feature: "exclusionConstraints" });
|
|
1231
|
+
if (params.usage === "indexPredicate" && !params.dialect.capabilities.partialIndexes) {
|
|
1232
|
+
throw unsupportedCapability8({ dialect: params.dialect.name, feature: "partialIndexes" });
|
|
1233
|
+
}
|
|
1234
|
+
const context = {
|
|
1235
|
+
model: params.model,
|
|
1236
|
+
dialect: params.dialect,
|
|
1237
|
+
enums: params.enums ?? [],
|
|
1238
|
+
usage: params.usage,
|
|
1239
|
+
site: params.site
|
|
1176
1240
|
};
|
|
1241
|
+
return renderNode({ node: params.predicate, context });
|
|
1177
1242
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
const collector = new ParamCollector(dialect);
|
|
1184
|
-
const namespace = collector.add(params.namespace);
|
|
1185
|
-
if (dialect.name === "postgres")
|
|
1186
|
-
return { text: `SELECT 'type' AS kind, t.typname::text AS name, n.nspname::text AS namespace, NULL::text AS parent FROM pg_catalog.pg_type t JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace WHERE n.nspname = ${namespace} AND (t.typtype IN ('b', 'd', 'r', 'm') AND t.typelem = 0 OR t.typtype = 'c' AND EXISTS (SELECT 1 FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid AND c.relkind = 'c')) AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.classid = 'pg_type'::regclass AND d.objid = t.oid AND d.deptype = 'e')
|
|
1187
|
-
UNION ALL SELECT 'routine', p.proname::text, n.nspname::text, NULL::text FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = ${namespace} AND p.prokind <> 'f' AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.classid = 'pg_proc'::regclass AND d.objid = p.oid AND d.deptype = 'e')
|
|
1188
|
-
UNION ALL SELECT 'relation-layout', c.relname::text, n.nspname::text, NULL::text FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = ${namespace} AND (c.relkind IN ('p', 'f') OR c.relispartition OR EXISTS (SELECT 1 FROM pg_catalog.pg_inherits i WHERE i.inhrelid = c.oid)) AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.classid = 'pg_class'::regclass AND d.objid = c.oid AND d.deptype = 'e')
|
|
1189
|
-
UNION ALL SELECT 'rule', r.rulename::text, n.nspname::text, c.relname::text FROM pg_catalog.pg_rewrite r JOIN pg_catalog.pg_class c ON c.oid = r.ev_class JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = ${namespace} AND r.rulename <> '_RETURN' AND NOT ${extensionMember("pg_class", "c.oid")}
|
|
1190
|
-
UNION ALL SELECT 'namespace', n.nspname::text, n.nspname::text, NULL::text FROM pg_catalog.pg_namespace n WHERE n.nspname <> ${namespace} AND n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg\\_%' ESCAPE '\\' AND NOT (${extensionOwnedNamespace})${params.privateNamespace === undefined ? "" : ` AND n.nspname <> ${collector.add(params.privateNamespace)}`}`, values: collector.values, returning: "none" };
|
|
1191
|
-
if (dialect.name === "mysql")
|
|
1192
|
-
return { text: `SELECT 'procedure' AS kind, ROUTINE_NAME AS name, ROUTINE_SCHEMA AS namespace, NULL AS parent FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA = ${namespace} AND ROUTINE_TYPE = 'PROCEDURE'
|
|
1193
|
-
UNION ALL SELECT 'event', EVENT_NAME, EVENT_SCHEMA, NULL FROM information_schema.EVENTS WHERE EVENT_SCHEMA = ${collector.add(params.namespace)}
|
|
1194
|
-
UNION ALL SELECT DISTINCT 'partition', TABLE_NAME, TABLE_SCHEMA, NULL FROM information_schema.PARTITIONS WHERE TABLE_SCHEMA = ${collector.add(params.namespace)} AND PARTITION_NAME IS NOT NULL`, values: collector.values, returning: "none" };
|
|
1195
|
-
return { text: `SELECT 'table-options' AS kind, name, schema AS namespace, NULL AS parent FROM pragma_table_list WHERE schema = ${namespace} AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\' AND (type = 'virtual' OR wr <> 0 OR strict <> 0)`, values: collector.values, returning: "none" };
|
|
1243
|
+
function renderSchemaExpression({ expression, model, dialect, enums = [], usage, site }) {
|
|
1244
|
+
assertPredicateCapabilities({ expression, dialect: dialect.name, usage, model });
|
|
1245
|
+
const context = { model, dialect, enums, usage, site };
|
|
1246
|
+
const { text, resultType } = renderExpression({ expression, context });
|
|
1247
|
+
return { text, resultType };
|
|
1196
1248
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1249
|
+
var PRECEDENCE = {
|
|
1250
|
+
or: 1,
|
|
1251
|
+
and: 2,
|
|
1252
|
+
not: 3,
|
|
1253
|
+
compare: 4,
|
|
1254
|
+
inList: 4,
|
|
1255
|
+
isNull: 4,
|
|
1256
|
+
isTrue: 4
|
|
1257
|
+
};
|
|
1258
|
+
var COMPARISON_SQL = {
|
|
1259
|
+
eq: "=",
|
|
1260
|
+
ne: "<>",
|
|
1261
|
+
gt: ">",
|
|
1262
|
+
gte: ">=",
|
|
1263
|
+
lt: "<",
|
|
1264
|
+
lte: "<="
|
|
1265
|
+
};
|
|
1266
|
+
var EXACT_INTEGER = /^-?(0|[1-9]\d*)$/;
|
|
1267
|
+
var EXACT_DECIMAL = /^-?(0|[1-9]\d*)(\.\d+)?$/;
|
|
1268
|
+
var INT32_MIN = -2147483648n;
|
|
1269
|
+
var INT32_MAX = 2147483647n;
|
|
1270
|
+
var INT64_MIN = -(2n ** 63n);
|
|
1271
|
+
var INT64_MAX = 2n ** 63n - 1n;
|
|
1272
|
+
function refuse2(params) {
|
|
1273
|
+
const { context, rule, message } = params;
|
|
1274
|
+
throw new VibeError7({
|
|
1275
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
1276
|
+
message: `${context.site} on "${context.model.name}": ${message}`,
|
|
1277
|
+
meta: { rule, model: context.model.name, site: context.site, ...params.meta ?? {} }
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
function renderChild(params) {
|
|
1281
|
+
const { node, parent, context } = params;
|
|
1282
|
+
if ((node.kind === "and" || node.kind === "or") && node.operands.length === 1) {
|
|
1283
|
+
return renderChild({ node: node.operands[0], parent, context });
|
|
1284
|
+
}
|
|
1285
|
+
const text = renderNode({ node, context });
|
|
1286
|
+
return PRECEDENCE[node.kind] < PRECEDENCE[parent] ? `(${text})` : text;
|
|
1287
|
+
}
|
|
1288
|
+
function renderNode(params) {
|
|
1289
|
+
const { node, context } = params;
|
|
1290
|
+
switch (node.kind) {
|
|
1291
|
+
case "and":
|
|
1292
|
+
case "or": {
|
|
1293
|
+
if (node.operands.length === 0) {
|
|
1294
|
+
refuse2({
|
|
1295
|
+
context,
|
|
1296
|
+
rule: "predicate-empty",
|
|
1297
|
+
message: `${node.kind === "and" ? "and()" : "or()"} has no operands \u2014 an empty boolean group has no honest SQL spelling`
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
if (node.operands.length === 1)
|
|
1301
|
+
return renderNode({ node: node.operands[0], context });
|
|
1302
|
+
const separator = node.kind === "and" ? " AND " : " OR ";
|
|
1303
|
+
return node.operands.map((operand) => renderChild({ node: operand, parent: node.kind, context })).join(separator);
|
|
1235
1304
|
}
|
|
1236
|
-
case "
|
|
1237
|
-
return
|
|
1238
|
-
case "
|
|
1239
|
-
return
|
|
1240
|
-
case "
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
return result(pg ? `SELECT 'namespace' AS kind, nspname::text AS name, nspname::text AS namespace FROM pg_catalog.pg_namespace WHERE nspname NOT IN ('public', 'information_schema') AND nspname NOT LIKE 'pg\\_%' ESCAPE '\\'
|
|
1244
|
-
UNION ALL SELECT 'relation', c.relname::text, n.nspname::text FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg\\_%' ESCAPE '\\'
|
|
1245
|
-
UNION ALL SELECT 'routine', p.proname::text, n.nspname::text FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg\\_%' ESCAPE '\\'
|
|
1246
|
-
UNION ALL SELECT 'type', t.typname::text, n.nspname::text FROM pg_catalog.pg_type t JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace WHERE n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg\\_%' ESCAPE '\\' AND t.typrelid = 0 AND t.typelem = 0
|
|
1247
|
-
UNION ALL SELECT 'extension', e.extname::text, n.nspname::text FROM pg_catalog.pg_extension e JOIN pg_catalog.pg_namespace n ON n.oid = e.extnamespace WHERE e.extname <> 'plpgsql'
|
|
1248
|
-
UNION ALL SELECT 'marker-dependency', d.objid::text, '${SHADOW_DATABASE.schema}' FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_class c ON d.classid = 'pg_class'::regclass AND d.objid = c.oid JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = '${SHADOW_DATABASE.schema}' AND d.deptype NOT IN ('i', 'a') AND d.refclassid <> 'pg_namespace'::regclass
|
|
1249
|
-
UNION ALL SELECT 'marker-trigger', t.tgname::text, n.nspname::text FROM pg_catalog.pg_trigger t JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = '${SHADOW_DATABASE.schema}'
|
|
1250
|
-
UNION ALL SELECT 'namespace-object', o.name::text, n.nspname::text FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_namespace n ON d.refclassid = 'pg_namespace'::regclass AND d.refobjid = n.oid CROSS JOIN LATERAL pg_catalog.pg_identify_object(d.classid, d.objid, d.objsubid) o WHERE n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg\\_%' ESCAPE '\\' AND d.classid NOT IN ('pg_class'::regclass, 'pg_proc'::regclass, 'pg_type'::regclass, 'pg_extension'::regclass)
|
|
1251
|
-
UNION ALL SELECT 'database-object', evtname::text, '' FROM pg_catalog.pg_event_trigger
|
|
1252
|
-
UNION ALL SELECT 'database-object', pubname::text, '' FROM pg_catalog.pg_publication
|
|
1253
|
-
UNION ALL SELECT 'database-object', subname::text, '' FROM pg_catalog.pg_subscription WHERE subdbid = (SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database())
|
|
1254
|
-
UNION ALL SELECT 'database-object', srvname::text, '' FROM pg_catalog.pg_foreign_server
|
|
1255
|
-
UNION ALL SELECT 'database-object', fdwname::text, '' FROM pg_catalog.pg_foreign_data_wrapper
|
|
1256
|
-
UNION ALL SELECT 'database-object', c.oid::text, '' FROM pg_catalog.pg_cast c JOIN pg_catalog.pg_type s ON s.oid = c.castsource JOIN pg_catalog.pg_type t ON t.oid = c.casttarget LEFT JOIN pg_catalog.pg_proc p ON p.oid = c.castfunc WHERE s.typnamespace <> 'pg_catalog'::regnamespace OR t.typnamespace <> 'pg_catalog'::regnamespace OR p.pronamespace <> 'pg_catalog'::regnamespace` : `SELECT TABLE_TYPE AS kind, TABLE_NAME AS name, TABLE_SCHEMA AS namespace FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()
|
|
1257
|
-
UNION ALL SELECT ROUTINE_TYPE, ROUTINE_NAME, ROUTINE_SCHEMA FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA = DATABASE()
|
|
1258
|
-
UNION ALL SELECT 'EVENT', EVENT_NAME, EVENT_SCHEMA FROM information_schema.EVENTS WHERE EVENT_SCHEMA = DATABASE()
|
|
1259
|
-
UNION ALL SELECT 'TRIGGER', TRIGGER_NAME, TRIGGER_SCHEMA FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = DATABASE()
|
|
1260
|
-
UNION ALL SELECT 'EXTERNAL FOREIGN KEY', CONCAT(TABLE_NAME, '.', CONSTRAINT_NAME), CONSTRAINT_SCHEMA FROM information_schema.REFERENTIAL_CONSTRAINTS WHERE UNIQUE_CONSTRAINT_SCHEMA = DATABASE() AND CAST(CONSTRAINT_SCHEMA AS BINARY) <> CAST(DATABASE() AS BINARY)`);
|
|
1261
|
-
case "resetPostgres":
|
|
1262
|
-
case "resetDedicatedPostgres": {
|
|
1263
|
-
if (!pg)
|
|
1264
|
-
throw unsupportedCapability8({ dialect: dialect.name, feature: "postgresShadowReset" });
|
|
1265
|
-
const owned = operation.kind === "resetDedicatedPostgres" ? operation.ownedNamespaces : [];
|
|
1266
|
-
for (const namespace of owned)
|
|
1267
|
-
assertSqlNamespace({ namespace, dialect: "postgres", allowPublic: false });
|
|
1268
|
-
return [
|
|
1269
|
-
...owned.map((namespace) => ({ text: `DROP SCHEMA IF EXISTS ${dialect.quoteIdent({ name: namespace })} CASCADE`, values: [], returning: "none" })),
|
|
1270
|
-
{ text: 'DROP SCHEMA "public" CASCADE', values: [], returning: "none" },
|
|
1271
|
-
{ text: 'CREATE SCHEMA "public"', values: [], returning: "none" }
|
|
1272
|
-
];
|
|
1305
|
+
case "not":
|
|
1306
|
+
return `NOT ${renderChild({ node: node.operand, parent: "not", context })}`;
|
|
1307
|
+
case "isTrue":
|
|
1308
|
+
return `COALESCE(${renderNode({ node: node.operand, context })}, FALSE)`;
|
|
1309
|
+
case "isNull": {
|
|
1310
|
+
const field = resolveField({ name: node.field.name, context, nullTest: true });
|
|
1311
|
+
return `${quoteColumn({ field, context })} IS ${node.negate === true ? "NOT NULL" : "NULL"}`;
|
|
1273
1312
|
}
|
|
1274
|
-
case "
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
return
|
|
1313
|
+
case "compare": {
|
|
1314
|
+
const left = renderExpression({ expression: node.left, context });
|
|
1315
|
+
const right = renderOperand({ operand: node.right, left, context });
|
|
1316
|
+
return `${left.text} ${COMPARISON_SQL[node.op]} ${right}`;
|
|
1278
1317
|
}
|
|
1279
|
-
case "
|
|
1280
|
-
if (
|
|
1281
|
-
|
|
1282
|
-
|
|
1318
|
+
case "inList": {
|
|
1319
|
+
if (node.values.length === 0) {
|
|
1320
|
+
refuse2({
|
|
1321
|
+
context,
|
|
1322
|
+
rule: "predicate-empty",
|
|
1323
|
+
message: `inList("${node.field.name}") has no values \u2014 an empty IN list is UNKNOWN on every row and never what an author means`,
|
|
1324
|
+
meta: { field: node.field.name }
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
const field = resolveField({ name: node.field.name, context });
|
|
1328
|
+
const values = node.values.map((value) => renderLiteral({ value, resultType: fieldResultType({ field, context }), fieldName: field.name, context })).join(", ");
|
|
1329
|
+
return `${compareColumn({ field, context })} ${node.negate === true ? "NOT IN" : "IN"} (${values})`;
|
|
1283
1330
|
}
|
|
1284
1331
|
}
|
|
1285
1332
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
case "columns":
|
|
1297
|
-
text = dialect.name === "sqlite" ? `PRAGMA table_info(${table})` : `SELECT column_name AS name FROM information_schema.columns WHERE table_schema = ${dialect.name === "postgres" ? "current_schema()" : "DATABASE()"} AND table_name = ${collector.add(params.table)}`;
|
|
1298
|
-
break;
|
|
1299
|
-
case "addColumn":
|
|
1300
|
-
text = `ALTER TABLE ${table} ADD COLUMN ${column} ${dialect.name === "mysql" ? "LONGTEXT" : "TEXT"}`;
|
|
1301
|
-
break;
|
|
1302
|
-
case "read":
|
|
1303
|
-
text = `SELECT * FROM ${table} ORDER BY ${q("id")}`;
|
|
1304
|
-
break;
|
|
1305
|
-
case "insert":
|
|
1306
|
-
text = `INSERT INTO ${table} (${["name", "checksum", "statement_index", MODULE_JOURNAL_COLUMN].map(q).join(", ")}) VALUES (${collector.addMany([operation.name, operation.checksum, operation.index, operation.entry]).join(", ")})`;
|
|
1307
|
-
break;
|
|
1308
|
-
case "complete":
|
|
1309
|
-
text = `UPDATE ${table} SET ${column} = ${collector.add(operation.entry)} WHERE ${q("name")} = ${collector.add(operation.name)}`;
|
|
1310
|
-
break;
|
|
1333
|
+
function resolveField(params) {
|
|
1334
|
+
const { name, context } = params;
|
|
1335
|
+
const field = context.model.fields.find((candidate) => candidate.name === name);
|
|
1336
|
+
if (field === undefined) {
|
|
1337
|
+
refuse2({
|
|
1338
|
+
context,
|
|
1339
|
+
rule: "predicate-unknown-field",
|
|
1340
|
+
message: `references field "${name}", which the model does not declare`,
|
|
1341
|
+
meta: { field: name }
|
|
1342
|
+
});
|
|
1311
1343
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
SQLITE_CAPABILITIES,
|
|
1319
|
-
MYSQL_CAPABILITIES
|
|
1320
|
-
} from "@vibeorm/schema";
|
|
1321
|
-
|
|
1322
|
-
// packages/sql/src/identifiers.ts
|
|
1323
|
-
import { VibeError as VibeError7 } from "@vibeorm/schema";
|
|
1324
|
-
function assertNoNul(params) {
|
|
1325
|
-
if (params.name.includes("\x00")) {
|
|
1326
|
-
throw new VibeError7({
|
|
1327
|
-
code: "VIBE_VALIDATION",
|
|
1328
|
-
message: "an SQL identifier cannot contain a NUL character \u2014 the server would stop reading it there and address a different object",
|
|
1329
|
-
meta: { name: params.name.replaceAll("\x00", "\\0") }
|
|
1344
|
+
if (field.isList) {
|
|
1345
|
+
refuse2({
|
|
1346
|
+
context,
|
|
1347
|
+
rule: "predicate-list-field",
|
|
1348
|
+
message: `compares scalar list "${name}" \u2014 array operators differ per dialect; use a raw expression for them`,
|
|
1349
|
+
meta: { field: name }
|
|
1330
1350
|
});
|
|
1331
1351
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
},
|
|
1348
|
-
excludedRowRef: { form: "keyword", name: "excluded" },
|
|
1349
|
-
doNothingForm: "onConflictDoNothing",
|
|
1350
|
-
insensitiveLike: "lower",
|
|
1351
|
-
strictMatchStrategy: "glob",
|
|
1352
|
-
greatestForm: "frame",
|
|
1353
|
-
orderByNullsMode: "native",
|
|
1354
|
-
offsetRequiresLimit: { limitText: "-1" },
|
|
1355
|
-
likeEscapeClause: " ESCAPE '\\'",
|
|
1356
|
-
jsonPathMode: "sqliteExtract",
|
|
1357
|
-
supportedJsonOps: [
|
|
1358
|
-
"equals",
|
|
1359
|
-
"string_contains",
|
|
1360
|
-
"string_starts_with",
|
|
1361
|
-
"string_ends_with",
|
|
1362
|
-
"array_contains",
|
|
1363
|
-
"array_starts_with",
|
|
1364
|
-
"array_ends_with"
|
|
1365
|
-
],
|
|
1366
|
-
jsonParamCast: "",
|
|
1367
|
-
valueCastTypes: Object.freeze({ String: "TEXT", Boolean: "INTEGER", Int: "INTEGER", BigInt: "INTEGER", Float: "REAL", Decimal: "TEXT", DateTime: "TEXT", Json: "TEXT", Bytes: "BLOB" }),
|
|
1368
|
-
lastInsertIdSql: null,
|
|
1369
|
-
rowLockSyntax: null,
|
|
1370
|
-
advisoryLockFunctions: null,
|
|
1371
|
-
rowDefaultKeyword: false,
|
|
1372
|
-
statementChain: null,
|
|
1373
|
-
paramBudget: 900,
|
|
1374
|
-
booleanLiteral: { true: "1", false: "0" },
|
|
1375
|
-
quoteIdent({ name }) {
|
|
1376
|
-
return `"${assertNoNul({ name }).replaceAll('"', '""')}"`;
|
|
1377
|
-
},
|
|
1378
|
-
quoteStringLiteral({ value }) {
|
|
1379
|
-
return `'${value.replaceAll("'", "''")}'`;
|
|
1380
|
-
},
|
|
1381
|
-
placeholder() {
|
|
1382
|
-
return "?";
|
|
1383
|
-
},
|
|
1384
|
-
jsonTextProjection({ expression }) {
|
|
1385
|
-
return expression;
|
|
1386
|
-
},
|
|
1387
|
-
renderDbNow({ offsetSeconds, collector }) {
|
|
1388
|
-
if (offsetSeconds === 0)
|
|
1389
|
-
return `strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
|
|
1390
|
-
return `strftime('%Y-%m-%dT%H:%M:%fZ', 'now', ${collector.add(offsetSeconds)} || ' seconds')`;
|
|
1352
|
+
if (field.isUnsupported === true) {
|
|
1353
|
+
refuse2({
|
|
1354
|
+
context,
|
|
1355
|
+
rule: "predicate-unsupported-type",
|
|
1356
|
+
message: `references "${name}", an Unsupported("${field.nativeType ?? "?"}") column whose operators VibeORM does not model \u2014 use a raw expression`,
|
|
1357
|
+
meta: { field: name }
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
if (params.nullTest !== true && (field.type === "Json" || field.type === "Bytes")) {
|
|
1361
|
+
refuse2({
|
|
1362
|
+
context,
|
|
1363
|
+
rule: "predicate-unsupported-type",
|
|
1364
|
+
message: `references ${field.type} column "${name}" \u2014 ${field.type} comparison operators differ per dialect; use a raw expression`,
|
|
1365
|
+
meta: { field: name, type: field.type }
|
|
1366
|
+
});
|
|
1391
1367
|
}
|
|
1368
|
+
if (context.usage === "check" && context.dialect.name === "mysql" && field.default?.kind === "autoincrement") {
|
|
1369
|
+
throw unsupportedCapability8({
|
|
1370
|
+
dialect: "mysql",
|
|
1371
|
+
feature: `CHECK constraint referencing auto-increment column "${context.model.name}.${name}"`,
|
|
1372
|
+
hint: "mysql refuses this with error 3818 \u2014 enforce the rule in application code or on a non-generated column"
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
return field;
|
|
1376
|
+
}
|
|
1377
|
+
function quoteColumn(params) {
|
|
1378
|
+
const { field, context } = params;
|
|
1379
|
+
return context.dialect.quoteIdent({ name: field.dbName ?? field.name });
|
|
1380
|
+
}
|
|
1381
|
+
function compareColumn(params) {
|
|
1382
|
+
const { field, context } = params;
|
|
1383
|
+
const quoted = quoteColumn({ field, context });
|
|
1384
|
+
return context.dialect.name === "sqlite" && field.type === "Decimal" ? `CAST(${quoted} AS REAL)` : quoted;
|
|
1385
|
+
}
|
|
1386
|
+
function fieldResultType({ field, context }) {
|
|
1387
|
+
if (isLocalDateField({ field }))
|
|
1388
|
+
return { type: field.type, refinement: "localDate" };
|
|
1389
|
+
if (context.dialect.name === "postgres" && field.type === "String" && field.nativeType?.trim().toLowerCase() === "uuid")
|
|
1390
|
+
return { type: "String", refinement: "nativeUuid" };
|
|
1391
|
+
return { type: field.type };
|
|
1392
|
+
}
|
|
1393
|
+
function resultIdentity({ resultType }) {
|
|
1394
|
+
return resultType.refinement === "localDate" ? "localDate" : fieldTypeIdentity({ type: resultType.type });
|
|
1395
|
+
}
|
|
1396
|
+
var ARITHMETIC_SQL = {
|
|
1397
|
+
add: "+",
|
|
1398
|
+
sub: "-",
|
|
1399
|
+
mul: "*"
|
|
1392
1400
|
};
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
function
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
if (quote !== "]" && sql[index + 1] === quote)
|
|
1409
|
-
index++;
|
|
1410
|
-
else
|
|
1411
|
-
quote = undefined;
|
|
1401
|
+
function renderSqliteArithmetic({ left, right, kind }) {
|
|
1402
|
+
return `(${left} ${ARITHMETIC_SQL[kind]} ${right})`;
|
|
1403
|
+
}
|
|
1404
|
+
function renderExpression({ expression, context }) {
|
|
1405
|
+
switch (expression.kind) {
|
|
1406
|
+
case "field": {
|
|
1407
|
+
const field = resolveField({ name: expression.name, context });
|
|
1408
|
+
return { text: compareColumn({ field, context }), resultType: fieldResultType({ field, context }), fieldName: field.name };
|
|
1409
|
+
}
|
|
1410
|
+
case "jsonText": {
|
|
1411
|
+
const field = resolveField({ name: expression.field.name, context, nullTest: true });
|
|
1412
|
+
if (field.type !== "Json")
|
|
1413
|
+
refuse2({ context, rule: "predicate-type-mismatch", message: `jsonText requires a scalar Json field, not "${field.name}"` });
|
|
1414
|
+
if (!Array.isArray(expression.path) || Array.from(expression.path).some((segment) => typeof segment !== "string" || segment.includes("\x00"))) {
|
|
1415
|
+
refuse2({ context, rule: "predicate-json-path", message: "jsonText requires a string-only path without NUL bytes" });
|
|
1412
1416
|
}
|
|
1413
|
-
|
|
1417
|
+
const path = expression.path.map((value) => context.dialect.quoteStringLiteral({ value })).join(", ");
|
|
1418
|
+
return { text: `(${quoteColumn({ field, context })} #>> ARRAY[${path}]::text[])`, resultType: { type: "String" }, fieldName: field.name };
|
|
1414
1419
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1420
|
+
case "length":
|
|
1421
|
+
case "lower":
|
|
1422
|
+
case "upper": {
|
|
1423
|
+
const inner = renderExpression({ expression: expression.expression, context });
|
|
1424
|
+
if (inner.resultType.type !== "String" || inner.resultType.refinement === "localDate") {
|
|
1425
|
+
refuse2({ context, rule: "predicate-type-mismatch", message: `${expression.kind} requires a text expression, not a local date or another scalar`, meta: { field: inner.fieldName } });
|
|
1426
|
+
}
|
|
1427
|
+
return { text: `${expression.kind}(${inner.text})`, resultType: { type: expression.kind === "length" ? "Int" : "String" }, fieldName: inner.fieldName };
|
|
1418
1428
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1429
|
+
case "add":
|
|
1430
|
+
case "sub":
|
|
1431
|
+
case "mul": {
|
|
1432
|
+
const left = renderExpression({ expression: expression.left, context });
|
|
1433
|
+
const type = left.resultType.type;
|
|
1434
|
+
if (type !== "Int" && type !== "BigInt" && type !== "Float" && type !== "Decimal") {
|
|
1435
|
+
refuse2({ context, rule: "predicate-type-mismatch", message: `${expression.kind} requires Int, BigInt, Float or Decimal operands`, meta: { field: left.fieldName } });
|
|
1436
|
+
}
|
|
1437
|
+
const right = renderOperand({ operand: expression.right, left, context });
|
|
1438
|
+
const text = context.dialect.name === "sqlite" ? renderSqliteArithmetic({ left: left.text, right, kind: expression.kind, type }) : `(${left.text} ${ARITHMETIC_SQL[expression.kind]} ${right})`;
|
|
1439
|
+
return { text, resultType: { type }, fieldName: left.fieldName };
|
|
1422
1440
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
function renderOperand({ operand, left, context }) {
|
|
1444
|
+
if (operand.kind === "value")
|
|
1445
|
+
return renderLiteral({ value: operand, resultType: left.resultType, fieldName: left.fieldName, context });
|
|
1446
|
+
const other = renderExpression({ expression: operand, context });
|
|
1447
|
+
const leftType = resultIdentity({ resultType: left.resultType });
|
|
1448
|
+
const rightType = resultIdentity({ resultType: other.resultType });
|
|
1449
|
+
if (leftType !== rightType) {
|
|
1450
|
+
refuse2({
|
|
1451
|
+
context,
|
|
1452
|
+
rule: "predicate-type-mismatch",
|
|
1453
|
+
message: `compares "${left.fieldName}" (${leftType}) with "${other.fieldName}" (${rightType}): both operands need the same scalar type`,
|
|
1454
|
+
meta: { field: left.fieldName, other: other.fieldName, fieldType: leftType, otherType: rightType }
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
return other.text;
|
|
1458
|
+
}
|
|
1459
|
+
function renderLiteral(params) {
|
|
1460
|
+
const { resultType, fieldName, context } = params;
|
|
1461
|
+
const { value } = params.value;
|
|
1462
|
+
const type = resultType.type;
|
|
1463
|
+
const wrongCarrier = (expected) => refuse2({
|
|
1464
|
+
context,
|
|
1465
|
+
rule: "predicate-type-mismatch",
|
|
1466
|
+
message: `compares ${fieldTypeIdentity({ type })} field "${fieldName}" with ${describe({ value })}: ${expected}`,
|
|
1467
|
+
meta: { field: fieldName, fieldType: fieldTypeIdentity({ type }), value }
|
|
1468
|
+
});
|
|
1469
|
+
if (isEnumType(type)) {
|
|
1470
|
+
if (typeof value !== "string")
|
|
1471
|
+
return wrongCarrier(`enum ${type.enum} takes one of its member names as a string`);
|
|
1472
|
+
const declared = context.enums.find((entry) => entry.name === type.enum);
|
|
1473
|
+
if (declared !== undefined && !declared.values.includes(value)) {
|
|
1474
|
+
refuse2({
|
|
1475
|
+
context,
|
|
1476
|
+
rule: "predicate-enum-member",
|
|
1477
|
+
message: `compares "${fieldName}" with "${value}", which is not a member of enum ${type.enum} (${declared.values.join(", ")})`,
|
|
1478
|
+
meta: { field: fieldName, enum: type.enum, value, members: [...declared.values] }
|
|
1479
|
+
});
|
|
1430
1480
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1481
|
+
return stringLiteral({ value, context, fieldName });
|
|
1482
|
+
}
|
|
1483
|
+
switch (type) {
|
|
1484
|
+
case "String":
|
|
1485
|
+
if (typeof value !== "string")
|
|
1486
|
+
return wrongCarrier("a String column takes a string literal");
|
|
1487
|
+
if (resultType.refinement === "localDate" && !isCanonicalLocalDate2({ value })) {
|
|
1488
|
+
refuse2({
|
|
1489
|
+
context,
|
|
1490
|
+
rule: "predicate-literal-invalid",
|
|
1491
|
+
message: `compares local-date field "${fieldName}" with a noncanonical or invalid calendar date`,
|
|
1492
|
+
meta: { field: fieldName, value }
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
return stringLiteral({ value, context, fieldName });
|
|
1496
|
+
case "Boolean":
|
|
1497
|
+
if (typeof value !== "boolean")
|
|
1498
|
+
return wrongCarrier("a Boolean column takes `true` or `false`");
|
|
1499
|
+
return value ? context.dialect.booleanLiteral.true : context.dialect.booleanLiteral.false;
|
|
1500
|
+
case "Int":
|
|
1501
|
+
return integerLiteral({ value, fieldName, context, min: INT32_MIN, max: INT32_MAX, label: "Int" });
|
|
1502
|
+
case "BigInt":
|
|
1503
|
+
return integerLiteral({ value, fieldName, context, min: INT64_MIN, max: INT64_MAX, label: "BigInt" });
|
|
1504
|
+
case "Float": {
|
|
1505
|
+
if (typeof value !== "number")
|
|
1506
|
+
return wrongCarrier("a Float column takes a number");
|
|
1507
|
+
if (!Number.isFinite(value)) {
|
|
1508
|
+
refuse2({
|
|
1509
|
+
context,
|
|
1510
|
+
rule: "predicate-literal-invalid",
|
|
1511
|
+
message: `compares "${fieldName}" with ${String(value)}: a Float literal must be finite`,
|
|
1512
|
+
meta: { field: fieldName, value: String(value) }
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
return String(value);
|
|
1437
1516
|
}
|
|
1438
|
-
|
|
1439
|
-
if (
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1517
|
+
case "Decimal": {
|
|
1518
|
+
if (typeof value !== "string") {
|
|
1519
|
+
return wrongCarrier('a Decimal column takes exact decimal TEXT ("0", "19.99") \u2014 a JS number cannot carry an exact decimal');
|
|
1520
|
+
}
|
|
1521
|
+
if (!EXACT_DECIMAL.test(value)) {
|
|
1522
|
+
refuse2({
|
|
1523
|
+
context,
|
|
1524
|
+
rule: "predicate-literal-invalid",
|
|
1525
|
+
message: `compares "${fieldName}" with "${value}", which is not an exact decimal literal`,
|
|
1526
|
+
meta: { field: fieldName, value }
|
|
1527
|
+
});
|
|
1447
1528
|
}
|
|
1448
|
-
|
|
1449
|
-
parts.push(sql.slice(start, index).trim());
|
|
1450
|
-
start = index + 1;
|
|
1529
|
+
return value;
|
|
1451
1530
|
}
|
|
1531
|
+
case "DateTime":
|
|
1532
|
+
refuse2({
|
|
1533
|
+
context,
|
|
1534
|
+
rule: "predicate-unsupported-literal",
|
|
1535
|
+
message: `compares DateTime field "${fieldName}" with a literal. DateTime literals are spelled differently on postgres, mysql and sqlite, so VibeORM does not guess one. Compare two DateTime columns, or write a raw expression for a fixed instant`,
|
|
1536
|
+
meta: { field: fieldName }
|
|
1537
|
+
});
|
|
1538
|
+
case "Json":
|
|
1539
|
+
case "Bytes":
|
|
1540
|
+
return wrongCarrier(`${type} columns take no predicate literal`);
|
|
1452
1541
|
}
|
|
1453
|
-
throw unavailable();
|
|
1454
|
-
}
|
|
1455
|
-
function unavailable() {
|
|
1456
|
-
return new VibeError8({ code: "VIBE_MIGRATION", message: "Cannot preserve table definition", meta: { reason: "table-definition-unavailable" } });
|
|
1457
1542
|
}
|
|
1458
|
-
function
|
|
1459
|
-
const
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
if (
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1543
|
+
function integerLiteral(params) {
|
|
1544
|
+
const { value, fieldName, context, min, max, label } = params;
|
|
1545
|
+
let text;
|
|
1546
|
+
if (typeof value === "number") {
|
|
1547
|
+
if (!Number.isSafeInteger(value)) {
|
|
1548
|
+
refuse2({
|
|
1549
|
+
context,
|
|
1550
|
+
rule: "predicate-literal-invalid",
|
|
1551
|
+
message: `compares "${fieldName}" with ${String(value)}: an ${label} literal must be an exact integer JavaScript can represent; pass a bigint or a decimal string for larger values`,
|
|
1552
|
+
meta: { field: fieldName, value: String(value) }
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
text = String(value);
|
|
1556
|
+
} else if (typeof value === "string") {
|
|
1557
|
+
if (!EXACT_INTEGER.test(value)) {
|
|
1558
|
+
refuse2({
|
|
1559
|
+
context,
|
|
1560
|
+
rule: "predicate-literal-invalid",
|
|
1561
|
+
message: `compares "${fieldName}" with "${value}", which is not an exact integer literal`,
|
|
1562
|
+
meta: { field: fieldName, value }
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
text = value;
|
|
1566
|
+
} else {
|
|
1567
|
+
refuse2({
|
|
1568
|
+
context,
|
|
1569
|
+
rule: "predicate-type-mismatch",
|
|
1570
|
+
message: `compares ${label} field "${fieldName}" with ${describe({ value })}: an ${label} column takes an integer`,
|
|
1571
|
+
meta: { field: fieldName, value }
|
|
1572
|
+
});
|
|
1468
1573
|
}
|
|
1469
|
-
|
|
1574
|
+
const parsed = BigInt(text);
|
|
1575
|
+
if (parsed < min || parsed > max) {
|
|
1576
|
+
refuse2({
|
|
1577
|
+
context,
|
|
1578
|
+
rule: "predicate-literal-invalid",
|
|
1579
|
+
message: `compares "${fieldName}" with ${text}, which is outside the range an ${label} column can hold (${min}\u2026${max})`,
|
|
1580
|
+
meta: { field: fieldName, value: text }
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
return text;
|
|
1470
1584
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
const target = `${quote(`${params.ftsTable}_au`)} AFTER UPDATE ON ${quote(params.table)}`;
|
|
1483
|
-
const body = `BEGIN
|
|
1484
|
-
INSERT INTO ${quote(params.ftsTable)}(${quote(params.ftsTable)}, rowid, ${columns}) VALUES ('delete', old.rowid, ${oldValues});
|
|
1485
|
-
INSERT INTO ${quote(params.ftsTable)}(rowid, ${columns}) VALUES (new.rowid, ${newValues});
|
|
1486
|
-
END`;
|
|
1487
|
-
const tail = `${target} WHEN (${changes}) ${body}`;
|
|
1488
|
-
return {
|
|
1489
|
-
create: `CREATE TRIGGER IF NOT EXISTS ${tail};`,
|
|
1490
|
-
definition: `CREATE TRIGGER ${tail}`,
|
|
1491
|
-
legacyDefinition: `CREATE TRIGGER ${target} ${body}`
|
|
1492
|
-
};
|
|
1585
|
+
function stringLiteral(params) {
|
|
1586
|
+
const { value, context, fieldName } = params;
|
|
1587
|
+
if (value.includes("\x00")) {
|
|
1588
|
+
refuse2({
|
|
1589
|
+
context,
|
|
1590
|
+
rule: "predicate-literal-invalid",
|
|
1591
|
+
message: `compares "${fieldName}" with a string containing a NUL byte, which no dialect here can store`,
|
|
1592
|
+
meta: { field: fieldName }
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
return context.dialect.quoteStringLiteral({ value });
|
|
1493
1596
|
}
|
|
1494
|
-
function
|
|
1495
|
-
const {
|
|
1496
|
-
return {
|
|
1597
|
+
function describe(params) {
|
|
1598
|
+
const { value } = params;
|
|
1599
|
+
return typeof value === "string" ? `the string ${JSON.stringify(value)}` : `the ${typeof value} ${String(value)}`;
|
|
1497
1600
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1601
|
+
|
|
1602
|
+
// packages/sql/src/dialects/postgres-index-expressions.ts
|
|
1603
|
+
import { unsupportedCapability as unsupportedCapability9 } from "@vibeorm/schema";
|
|
1604
|
+
|
|
1605
|
+
// packages/sql/src/dialects/sqlite-table-definition.ts
|
|
1606
|
+
import { VibeError as VibeError8 } from "@vibeorm/schema";
|
|
1607
|
+
function tableDefinitionTokens({ sql, backslashEscapes = false }) {
|
|
1608
|
+
const tokens = [];
|
|
1609
|
+
let depth = 0;
|
|
1610
|
+
for (let index = 0;index < sql.length; ) {
|
|
1611
|
+
const char = sql[index];
|
|
1612
|
+
if (/\s/.test(char)) {
|
|
1613
|
+
index++;
|
|
1505
1614
|
continue;
|
|
1506
|
-
|
|
1507
|
-
if (
|
|
1615
|
+
}
|
|
1616
|
+
if (char === "-" && sql[index + 1] === "-" || char === "#") {
|
|
1617
|
+
const end = sql.indexOf(`
|
|
1618
|
+
`, index + 1);
|
|
1619
|
+
index = end < 0 ? sql.length : end + 1;
|
|
1508
1620
|
continue;
|
|
1509
|
-
|
|
1510
|
-
if (
|
|
1621
|
+
}
|
|
1622
|
+
if (char === "/" && sql[index + 1] === "*") {
|
|
1623
|
+
const end = sql.indexOf("*/", index + 2);
|
|
1624
|
+
if (end < 0)
|
|
1625
|
+
throw unavailable();
|
|
1626
|
+
index = end + 2;
|
|
1511
1627
|
continue;
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1628
|
+
}
|
|
1629
|
+
const start = index;
|
|
1630
|
+
if (char === "'" || char === '"' || char === "`" || char === "[") {
|
|
1631
|
+
const close = char === "[" ? "]" : char;
|
|
1632
|
+
let value = "";
|
|
1633
|
+
let closed = false;
|
|
1634
|
+
index++;
|
|
1635
|
+
while (index < sql.length) {
|
|
1636
|
+
const current = sql[index++];
|
|
1637
|
+
if (backslashEscapes && current === "\\" && (char === "'" || char === '"')) {
|
|
1638
|
+
if (index >= sql.length)
|
|
1639
|
+
throw unavailable();
|
|
1640
|
+
value += sql[index++];
|
|
1641
|
+
} else if (current === close) {
|
|
1642
|
+
if (close !== "]" && sql[index] === close) {
|
|
1643
|
+
value += close;
|
|
1644
|
+
index++;
|
|
1645
|
+
} else {
|
|
1646
|
+
closed = true;
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
1649
|
+
} else
|
|
1650
|
+
value += current;
|
|
1651
|
+
}
|
|
1652
|
+
if (!closed)
|
|
1653
|
+
throw unavailable();
|
|
1654
|
+
tokens.push({ value, kind: char === "'" ? "literal" : "quoted", start, end: index, depth });
|
|
1516
1655
|
continue;
|
|
1517
|
-
|
|
1656
|
+
}
|
|
1657
|
+
if (/[\p{L}\p{N}_$]/u.test(char)) {
|
|
1658
|
+
index++;
|
|
1659
|
+
while (index < sql.length && /[\p{L}\p{N}_$]/u.test(sql[index]))
|
|
1660
|
+
index++;
|
|
1661
|
+
tokens.push({ value: sql.slice(start, index), kind: "word", start, end: index, depth });
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
if (char === ")") {
|
|
1665
|
+
depth--;
|
|
1666
|
+
if (depth < 0)
|
|
1667
|
+
throw unavailable();
|
|
1668
|
+
}
|
|
1669
|
+
tokens.push({ value: char, kind: "symbol", start, end: ++index, depth });
|
|
1670
|
+
if (char === "(")
|
|
1671
|
+
depth++;
|
|
1518
1672
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
var SQLITE_SESSION_SQL = Object.freeze({
|
|
1523
|
-
lockingMode: "PRAGMA main.locking_mode",
|
|
1524
|
-
exclusive: "PRAGMA main.locking_mode = EXCLUSIVE",
|
|
1525
|
-
normal: "PRAGMA main.locking_mode = NORMAL",
|
|
1526
|
-
acquire: "BEGIN EXCLUSIVE",
|
|
1527
|
-
commit: "COMMIT",
|
|
1528
|
-
rollback: "ROLLBACK",
|
|
1529
|
-
touch: "SELECT name FROM main.sqlite_schema LIMIT 1",
|
|
1530
|
-
foreignKeysOn: "PRAGMA foreign_keys = ON"
|
|
1531
|
-
});
|
|
1532
|
-
// packages/sql/src/composable.ts
|
|
1533
|
-
import { VibeError as VibeError9 } from "@vibeorm/schema";
|
|
1534
|
-
var fragments = new WeakMap;
|
|
1535
|
-
registerSqlHandleCheck({ check: (value) => fragments.has(value) });
|
|
1536
|
-
function invalid3(message) {
|
|
1537
|
-
throw new VibeError9({ code: "VIBE_VALIDATION", message });
|
|
1538
|
-
}
|
|
1539
|
-
function construct(parts) {
|
|
1540
|
-
const snapshot = Object.freeze([...parts]);
|
|
1541
|
-
const fragment = Object.freeze(Object.defineProperty(Object.create(null), "toJSON", {
|
|
1542
|
-
value: () => ({ parts: snapshot })
|
|
1543
|
-
}));
|
|
1544
|
-
fragments.set(fragment, snapshot);
|
|
1545
|
-
return fragment;
|
|
1546
|
-
}
|
|
1547
|
-
function isSqlFragment(value) {
|
|
1548
|
-
return typeof value === "object" && value !== null && fragments.has(value);
|
|
1673
|
+
if (depth !== 0)
|
|
1674
|
+
throw unavailable();
|
|
1675
|
+
return tokens;
|
|
1549
1676
|
}
|
|
1550
|
-
function
|
|
1551
|
-
|
|
1677
|
+
function tableDefinitionParts({ sql, backslashEscapes }) {
|
|
1678
|
+
const tokens = tableDefinitionTokens({ sql, backslashEscapes });
|
|
1679
|
+
const words = tokens.filter((token) => token.depth === 0);
|
|
1680
|
+
if (words[0]?.kind !== "word" || words[0].value.toUpperCase() !== "CREATE")
|
|
1681
|
+
throw unavailable();
|
|
1682
|
+
const tablePosition = /^(TEMP|TEMPORARY)$/i.test(words[1]?.value ?? "") ? 2 : 1;
|
|
1683
|
+
if (words[tablePosition]?.kind !== "word" || words[tablePosition].value.toUpperCase() !== "TABLE")
|
|
1684
|
+
throw unavailable();
|
|
1685
|
+
const open = words.find((token) => token.kind === "symbol" && token.value === "(");
|
|
1686
|
+
if (open === undefined)
|
|
1687
|
+
throw unavailable();
|
|
1688
|
+
const close = words.find((token) => token.start > open.start && token.kind === "symbol" && token.value === ")");
|
|
1689
|
+
if (close === undefined)
|
|
1690
|
+
throw unavailable();
|
|
1691
|
+
const delimiters = tokens.filter((token) => token.start > open.start && token.start < close.start && token.depth === 1 && token.kind === "symbol" && token.value === ",");
|
|
1692
|
+
const clauses = [];
|
|
1693
|
+
let start = open.end;
|
|
1694
|
+
for (const delimiter of [...delimiters, close]) {
|
|
1695
|
+
const clause = sql.slice(start, delimiter.start).trim();
|
|
1696
|
+
if (clause.length === 0)
|
|
1697
|
+
throw unavailable();
|
|
1698
|
+
clauses.push(clause);
|
|
1699
|
+
start = delimiter.end;
|
|
1700
|
+
}
|
|
1701
|
+
return { clauses, suffix: sql.slice(close.end) };
|
|
1552
1702
|
}
|
|
1553
|
-
function
|
|
1554
|
-
return
|
|
1703
|
+
function unavailable() {
|
|
1704
|
+
return new VibeError8({ code: "VIBE_MIGRATION", message: "Cannot preserve table definition", meta: { reason: "table-definition-unavailable" } });
|
|
1555
1705
|
}
|
|
1556
|
-
function
|
|
1557
|
-
|
|
1558
|
-
|
|
1706
|
+
function tableColumnClauses({ sql, backslashEscapes, mysql = false }) {
|
|
1707
|
+
const columns = [];
|
|
1708
|
+
for (const part of tableDefinitionParts({ sql, backslashEscapes }).clauses) {
|
|
1709
|
+
const first = tableDefinitionTokens({ sql: part, backslashEscapes })[0];
|
|
1710
|
+
if (first === undefined || first.kind === "symbol")
|
|
1711
|
+
throw unavailable();
|
|
1712
|
+
if (first.kind === "word" && /^(CONSTRAINT|PRIMARY|UNIQUE|CHECK|FOREIGN)$/i.test(first.value))
|
|
1713
|
+
continue;
|
|
1714
|
+
if (mysql && first.kind === "word" && /^(KEY|INDEX|FULLTEXT|SPATIAL)$/i.test(first.value))
|
|
1715
|
+
continue;
|
|
1716
|
+
columns.push({ name: first.value, sql: part });
|
|
1559
1717
|
}
|
|
1718
|
+
return columns;
|
|
1560
1719
|
}
|
|
1561
|
-
function
|
|
1562
|
-
|
|
1563
|
-
invalid3("Use sql as a template tag");
|
|
1564
|
-
}
|
|
1565
|
-
const parts = [];
|
|
1566
|
-
for (let index = 0;index < strings.length; index++) {
|
|
1567
|
-
parts.push(strings[index]);
|
|
1568
|
-
if (index < values.length) {
|
|
1569
|
-
const value = values[index];
|
|
1570
|
-
if (isSqlFragment(value))
|
|
1571
|
-
parts.push(...partsOf(value));
|
|
1572
|
-
else
|
|
1573
|
-
parts.push(Object.freeze({ value: snapshotSqlValue(value) }));
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
return construct(parts);
|
|
1720
|
+
function sqliteColumnClauses({ sql }) {
|
|
1721
|
+
return tableColumnClauses({ sql });
|
|
1577
1722
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
const
|
|
1593
|
-
const
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
const { dialect, collector, conflict } = params;
|
|
1617
|
-
const render = (fragment) => renderSqlFragment({ ...params, fragment });
|
|
1618
|
-
const encode = (part) => {
|
|
1619
|
-
if (part.value === undefined)
|
|
1620
|
-
invalid3("SQL parameters cannot be undefined; use null explicitly");
|
|
1621
|
-
return snapshotSqlValue(part.decoder === undefined ? part.value : part.decoder.encode({ value: snapshotSqlValue(part.value), dialect: dialect.name }));
|
|
1622
|
-
};
|
|
1623
|
-
const recording = isSqlBindingCollector({ collector });
|
|
1624
|
-
const recordValue = (part) => recordSqlBinding({
|
|
1625
|
-
collector,
|
|
1626
|
-
source: { kind: "literal", value: part.value },
|
|
1627
|
-
transport: part.decoder === undefined ? sqlParameterTransport({ value: part.value }) : { kind: "plain" },
|
|
1628
|
-
encode: ({ value, dialect: target }) => part.decoder === undefined ? value : part.decoder.encode({ value, dialect: target.name })
|
|
1629
|
-
});
|
|
1630
|
-
const frameOperand = (fragment) => {
|
|
1631
|
-
const parts = partsOf(fragment).filter((part) => part !== "");
|
|
1632
|
-
const only = parts.length === 1 ? parts[0] : undefined;
|
|
1633
|
-
if (recording && only !== undefined && typeof only !== "string" && "value" in only) {
|
|
1634
|
-
if (only.decoder !== undefined && resultScalarContract({ decoder: only.decoder }) === undefined)
|
|
1635
|
-
throw new VibeError9({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Reusable SQL cannot compile an opaque encoder whose output controls a greatest NULL frame" });
|
|
1636
|
-
if (only.value === null) {
|
|
1637
|
-
if (only.decoder !== undefined)
|
|
1638
|
-
recordSqlBindingCheck({ collector, check: () => {
|
|
1639
|
-
if (encode({ ...only, value: snapshotSqlValue(only.value) }) !== null)
|
|
1640
|
-
throw new VibeError9({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Reusable SQL NULL frame encoding changed its declared structure" });
|
|
1641
|
-
} });
|
|
1642
|
-
return "NULL";
|
|
1723
|
+
|
|
1724
|
+
// packages/sql/src/dialects/sqlite-index-expressions.ts
|
|
1725
|
+
function extractIndexKeyList({ sql, open, close, tokens, wrapped = false, removeWrapper = wrapped }) {
|
|
1726
|
+
const commas = tokens.filter((token) => token.start > open.start && token.start < close.start && token.depth === open.depth + 1 && token.kind === "symbol" && token.value === ",");
|
|
1727
|
+
const keys = [];
|
|
1728
|
+
let start = open.end;
|
|
1729
|
+
for (const delimiter of [...commas, close]) {
|
|
1730
|
+
const termTokens = tokens.filter((token) => token.start >= start && token.end <= delimiter.start);
|
|
1731
|
+
if (termTokens.length === 0)
|
|
1732
|
+
return;
|
|
1733
|
+
const last = termTokens.at(-1);
|
|
1734
|
+
const sorted = last.depth === open.depth + 1 && last.kind === "word" && /^(ASC|DESC)$/i.test(last.value);
|
|
1735
|
+
const desc = sorted && /^DESC$/i.test(last.value);
|
|
1736
|
+
const bodyTokens = sorted ? termTokens.slice(0, -1) : termTokens;
|
|
1737
|
+
const first = bodyTokens[0];
|
|
1738
|
+
const end = bodyTokens.at(-1);
|
|
1739
|
+
if (first === undefined || end === undefined)
|
|
1740
|
+
return;
|
|
1741
|
+
if (bodyTokens.length === 1 && (first.kind === "word" || first.kind === "quoted")) {
|
|
1742
|
+
keys.push({ kind: "field", name: first.value, desc });
|
|
1743
|
+
} else {
|
|
1744
|
+
const top = bodyTokens.filter((token) => token.depth === open.depth + 1);
|
|
1745
|
+
if (top.some((token) => token.kind === "word" && /^(COLLATE|NULLS)$/i.test(token.value)))
|
|
1746
|
+
return;
|
|
1747
|
+
if (first.kind === "symbol" && first.value === "(" && end.kind === "symbol" && end.value === ")" && end.depth === first.depth) {
|
|
1748
|
+
if (top.length !== 2)
|
|
1749
|
+
return;
|
|
1750
|
+
const text = sql.slice(removeWrapper ? first.end : start, removeWrapper ? end.start : sorted ? last.start : delimiter.start).trim();
|
|
1751
|
+
if (!text)
|
|
1752
|
+
return;
|
|
1753
|
+
keys.push({ kind: "expression", text, desc });
|
|
1754
|
+
} else {
|
|
1755
|
+
if (wrapped)
|
|
1756
|
+
return;
|
|
1757
|
+
const text = sql.slice(start, sorted ? last.start : delimiter.start).trim();
|
|
1758
|
+
if (!text || bodyTokens.some((token) => token.depth === open.depth + 1 && token.kind === "word" && /^(ASC|DESC)$/i.test(token.value)))
|
|
1759
|
+
return;
|
|
1760
|
+
keys.push({ kind: "expression", text, desc });
|
|
1643
1761
|
}
|
|
1644
|
-
const text = recordValue(only);
|
|
1645
|
-
return dialect.greatestForm === "wrapped-frame" ? `(SELECT ${text})` : text;
|
|
1646
1762
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1763
|
+
start = delimiter.end;
|
|
1764
|
+
}
|
|
1765
|
+
return keys.length === 0 ? undefined : keys;
|
|
1766
|
+
}
|
|
1767
|
+
function extractStandaloneIndexKeys({ definition, index, postgres = false }) {
|
|
1768
|
+
const tokens = tableDefinitionTokens({ sql: definition });
|
|
1769
|
+
const top = tokens.filter((token) => token.depth === 0);
|
|
1770
|
+
let position = 0;
|
|
1771
|
+
const word = ({ value }) => {
|
|
1772
|
+
const token = top[position];
|
|
1773
|
+
if (token?.kind !== "word" || token.value.toUpperCase() !== value)
|
|
1774
|
+
return false;
|
|
1775
|
+
position++;
|
|
1776
|
+
return true;
|
|
1652
1777
|
};
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
if (
|
|
1673
|
-
return
|
|
1778
|
+
if (!word({ value: "CREATE" }))
|
|
1779
|
+
return;
|
|
1780
|
+
word({ value: "UNIQUE" });
|
|
1781
|
+
if (!word({ value: "INDEX" }))
|
|
1782
|
+
return;
|
|
1783
|
+
if (word({ value: "IF" }) && (!word({ value: "NOT" }) || !word({ value: "EXISTS" })))
|
|
1784
|
+
return;
|
|
1785
|
+
let name = top[position++];
|
|
1786
|
+
if (top[position]?.value === ".") {
|
|
1787
|
+
position++;
|
|
1788
|
+
name = top[position++];
|
|
1789
|
+
}
|
|
1790
|
+
if (name === undefined || !["quoted", "word"].includes(name.kind) || name.value !== index || !word({ value: "ON" }))
|
|
1791
|
+
return;
|
|
1792
|
+
const table = top[position++];
|
|
1793
|
+
if (table === undefined || !["quoted", "word"].includes(table.kind))
|
|
1794
|
+
return;
|
|
1795
|
+
if (top[position]?.value === ".") {
|
|
1796
|
+
position++;
|
|
1797
|
+
if (top[position++] === undefined)
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
if (word({ value: "USING" })) {
|
|
1801
|
+
if (!postgres || !word({ value: "BTREE" }))
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
const open = top[position++];
|
|
1805
|
+
const close = top[position++];
|
|
1806
|
+
if (open?.kind !== "symbol" || open.value !== "(" || close?.kind !== "symbol" || close.value !== ")")
|
|
1807
|
+
return;
|
|
1808
|
+
if (postgres && word({ value: "NULLS" }) && (!word({ value: "NOT" }) || !word({ value: "DISTINCT" })))
|
|
1809
|
+
return;
|
|
1810
|
+
const tail = top[position];
|
|
1811
|
+
if (tail !== undefined && !(tail.kind === "word" && tail.value.toUpperCase() === "WHERE") && !(tail.value === ";" && position === top.length - 1))
|
|
1812
|
+
return;
|
|
1813
|
+
const keys = extractIndexKeyList({ sql: definition, open, close, tokens, removeWrapper: postgres });
|
|
1814
|
+
if (postgres && keys !== undefined) {
|
|
1815
|
+
const delimiters = tokens.filter((token) => token.start > open.start && token.start < close.start && token.depth === 1 && token.value === ",");
|
|
1816
|
+
let start = open.end;
|
|
1817
|
+
for (const delimiter of [...delimiters, close]) {
|
|
1818
|
+
const keyTop = tokens.filter((token) => token.start >= start && token.end <= delimiter.start && token.depth === 1);
|
|
1819
|
+
if (keyTop.length > 1 && (keyTop[0]?.kind === "word" || keyTop[0]?.kind === "quoted") && (keyTop[1]?.kind === "quoted" || keyTop[1]?.kind === "word" && !/^(ASC|DESC)$/i.test(keyTop[1].value)))
|
|
1820
|
+
return;
|
|
1821
|
+
start = delimiter.end;
|
|
1674
1822
|
}
|
|
1675
|
-
const
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
}
|
|
1823
|
+
const slices = tokens.filter((token) => token.start >= open.end && token.end <= close.start && token.depth === 1);
|
|
1824
|
+
if (slices.some((token, ordinal) => slices[ordinal - 1]?.value === ")" && (token.kind === "quoted" || token.kind === "word" && !/^(ASC|DESC)$/i.test(token.value))))
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
return keys;
|
|
1679
1828
|
}
|
|
1680
|
-
function
|
|
1681
|
-
return
|
|
1682
|
-
if (typeof part === "string")
|
|
1683
|
-
return part;
|
|
1684
|
-
if ("row" in part) {
|
|
1685
|
-
const column = Object.hasOwn(params.columns, part.column) ? params.columns[part.column] : undefined;
|
|
1686
|
-
if (column === undefined)
|
|
1687
|
-
invalid3(`Unknown conflict-expression field "${part.column}"`);
|
|
1688
|
-
return Object.freeze({ ...part, column });
|
|
1689
|
-
}
|
|
1690
|
-
if ("greatest" in part)
|
|
1691
|
-
return Object.freeze({ greatest: Object.freeze(part.greatest.map((fragment) => mapConflictColumns({ ...params, fragment }))) });
|
|
1692
|
-
return part;
|
|
1693
|
-
}));
|
|
1829
|
+
function extractSqliteIndexKeys({ definition, index }) {
|
|
1830
|
+
return extractStandaloneIndexKeys({ definition, index });
|
|
1694
1831
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
return {
|
|
1832
|
+
|
|
1833
|
+
// packages/sql/src/dialects/postgres-index-expressions.ts
|
|
1834
|
+
function extractPostgresIndexKeys({ definition, index }) {
|
|
1835
|
+
return extractStandaloneIndexKeys({ definition, index, postgres: true });
|
|
1836
|
+
}
|
|
1837
|
+
function renderPostgresIndexExpressionProbe({ ...params }) {
|
|
1838
|
+
const { dialect, operation, probe } = params;
|
|
1839
|
+
if (operation === "context")
|
|
1840
|
+
return [{ text: "SELECT current_setting('server_version') AS version, current_setting('server_version_num') AS version_number, current_setting('search_path') AS search_path", values: [], returning: "none" }];
|
|
1841
|
+
const table = dialect.quoteIdent({ name: probe.table });
|
|
1842
|
+
if (operation === "read")
|
|
1843
|
+
return [renderPostgresIndexKeys({ dialect, table: { namespace: "pg_temp", name: probe.table }, expressionMetadata: true })];
|
|
1844
|
+
if (operation === "drop")
|
|
1845
|
+
return [{ text: `DROP TABLE pg_temp.${table}`, values: [], returning: "none" }];
|
|
1846
|
+
if (operation === "clone") {
|
|
1847
|
+
if (params.table === undefined)
|
|
1848
|
+
throw unsupportedCapability9({ dialect: "postgres", feature: "indexExpressionProbeSource" });
|
|
1849
|
+
return [{ text: `CREATE TEMP TABLE ${table} (LIKE ${dialect.quoteIdent({ name: params.table.namespace })}.${dialect.quoteIdent({ name: params.table.name })} INCLUDING DEFAULTS INCLUDING GENERATED INCLUDING IDENTITY INCLUDING STORAGE INCLUDING CONSTRAINTS)`, values: [], returning: "none" }];
|
|
1850
|
+
}
|
|
1851
|
+
if (params.keys === undefined)
|
|
1852
|
+
throw unsupportedCapability9({ dialect: "postgres", feature: "indexExpressionProbeKeys" });
|
|
1853
|
+
if (params.nullsNotDistinct === true && params.unique !== true)
|
|
1854
|
+
throw unsupportedCapability9({ dialect: "postgres", feature: "nonUniqueNullsNotDistinct" });
|
|
1855
|
+
const keys = renderIndexKeys({ dialect, keys: params.keys, unique: params.unique === true });
|
|
1856
|
+
return [{ text: `CREATE ${params.unique ? "UNIQUE " : ""}INDEX ${dialect.quoteIdent({ name: probe.index })} ON pg_temp.${table} (${keys.join(", ")})${params.nullsNotDistinct ? " NULLS NOT DISTINCT" : ""}${params.where === undefined ? "" : ` WHERE ${params.where}`}`, values: [], returning: "none" }];
|
|
1699
1857
|
}
|
|
1700
|
-
|
|
1701
|
-
|
|
1858
|
+
|
|
1859
|
+
// packages/sql/src/dialects/mysql-index-expressions.ts
|
|
1860
|
+
import { MYSQL_FUNCTIONAL_KEY_LIMITS, mysqlIndexKeyWidth, unsupportedCapability as unsupportedCapability11 } from "@vibeorm/schema";
|
|
1861
|
+
|
|
1862
|
+
// packages/sql/src/dialects/mysql-generated-columns.ts
|
|
1863
|
+
import { VibeError as VibeError10, unsupportedCapability as unsupportedCapability10 } from "@vibeorm/schema";
|
|
1864
|
+
|
|
1865
|
+
// packages/sql/src/dialects/sqlite-generated-columns.ts
|
|
1866
|
+
import { VibeError as VibeError9 } from "@vibeorm/schema";
|
|
1867
|
+
function renderSqliteGeneratedColumnClause({ generated }) {
|
|
1868
|
+
return `GENERATED ALWAYS AS (${generated.expression}) STORED`;
|
|
1869
|
+
}
|
|
1870
|
+
function unavailable2({ column }) {
|
|
1871
|
+
return new VibeError9({ code: "VIBE_MIGRATION", message: `Cannot inspect generated expression for column "${column}"`, meta: { reason: "generated-expression-unavailable", column } });
|
|
1872
|
+
}
|
|
1873
|
+
function extractGenerationFromDefinition({ definition, column, backslashEscapes = false, mysql = false }) {
|
|
1874
|
+
const matches = tableColumnClauses({ sql: definition, backslashEscapes, mysql }).filter((clause) => clause.name === column);
|
|
1875
|
+
if (matches.length !== 1)
|
|
1876
|
+
throw unavailable2({ column });
|
|
1877
|
+
const columnSql = matches[0].sql;
|
|
1878
|
+
const tokens = tableDefinitionTokens({ sql: columnSql, backslashEscapes }).filter((token) => token.depth === 0);
|
|
1879
|
+
const name = tokens[0];
|
|
1880
|
+
let result;
|
|
1881
|
+
for (let index = 1;index < tokens.length; index++) {
|
|
1882
|
+
const token = tokens[index];
|
|
1883
|
+
if (token.kind !== "word" || token.value.toUpperCase() !== "AS")
|
|
1884
|
+
continue;
|
|
1885
|
+
const open = tokens[index + 1];
|
|
1886
|
+
const close = tokens[index + 2];
|
|
1887
|
+
if (open?.kind !== "symbol" || open.value !== "(" || close?.kind !== "symbol" || close.value !== ")")
|
|
1888
|
+
throw unavailable2({ column });
|
|
1889
|
+
if (result !== undefined)
|
|
1890
|
+
throw unavailable2({ column });
|
|
1891
|
+
const expression = columnSql.slice(open.end, close.start).trim();
|
|
1892
|
+
if (expression.length === 0 || expression.includes("\x00"))
|
|
1893
|
+
throw unavailable2({ column });
|
|
1894
|
+
const preceding = tokens[index - 1];
|
|
1895
|
+
const generated = preceding?.kind === "word" && preceding.value.toUpperCase() === "ALWAYS" ? tokens[index - 2] : preceding;
|
|
1896
|
+
const explicit = generated?.kind === "word" && generated.value.toUpperCase() === "GENERATED";
|
|
1897
|
+
if (preceding?.kind === "word" && preceding.value.toUpperCase() === "ALWAYS" && !explicit)
|
|
1898
|
+
throw unavailable2({ column });
|
|
1899
|
+
const next = tokens[index + 3];
|
|
1900
|
+
const storage = next?.kind === "word" && next.value.toUpperCase() === "STORED" ? "stored" : "virtual";
|
|
1901
|
+
result = { expression, storage, columnSql, typeSql: columnSql.slice(name.end, explicit ? generated.start : token.start).trim() };
|
|
1902
|
+
index += 2;
|
|
1903
|
+
}
|
|
1904
|
+
if (result === undefined && tokens.some((token, index) => token.kind === "word" && token.value.toUpperCase() === "GENERATED" && tokens[index + 1]?.kind === "word" && tokens[index + 1]?.value.toUpperCase() === "ALWAYS"))
|
|
1905
|
+
throw unavailable2({ column });
|
|
1906
|
+
return result;
|
|
1702
1907
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
var queries = new WeakMap;
|
|
1706
|
-
var commands = new WeakMap;
|
|
1707
|
-
var reusableQueries = new WeakMap;
|
|
1708
|
-
var reusableCommands = new WeakMap;
|
|
1709
|
-
registerSqlHandleCheck({ check: (value) => queries.has(value) || commands.has(value) || reusableQueries.has(value) || reusableCommands.has(value) });
|
|
1710
|
-
function invalid4(message) {
|
|
1711
|
-
throw new VibeError10({ code: "VIBE_VALIDATION", message });
|
|
1908
|
+
function extractSqliteGeneratedColumn({ definition, column }) {
|
|
1909
|
+
return extractGenerationFromDefinition({ definition, column });
|
|
1712
1910
|
}
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1911
|
+
|
|
1912
|
+
// packages/sql/src/dialects/mysql-generated-columns.ts
|
|
1913
|
+
function renderMysqlGeneratedColumnClause({ generated }) {
|
|
1914
|
+
return `GENERATED ALWAYS AS (${generated.expression}) STORED`;
|
|
1915
|
+
}
|
|
1916
|
+
function renderMysqlGeneratedColumnProbe({ dialect, probe }) {
|
|
1917
|
+
if (dialect.name !== "mysql")
|
|
1918
|
+
throw unsupportedCapability10({ dialect: dialect.name, feature: "temporaryGeneratedColumnProbe" });
|
|
1919
|
+
if (probe.operation === "context")
|
|
1920
|
+
return { text: "SELECT @@SESSION.sql_mode AS sqlMode", values: [], returning: "none" };
|
|
1921
|
+
const q = ({ name }) => dialect.quoteIdent({ name });
|
|
1922
|
+
const name = q({ name: probe.probe });
|
|
1923
|
+
let text;
|
|
1924
|
+
switch (probe.operation) {
|
|
1925
|
+
case "clone":
|
|
1926
|
+
text = `CREATE TEMPORARY TABLE ${name} LIKE ${q({ name: probe.table.namespace })}.${q({ name: probe.table.name })}`;
|
|
1927
|
+
break;
|
|
1928
|
+
case "read":
|
|
1929
|
+
text = `SHOW CREATE TABLE ${probe.table === undefined ? name : `${q({ name: probe.table.namespace })}.${q({ name: probe.table.name })}`}`;
|
|
1930
|
+
break;
|
|
1931
|
+
case "drop":
|
|
1932
|
+
text = `DROP TEMPORARY TABLE ${name}`;
|
|
1933
|
+
break;
|
|
1934
|
+
case "create": {
|
|
1935
|
+
const parts = tableDefinitionParts({ sql: probe.definition, backslashEscapes: probe.backslashEscapes });
|
|
1936
|
+
const columns = tableColumnClauses({ sql: probe.definition, backslashEscapes: probe.backslashEscapes, mysql: true });
|
|
1937
|
+
const targets = columns.filter((column) => column.name === probe.column);
|
|
1938
|
+
if (targets.length > 1)
|
|
1939
|
+
throw unavailable3({ column: probe.column });
|
|
1940
|
+
const targetDefinition = extractMysqlGeneratedColumn({ definition: `CREATE TABLE ${name} (${probe.columnDefinition})`, column: probe.column, backslashEscapes: probe.backslashEscapes });
|
|
1941
|
+
if (targetDefinition?.storage !== "stored")
|
|
1942
|
+
throw unavailable3({ column: probe.column });
|
|
1943
|
+
const clauses = [...parts.clauses];
|
|
1944
|
+
if (targets.length === 1) {
|
|
1945
|
+
clauses[clauses.indexOf(targets[0].sql)] = probe.columnDefinition;
|
|
1946
|
+
} else if (probe.after !== undefined) {
|
|
1947
|
+
const after = columns.filter((column) => column.name === probe.after);
|
|
1948
|
+
if (after.length !== 1)
|
|
1949
|
+
throw unavailable3({ column: probe.after });
|
|
1950
|
+
clauses.splice(clauses.indexOf(after[0].sql) + 1, 0, probe.columnDefinition);
|
|
1951
|
+
} else {
|
|
1952
|
+
const last = columns.at(-1);
|
|
1953
|
+
if (last === undefined)
|
|
1954
|
+
throw unavailable3({ column: probe.column });
|
|
1955
|
+
clauses.splice(clauses.indexOf(last.sql) + 1, 0, probe.columnDefinition);
|
|
1956
|
+
}
|
|
1957
|
+
text = `CREATE TEMPORARY TABLE ${name} (
|
|
1958
|
+
${clauses.join(`,
|
|
1959
|
+
`)}
|
|
1960
|
+
)${parts.suffix}`;
|
|
1961
|
+
break;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
return { text, values: [], returning: "none" };
|
|
1719
1965
|
}
|
|
1720
|
-
function
|
|
1721
|
-
|
|
1722
|
-
invalid4("SQL commands require a renderer");
|
|
1723
|
-
const command = Object.freeze(Object.create(null));
|
|
1724
|
-
commands.set(command, params.render);
|
|
1725
|
-
return command;
|
|
1966
|
+
function unavailable3({ column }) {
|
|
1967
|
+
return new VibeError10({ code: "VIBE_MIGRATION", message: `Cannot build temporary generated-column definition for "${column}"`, meta: { reason: "generated-expression-unavailable", column } });
|
|
1726
1968
|
}
|
|
1727
|
-
function
|
|
1728
|
-
return
|
|
1969
|
+
function extractMysqlGeneratedColumn({ definition, column, backslashEscapes }) {
|
|
1970
|
+
return extractGenerationFromDefinition({ definition, column, backslashEscapes, mysql: true });
|
|
1729
1971
|
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1972
|
+
|
|
1973
|
+
// packages/sql/src/dialects/mysql-index-expressions.ts
|
|
1974
|
+
function assertMysqlIndexKeyStorage({ model, terms }) {
|
|
1975
|
+
const width = mysqlIndexKeyWidth({ terms, model });
|
|
1976
|
+
if (width.kind === "refused")
|
|
1977
|
+
throw unsupportedCapability11({ dialect: "mysql", feature: width.feature, hint: `${model.name}.${width.field}: use bounded VARCHAR/CHAR, an explicit raw cast with intentional width/collation, or a generated column; no automatic cast is inserted` });
|
|
1978
|
+
if (width.kind === "bytes" && width.bytes > MYSQL_FUNCTIONAL_KEY_LIMITS.bytes)
|
|
1979
|
+
throw unsupportedCapability11({ dialect: "mysql", feature: "mysqlFunctionalKeyWidth", hint: `The complete key needs up to ${width.bytes} bytes; modern InnoDB admits at most ${MYSQL_FUNCTIONAL_KEY_LIMITS.bytes}. Use narrower declared storage or an explicit reviewed raw key` });
|
|
1980
|
+
}
|
|
1981
|
+
function extractMysqlIndexKeys({ definition, index, backslashEscapes }) {
|
|
1982
|
+
const matching = tableDefinitionParts({ sql: definition, backslashEscapes }).clauses.flatMap((sql) => {
|
|
1983
|
+
const tokens = tableDefinitionTokens({ sql, backslashEscapes });
|
|
1984
|
+
const top = tokens.filter((token) => token.depth === 0);
|
|
1985
|
+
let position = top[0]?.kind === "word" && top[0].value.toUpperCase() === "UNIQUE" ? 1 : 0;
|
|
1986
|
+
const key = top[position++];
|
|
1987
|
+
if (key?.kind !== "word" || !/^(KEY|INDEX)$/i.test(key.value))
|
|
1988
|
+
return [];
|
|
1989
|
+
const name = top[position++];
|
|
1990
|
+
if (name === undefined || !["word", "quoted"].includes(name.kind) || name.value !== index)
|
|
1991
|
+
return [];
|
|
1992
|
+
if (top[position]?.kind === "word" && top[position]?.value.toUpperCase() === "USING") {
|
|
1993
|
+
position++;
|
|
1994
|
+
if (top[position++]?.value.toUpperCase() !== "BTREE")
|
|
1995
|
+
return [{ sql, tokens, open: undefined, close: undefined }];
|
|
1996
|
+
}
|
|
1997
|
+
const open = top[position++];
|
|
1998
|
+
const close = top[position++];
|
|
1999
|
+
const tail = top.slice(position);
|
|
2000
|
+
const supported = tail.length === 0 || tail.length === 1 && tail[0]?.value.toUpperCase() === "VISIBLE" || tail.length === 2 && tail[0]?.value.toUpperCase() === "USING" && tail[1]?.value.toUpperCase() === "BTREE";
|
|
2001
|
+
return [{ sql, tokens, open: supported && open?.value === "(" ? open : undefined, close: close?.value === ")" ? close : undefined }];
|
|
2002
|
+
});
|
|
2003
|
+
if (matching.length !== 1)
|
|
2004
|
+
return;
|
|
2005
|
+
const clause = matching[0];
|
|
2006
|
+
if (clause.open === undefined || clause.close === undefined)
|
|
2007
|
+
return;
|
|
2008
|
+
return extractIndexKeyList({ ...clause, open: clause.open, close: clause.close, wrapped: true });
|
|
2009
|
+
}
|
|
2010
|
+
function renderMysqlIndexExpressionProbe({ ...params }) {
|
|
2011
|
+
const { dialect, probe, operation } = params;
|
|
2012
|
+
if (operation === "context")
|
|
2013
|
+
return [{ text: "SELECT VERSION() AS version, @@SESSION.sql_mode AS sqlMode, @@SESSION.character_set_connection AS characterSet, @@SESSION.collation_connection AS collation", values: [], returning: "none" }];
|
|
2014
|
+
if (operation === "clone") {
|
|
2015
|
+
if (params.table === undefined)
|
|
2016
|
+
throw unsupportedCapability11({ dialect: "mysql", feature: "indexExpressionProbeSource" });
|
|
2017
|
+
return [renderMysqlGeneratedColumnProbe({ dialect, probe: { operation: "clone", probe: probe.table, table: params.table } })];
|
|
2018
|
+
}
|
|
2019
|
+
if (operation === "read" || operation === "drop")
|
|
2020
|
+
return [renderMysqlGeneratedColumnProbe({ dialect, probe: { operation, probe: probe.table } })];
|
|
2021
|
+
if (params.definition === undefined || params.keys === undefined)
|
|
2022
|
+
throw unsupportedCapability11({ dialect: "mysql", feature: "indexExpressionProbeDefinition" });
|
|
2023
|
+
if (params.where !== undefined || params.nullsNotDistinct === true)
|
|
2024
|
+
throw unsupportedCapability11({ dialect: "mysql", feature: "partialOrNullsNotDistinctExpressionIndex" });
|
|
2025
|
+
if (params.backslashEscapes === undefined)
|
|
2026
|
+
throw unsupportedCapability11({ dialect: "mysql", feature: "indexExpressionProbeSqlMode", hint: "Read sqlMode on the pinned session and supply backslashEscapes explicitly" });
|
|
2027
|
+
const parts = tableDefinitionParts({ sql: params.definition, backslashEscapes: params.backslashEscapes });
|
|
2028
|
+
const clauses = parts.clauses.filter((sql) => {
|
|
2029
|
+
const top = tableDefinitionTokens({ sql, backslashEscapes: params.backslashEscapes }).filter((token) => token.depth === 0);
|
|
2030
|
+
const position = top[0]?.kind === "word" && /^UNIQUE$/i.test(top[0].value) ? 1 : 0;
|
|
2031
|
+
return !(top[position]?.kind === "word" && /^(KEY|INDEX)$/i.test(top[position].value) && top[position + 1]?.value === probe.index);
|
|
2032
|
+
});
|
|
2033
|
+
const keys = renderIndexKeys({ dialect, keys: params.keys, unique: params.unique === true });
|
|
2034
|
+
clauses.push(`${params.unique ? "UNIQUE " : ""}KEY ${dialect.quoteIdent({ name: probe.index })} (${keys.join(", ")})`);
|
|
2035
|
+
return [{ text: `CREATE TEMPORARY TABLE ${dialect.quoteIdent({ name: probe.table })} (
|
|
2036
|
+
${clauses.join(`,
|
|
2037
|
+
`)}
|
|
2038
|
+
)${parts.suffix}`, values: [], returning: "none" }];
|
|
1732
2039
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
2040
|
+
|
|
2041
|
+
// packages/sql/src/index-expressions.ts
|
|
2042
|
+
function resolveIndexKeys({ index, model, dialect, enums = [], site }) {
|
|
2043
|
+
const computed = isExpressionIndex({ index });
|
|
2044
|
+
if (computed)
|
|
2045
|
+
indexKeyDefinitionKey({ index, model });
|
|
2046
|
+
const terms = indexKeyTerms({ index });
|
|
2047
|
+
if (computed && index.where !== undefined && !dialect.capabilities.partialIndexes)
|
|
2048
|
+
throw unsupportedCapability12({ dialect: dialect.name, feature: "partialIndexes" });
|
|
2049
|
+
if (computed && index.nullsNotDistinct === true && dialect.name !== "postgres")
|
|
2050
|
+
throw unsupportedCapability12({ dialect: dialect.name, feature: "nullsNotDistinct" });
|
|
2051
|
+
if (computed && !dialect.capabilities.expressionIndexes)
|
|
2052
|
+
throw unsupportedCapability12({ dialect: dialect.name, feature: "expressionIndexes" });
|
|
2053
|
+
if (computed && (index.fields.length !== 0 || !terms.some((term) => term.kind !== "field")))
|
|
2054
|
+
throw invalid3({ site, reason: "index-expression-terms" });
|
|
2055
|
+
if (terms.length === 0)
|
|
2056
|
+
throw invalid3({ site, reason: "index-empty-keys" });
|
|
2057
|
+
if (computed && (index.clientName !== undefined || index.constraintName !== undefined))
|
|
2058
|
+
throw invalid3({ site, reason: "index-expression-selector" });
|
|
2059
|
+
if (computed && (index.kind !== "btree" && index.kind !== "unique" || terms.some((term) => term.kind === "field" && term.field.operatorClass !== undefined)))
|
|
2060
|
+
throw unsupportedCapability12({ dialect: dialect.name, feature: "expressionIndexOperatorClasses" });
|
|
2061
|
+
const keys = terms.map((term) => {
|
|
2062
|
+
if (term.kind === "field") {
|
|
2063
|
+
const field = model.fields.find((candidate) => candidate.name === term.field.name);
|
|
2064
|
+
if (field === undefined)
|
|
2065
|
+
throw invalid3({ site, reason: "index-unknown-field" });
|
|
2066
|
+
return { kind: "field", name: field.dbName ?? field.name, desc: term.field.sort === "desc", ...dialect.name === "sqlite" && field.type === "Decimal" ? { decimal: true } : {}, ...term.field.operatorClass === undefined ? {} : { operatorClass: term.field.operatorClass } };
|
|
2067
|
+
}
|
|
2068
|
+
if (term.kind === "raw") {
|
|
2069
|
+
if (index.name === undefined || typeof term.sql !== "string" || !term.sql.trim() || term.sql.includes("\x00"))
|
|
2070
|
+
throw invalid3({ site, reason: "index-raw-expression" });
|
|
2071
|
+
return { kind: "expression", text: term.sql, desc: term.sort === "desc" };
|
|
2072
|
+
}
|
|
2073
|
+
const rendered = renderSchemaExpression({ expression: term.expression, model, dialect, enums, usage: "indexExpression", site });
|
|
2074
|
+
if (!["lower", "upper", "add", "sub", "mul"].includes(term.expression.kind))
|
|
2075
|
+
throw invalid3({ site, reason: "index-expression-root" });
|
|
2076
|
+
if (rendered.resultType.refinement !== undefined || typeof rendered.resultType.type !== "string" || !["String", "Int", "BigInt", "Float", "Decimal"].includes(rendered.resultType.type))
|
|
2077
|
+
throw invalid3({ site, reason: "index-expression-result" });
|
|
2078
|
+
return { kind: "expression", text: rendered.text, desc: term.sort === "desc" };
|
|
2079
|
+
});
|
|
2080
|
+
if (computed && dialect.name === "mysql")
|
|
2081
|
+
assertMysqlIndexKeyStorage({ dialect, model, terms });
|
|
2082
|
+
return keys;
|
|
2083
|
+
}
|
|
2084
|
+
function renderIndexKeys({ dialect, keys, unique }) {
|
|
2085
|
+
if (keys.length === 0)
|
|
2086
|
+
throw invalid3({ site: "index", reason: "index-empty-keys" });
|
|
2087
|
+
const computed = keys.some((key) => key.kind === "expression");
|
|
2088
|
+
if (computed && !dialect.capabilities.expressionIndexes)
|
|
2089
|
+
throw unsupportedCapability12({ dialect: dialect.name, feature: "expressionIndexes" });
|
|
2090
|
+
if (computed && keys.some((key) => key.kind === "field" && key.operatorClass !== undefined))
|
|
2091
|
+
throw unsupportedCapability12({ dialect: dialect.name, feature: "expressionIndexOperatorClasses" });
|
|
2092
|
+
return keys.map((key) => {
|
|
2093
|
+
if (key.kind === "field") {
|
|
2094
|
+
if (dialect.name === "sqlite" && key.decimal === true)
|
|
2095
|
+
return `CAST(${dialect.quoteIdent({ name: key.name })} AS REAL)${key.desc ? " DESC" : ""}`;
|
|
2096
|
+
return renderIndexColumns({ dialect, columns: [key], unique })[0];
|
|
2097
|
+
}
|
|
2098
|
+
if (!key.text.trim() || key.text.includes("\x00"))
|
|
2099
|
+
throw invalid3({ site: "index", reason: "index-raw-expression" });
|
|
2100
|
+
const expression = dialect.name === "sqlite" ? key.text : `(${key.text})`;
|
|
2101
|
+
return `${expression}${key.desc ? " DESC" : ""}`;
|
|
2102
|
+
});
|
|
1735
2103
|
}
|
|
1736
|
-
function
|
|
1737
|
-
|
|
2104
|
+
function extractIndexKeys({ dialect, definition, index, backslashEscapes = true }) {
|
|
2105
|
+
switch (dialect) {
|
|
2106
|
+
case "postgres":
|
|
2107
|
+
return extractPostgresIndexKeys({ definition, index });
|
|
2108
|
+
case "sqlite":
|
|
2109
|
+
return extractSqliteIndexKeys({ definition, index });
|
|
2110
|
+
case "mysql":
|
|
2111
|
+
return extractMysqlIndexKeys({ definition, index, backslashEscapes });
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
function renderIndexExpressionProbe({ ...params }) {
|
|
2115
|
+
if (params.operation === "context" && params.dialect.name === "sqlite")
|
|
2116
|
+
return [{ text: "SELECT sqlite_version() AS version", values: [], returning: "none" }];
|
|
2117
|
+
if (!params.dialect.capabilities.expressionIndexes)
|
|
2118
|
+
throw unsupportedCapability12({ dialect: params.dialect.name, feature: "expressionIndexes" });
|
|
2119
|
+
switch (params.dialect.name) {
|
|
2120
|
+
case "postgres":
|
|
2121
|
+
return renderPostgresIndexExpressionProbe(params);
|
|
2122
|
+
case "mysql":
|
|
2123
|
+
return renderMysqlIndexExpressionProbe(params);
|
|
2124
|
+
case "sqlite":
|
|
2125
|
+
throw unsupportedCapability12({ dialect: "sqlite", feature: "indexExpressionNormalizationProbe", hint: "SQLite expression indexes use quote-aware source extraction, not a normalization probe" });
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
function assertIndexExpressionVersion({ dialect, version }) {
|
|
2129
|
+
if (dialect === "postgres")
|
|
2130
|
+
return;
|
|
2131
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:[-\s].*)?$/);
|
|
2132
|
+
const parts = match?.slice(1, 4).map(Number);
|
|
2133
|
+
const minimum = dialect === "mysql" ? [8, 0, 13] : [3, 9, 0];
|
|
2134
|
+
const admitted = parts !== undefined && !/mariadb/i.test(version) && parts.some((part, ordinal) => part > minimum[ordinal] && parts.slice(0, ordinal).every((prefix, position) => prefix === minimum[position])) || parts !== undefined && parts.every((part, ordinal) => part === minimum[ordinal]);
|
|
2135
|
+
if (!admitted)
|
|
2136
|
+
throw unsupportedCapability12({ dialect, feature: "expressionIndexServerVersion", hint: `Expression indexes require a known ${dialect} version at least ${minimum.join(".")}; received ${JSON.stringify(version)}` });
|
|
1738
2137
|
}
|
|
1739
|
-
function
|
|
1740
|
-
|
|
1741
|
-
sqlInputScopeKeys({ scope: params.scope });
|
|
1742
|
-
const query = Object.freeze(Object.create(null));
|
|
1743
|
-
reusableQueries.set(query, { ...state, scope: params.scope, cache: new WeakMap });
|
|
1744
|
-
return query;
|
|
2138
|
+
function invalid3({ site, reason }) {
|
|
2139
|
+
return new VibeError11({ code: "VIBE_SCHEMA_INVALID", message: `Invalid index keys at ${site}: ${reason}`, meta: { rule: reason, site } });
|
|
1745
2140
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
2141
|
+
// packages/sql/src/dialects/module-dependency.ts
|
|
2142
|
+
function renderModuleDependencyQuery(params) {
|
|
2143
|
+
const { dialect, operation } = params;
|
|
2144
|
+
if (operation.kind === "visibility")
|
|
2145
|
+
return dialect.name === "mysql" ? { text: "SHOW GRANTS FOR CURRENT_USER", values: [], returning: "none" } : undefined;
|
|
2146
|
+
if (dialect.name === "sqlite" || operation.tables.length === 0)
|
|
2147
|
+
return;
|
|
2148
|
+
const collector = new ParamCollector(dialect);
|
|
2149
|
+
const owned = operation.tables.map((table, index) => `SELECT ${collector.add(operation.namespace)}${index === 0 ? " AS namespace" : ""}, ${collector.add(table)}${index === 0 ? " AS table_name" : ""}`).join(" UNION ALL ");
|
|
2150
|
+
const pg = dialect.name === "postgres";
|
|
2151
|
+
const nameKey = ({ column }) => pg ? column : `CAST(IF(@@lower_case_table_names = 0, ${column}, LOWER(${column})) AS BINARY)`;
|
|
2152
|
+
const isOwned = ({ namespace, table }) => `EXISTS (SELECT 1 FROM owned_tables o WHERE ${nameKey({ column: namespace })} = ${nameKey({ column: "o.namespace" })} AND ${nameKey({ column: table })} = ${nameKey({ column: "o.table_name" })})`;
|
|
2153
|
+
const sourceNamespace = pg ? "sn.nspname" : "fk.CONSTRAINT_SCHEMA";
|
|
2154
|
+
const sourceTable = pg ? "s.relname" : "fk.TABLE_NAME";
|
|
2155
|
+
const targetNamespace = pg ? "tn.nspname" : "fk.UNIQUE_CONSTRAINT_SCHEMA";
|
|
2156
|
+
const targetTable = pg ? "t.relname" : "fk.REFERENCED_TABLE_NAME";
|
|
2157
|
+
const from = pg ? "pg_catalog.pg_constraint fk JOIN pg_catalog.pg_class s ON s.oid = fk.conrelid JOIN pg_catalog.pg_namespace sn ON sn.oid = s.relnamespace JOIN pg_catalog.pg_class t ON t.oid = fk.confrelid JOIN pg_catalog.pg_namespace tn ON tn.oid = t.relnamespace WHERE fk.contype = 'f'" : "information_schema.REFERENTIAL_CONSTRAINTS fk";
|
|
2158
|
+
return {
|
|
2159
|
+
text: `WITH owned_tables AS (${owned})
|
|
2160
|
+
SELECT * FROM (SELECT ${pg ? "fk.conname" : "fk.CONSTRAINT_NAME"} AS constraint_name, ${sourceNamespace} AS source_namespace, ${sourceTable} AS source_table, ${targetNamespace} AS target_namespace, ${targetTable} AS target_table, ${isOwned({ namespace: sourceNamespace, table: sourceTable })} AS source_owned, ${isOwned({ namespace: targetNamespace, table: targetTable })} AS target_owned FROM ${from}) dependencies
|
|
2161
|
+
WHERE source_owned OR target_owned
|
|
2162
|
+
ORDER BY source_namespace, source_table, constraint_name`,
|
|
2163
|
+
values: collector.values,
|
|
2164
|
+
returning: "none"
|
|
2165
|
+
};
|
|
1752
2166
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
const
|
|
1758
|
-
|
|
1759
|
-
const
|
|
1760
|
-
if (
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
2167
|
+
// packages/sql/src/dialects/migration-replay-coverage.ts
|
|
2168
|
+
var extensionMember = (classid, objid) => `EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.classid = '${classid}'::regclass AND d.objid = ${objid} AND d.deptype = 'e')`;
|
|
2169
|
+
var extensionOwnedNamespace = `(EXISTS (SELECT 1 FROM pg_catalog.pg_extension e WHERE e.extnamespace = n.oid) OR ${extensionMember("pg_namespace", "n.oid")}) AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_class c WHERE c.relnamespace = n.oid AND c.relkind IN ('r', 'p', 'v', 'm', 'f', 'S') AND NOT ${extensionMember("pg_class", "c.oid")}) AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_proc p WHERE p.pronamespace = n.oid AND NOT ${extensionMember("pg_proc", "p.oid")}) AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_type t WHERE t.typnamespace = n.oid AND (t.typrelid = 0 AND t.typelem = 0 OR t.typtype = 'c' AND EXISTS (SELECT 1 FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid AND c.relkind = 'c')) AND NOT ${extensionMember("pg_type", "t.oid")})`;
|
|
2170
|
+
function renderReplayCoverageGaps(params) {
|
|
2171
|
+
const { dialect } = params;
|
|
2172
|
+
const collector = new ParamCollector(dialect);
|
|
2173
|
+
const namespace = collector.add(params.namespace);
|
|
2174
|
+
if (dialect.name === "postgres")
|
|
2175
|
+
return { text: `SELECT 'type' AS kind, t.typname::text AS name, n.nspname::text AS namespace, NULL::text AS parent FROM pg_catalog.pg_type t JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace WHERE n.nspname = ${namespace} AND (t.typtype IN ('b', 'd', 'r', 'm') AND t.typelem = 0 OR t.typtype = 'c' AND EXISTS (SELECT 1 FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid AND c.relkind = 'c')) AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.classid = 'pg_type'::regclass AND d.objid = t.oid AND d.deptype = 'e')
|
|
2176
|
+
UNION ALL SELECT 'routine', p.proname::text, n.nspname::text, NULL::text FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = ${namespace} AND p.prokind <> 'f' AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.classid = 'pg_proc'::regclass AND d.objid = p.oid AND d.deptype = 'e')
|
|
2177
|
+
UNION ALL SELECT 'relation-layout', c.relname::text, n.nspname::text, NULL::text FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = ${namespace} AND (c.relkind IN ('p', 'f') OR c.relispartition OR EXISTS (SELECT 1 FROM pg_catalog.pg_inherits i WHERE i.inhrelid = c.oid)) AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.classid = 'pg_class'::regclass AND d.objid = c.oid AND d.deptype = 'e')
|
|
2178
|
+
UNION ALL SELECT 'rule', r.rulename::text, n.nspname::text, c.relname::text FROM pg_catalog.pg_rewrite r JOIN pg_catalog.pg_class c ON c.oid = r.ev_class JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = ${namespace} AND r.rulename <> '_RETURN' AND NOT ${extensionMember("pg_class", "c.oid")}
|
|
2179
|
+
UNION ALL SELECT 'namespace', n.nspname::text, n.nspname::text, NULL::text FROM pg_catalog.pg_namespace n WHERE n.nspname <> ${namespace} AND n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg\\_%' ESCAPE '\\' AND NOT (${extensionOwnedNamespace})${params.privateNamespace === undefined ? "" : ` AND n.nspname <> ${collector.add(params.privateNamespace)}`}`, values: collector.values, returning: "none" };
|
|
2180
|
+
if (dialect.name === "mysql")
|
|
2181
|
+
return { text: `SELECT 'procedure' AS kind, ROUTINE_NAME AS name, ROUTINE_SCHEMA AS namespace, NULL AS parent FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA = ${namespace} AND ROUTINE_TYPE = 'PROCEDURE'
|
|
2182
|
+
UNION ALL SELECT 'event', EVENT_NAME, EVENT_SCHEMA, NULL FROM information_schema.EVENTS WHERE EVENT_SCHEMA = ${collector.add(params.namespace)}
|
|
2183
|
+
UNION ALL SELECT DISTINCT 'partition', TABLE_NAME, TABLE_SCHEMA, NULL FROM information_schema.PARTITIONS WHERE TABLE_SCHEMA = ${collector.add(params.namespace)} AND PARTITION_NAME IS NOT NULL`, values: collector.values, returning: "none" };
|
|
2184
|
+
return { text: `SELECT 'table-options' AS kind, name, schema AS namespace, NULL AS parent FROM pragma_table_list WHERE schema = ${namespace} AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\' AND (type = 'virtual' OR wr <> 0 OR strict <> 0)`, values: collector.values, returning: "none" };
|
|
1766
2185
|
}
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
2186
|
+
// packages/sql/src/dialects/migration-shadow.ts
|
|
2187
|
+
import { assertSqlNamespace, unsupportedCapability as unsupportedCapability13 } from "@vibeorm/schema";
|
|
2188
|
+
var SHADOW_DATABASE = Object.freeze({ schema: "_vibeorm_shadow", table: "_vibe_shadow", lock: 1447645267, timeoutSeconds: 60 });
|
|
2189
|
+
var SHADOW_CAPABILITIES = Object.freeze({
|
|
2190
|
+
postgres: Object.freeze({ serverProvisioning: true, dedicated: true }),
|
|
2191
|
+
mysql: Object.freeze({ serverProvisioning: true, dedicated: true }),
|
|
2192
|
+
sqlite: Object.freeze({ serverProvisioning: false, dedicated: false })
|
|
2193
|
+
});
|
|
2194
|
+
function renderShadowOperation(params) {
|
|
2195
|
+
const { dialect, operation } = params;
|
|
2196
|
+
if (!SHADOW_CAPABILITIES[dialect.name].serverProvisioning)
|
|
2197
|
+
throw unsupportedCapability13({ dialect: dialect.name, feature: "shadowDatabaseProvisioning" });
|
|
2198
|
+
const pg = dialect.name === "postgres";
|
|
2199
|
+
const collector = new ParamCollector(dialect);
|
|
2200
|
+
const marker = pg ? `${dialect.quoteIdent({ name: SHADOW_DATABASE.schema })}.${dialect.quoteIdent({ name: SHADOW_DATABASE.table })}` : dialect.quoteIdent({ name: SHADOW_DATABASE.table });
|
|
2201
|
+
const result = (text) => [{ text, values: collector.values, returning: "none" }];
|
|
2202
|
+
switch (operation.kind) {
|
|
2203
|
+
case "createDatabase":
|
|
2204
|
+
return result(`CREATE DATABASE ${dialect.quoteIdent({ name: operation.name })}${pg ? " TEMPLATE template0" : ""}`);
|
|
2205
|
+
case "dropDatabase":
|
|
2206
|
+
return result(`DROP DATABASE ${dialect.quoteIdent({ name: operation.name })}`);
|
|
2207
|
+
case "identity":
|
|
2208
|
+
return result(pg ? `SELECT current_database()::text AS database, (SELECT oid::text FROM pg_catalog.pg_database WHERE datname = current_database()) AS database_id, current_schema()::text AS namespace` : `SELECT DATABASE() AS \`database\`, @@lower_case_table_names AS name_folding, @@server_uuid AS server_id`);
|
|
2209
|
+
case "lock":
|
|
2210
|
+
return result(pg ? `SELECT pg_catalog.pg_advisory_lock(${collector.add(SHADOW_DATABASE.lock)})` : `SELECT GET_LOCK(${collector.add(`vibeorm_shadow:${operation.database}`)}, ${collector.add(SHADOW_DATABASE.timeoutSeconds)}) AS acquired`);
|
|
2211
|
+
case "unlock":
|
|
2212
|
+
return result(pg ? `SELECT pg_catalog.pg_advisory_unlock(${collector.add(SHADOW_DATABASE.lock)}) AS released` : `SELECT RELEASE_LOCK(${collector.add(`vibeorm_shadow:${operation.database}`)}) AS released`);
|
|
2213
|
+
case "markerExists":
|
|
2214
|
+
return result(pg ? `SELECT c.relname AS name FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = ${collector.add(SHADOW_DATABASE.schema)} AND c.relname = ${collector.add(SHADOW_DATABASE.table)}` : `SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ${collector.add(SHADOW_DATABASE.table)}`);
|
|
2215
|
+
case "markerRead":
|
|
2216
|
+
return result(`SELECT payload FROM ${marker}`);
|
|
2217
|
+
case "markerColumns":
|
|
2218
|
+
return result(`SELECT column_name AS name, data_type AS type, is_nullable AS nullable FROM information_schema.columns AS marker_column WHERE table_schema = ${pg ? collector.add(SHADOW_DATABASE.schema) : "DATABASE()"} AND table_name = ${collector.add(SHADOW_DATABASE.table)} AND ${pg ? `EXISTS (SELECT 1 FROM pg_catalog.pg_class r JOIN pg_catalog.pg_namespace n ON n.oid = r.relnamespace WHERE n.nspname = marker_column.table_schema AND r.relname = marker_column.table_name AND r.relkind = 'r' AND r.relpersistence = 'p' AND NOT r.relrowsecurity AND NOT r.relforcerowsecurity AND NOT r.relhasrules AND NOT r.relhastriggers AND NOT r.relispartition AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_attrdef a WHERE a.adrelid = r.oid) AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_constraint c WHERE c.conrelid = r.oid AND c.contype <> 'n') AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_index i WHERE i.indrelid = r.oid) AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_inherits i WHERE i.inhrelid = r.oid OR i.inhparent = r.oid) AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_attribute a WHERE a.attrelid = r.oid AND (a.attisdropped OR a.attidentity <> '' OR a.attgenerated <> '')))` : `EXISTS (SELECT 1 FROM information_schema.TABLES t WHERE t.TABLE_SCHEMA = marker_column.table_schema AND t.TABLE_NAME = marker_column.table_name AND t.TABLE_TYPE = 'BASE TABLE' AND t.ENGINE = 'InnoDB') AND NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS c WHERE c.TABLE_SCHEMA = marker_column.table_schema AND c.TABLE_NAME = marker_column.table_name AND (c.COLUMN_DEFAULT IS NOT NULL OR c.EXTRA <> '')) AND NOT EXISTS (SELECT 1 FROM information_schema.TRIGGERS t WHERE t.EVENT_OBJECT_SCHEMA = DATABASE() AND t.EVENT_OBJECT_TABLE = ${collector.add(SHADOW_DATABASE.table)}) AND NOT EXISTS (SELECT 1 FROM information_schema.TABLE_CONSTRAINTS c WHERE c.TABLE_SCHEMA = DATABASE() AND c.TABLE_NAME = ${collector.add(SHADOW_DATABASE.table)})`}
|
|
2219
|
+
ORDER BY ordinal_position`);
|
|
2220
|
+
case "grants": {
|
|
2221
|
+
if (pg)
|
|
2222
|
+
throw unsupportedCapability13({ dialect: dialect.name, feature: "mysqlShadowPrivileges" });
|
|
2223
|
+
return result("SHOW GRANTS FOR CURRENT_USER");
|
|
2224
|
+
}
|
|
2225
|
+
case "markerCreate":
|
|
2226
|
+
return pg ? [{ text: `CREATE SCHEMA ${dialect.quoteIdent({ name: SHADOW_DATABASE.schema })}`, values: [], returning: "none" }, { text: `CREATE TABLE ${marker} (payload text NOT NULL)`, values: [], returning: "none" }] : result(`CREATE TABLE ${marker} (payload LONGTEXT NOT NULL) ENGINE=InnoDB`);
|
|
2227
|
+
case "markerWrite":
|
|
2228
|
+
return result(operation.insert ? `INSERT INTO ${marker} (payload) VALUES (${collector.add(operation.value)})` : `UPDATE ${marker} SET payload = ${collector.add(operation.value)}`);
|
|
2229
|
+
case "journal":
|
|
2230
|
+
return result(`SELECT name, checksum, statement_index, error FROM ${dialect.quoteIdent({ name: "_vibe_migrations" })} ORDER BY name`);
|
|
2231
|
+
case "inventory":
|
|
2232
|
+
return result(pg ? `SELECT 'namespace' AS kind, nspname::text AS name, nspname::text AS namespace FROM pg_catalog.pg_namespace WHERE nspname NOT IN ('public', 'information_schema') AND nspname NOT LIKE 'pg\\_%' ESCAPE '\\'
|
|
2233
|
+
UNION ALL SELECT 'relation', c.relname::text, n.nspname::text FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg\\_%' ESCAPE '\\'
|
|
2234
|
+
UNION ALL SELECT 'routine', p.proname::text, n.nspname::text FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg\\_%' ESCAPE '\\'
|
|
2235
|
+
UNION ALL SELECT 'type', t.typname::text, n.nspname::text FROM pg_catalog.pg_type t JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace WHERE n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg\\_%' ESCAPE '\\' AND t.typrelid = 0 AND t.typelem = 0
|
|
2236
|
+
UNION ALL SELECT 'extension', e.extname::text, n.nspname::text FROM pg_catalog.pg_extension e JOIN pg_catalog.pg_namespace n ON n.oid = e.extnamespace WHERE e.extname <> 'plpgsql'
|
|
2237
|
+
UNION ALL SELECT 'marker-dependency', d.objid::text, '${SHADOW_DATABASE.schema}' FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_class c ON d.classid = 'pg_class'::regclass AND d.objid = c.oid JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = '${SHADOW_DATABASE.schema}' AND d.deptype NOT IN ('i', 'a') AND d.refclassid <> 'pg_namespace'::regclass
|
|
2238
|
+
UNION ALL SELECT 'marker-trigger', t.tgname::text, n.nspname::text FROM pg_catalog.pg_trigger t JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = '${SHADOW_DATABASE.schema}'
|
|
2239
|
+
UNION ALL SELECT 'namespace-object', o.name::text, n.nspname::text FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_namespace n ON d.refclassid = 'pg_namespace'::regclass AND d.refobjid = n.oid CROSS JOIN LATERAL pg_catalog.pg_identify_object(d.classid, d.objid, d.objsubid) o WHERE n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg\\_%' ESCAPE '\\' AND d.classid NOT IN ('pg_class'::regclass, 'pg_proc'::regclass, 'pg_type'::regclass, 'pg_extension'::regclass)
|
|
2240
|
+
UNION ALL SELECT 'database-object', evtname::text, '' FROM pg_catalog.pg_event_trigger
|
|
2241
|
+
UNION ALL SELECT 'database-object', pubname::text, '' FROM pg_catalog.pg_publication
|
|
2242
|
+
UNION ALL SELECT 'database-object', subname::text, '' FROM pg_catalog.pg_subscription WHERE subdbid = (SELECT oid FROM pg_catalog.pg_database WHERE datname = current_database())
|
|
2243
|
+
UNION ALL SELECT 'database-object', srvname::text, '' FROM pg_catalog.pg_foreign_server
|
|
2244
|
+
UNION ALL SELECT 'database-object', fdwname::text, '' FROM pg_catalog.pg_foreign_data_wrapper
|
|
2245
|
+
UNION ALL SELECT 'database-object', c.oid::text, '' FROM pg_catalog.pg_cast c JOIN pg_catalog.pg_type s ON s.oid = c.castsource JOIN pg_catalog.pg_type t ON t.oid = c.casttarget LEFT JOIN pg_catalog.pg_proc p ON p.oid = c.castfunc WHERE s.typnamespace <> 'pg_catalog'::regnamespace OR t.typnamespace <> 'pg_catalog'::regnamespace OR p.pronamespace <> 'pg_catalog'::regnamespace` : `SELECT TABLE_TYPE AS kind, TABLE_NAME AS name, TABLE_SCHEMA AS namespace FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()
|
|
2246
|
+
UNION ALL SELECT ROUTINE_TYPE, ROUTINE_NAME, ROUTINE_SCHEMA FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA = DATABASE()
|
|
2247
|
+
UNION ALL SELECT 'EVENT', EVENT_NAME, EVENT_SCHEMA FROM information_schema.EVENTS WHERE EVENT_SCHEMA = DATABASE()
|
|
2248
|
+
UNION ALL SELECT 'TRIGGER', TRIGGER_NAME, TRIGGER_SCHEMA FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = DATABASE()
|
|
2249
|
+
UNION ALL SELECT 'EXTERNAL FOREIGN KEY', CONCAT(TABLE_NAME, '.', CONSTRAINT_NAME), CONSTRAINT_SCHEMA FROM information_schema.REFERENTIAL_CONSTRAINTS WHERE UNIQUE_CONSTRAINT_SCHEMA = DATABASE() AND CAST(CONSTRAINT_SCHEMA AS BINARY) <> CAST(DATABASE() AS BINARY)`);
|
|
2250
|
+
case "resetPostgres":
|
|
2251
|
+
case "resetDedicatedPostgres": {
|
|
2252
|
+
if (!pg)
|
|
2253
|
+
throw unsupportedCapability13({ dialect: dialect.name, feature: "postgresShadowReset" });
|
|
2254
|
+
const owned = operation.kind === "resetDedicatedPostgres" ? operation.ownedNamespaces : [];
|
|
2255
|
+
for (const namespace of owned)
|
|
2256
|
+
assertSqlNamespace({ namespace, dialect: "postgres", allowPublic: false });
|
|
2257
|
+
return [
|
|
2258
|
+
...owned.map((namespace) => ({ text: `DROP SCHEMA IF EXISTS ${dialect.quoteIdent({ name: namespace })} CASCADE`, values: [], returning: "none" })),
|
|
2259
|
+
{ text: 'DROP SCHEMA "public" CASCADE', values: [], returning: "none" },
|
|
2260
|
+
{ text: 'CREATE SCHEMA "public"', values: [], returning: "none" }
|
|
2261
|
+
];
|
|
2262
|
+
}
|
|
2263
|
+
case "mysqlForeignKeys": {
|
|
2264
|
+
if (pg)
|
|
2265
|
+
throw unsupportedCapability13({ dialect: dialect.name, feature: "mysqlShadowReset" });
|
|
2266
|
+
return result(`SET FOREIGN_KEY_CHECKS = ${operation.enabled ? "1" : "0"}`);
|
|
2267
|
+
}
|
|
2268
|
+
case "dropMysqlObject": {
|
|
2269
|
+
if (pg)
|
|
2270
|
+
throw unsupportedCapability13({ dialect: dialect.name, feature: "mysqlShadowReset" });
|
|
2271
|
+
return result(`DROP ${operation.object} ${dialect.quoteIdent({ name: operation.name })}`);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
1770
2274
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
2275
|
+
// packages/sql/src/module-journal.ts
|
|
2276
|
+
var MODULE_JOURNAL_COLUMN = "module_origin";
|
|
2277
|
+
function renderModuleJournal(params) {
|
|
2278
|
+
const { dialect, operation } = params;
|
|
2279
|
+
const q = (name) => dialect.quoteIdent({ name });
|
|
2280
|
+
const table = q(params.table);
|
|
2281
|
+
const column = q(MODULE_JOURNAL_COLUMN);
|
|
2282
|
+
const collector = new ParamCollector(dialect);
|
|
2283
|
+
let text;
|
|
2284
|
+
switch (operation.kind) {
|
|
2285
|
+
case "columns":
|
|
2286
|
+
text = dialect.name === "sqlite" ? `PRAGMA table_info(${table})` : `SELECT column_name AS name FROM information_schema.columns WHERE table_schema = ${dialect.name === "postgres" ? "current_schema()" : "DATABASE()"} AND table_name = ${collector.add(params.table)}`;
|
|
2287
|
+
break;
|
|
2288
|
+
case "addColumn":
|
|
2289
|
+
text = `ALTER TABLE ${table} ADD COLUMN ${column} ${dialect.name === "mysql" ? "LONGTEXT" : "TEXT"}`;
|
|
2290
|
+
break;
|
|
2291
|
+
case "read":
|
|
2292
|
+
text = `SELECT * FROM ${table} ORDER BY ${q("id")}`;
|
|
2293
|
+
break;
|
|
2294
|
+
case "insert":
|
|
2295
|
+
text = `INSERT INTO ${table} (${["name", "checksum", "statement_index", MODULE_JOURNAL_COLUMN].map(q).join(", ")}) VALUES (${collector.addMany([operation.name, operation.checksum, operation.index, operation.entry]).join(", ")})`;
|
|
2296
|
+
break;
|
|
2297
|
+
case "complete":
|
|
2298
|
+
text = `UPDATE ${table} SET ${column} = ${collector.add(operation.entry)} WHERE ${q("name")} = ${collector.add(operation.name)}`;
|
|
2299
|
+
break;
|
|
2300
|
+
}
|
|
2301
|
+
return { text, values: collector.values };
|
|
1774
2302
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
}
|
|
1785
|
-
function
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
const render = commands.get(params.query) ?? invalid4("Expected an authentic SQL command");
|
|
1795
|
-
const collector = new ParamCollector(params.dialect);
|
|
1796
|
-
const text = render({ dialect: params.dialect, collector });
|
|
1797
|
-
if (collector.values.length > params.dialect.paramBudget)
|
|
1798
|
-
invalid4("SQL command exceeds this dialect's parameter budget; statements are never split implicitly");
|
|
1799
|
-
return { text, values: collector.values };
|
|
2303
|
+
// packages/sql/src/capabilities.ts
|
|
2304
|
+
import {
|
|
2305
|
+
DIALECT_CAPABILITIES,
|
|
2306
|
+
POSTGRES_CAPABILITIES,
|
|
2307
|
+
SQLITE_CAPABILITIES,
|
|
2308
|
+
MYSQL_CAPABILITIES
|
|
2309
|
+
} from "@vibeorm/schema";
|
|
2310
|
+
|
|
2311
|
+
// packages/sql/src/identifiers.ts
|
|
2312
|
+
import { VibeError as VibeError12 } from "@vibeorm/schema";
|
|
2313
|
+
function assertNoNul(params) {
|
|
2314
|
+
if (params.name.includes("\x00")) {
|
|
2315
|
+
throw new VibeError12({
|
|
2316
|
+
code: "VIBE_VALIDATION",
|
|
2317
|
+
message: "an SQL identifier cannot contain a NUL character \u2014 the server would stop reading it there and address a different object",
|
|
2318
|
+
meta: { name: params.name.replaceAll("\x00", "\\0") }
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
return params.name;
|
|
1800
2322
|
}
|
|
1801
|
-
// packages/sql/src/exclusion.ts
|
|
1802
|
-
import { VibeError as VibeError12, fieldTypeIdentity as fieldTypeIdentity2, isEnumType as isEnumType2, unsupportedCapability as unsupportedCapability10 } from "@vibeorm/schema";
|
|
1803
2323
|
|
|
1804
|
-
// packages/sql/src/
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
2324
|
+
// packages/sql/src/dialects/sqlite.ts
|
|
2325
|
+
var sqliteDialect = {
|
|
2326
|
+
name: "sqlite",
|
|
2327
|
+
capabilities: SQLITE_CAPABILITIES,
|
|
2328
|
+
inArrayStrategy: "expand",
|
|
2329
|
+
tupleInStrategy: "rowValue",
|
|
2330
|
+
keyListStrategy: "values",
|
|
2331
|
+
keysetSeekStrategy: "rowValue",
|
|
2332
|
+
keyPageStrategy: "inLimit",
|
|
2333
|
+
relationFilterStrategy: "inSelect",
|
|
2334
|
+
deferredJoinPage: null,
|
|
2335
|
+
nativeNullsOrder: "low",
|
|
2336
|
+
nullsUnionPage: false,
|
|
2337
|
+
timeOfDayInStrategy: "in",
|
|
2338
|
+
upsertForm: "onConflict",
|
|
2339
|
+
conflictKeyEquality: {
|
|
2340
|
+
foldingScalars: [],
|
|
2341
|
+
textIsBinaryByDefault: true,
|
|
2342
|
+
duplicatesInOneStatement: "lastWriteWins"
|
|
2343
|
+
},
|
|
2344
|
+
excludedRowRef: { form: "keyword", name: "excluded" },
|
|
2345
|
+
doNothingForm: "onConflictDoNothing",
|
|
2346
|
+
insensitiveLike: "lower",
|
|
2347
|
+
strictMatchStrategy: "glob",
|
|
2348
|
+
greatestForm: "frame",
|
|
2349
|
+
orderByNullsMode: "native",
|
|
2350
|
+
offsetRequiresLimit: { limitText: "-1" },
|
|
2351
|
+
likeEscapeClause: " ESCAPE '\\'",
|
|
2352
|
+
jsonPathMode: "sqliteExtract",
|
|
2353
|
+
supportedJsonOps: [
|
|
2354
|
+
"equals",
|
|
2355
|
+
"string_contains",
|
|
2356
|
+
"string_starts_with",
|
|
2357
|
+
"string_ends_with",
|
|
2358
|
+
"array_contains",
|
|
2359
|
+
"array_starts_with",
|
|
2360
|
+
"array_ends_with"
|
|
2361
|
+
],
|
|
2362
|
+
jsonParamCast: "",
|
|
2363
|
+
valueCastTypes: Object.freeze({ String: "TEXT", Boolean: "INTEGER", Int: "INTEGER", BigInt: "INTEGER", Float: "REAL", Decimal: "TEXT", DateTime: "TEXT", Json: "TEXT", Bytes: "BLOB" }),
|
|
2364
|
+
lastInsertIdSql: null,
|
|
2365
|
+
rowLockSyntax: null,
|
|
2366
|
+
advisoryLockFunctions: null,
|
|
2367
|
+
rowDefaultKeyword: false,
|
|
2368
|
+
bulkInsertForm: "values",
|
|
2369
|
+
emptyInsert: { form: "defaultValues", conflictClause: false },
|
|
2370
|
+
statementChain: null,
|
|
2371
|
+
paramBudget: 900,
|
|
2372
|
+
booleanLiteral: { true: "1", false: "0" },
|
|
2373
|
+
quoteIdent({ name }) {
|
|
2374
|
+
return `"${assertNoNul({ name }).replaceAll('"', '""')}"`;
|
|
2375
|
+
},
|
|
2376
|
+
quoteStringLiteral({ value }) {
|
|
2377
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
2378
|
+
},
|
|
2379
|
+
placeholder() {
|
|
2380
|
+
return "?";
|
|
2381
|
+
},
|
|
2382
|
+
jsonTextProjection({ expression }) {
|
|
2383
|
+
return expression;
|
|
2384
|
+
},
|
|
2385
|
+
renderDbNow({ offsetSeconds, collector }) {
|
|
2386
|
+
if (offsetSeconds === 0)
|
|
2387
|
+
return `strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
|
|
2388
|
+
return `strftime('%Y-%m-%dT%H:%M:%fZ', 'now', ${collector.add(offsetSeconds)} || ' seconds')`;
|
|
1812
2389
|
}
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
2390
|
+
};
|
|
2391
|
+
|
|
2392
|
+
// packages/sql/src/dialects/sqlite-fts.ts
|
|
2393
|
+
function updateStatements(params) {
|
|
2394
|
+
const quote = (name) => sqliteDialect.quoteIdent({ name });
|
|
2395
|
+
const columns = params.columns.map((column) => quote(column)).join(", ");
|
|
2396
|
+
const oldValues = params.columns.map((column) => `old.${quote(column)}`).join(", ");
|
|
2397
|
+
const newValues = params.columns.map((column) => `new.${quote(column)}`).join(", ");
|
|
2398
|
+
const changes = [
|
|
2399
|
+
"old.rowid IS NOT new.rowid",
|
|
2400
|
+
...params.columns.map((column) => `old.${quote(column)} COLLATE BINARY IS NOT new.${quote(column)} COLLATE BINARY`)
|
|
2401
|
+
].join(" OR ");
|
|
2402
|
+
const target = `${quote(`${params.ftsTable}_au`)} AFTER UPDATE ON ${quote(params.table)}`;
|
|
2403
|
+
const body = `BEGIN
|
|
2404
|
+
INSERT INTO ${quote(params.ftsTable)}(${quote(params.ftsTable)}, rowid, ${columns}) VALUES ('delete', old.rowid, ${oldValues});
|
|
2405
|
+
INSERT INTO ${quote(params.ftsTable)}(rowid, ${columns}) VALUES (new.rowid, ${newValues});
|
|
2406
|
+
END`;
|
|
2407
|
+
const tail = `${target} WHEN (${changes}) ${body}`;
|
|
2408
|
+
return {
|
|
2409
|
+
create: `CREATE TRIGGER IF NOT EXISTS ${tail};`,
|
|
2410
|
+
definition: `CREATE TRIGGER ${tail}`,
|
|
2411
|
+
legacyDefinition: `CREATE TRIGGER ${target} ${body}`
|
|
1819
2412
|
};
|
|
1820
|
-
return renderNode({ node: params.predicate, context });
|
|
1821
2413
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
not: 3,
|
|
1826
|
-
compare: 4,
|
|
1827
|
-
inList: 4,
|
|
1828
|
-
isNull: 4,
|
|
1829
|
-
isTrue: 4
|
|
1830
|
-
};
|
|
1831
|
-
var COMPARISON_SQL = {
|
|
1832
|
-
eq: "=",
|
|
1833
|
-
ne: "<>",
|
|
1834
|
-
gt: ">",
|
|
1835
|
-
gte: ">=",
|
|
1836
|
-
lt: "<",
|
|
1837
|
-
lte: "<="
|
|
1838
|
-
};
|
|
1839
|
-
var EXACT_INTEGER = /^-?(0|[1-9]\d*)$/;
|
|
1840
|
-
var EXACT_DECIMAL = /^-?(0|[1-9]\d*)(\.\d+)?$/;
|
|
1841
|
-
var INT32_MIN = -2147483648n;
|
|
1842
|
-
var INT32_MAX = 2147483647n;
|
|
1843
|
-
var INT64_MIN = -(2n ** 63n);
|
|
1844
|
-
var INT64_MAX = 2n ** 63n - 1n;
|
|
1845
|
-
function refuse2(params) {
|
|
1846
|
-
const { context, rule, message } = params;
|
|
1847
|
-
throw new VibeError11({
|
|
1848
|
-
code: "VIBE_SCHEMA_INVALID",
|
|
1849
|
-
message: `${context.site} on "${context.model.name}": ${message}`,
|
|
1850
|
-
meta: { rule, model: context.model.name, site: context.site, ...params.meta ?? {} }
|
|
1851
|
-
});
|
|
2414
|
+
function renderSqliteFtsUpdateTrigger(params) {
|
|
2415
|
+
const { create, definition } = updateStatements(params);
|
|
2416
|
+
return { create, definition };
|
|
1852
2417
|
}
|
|
1853
|
-
function
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1856
|
-
|
|
2418
|
+
function sqliteFtsUpdateUpgrade(params) {
|
|
2419
|
+
const quoted = '"(?:[^"]|"")+"';
|
|
2420
|
+
const body = new RegExp(`BEGIN\\n INSERT INTO (${quoted})\\((${quoted}), rowid, (${quoted}(?:, ${quoted})*)\\) VALUES \\('delete', old\\.rowid, `, "g");
|
|
2421
|
+
for (const match of params.create.matchAll(body)) {
|
|
2422
|
+
const ftsTable = sqliteColumnClauses({ sql: `CREATE TABLE x (${match[1]})` })[0].name;
|
|
2423
|
+
const columns = sqliteColumnClauses({ sql: `CREATE TABLE x (${match[3]})` }).map((column) => column.name);
|
|
2424
|
+
if (params.name !== `${ftsTable}_au`)
|
|
2425
|
+
continue;
|
|
2426
|
+
const statements = updateStatements({ table: params.table, ftsTable, columns });
|
|
2427
|
+
if (params.create !== statements.create)
|
|
2428
|
+
continue;
|
|
2429
|
+
const installed = params.installed === statements.legacyDefinition ? "legacy" : params.installed === statements.definition ? "current" : undefined;
|
|
2430
|
+
if (installed === undefined)
|
|
2431
|
+
continue;
|
|
2432
|
+
if (params.tableDefinition === undefined)
|
|
2433
|
+
return { kind: "tableRequired", ftsTable };
|
|
2434
|
+
const tableTail = `${sqliteDialect.quoteIdent({ name: ftsTable })} USING fts5(${columns.map((name) => sqliteDialect.quoteIdent({ name })).join(", ")}, content=${sqliteDialect.quoteStringLiteral({ value: params.table })})`;
|
|
2435
|
+
if (params.tableDefinition !== `CREATE VIRTUAL TABLE ${tableTail}` && params.tableDefinition !== `CREATE VIRTUAL TABLE IF NOT EXISTS ${tableTail};`)
|
|
2436
|
+
continue;
|
|
2437
|
+
return { kind: "recognized", ftsTable, legacyDefinition: statements.legacyDefinition, currentDefinition: statements.definition, drop: `DROP TRIGGER IF EXISTS ${sqliteDialect.quoteIdent({ name: `${ftsTable}_au` })};`, installed };
|
|
1857
2438
|
}
|
|
1858
|
-
|
|
1859
|
-
return PRECEDENCE[node.kind] < PRECEDENCE[parent] ? `(${text})` : text;
|
|
2439
|
+
return;
|
|
1860
2440
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
case "not":
|
|
1879
|
-
return `NOT ${renderChild({ node: node.operand, parent: "not", context })}`;
|
|
1880
|
-
case "isTrue":
|
|
1881
|
-
return `COALESCE(${renderNode({ node: node.operand, context })}, FALSE)`;
|
|
1882
|
-
case "isNull": {
|
|
1883
|
-
const field = resolveField({ name: node.field.name, context, nullTest: true });
|
|
1884
|
-
return `${quoteColumn({ field, context })} IS ${node.negate === true ? "NOT NULL" : "NULL"}`;
|
|
1885
|
-
}
|
|
1886
|
-
case "compare": {
|
|
1887
|
-
const left = renderExpression({ expression: node.left, context });
|
|
1888
|
-
const right = renderOperand({ operand: node.right, field: left.field, context });
|
|
1889
|
-
return `${left.text} ${COMPARISON_SQL[node.op]} ${right}`;
|
|
1890
|
-
}
|
|
1891
|
-
case "inList": {
|
|
1892
|
-
if (node.values.length === 0) {
|
|
1893
|
-
refuse2({
|
|
1894
|
-
context,
|
|
1895
|
-
rule: "predicate-empty",
|
|
1896
|
-
message: `inList("${node.field.name}") has no values \u2014 an empty IN list is UNKNOWN on every row and never what an author means`,
|
|
1897
|
-
meta: { field: node.field.name }
|
|
1898
|
-
});
|
|
1899
|
-
}
|
|
1900
|
-
const field = resolveField({ name: node.field.name, context });
|
|
1901
|
-
const values = node.values.map((value) => renderLiteral({ value, field, context })).join(", ");
|
|
1902
|
-
return `${compareColumn({ field, context })} ${node.negate === true ? "NOT IN" : "IN"} (${values})`;
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
2441
|
+
// packages/sql/src/dialects/sqlite-session.ts
|
|
2442
|
+
var SQLITE_SESSION_SQL = Object.freeze({
|
|
2443
|
+
lockingMode: "PRAGMA main.locking_mode",
|
|
2444
|
+
exclusive: "PRAGMA main.locking_mode = EXCLUSIVE",
|
|
2445
|
+
normal: "PRAGMA main.locking_mode = NORMAL",
|
|
2446
|
+
acquire: "BEGIN EXCLUSIVE",
|
|
2447
|
+
commit: "COMMIT",
|
|
2448
|
+
rollback: "ROLLBACK",
|
|
2449
|
+
touch: "SELECT name FROM main.sqlite_schema LIMIT 1",
|
|
2450
|
+
foreignKeysOn: "PRAGMA foreign_keys = ON"
|
|
2451
|
+
});
|
|
2452
|
+
// packages/sql/src/composable.ts
|
|
2453
|
+
import { VibeError as VibeError13 } from "@vibeorm/schema";
|
|
2454
|
+
var fragments = new WeakMap;
|
|
2455
|
+
registerSqlHandleCheck({ check: (value) => fragments.has(value) });
|
|
2456
|
+
function invalid4(message) {
|
|
2457
|
+
throw new VibeError13({ code: "VIBE_VALIDATION", message });
|
|
1905
2458
|
}
|
|
1906
|
-
function
|
|
1907
|
-
const
|
|
1908
|
-
const
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
message: `references field "${name}", which the model does not declare`,
|
|
1914
|
-
meta: { field: name }
|
|
1915
|
-
});
|
|
1916
|
-
}
|
|
1917
|
-
if (field.isList) {
|
|
1918
|
-
refuse2({
|
|
1919
|
-
context,
|
|
1920
|
-
rule: "predicate-list-field",
|
|
1921
|
-
message: `compares scalar list "${name}" \u2014 array operators differ per dialect; use a raw expression for them`,
|
|
1922
|
-
meta: { field: name }
|
|
1923
|
-
});
|
|
1924
|
-
}
|
|
1925
|
-
if (field.isUnsupported === true) {
|
|
1926
|
-
refuse2({
|
|
1927
|
-
context,
|
|
1928
|
-
rule: "predicate-unsupported-type",
|
|
1929
|
-
message: `references "${name}", an Unsupported("${field.nativeType ?? "?"}") column whose operators VibeORM does not model \u2014 use a raw expression`,
|
|
1930
|
-
meta: { field: name }
|
|
1931
|
-
});
|
|
1932
|
-
}
|
|
1933
|
-
if (params.nullTest !== true && (field.type === "Json" || field.type === "Bytes")) {
|
|
1934
|
-
refuse2({
|
|
1935
|
-
context,
|
|
1936
|
-
rule: "predicate-unsupported-type",
|
|
1937
|
-
message: `references ${field.type} column "${name}" \u2014 ${field.type} comparison operators differ per dialect; use a raw expression`,
|
|
1938
|
-
meta: { field: name, type: field.type }
|
|
1939
|
-
});
|
|
1940
|
-
}
|
|
1941
|
-
if (context.usage === "check" && context.dialect.name === "mysql" && field.default?.kind === "autoincrement") {
|
|
1942
|
-
throw unsupportedCapability9({
|
|
1943
|
-
dialect: "mysql",
|
|
1944
|
-
feature: `CHECK constraint referencing auto-increment column "${context.model.name}.${name}"`,
|
|
1945
|
-
hint: "mysql refuses this with error 3818 \u2014 enforce the rule in application code or on a non-generated column"
|
|
1946
|
-
});
|
|
1947
|
-
}
|
|
1948
|
-
return field;
|
|
2459
|
+
function construct(parts) {
|
|
2460
|
+
const snapshot = Object.freeze([...parts]);
|
|
2461
|
+
const fragment = Object.freeze(Object.defineProperty(Object.create(null), "toJSON", {
|
|
2462
|
+
value: () => ({ parts: snapshot })
|
|
2463
|
+
}));
|
|
2464
|
+
fragments.set(fragment, snapshot);
|
|
2465
|
+
return fragment;
|
|
1949
2466
|
}
|
|
1950
|
-
function
|
|
1951
|
-
|
|
1952
|
-
return context.dialect.quoteIdent({ name: field.dbName ?? field.name });
|
|
2467
|
+
function isSqlFragment(value) {
|
|
2468
|
+
return typeof value === "object" && value !== null && fragments.has(value);
|
|
1953
2469
|
}
|
|
1954
|
-
function
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
2470
|
+
function partsOf(value) {
|
|
2471
|
+
return fragments.get(value) ?? invalid4("Expected a constructed SQL fragment");
|
|
2472
|
+
}
|
|
2473
|
+
function containsGreatest(fragment) {
|
|
2474
|
+
return partsOf(fragment).some((part) => typeof part !== "string" && ("greatest" in part));
|
|
2475
|
+
}
|
|
2476
|
+
function identifier(parts) {
|
|
2477
|
+
if (!Array.isArray(parts) || parts.length === 0 || parts.some((part) => typeof part !== "string" || part.length === 0 || part.includes("\x00"))) {
|
|
2478
|
+
invalid4("SQL identifiers require non-empty, NUL-free name components");
|
|
2479
|
+
}
|
|
1958
2480
|
}
|
|
1959
|
-
function
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
2481
|
+
function template(strings, ...values) {
|
|
2482
|
+
if (!Array.isArray(strings) || !Array.isArray(strings.raw) || strings.length !== values.length + 1 || strings.some((text) => typeof text !== "string")) {
|
|
2483
|
+
invalid4("Use sql as a template tag");
|
|
2484
|
+
}
|
|
2485
|
+
const parts = [];
|
|
2486
|
+
for (let index = 0;index < strings.length; index++) {
|
|
2487
|
+
parts.push(strings[index]);
|
|
2488
|
+
if (index < values.length) {
|
|
2489
|
+
const value = values[index];
|
|
2490
|
+
if (isSqlFragment(value))
|
|
2491
|
+
parts.push(...partsOf(value));
|
|
2492
|
+
else
|
|
2493
|
+
parts.push(Object.freeze({ value: snapshotSqlValue(value) }));
|
|
1971
2494
|
}
|
|
1972
|
-
const path = expression.path.map((value) => context.dialect.quoteStringLiteral({ value })).join(", ");
|
|
1973
|
-
return { text: `(${quoteColumn({ field, context })} #>> ARRAY[${path}]::text[])`, field: { ...field, type: "String" } };
|
|
1974
2495
|
}
|
|
1975
|
-
|
|
1976
|
-
if (inner.field.type !== "String")
|
|
1977
|
-
refuse2({ context, rule: "predicate-type-mismatch", message: "length requires a text expression" });
|
|
1978
|
-
return { text: `length(${inner.text})`, field: { ...inner.field, type: "Int" } };
|
|
2496
|
+
return construct(parts);
|
|
1979
2497
|
}
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
2498
|
+
var sql = Object.freeze(Object.assign(template, {
|
|
2499
|
+
text: ({ text }) => {
|
|
2500
|
+
if (typeof text !== "string" || text.includes("\x00"))
|
|
2501
|
+
invalid4("SQL text must be a NUL-free string");
|
|
2502
|
+
return construct([text]);
|
|
2503
|
+
},
|
|
2504
|
+
ident: ({ parts }) => {
|
|
2505
|
+
identifier(parts);
|
|
2506
|
+
return construct([Object.freeze({ ident: Object.freeze([...parts]) })]);
|
|
2507
|
+
},
|
|
2508
|
+
value: ({ value, decoder }) => construct([Object.freeze({ value: snapshotSqlValue(value), ...decoder === undefined ? {} : { decoder } })]),
|
|
2509
|
+
join: ({ items, separator }) => {
|
|
2510
|
+
if (!Array.isArray(items))
|
|
2511
|
+
invalid4("sql.join requires an array");
|
|
2512
|
+
const between = separator === undefined ? [", "] : partsOf(separator);
|
|
2513
|
+
const parts = [];
|
|
2514
|
+
items.forEach((value, index) => {
|
|
2515
|
+
if (index > 0)
|
|
2516
|
+
parts.push(...between);
|
|
2517
|
+
parts.push(...isSqlFragment(value) ? partsOf(value) : [Object.freeze({ value: snapshotSqlValue(value) })]);
|
|
1993
2518
|
});
|
|
2519
|
+
return construct(parts);
|
|
2520
|
+
},
|
|
2521
|
+
existing: ({ column }) => {
|
|
2522
|
+
identifier([column]);
|
|
2523
|
+
return construct([Object.freeze({ row: "existing", column })]);
|
|
2524
|
+
},
|
|
2525
|
+
incoming: ({ column }) => {
|
|
2526
|
+
identifier([column]);
|
|
2527
|
+
return construct([Object.freeze({ row: "incoming", column })]);
|
|
2528
|
+
},
|
|
2529
|
+
greatest: ({ values }) => {
|
|
2530
|
+
if (!Array.isArray(values) || values.length !== 2 || values.some((value) => !isSqlFragment(value)))
|
|
2531
|
+
invalid4("sql.greatest requires two SQL fragments");
|
|
2532
|
+
return construct([Object.freeze({ greatest: Object.freeze([...values]) })]);
|
|
1994
2533
|
}
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
const {
|
|
1999
|
-
const
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2534
|
+
}));
|
|
2535
|
+
function renderSqlFragment(params) {
|
|
2536
|
+
const { dialect, collector, conflict } = params;
|
|
2537
|
+
const render = (fragment) => renderSqlFragment({ ...params, fragment });
|
|
2538
|
+
const encode = (part) => {
|
|
2539
|
+
if (part.value === undefined)
|
|
2540
|
+
invalid4("SQL parameters cannot be undefined; use null explicitly");
|
|
2541
|
+
return snapshotSqlValue(part.decoder === undefined ? part.value : part.decoder.encode({ value: snapshotSqlValue(part.value), dialect: dialect.name }));
|
|
2542
|
+
};
|
|
2543
|
+
const recording = isSqlBindingCollector({ collector });
|
|
2544
|
+
const recordValue = (part) => recordSqlBinding({
|
|
2545
|
+
collector,
|
|
2546
|
+
source: { kind: "literal", value: part.value },
|
|
2547
|
+
transport: part.decoder === undefined ? sqlParameterTransport({ value: part.value }) : { kind: "plain" },
|
|
2548
|
+
encode: ({ value, dialect: target }) => part.decoder === undefined ? value : part.decoder.encode({ value, dialect: target.name })
|
|
2006
2549
|
});
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
}
|
|
2021
|
-
switch (type) {
|
|
2022
|
-
case "String":
|
|
2023
|
-
if (typeof value !== "string")
|
|
2024
|
-
return wrongCarrier("a String column takes a string literal");
|
|
2025
|
-
return stringLiteral({ value, context, field });
|
|
2026
|
-
case "Boolean":
|
|
2027
|
-
if (typeof value !== "boolean")
|
|
2028
|
-
return wrongCarrier("a Boolean column takes `true` or `false`");
|
|
2029
|
-
return value ? context.dialect.booleanLiteral.true : context.dialect.booleanLiteral.false;
|
|
2030
|
-
case "Int":
|
|
2031
|
-
return integerLiteral({ value, field, context, min: INT32_MIN, max: INT32_MAX, label: "Int" });
|
|
2032
|
-
case "BigInt":
|
|
2033
|
-
return integerLiteral({ value, field, context, min: INT64_MIN, max: INT64_MAX, label: "BigInt" });
|
|
2034
|
-
case "Float": {
|
|
2035
|
-
if (typeof value !== "number")
|
|
2036
|
-
return wrongCarrier("a Float column takes a number");
|
|
2037
|
-
if (!Number.isFinite(value)) {
|
|
2038
|
-
refuse2({
|
|
2039
|
-
context,
|
|
2040
|
-
rule: "predicate-literal-invalid",
|
|
2041
|
-
message: `compares "${field.name}" with ${String(value)} \u2014 a Float literal must be finite`,
|
|
2042
|
-
meta: { field: field.name, value: String(value) }
|
|
2043
|
-
});
|
|
2550
|
+
const frameOperand = (fragment) => {
|
|
2551
|
+
const parts = partsOf(fragment).filter((part) => part !== "");
|
|
2552
|
+
const only = parts.length === 1 ? parts[0] : undefined;
|
|
2553
|
+
if (recording && only !== undefined && typeof only !== "string" && "value" in only) {
|
|
2554
|
+
if (only.decoder !== undefined && resultScalarContract({ decoder: only.decoder }) === undefined)
|
|
2555
|
+
throw new VibeError13({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Reusable SQL cannot compile an opaque encoder whose output controls a greatest NULL frame" });
|
|
2556
|
+
if (only.value === null) {
|
|
2557
|
+
if (only.decoder !== undefined)
|
|
2558
|
+
recordSqlBindingCheck({ collector, check: () => {
|
|
2559
|
+
if (encode({ ...only, value: snapshotSqlValue(only.value) }) !== null)
|
|
2560
|
+
throw new VibeError13({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Reusable SQL NULL frame encoding changed its declared structure" });
|
|
2561
|
+
} });
|
|
2562
|
+
return "NULL";
|
|
2044
2563
|
}
|
|
2045
|
-
|
|
2564
|
+
const text = recordValue(only);
|
|
2565
|
+
return dialect.greatestForm === "wrapped-frame" ? `(SELECT ${text})` : text;
|
|
2046
2566
|
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
return
|
|
2567
|
+
const value = only !== undefined && typeof only !== "string" && "value" in only ? { encoded: encode(only) } : undefined;
|
|
2568
|
+
if (value !== undefined && value.encoded === null)
|
|
2569
|
+
return "NULL";
|
|
2570
|
+
const text = value === undefined ? render(fragment) : collector.add(value.encoded);
|
|
2571
|
+
return dialect.greatestForm === "wrapped-frame" ? `(SELECT ${text})` : text;
|
|
2572
|
+
};
|
|
2573
|
+
return partsOf(params.fragment).map((part) => {
|
|
2574
|
+
if (typeof part === "string")
|
|
2575
|
+
return part;
|
|
2576
|
+
if ("value" in part)
|
|
2577
|
+
return recording ? recordValue(part) : collector.add(encode(part));
|
|
2578
|
+
if ("ident" in part)
|
|
2579
|
+
return part.ident.map((name) => dialect.quoteIdent({ name })).join(".");
|
|
2580
|
+
if ("row" in part) {
|
|
2581
|
+
if (conflict === undefined)
|
|
2582
|
+
invalid4("Existing/incoming row references require a conflict update");
|
|
2583
|
+
const name = conflict.columns === undefined ? part.column : conflict.columns[part.column];
|
|
2584
|
+
if (name === undefined)
|
|
2585
|
+
invalid4(`Unknown conflict-expression field "${part.column}"`);
|
|
2586
|
+
const ref = dialect.excludedRowRef;
|
|
2587
|
+
const qualifier = part.row === "existing" ? dialect.quoteIdent({ name: conflict.table }) : ref.form === "keyword" ? ref.name : dialect.quoteIdent({ name: ref.alias });
|
|
2588
|
+
return `${qualifier}.${dialect.quoteIdent({ name })}`;
|
|
2060
2589
|
}
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
message: `compares DateTime field "${field.name}" with a literal \u2014 DateTime literals are spelled differently on postgres, mysql and sqlite, so VibeORM does not guess one. Compare two DateTime columns, or write a raw expression for a fixed instant`,
|
|
2066
|
-
meta: { field: field.name }
|
|
2067
|
-
});
|
|
2068
|
-
case "Json":
|
|
2069
|
-
case "Bytes":
|
|
2070
|
-
return wrongCarrier(`${type} columns take no predicate literal`);
|
|
2071
|
-
}
|
|
2072
|
-
}
|
|
2073
|
-
function integerLiteral(params) {
|
|
2074
|
-
const { value, field, context, min, max, label } = params;
|
|
2075
|
-
let text;
|
|
2076
|
-
if (typeof value === "number") {
|
|
2077
|
-
if (!Number.isSafeInteger(value)) {
|
|
2078
|
-
refuse2({
|
|
2079
|
-
context,
|
|
2080
|
-
rule: "predicate-literal-invalid",
|
|
2081
|
-
message: `compares "${field.name}" with ${String(value)} \u2014 an ${label} literal must be an exact integer JavaScript can represent; pass a bigint or a decimal string for larger values`,
|
|
2082
|
-
meta: { field: field.name, value: String(value) }
|
|
2083
|
-
});
|
|
2590
|
+
if (dialect.greatestForm === "native")
|
|
2591
|
+
return `GREATEST(${part.greatest.map(render).join(", ")})`;
|
|
2592
|
+
if (!part.greatest.some(containsGreatest)) {
|
|
2593
|
+
return render(part.greatest.reduce((left, right) => sql`(CASE WHEN ${left} IS NULL THEN ${right} WHEN ${right} IS NULL THEN ${left} WHEN ${left} >= ${right} THEN ${left} ELSE ${right} END)`));
|
|
2084
2594
|
}
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2595
|
+
const [left, right] = part.greatest.map(frameOperand);
|
|
2596
|
+
const [a, b, frame] = ["a", "b", "g"].map((name) => dialect.quoteIdent({ name }));
|
|
2597
|
+
return `(SELECT CASE WHEN ${a} IS NULL THEN ${b} WHEN ${b} IS NULL THEN ${a} WHEN ${a} >= ${b} THEN ${a} ELSE ${b} END FROM (SELECT ${left} AS ${a}, ${right} AS ${b}) AS ${frame})`;
|
|
2598
|
+
}).join("");
|
|
2599
|
+
}
|
|
2600
|
+
function mapConflictColumns(params) {
|
|
2601
|
+
return construct(partsOf(params.fragment).map((part) => {
|
|
2602
|
+
if (typeof part === "string")
|
|
2603
|
+
return part;
|
|
2604
|
+
if ("row" in part) {
|
|
2605
|
+
const column = Object.hasOwn(params.columns, part.column) ? params.columns[part.column] : undefined;
|
|
2606
|
+
if (column === undefined)
|
|
2607
|
+
invalid4(`Unknown conflict-expression field "${part.column}"`);
|
|
2608
|
+
return Object.freeze({ ...part, column });
|
|
2094
2609
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
rule: "predicate-type-mismatch",
|
|
2100
|
-
message: `compares ${label} field "${field.name}" with ${describe({ value })} \u2014 an ${label} column takes an integer`,
|
|
2101
|
-
meta: { field: field.name, value }
|
|
2102
|
-
});
|
|
2103
|
-
}
|
|
2104
|
-
const parsed = BigInt(text);
|
|
2105
|
-
if (parsed < min || parsed > max) {
|
|
2106
|
-
refuse2({
|
|
2107
|
-
context,
|
|
2108
|
-
rule: "predicate-literal-invalid",
|
|
2109
|
-
message: `compares "${field.name}" with ${text}, which is outside the range an ${label} column can hold (${min}\u2026${max})`,
|
|
2110
|
-
meta: { field: field.name, value: text }
|
|
2111
|
-
});
|
|
2112
|
-
}
|
|
2113
|
-
return text;
|
|
2610
|
+
if ("greatest" in part)
|
|
2611
|
+
return Object.freeze({ greatest: Object.freeze(part.greatest.map((fragment) => mapConflictColumns({ ...params, fragment }))) });
|
|
2612
|
+
return part;
|
|
2613
|
+
}));
|
|
2114
2614
|
}
|
|
2115
|
-
function
|
|
2116
|
-
const
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
context,
|
|
2120
|
-
rule: "predicate-literal-invalid",
|
|
2121
|
-
message: `compares "${field.name}" with a string containing a NUL byte, which no dialect here can store`,
|
|
2122
|
-
meta: { field: field.name }
|
|
2123
|
-
});
|
|
2124
|
-
}
|
|
2125
|
-
return context.dialect.quoteStringLiteral({ value });
|
|
2615
|
+
function compileSql(params) {
|
|
2616
|
+
const collector = new ParamCollector(params.dialect);
|
|
2617
|
+
const text = renderSqlFragment({ dialect: params.dialect, fragment: params.query, collector });
|
|
2618
|
+
return { text, values: collector.values };
|
|
2126
2619
|
}
|
|
2127
|
-
function
|
|
2620
|
+
function sqlUsesIncoming(params) {
|
|
2621
|
+
return partsOf(params.fragment).some((part) => typeof part !== "string" && (("row" in part) && part.row === "incoming" || ("greatest" in part) && part.greatest.some((fragment) => sqlUsesIncoming({ fragment }))));
|
|
2622
|
+
}
|
|
2623
|
+
// packages/sql/src/query-plan.ts
|
|
2624
|
+
import { VibeError as VibeError14 } from "@vibeorm/schema";
|
|
2625
|
+
var queries = new WeakMap;
|
|
2626
|
+
var commands = new WeakMap;
|
|
2627
|
+
var reusableQueries = new WeakMap;
|
|
2628
|
+
var reusableCommands = new WeakMap;
|
|
2629
|
+
registerSqlHandleCheck({ check: (value) => queries.has(value) || commands.has(value) || reusableQueries.has(value) || reusableCommands.has(value) });
|
|
2630
|
+
function invalid5(message) {
|
|
2631
|
+
throw new VibeError14({ code: "VIBE_VALIDATION", message });
|
|
2632
|
+
}
|
|
2633
|
+
function createSqlQuery(params) {
|
|
2634
|
+
if (typeof params.render !== "function" || typeof params.decoder?.decode !== "function")
|
|
2635
|
+
invalid5("SQL queries require a renderer and result decoder");
|
|
2636
|
+
const query = Object.freeze(Object.create(null));
|
|
2637
|
+
queries.set(query, Object.freeze({ render: params.render, decoder: params.decoder }));
|
|
2638
|
+
return query;
|
|
2639
|
+
}
|
|
2640
|
+
function createSqlCommand(params) {
|
|
2641
|
+
if (typeof params.render !== "function")
|
|
2642
|
+
invalid5("SQL commands require a renderer");
|
|
2643
|
+
const command = Object.freeze(Object.create(null));
|
|
2644
|
+
commands.set(command, params.render);
|
|
2645
|
+
return command;
|
|
2646
|
+
}
|
|
2647
|
+
function isSqlQuery(value) {
|
|
2648
|
+
return typeof value === "object" && value !== null && queries.has(value);
|
|
2649
|
+
}
|
|
2650
|
+
function isSqlCommand(value) {
|
|
2651
|
+
return typeof value === "object" && value !== null && commands.has(value);
|
|
2652
|
+
}
|
|
2653
|
+
function isReusableSqlQuery(value) {
|
|
2654
|
+
return typeof value === "object" && value !== null && reusableQueries.has(value);
|
|
2655
|
+
}
|
|
2656
|
+
function isReusableSqlCommand(value) {
|
|
2657
|
+
return typeof value === "object" && value !== null && reusableCommands.has(value);
|
|
2658
|
+
}
|
|
2659
|
+
function createReusableSqlQuery(params) {
|
|
2660
|
+
const state = queries.get(params.query) ?? invalid5("Expected an authentic SQL row query");
|
|
2661
|
+
sqlInputScopeKeys({ scope: params.scope });
|
|
2662
|
+
const query = Object.freeze(Object.create(null));
|
|
2663
|
+
reusableQueries.set(query, { ...state, scope: params.scope, cache: new WeakMap });
|
|
2664
|
+
return query;
|
|
2665
|
+
}
|
|
2666
|
+
function createReusableSqlCommand(params) {
|
|
2667
|
+
const render = commands.get(params.query) ?? invalid5("Expected an authentic SQL command");
|
|
2668
|
+
sqlInputScopeKeys({ scope: params.scope });
|
|
2669
|
+
const query = Object.freeze(Object.create(null));
|
|
2670
|
+
reusableCommands.set(query, { render, scope: params.scope, cache: new WeakMap });
|
|
2671
|
+
return query;
|
|
2672
|
+
}
|
|
2673
|
+
function compiledTemplate(params) {
|
|
2674
|
+
const cached = params.state.cache.get(params.dialect);
|
|
2675
|
+
if (cached !== undefined)
|
|
2676
|
+
return cached;
|
|
2677
|
+
const collector = new ParamCollector(params.dialect);
|
|
2678
|
+
startSqlBindingCollection({ collector, dialect: params.dialect, scope: params.state.scope });
|
|
2679
|
+
const text = params.state.render({ dialect: params.dialect, collector });
|
|
2680
|
+
if (collector.count() > params.dialect.paramBudget)
|
|
2681
|
+
invalid5("SQL query exceeds this dialect's parameter budget; statements are never split implicitly");
|
|
2682
|
+
const program = finishSqlBindingCollection({ collector });
|
|
2683
|
+
const compiled = Object.freeze({ text, program });
|
|
2684
|
+
params.state.cache.set(params.dialect, compiled);
|
|
2685
|
+
return compiled;
|
|
2686
|
+
}
|
|
2687
|
+
function executionHandle(params) {
|
|
2128
2688
|
const { value } = params;
|
|
2129
|
-
return
|
|
2689
|
+
return queries.has(value) || commands.has(value) || reusableQueries.has(value) || reusableCommands.has(value) || isSqlFragment(value) || isSqlInput(value) || sqlInputScopeHandle(params);
|
|
2690
|
+
}
|
|
2691
|
+
function bindTemplate(params) {
|
|
2692
|
+
const { scope } = params.state;
|
|
2693
|
+
return bindSqlBindingProgram({ program: params.compiled.program, params: params.inputs, isHandle: (value) => executionHandle({ scope, value }) });
|
|
2694
|
+
}
|
|
2695
|
+
function compileReusableSqlQuery(params) {
|
|
2696
|
+
const state = reusableQueries.get(params.query) ?? invalid5("Expected an authentic reusable SQL row query");
|
|
2697
|
+
const compiled = compiledTemplate({ dialect: params.dialect, state });
|
|
2698
|
+
return { text: compiled.text, values: bindTemplate({ compiled, state, inputs: params.params }), decoder: state.decoder };
|
|
2699
|
+
}
|
|
2700
|
+
function compileReusableSqlCommand(params) {
|
|
2701
|
+
const state = reusableCommands.get(params.query) ?? invalid5("Expected an authentic reusable SQL command");
|
|
2702
|
+
const compiled = compiledTemplate({ dialect: params.dialect, state });
|
|
2703
|
+
return { text: compiled.text, values: bindTemplate({ compiled, state, inputs: params.params }) };
|
|
2704
|
+
}
|
|
2705
|
+
function compileSqlQuery(params) {
|
|
2706
|
+
const state = queries.get(params.query) ?? invalid5("Expected an authentic SQL row query");
|
|
2707
|
+
const collector = new ParamCollector(params.dialect);
|
|
2708
|
+
const text = state.render({ dialect: params.dialect, collector });
|
|
2709
|
+
if (collector.values.length > params.dialect.paramBudget)
|
|
2710
|
+
invalid5("SQL query exceeds this dialect's parameter budget; statements are never split implicitly");
|
|
2711
|
+
return { text, values: collector.values, decoder: state.decoder };
|
|
2712
|
+
}
|
|
2713
|
+
function compileSqlCommand(params) {
|
|
2714
|
+
const render = commands.get(params.query) ?? invalid5("Expected an authentic SQL command");
|
|
2715
|
+
const collector = new ParamCollector(params.dialect);
|
|
2716
|
+
const text = render({ dialect: params.dialect, collector });
|
|
2717
|
+
if (collector.values.length > params.dialect.paramBudget)
|
|
2718
|
+
invalid5("SQL command exceeds this dialect's parameter budget; statements are never split implicitly");
|
|
2719
|
+
return { text, values: collector.values };
|
|
2130
2720
|
}
|
|
2131
|
-
|
|
2132
2721
|
// packages/sql/src/exclusion.ts
|
|
2133
|
-
|
|
2134
|
-
|
|
2722
|
+
import { VibeError as VibeError15, fieldTypeIdentity as fieldTypeIdentity2, isEnumType as isEnumType2, unsupportedCapability as unsupportedCapability14 } from "@vibeorm/schema";
|
|
2723
|
+
function invalid6(rule) {
|
|
2724
|
+
throw new VibeError15({ code: "VIBE_SCHEMA_INVALID", message: `Invalid exclusion constraint: ${rule}`, meta: { rule: `exclusion-${rule}` } });
|
|
2135
2725
|
}
|
|
2136
2726
|
function compileExclusion(params) {
|
|
2137
2727
|
const { exclusion: spec, model, dialect } = params;
|
|
2138
2728
|
if (!dialect.capabilities.exclusionConstraints)
|
|
2139
|
-
throw
|
|
2729
|
+
throw unsupportedCapability14({ dialect: dialect.name, feature: "exclusionConstraints" });
|
|
2140
2730
|
if (!spec.name || spec.name.includes("\x00"))
|
|
2141
|
-
|
|
2731
|
+
invalid6("name");
|
|
2142
2732
|
if (!["int8range", "tsrange", "tstzrange"].includes(spec.range.type))
|
|
2143
|
-
|
|
2733
|
+
invalid6("range-type");
|
|
2144
2734
|
if (!["[)", "[]", "()", "(]"].includes(spec.range.bounds))
|
|
2145
|
-
|
|
2735
|
+
invalid6("bounds");
|
|
2146
2736
|
const field = (name) => {
|
|
2147
2737
|
const value = model.fields.find((entry) => entry.name === name);
|
|
2148
2738
|
if (value === undefined || value.isList || value.isUnsupported)
|
|
2149
|
-
|
|
2739
|
+
invalid6("field");
|
|
2150
2740
|
return value;
|
|
2151
2741
|
};
|
|
2152
2742
|
const endpoint = (value) => {
|
|
@@ -2154,17 +2744,17 @@ function compileExclusion(params) {
|
|
|
2154
2744
|
return "NULL";
|
|
2155
2745
|
const infinity = "infinity" in value ? value.infinity : value.nullAs;
|
|
2156
2746
|
if (infinity !== undefined && (spec.range.type === "int8range" || !["infinity", "-infinity"].includes(infinity)))
|
|
2157
|
-
|
|
2747
|
+
invalid6("infinity");
|
|
2158
2748
|
const literal = infinity === undefined ? undefined : `${dialect.quoteStringLiteral({ value: infinity })}::${spec.range.type === "tsrange" ? "timestamp" : "timestamptz"}`;
|
|
2159
2749
|
if ("infinity" in value)
|
|
2160
2750
|
return literal;
|
|
2161
2751
|
const column = field(value.field);
|
|
2162
2752
|
if (fieldTypeIdentity2({ type: column.type }) !== (spec.range.type === "int8range" ? "BigInt" : "DateTime"))
|
|
2163
|
-
|
|
2753
|
+
invalid6("range-field-type");
|
|
2164
2754
|
if (column.type === "DateTime") {
|
|
2165
2755
|
const timezone = /timestamptz|timestamp.*with time zone/i.test(column.nativeType ?? "timestamptz");
|
|
2166
2756
|
if (timezone !== (spec.range.type === "tstzrange"))
|
|
2167
|
-
|
|
2757
|
+
invalid6("range-timezone");
|
|
2168
2758
|
}
|
|
2169
2759
|
const sql = dialect.quoteIdent({ name: column.dbName ?? column.name });
|
|
2170
2760
|
return literal === undefined ? sql : `COALESCE(${sql}, ${literal})`;
|
|
@@ -2172,15 +2762,15 @@ function compileExclusion(params) {
|
|
|
2172
2762
|
const keys = spec.keys.map((key) => {
|
|
2173
2763
|
if ("expression" in key) {
|
|
2174
2764
|
if (!key.expression.trim() || key.expression.includes("\x00") || key.fields.length === 0)
|
|
2175
|
-
|
|
2765
|
+
invalid6("expression");
|
|
2176
2766
|
key.fields.forEach(field);
|
|
2177
2767
|
return `(${key.expression}) WITH =`;
|
|
2178
2768
|
}
|
|
2179
2769
|
const column = field(key.field);
|
|
2180
2770
|
if (column.isOptional)
|
|
2181
|
-
|
|
2771
|
+
invalid6("nullable-key-requires-expression");
|
|
2182
2772
|
if (!isEnumType2(column.type) && !["String", "Int", "BigInt", "Float", "Decimal", "Boolean", "DateTime"].includes(fieldTypeIdentity2({ type: column.type })))
|
|
2183
|
-
|
|
2773
|
+
invalid6("key-type");
|
|
2184
2774
|
return `${dialect.quoteIdent({ name: column.dbName ?? column.name })} WITH =`;
|
|
2185
2775
|
});
|
|
2186
2776
|
keys.push(`${spec.range.type}(${endpoint(spec.range.lower)}, ${endpoint(spec.range.upper)}, ${dialect.quoteStringLiteral({ value: spec.range.bounds })}) WITH &&`);
|
|
@@ -2189,13 +2779,13 @@ function compileExclusion(params) {
|
|
|
2189
2779
|
}
|
|
2190
2780
|
function renderExclusionSetup(params) {
|
|
2191
2781
|
if (!params.dialect.capabilities.exclusionConstraints)
|
|
2192
|
-
throw
|
|
2782
|
+
throw unsupportedCapability14({ dialect: params.dialect.name, feature: "exclusionConstraints" });
|
|
2193
2783
|
return params.exclusion.requiredExtensions.map((name) => `CREATE EXTENSION IF NOT EXISTS ${params.dialect.quoteIdent({ name })};`);
|
|
2194
2784
|
}
|
|
2195
2785
|
function renderExclusionDdl(params) {
|
|
2196
2786
|
const { dialect, table, exclusion, operation } = params;
|
|
2197
2787
|
if (!dialect.capabilities.exclusionConstraints)
|
|
2198
|
-
throw
|
|
2788
|
+
throw unsupportedCapability14({ dialect: dialect.name, feature: "exclusionConstraints" });
|
|
2199
2789
|
const target = `ALTER TABLE ${dialect.quoteIdent({ name: table })}`;
|
|
2200
2790
|
if (operation === "drop")
|
|
2201
2791
|
return [`${target} DROP CONSTRAINT ${dialect.quoteIdent({ name: exclusion.name })};`];
|
|
@@ -2204,14 +2794,14 @@ function renderExclusionDdl(params) {
|
|
|
2204
2794
|
function renderExclusionCatalog(params) {
|
|
2205
2795
|
const { dialect, probe } = params;
|
|
2206
2796
|
if (!dialect.capabilities.exclusionConstraints)
|
|
2207
|
-
throw
|
|
2797
|
+
throw unsupportedCapability14({ dialect: dialect.name, feature: "exclusionConstraints" });
|
|
2208
2798
|
const scope = probe === undefined ? `n.nspname = ${dialect.quoteStringLiteral({ value: params.namespace ?? "public" })}` : `t.oid = ${dialect.quoteStringLiteral({ value: probe })}::regclass`;
|
|
2209
2799
|
return `SELECT t.relname AS table_name, c.conname AS constraint_name, pg_catalog.pg_get_constraintdef(c.oid, true) AS definition, c.convalidated AS validated, i.indisvalid AS valid, i.indisready AS ready FROM pg_catalog.pg_constraint c JOIN pg_catalog.pg_class t ON t.oid = c.conrelid JOIN pg_catalog.pg_namespace n ON n.oid = t.relnamespace JOIN pg_catalog.pg_index i ON i.indexrelid = c.conindid WHERE c.contype = 'x' AND ${scope} ORDER BY t.relname, c.conname`;
|
|
2210
2800
|
}
|
|
2211
2801
|
function renderExclusionProbe(params) {
|
|
2212
2802
|
const { dialect, table, probe, definition } = params;
|
|
2213
2803
|
if (!dialect.capabilities.exclusionConstraints)
|
|
2214
|
-
throw
|
|
2804
|
+
throw unsupportedCapability14({ dialect: dialect.name, feature: "exclusionConstraints" });
|
|
2215
2805
|
return [`CREATE TEMP TABLE ${dialect.quoteIdent({ name: probe })} (LIKE ${dialect.quoteIdent({ name: params.namespace ?? "public" })}.${dialect.quoteIdent({ name: table })})`, `ALTER TABLE ${dialect.quoteIdent({ name: probe })} ADD CONSTRAINT ${dialect.quoteIdent({ name: `${probe}_x` })} ${definition}`];
|
|
2216
2806
|
}
|
|
2217
2807
|
// packages/sql/src/ast.ts
|
|
@@ -2243,6 +2833,8 @@ function dbNowMarker(params) {
|
|
|
2243
2833
|
function isDbNowMarker(value) {
|
|
2244
2834
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
2245
2835
|
return false;
|
|
2836
|
+
if (!Object.hasOwn(value, "__dbNow"))
|
|
2837
|
+
return false;
|
|
2246
2838
|
const keys = Object.keys(value);
|
|
2247
2839
|
if (keys.length !== 2 || !keys.includes("__dbNow") || !keys.includes("offsetSeconds"))
|
|
2248
2840
|
return false;
|
|
@@ -2290,7 +2882,7 @@ function chainVetoToken(params) {
|
|
|
2290
2882
|
return;
|
|
2291
2883
|
}
|
|
2292
2884
|
// packages/sql/src/locks.ts
|
|
2293
|
-
import { internalError, unsupportedCapability as
|
|
2885
|
+
import { internalError, unsupportedCapability as unsupportedCapability15 } from "@vibeorm/schema";
|
|
2294
2886
|
var ROW_LOCK_PROSE = Object.freeze({
|
|
2295
2887
|
strengths: Object.freeze({
|
|
2296
2888
|
update: "FOR UPDATE",
|
|
@@ -2310,7 +2902,7 @@ function renderRowLockClause(params) {
|
|
|
2310
2902
|
if (syntax === null) {
|
|
2311
2903
|
if (lock.strength === "update" && lock.wait === "wait")
|
|
2312
2904
|
return null;
|
|
2313
|
-
throw
|
|
2905
|
+
throw unsupportedCapability15({
|
|
2314
2906
|
dialect: dialect.name,
|
|
2315
2907
|
feature: `a ${describeRowLock({ lock })} locking read`,
|
|
2316
2908
|
hint: `${dialect.name} has no locking-read clause at all`
|
|
@@ -2318,7 +2910,7 @@ function renderRowLockClause(params) {
|
|
|
2318
2910
|
}
|
|
2319
2911
|
const strengthText = syntax.strengths[lock.strength];
|
|
2320
2912
|
if (strengthText === undefined) {
|
|
2321
|
-
throw
|
|
2913
|
+
throw unsupportedCapability15({
|
|
2322
2914
|
dialect: dialect.name,
|
|
2323
2915
|
feature: `the ${describeRowLock({ lock: { ...lock, wait: "wait" } })} row-lock strength`,
|
|
2324
2916
|
hint: `${dialect.name} can express: ${Object.keys(syntax.strengths).join(", ")}`
|
|
@@ -2326,7 +2918,7 @@ function renderRowLockClause(params) {
|
|
|
2326
2918
|
}
|
|
2327
2919
|
const waitText = syntax.wait[lock.wait];
|
|
2328
2920
|
if (waitText === undefined) {
|
|
2329
|
-
throw
|
|
2921
|
+
throw unsupportedCapability15({
|
|
2330
2922
|
dialect: dialect.name,
|
|
2331
2923
|
feature: `the "${lock.wait}" lock wait policy`,
|
|
2332
2924
|
hint: `${dialect.name} can express: ${Object.keys(syntax.wait).join(", ")}`
|
|
@@ -2345,7 +2937,7 @@ function renderAdvisoryLock(params) {
|
|
|
2345
2937
|
const { dialect, statement, collector } = params;
|
|
2346
2938
|
const functions = dialect.advisoryLockFunctions;
|
|
2347
2939
|
if (functions === null) {
|
|
2348
|
-
throw
|
|
2940
|
+
throw unsupportedCapability15({
|
|
2349
2941
|
dialect: dialect.name,
|
|
2350
2942
|
feature: "advisory locks",
|
|
2351
2943
|
hint: dialect.name === "mysql" ? "mysql's GET_LOCK is SESSION-scoped \u2014 it survives a rollback, so it cannot be released with the transaction" : `${dialect.name} has no advisory-lock functions`
|
|
@@ -2384,6 +2976,13 @@ var postgresDialect = {
|
|
|
2384
2976
|
capabilities: POSTGRES_CAPABILITIES,
|
|
2385
2977
|
inArrayStrategy: "any",
|
|
2386
2978
|
tupleInStrategy: "rowValue",
|
|
2979
|
+
keyListStrategy: "unnest",
|
|
2980
|
+
keysetSeekStrategy: "rowValue",
|
|
2981
|
+
keyPageStrategy: "lateral",
|
|
2982
|
+
relationFilterStrategy: "exists",
|
|
2983
|
+
deferredJoinPage: { minSkip: 1000, requiresCoveringIndex: true },
|
|
2984
|
+
nativeNullsOrder: "high",
|
|
2985
|
+
nullsUnionPage: true,
|
|
2387
2986
|
timeOfDayInStrategy: "in",
|
|
2388
2987
|
upsertForm: "onConflict",
|
|
2389
2988
|
conflictKeyEquality: {
|
|
@@ -2433,6 +3032,8 @@ var postgresDialect = {
|
|
|
2433
3032
|
keyCast: Object.freeze({ single: "::bigint", pair: "::int4" })
|
|
2434
3033
|
},
|
|
2435
3034
|
rowDefaultKeyword: true,
|
|
3035
|
+
bulkInsertForm: "unnest",
|
|
3036
|
+
emptyInsert: { form: "defaultValues", conflictClause: true },
|
|
2436
3037
|
statementChain: "dataModifyingCte",
|
|
2437
3038
|
paramBudget: 30000,
|
|
2438
3039
|
booleanLiteral: { true: "true", false: "false" },
|
|
@@ -2460,6 +3061,13 @@ var mysqlDialect = {
|
|
|
2460
3061
|
capabilities: MYSQL_CAPABILITIES,
|
|
2461
3062
|
inArrayStrategy: "expand",
|
|
2462
3063
|
tupleInStrategy: "rowValue",
|
|
3064
|
+
keyListStrategy: "rowValue",
|
|
3065
|
+
keysetSeekStrategy: "or",
|
|
3066
|
+
keyPageStrategy: "window",
|
|
3067
|
+
relationFilterStrategy: "exists",
|
|
3068
|
+
deferredJoinPage: { minSkip: 1000, requiresCoveringIndex: false },
|
|
3069
|
+
nativeNullsOrder: "low",
|
|
3070
|
+
nullsUnionPage: true,
|
|
2463
3071
|
timeOfDayInStrategy: "equalities",
|
|
2464
3072
|
upsertForm: "onDuplicateKey",
|
|
2465
3073
|
conflictKeyEquality: {
|
|
@@ -2495,6 +3103,8 @@ var mysqlDialect = {
|
|
|
2495
3103
|
},
|
|
2496
3104
|
advisoryLockFunctions: null,
|
|
2497
3105
|
rowDefaultKeyword: true,
|
|
3106
|
+
bulkInsertForm: "values",
|
|
3107
|
+
emptyInsert: { form: "emptyRow" },
|
|
2498
3108
|
statementChain: null,
|
|
2499
3109
|
paramBudget: 1e4,
|
|
2500
3110
|
booleanLiteral: { true: "1", false: "0" },
|
|
@@ -2525,6 +3135,389 @@ var SQL_DIALECTS = {
|
|
|
2525
3135
|
function getSqlDialect(params) {
|
|
2526
3136
|
return SQL_DIALECTS[params.dialect];
|
|
2527
3137
|
}
|
|
3138
|
+
// packages/sql/src/key-list.ts
|
|
3139
|
+
import { unsupportedCapability as unsupportedCapability16 } from "@vibeorm/schema";
|
|
3140
|
+
var SCALAR_ELEMENT_TYPES = Object.freeze({
|
|
3141
|
+
String: "text",
|
|
3142
|
+
Boolean: "bool",
|
|
3143
|
+
Int: "int4",
|
|
3144
|
+
BigInt: "int8",
|
|
3145
|
+
Float: "float8",
|
|
3146
|
+
Decimal: "numeric",
|
|
3147
|
+
DateTime: "timestamptz",
|
|
3148
|
+
Json: null,
|
|
3149
|
+
Bytes: "bytea"
|
|
3150
|
+
});
|
|
3151
|
+
var NATIVE_ELEMENT_TYPES = Object.freeze({
|
|
3152
|
+
text: "text",
|
|
3153
|
+
varchar: "varchar",
|
|
3154
|
+
charactervarying: "varchar",
|
|
3155
|
+
char: "bpchar",
|
|
3156
|
+
character: "bpchar",
|
|
3157
|
+
bpchar: "bpchar",
|
|
3158
|
+
uuid: "uuid",
|
|
3159
|
+
citext: "citext",
|
|
3160
|
+
xml: "xml",
|
|
3161
|
+
inet: "inet",
|
|
3162
|
+
money: "money",
|
|
3163
|
+
bit: '"bit"',
|
|
3164
|
+
varbit: "varbit",
|
|
3165
|
+
bitvarying: "varbit",
|
|
3166
|
+
smallint: "int2",
|
|
3167
|
+
int2: "int2",
|
|
3168
|
+
integer: "int4",
|
|
3169
|
+
int: "int4",
|
|
3170
|
+
int4: "int4",
|
|
3171
|
+
bigint: "int8",
|
|
3172
|
+
int8: "int8",
|
|
3173
|
+
oid: "oid",
|
|
3174
|
+
real: "float4",
|
|
3175
|
+
float4: "float4",
|
|
3176
|
+
doubleprecision: "float8",
|
|
3177
|
+
double: "float8",
|
|
3178
|
+
float8: "float8",
|
|
3179
|
+
decimal: "numeric",
|
|
3180
|
+
numeric: "numeric",
|
|
3181
|
+
boolean: "bool",
|
|
3182
|
+
bool: "bool",
|
|
3183
|
+
timestamp: "timestamp",
|
|
3184
|
+
timestampwithouttimezone: "timestamp",
|
|
3185
|
+
timestamptz: "timestamptz",
|
|
3186
|
+
timestampwithtimezone: "timestamptz",
|
|
3187
|
+
date: "date",
|
|
3188
|
+
time: "time",
|
|
3189
|
+
timewithouttimezone: "time",
|
|
3190
|
+
timetz: "timetz",
|
|
3191
|
+
timewithtimezone: "timetz",
|
|
3192
|
+
bytea: "bytea"
|
|
3193
|
+
});
|
|
3194
|
+
function postgresKeyListElementTypeOf(params) {
|
|
3195
|
+
const { dialect, member } = params;
|
|
3196
|
+
if (member.type === "Enum") {
|
|
3197
|
+
if (member.enumType === undefined)
|
|
3198
|
+
return "text";
|
|
3199
|
+
const name = dialect.quoteIdent({ name: member.enumType.name });
|
|
3200
|
+
return member.enumType.namespace === undefined ? name : `${dialect.quoteIdent({ name: member.enumType.namespace })}.${name}`;
|
|
3201
|
+
}
|
|
3202
|
+
const native = member.nativeType === undefined ? undefined : NATIVE_ELEMENT_TYPES[member.nativeType.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\s+/g, "")];
|
|
3203
|
+
const type = member.nativeType === undefined ? SCALAR_ELEMENT_TYPES[member.type] : native;
|
|
3204
|
+
return type === null ? undefined : type;
|
|
3205
|
+
}
|
|
3206
|
+
function postgresKeyListElementType(params) {
|
|
3207
|
+
const { dialect, member } = params;
|
|
3208
|
+
const type = postgresKeyListElementTypeOf({ dialect, member });
|
|
3209
|
+
if (type === undefined) {
|
|
3210
|
+
throw unsupportedCapability16({
|
|
3211
|
+
dialect: dialect.name,
|
|
3212
|
+
feature: `the key-list filter (IN) on a ${member.type}${member.nativeType === undefined ? "" : ` @db.${member.nativeType}`} member`,
|
|
3213
|
+
hint: "IN binds every member as a typed array; filter this field with equality inside OR instead"
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
return type;
|
|
3217
|
+
}
|
|
3218
|
+
function keyListBindsUntypedRows(params) {
|
|
3219
|
+
return params.members.some((member) => member.type === "DateTime" && member.nativeType === undefined);
|
|
3220
|
+
}
|
|
3221
|
+
function postgresBulkInsertArrayCastOf(params) {
|
|
3222
|
+
const { dialect, member } = params;
|
|
3223
|
+
if (member.type === "Json")
|
|
3224
|
+
return member.nativeType?.trim().toLowerCase() === "json" ? "text::json" : "text::jsonb";
|
|
3225
|
+
return postgresKeyListElementTypeOf({ dialect, member });
|
|
3226
|
+
}
|
|
3227
|
+
function postgresBulkInsertArrayCast(params) {
|
|
3228
|
+
const { dialect, member } = params;
|
|
3229
|
+
const cast = postgresBulkInsertArrayCastOf({ dialect, member });
|
|
3230
|
+
if (cast === undefined) {
|
|
3231
|
+
throw unsupportedCapability16({
|
|
3232
|
+
dialect: dialect.name,
|
|
3233
|
+
feature: `a bulk insert binding a ${member.type}${member.nativeType === undefined ? "" : ` @db.${member.nativeType}`} column as a typed array`,
|
|
3234
|
+
hint: "the query builder keeps VALUES for this column type"
|
|
3235
|
+
});
|
|
3236
|
+
}
|
|
3237
|
+
return cast;
|
|
3238
|
+
}
|
|
3239
|
+
// packages/sql/src/local-date.ts
|
|
3240
|
+
import { localDateCheckName } from "@vibeorm/schema";
|
|
3241
|
+
function renderLocalDateCheck({ dialect, table, column }) {
|
|
3242
|
+
if (dialect.name !== "sqlite")
|
|
3243
|
+
return;
|
|
3244
|
+
const quoted = dialect.quoteIdent({ name: column });
|
|
3245
|
+
return {
|
|
3246
|
+
name: localDateCheckName({ table, column }),
|
|
3247
|
+
expression: `${quoted} IS date(${quoted}) AND (${quoted} IS NULL OR ${quoted} >= ${dialect.quoteStringLiteral({ value: "0001-01-01" })})`
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3250
|
+
// packages/sql/src/generated-columns.ts
|
|
3251
|
+
import { VibeError as VibeError16, unsupportedCapability as unsupportedCapability18 } from "@vibeorm/schema";
|
|
3252
|
+
|
|
3253
|
+
// packages/sql/src/dialects/postgres-generated-columns.ts
|
|
3254
|
+
import { unsupportedCapability as unsupportedCapability17 } from "@vibeorm/schema";
|
|
3255
|
+
function renderPostgresGeneratedColumnClause({ generated }) {
|
|
3256
|
+
return `GENERATED ALWAYS AS (${generated.expression}) STORED`;
|
|
3257
|
+
}
|
|
3258
|
+
function renderPostgresGeneratedColumnProbe({ dialect, probe }) {
|
|
3259
|
+
if (dialect.name !== "postgres")
|
|
3260
|
+
throw unsupportedCapability17({ dialect: dialect.name, feature: "rollbackGeneratedColumnProbe" });
|
|
3261
|
+
const q = ({ name }) => dialect.quoteIdent({ name });
|
|
3262
|
+
let text;
|
|
3263
|
+
switch (probe.operation) {
|
|
3264
|
+
case "savepoint":
|
|
3265
|
+
text = `SAVEPOINT ${q({ name: probe.savepoint })}`;
|
|
3266
|
+
break;
|
|
3267
|
+
case "rollback":
|
|
3268
|
+
text = `ROLLBACK TO SAVEPOINT ${q({ name: probe.savepoint })}`;
|
|
3269
|
+
break;
|
|
3270
|
+
case "release":
|
|
3271
|
+
text = `RELEASE SAVEPOINT ${q({ name: probe.savepoint })}`;
|
|
3272
|
+
break;
|
|
3273
|
+
case "add":
|
|
3274
|
+
text = `ALTER TABLE ${q({ name: probe.table.namespace })}.${q({ name: probe.table.name })} ADD COLUMN ${q({ name: probe.column })} ${probe.definition}`;
|
|
3275
|
+
break;
|
|
3276
|
+
}
|
|
3277
|
+
return { text, values: [], returning: "none" };
|
|
3278
|
+
}
|
|
3279
|
+
function renderPostgresDropGeneratedExpression({ dialect, table, column }) {
|
|
3280
|
+
if (dialect.name !== "postgres")
|
|
3281
|
+
throw unsupportedCapability17({ dialect: dialect.name, feature: "dropGeneratedExpression" });
|
|
3282
|
+
return { text: `ALTER TABLE ${dialect.quoteIdent({ name: table.namespace })}.${dialect.quoteIdent({ name: table.name })} ALTER COLUMN ${dialect.quoteIdent({ name: column })} DROP EXPRESSION`, values: [], returning: "none" };
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
// packages/sql/src/generated-columns.ts
|
|
3286
|
+
function renderGeneratedColumnClause({ dialect, generated }) {
|
|
3287
|
+
if (!("generatedColumns" in dialect.capabilities) || dialect.capabilities.generatedColumns !== true) {
|
|
3288
|
+
throw unsupportedCapability18({ dialect: dialect.name, feature: "generatedColumns" });
|
|
3289
|
+
}
|
|
3290
|
+
if (generated.storage !== "stored") {
|
|
3291
|
+
throw unsupportedCapability18({ dialect: dialect.name, feature: "virtualGeneratedColumns" });
|
|
3292
|
+
}
|
|
3293
|
+
if (typeof generated.expression !== "string" || generated.expression.trim().length === 0 || generated.expression.includes("\x00")) {
|
|
3294
|
+
throw new VibeError16({ code: "VIBE_SCHEMA_INVALID", message: "A generated column requires non-empty, NUL-free SQL", meta: { reason: "generated-expression-invalid" } });
|
|
3295
|
+
}
|
|
3296
|
+
switch (dialect.name) {
|
|
3297
|
+
case "postgres":
|
|
3298
|
+
return renderPostgresGeneratedColumnClause({ generated });
|
|
3299
|
+
case "mysql":
|
|
3300
|
+
return renderMysqlGeneratedColumnClause({ generated });
|
|
3301
|
+
case "sqlite":
|
|
3302
|
+
return renderSqliteGeneratedColumnClause({ generated });
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
// packages/sql/src/row-rules.ts
|
|
3306
|
+
import { VibeError as VibeError20, internalError as internalError2, normalizeRowRules, rowRuleGateField, rowRuleIdentities, rowRuleProtectedFields, rowRuleShapeIssue, rowRuleTriggerName, unsupportedCapability as unsupportedCapability22 } from "@vibeorm/schema";
|
|
3307
|
+
|
|
3308
|
+
// packages/sql/src/dialects/postgres-row-rules.ts
|
|
3309
|
+
import { VibeError as VibeError17, unsupportedCapability as unsupportedCapability19 } from "@vibeorm/schema";
|
|
3310
|
+
function comparison({ dialect, field }) {
|
|
3311
|
+
const column = dialect.quoteIdent({ name: field.dbName ?? field.name });
|
|
3312
|
+
const cast = field.type === "Json" ? `::pg_catalog.jsonb${field.isList ? "[]" : ""}` : "";
|
|
3313
|
+
return `OLD.${column}${cast} IS DISTINCT FROM NEW.${column}${cast}`;
|
|
3314
|
+
}
|
|
3315
|
+
function renderPostgresRowRule({ dialect, context }) {
|
|
3316
|
+
if (dialect.name !== "postgres")
|
|
3317
|
+
throw unsupportedCapability19({ dialect: dialect.name, feature: "postgresRowRules" });
|
|
3318
|
+
const { rule, table, functionIdentity, triggerIdentity, protectedFields, gateField } = context;
|
|
3319
|
+
const qualify = ({ name }) => `${dialect.quoteIdent({ name: name.namespace })}.${dialect.quoteIdent({ name: name.name })}`;
|
|
3320
|
+
const functionName = qualify({ name: functionIdentity.name });
|
|
3321
|
+
const tableName = qualify({ name: table });
|
|
3322
|
+
const triggerName = dialect.quoteIdent({ name: triggerIdentity.name });
|
|
3323
|
+
const constraint = dialect.quoteStringLiteral({ value: triggerIdentity.name });
|
|
3324
|
+
const raise = `RAISE EXCEPTION 'VibeORM row rule refused mutation'
|
|
3325
|
+
${rule.kind === "appendOnly" ? " " : " "}USING ERRCODE = 'check_violation', CONSTRAINT = ${constraint};`;
|
|
3326
|
+
let body;
|
|
3327
|
+
if (rule.kind === "appendOnly") {
|
|
3328
|
+
body = `BEGIN
|
|
3329
|
+
${raise}
|
|
3330
|
+
END;`;
|
|
3331
|
+
} else {
|
|
3332
|
+
const terms = protectedFields.map((field) => comparison({ dialect, field }));
|
|
3333
|
+
if (terms.length === 0) {
|
|
3334
|
+
throw new VibeError17({ code: "VIBE_SCHEMA_INVALID", message: `Row rule ${rule.name} has no protected fields`, meta: { model: context.model, rule: rule.name } });
|
|
3335
|
+
}
|
|
3336
|
+
const condition = gateField === undefined ? terms.join(`
|
|
3337
|
+
OR `) : `OLD.${dialect.quoteIdent({ name: gateField.dbName ?? gateField.name })} IS NOT NULL AND (
|
|
3338
|
+
${terms.join(`
|
|
3339
|
+
OR `)}
|
|
3340
|
+
)`;
|
|
3341
|
+
body = `BEGIN
|
|
3342
|
+
IF ${condition} THEN
|
|
3343
|
+
${raise}
|
|
3344
|
+
END IF;
|
|
3345
|
+
RETURN NEW;
|
|
3346
|
+
END;`;
|
|
3347
|
+
}
|
|
3348
|
+
let marker = "$vibe_row_rule$";
|
|
3349
|
+
for (let suffix = 1;body.includes(marker); suffix += 1)
|
|
3350
|
+
marker = `$vibe_row_rule_${suffix}$`;
|
|
3351
|
+
const createFunction = `CREATE FUNCTION ${functionName}() RETURNS pg_catalog.trigger
|
|
3352
|
+
LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT SECURITY INVOKER
|
|
3353
|
+
SET search_path = pg_catalog
|
|
3354
|
+
AS ${marker}
|
|
3355
|
+
${body}
|
|
3356
|
+
${marker};`;
|
|
3357
|
+
const createTrigger = `CREATE TRIGGER ${triggerName} BEFORE UPDATE${rule.kind === "appendOnly" ? " OR DELETE" : ""} ON ${tableName}
|
|
3358
|
+
FOR EACH ROW EXECUTE FUNCTION ${functionName}();`;
|
|
3359
|
+
return {
|
|
3360
|
+
identities: [functionIdentity, triggerIdentity],
|
|
3361
|
+
create: [{ text: createFunction, values: [] }, { text: createTrigger, values: [] }],
|
|
3362
|
+
drop: [{ text: `DROP TRIGGER ${triggerName} ON ${tableName};`, values: [] }, { text: `DROP FUNCTION ${functionName}();`, values: [] }],
|
|
3363
|
+
protectedFields: protectedFields.map((field) => field.name),
|
|
3364
|
+
...gateField === undefined ? {} : { gateField: gateField.name }
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
// packages/sql/src/dialects/sqlite-row-rules.ts
|
|
3369
|
+
import { SQLITE_ROW_RULE_ERROR_PREFIX, VibeError as VibeError18, unsupportedCapability as unsupportedCapability20 } from "@vibeorm/schema";
|
|
3370
|
+
function renderSqliteRowRule({ dialect, context }) {
|
|
3371
|
+
if (dialect.name !== "sqlite")
|
|
3372
|
+
throw unsupportedCapability20({ dialect: dialect.name, feature: "sqliteRowRules" });
|
|
3373
|
+
const { rule, table, triggers, protectedFields, gateField } = context;
|
|
3374
|
+
if (table.namespace !== "main")
|
|
3375
|
+
throw unsupportedCapability20({ dialect: dialect.name, feature: "fixedNamespaces" });
|
|
3376
|
+
const terms = protectedFields.map((field) => {
|
|
3377
|
+
const column = dialect.quoteIdent({ name: field.dbName ?? field.name });
|
|
3378
|
+
return `OLD.${column} IS NOT NEW.${column}`;
|
|
3379
|
+
});
|
|
3380
|
+
if (rule.kind !== "appendOnly" && terms.length === 0)
|
|
3381
|
+
throw new VibeError18({ code: "VIBE_SCHEMA_INVALID", message: `Row rule ${rule.name} has no protected fields`, meta: { model: context.model, rule: rule.name } });
|
|
3382
|
+
const condition = rule.kind === "appendOnly" ? "" : gateField === undefined ? `WHEN ${terms.join(`
|
|
3383
|
+
OR `)}
|
|
3384
|
+
` : `WHEN OLD.${dialect.quoteIdent({ name: gateField.dbName ?? gateField.name })} IS NOT NULL AND (
|
|
3385
|
+
${terms.join(`
|
|
3386
|
+
OR `)}
|
|
3387
|
+
)
|
|
3388
|
+
`;
|
|
3389
|
+
return {
|
|
3390
|
+
identities: triggers.map(({ identity }) => identity),
|
|
3391
|
+
create: triggers.map(({ event, identity }) => ({
|
|
3392
|
+
text: `CREATE TRIGGER ${dialect.quoteIdent({ name: identity.name })} BEFORE ${event.toUpperCase()} ON ${dialect.quoteIdent({ name: table.name })}
|
|
3393
|
+
FOR EACH ROW
|
|
3394
|
+
${condition}BEGIN
|
|
3395
|
+
SELECT RAISE(ABORT, ${dialect.quoteStringLiteral({ value: `${SQLITE_ROW_RULE_ERROR_PREFIX}${identity.name}` })});
|
|
3396
|
+
END;`,
|
|
3397
|
+
values: []
|
|
3398
|
+
})),
|
|
3399
|
+
drop: [...triggers].reverse().map(({ identity }) => ({ text: `DROP TRIGGER ${dialect.quoteIdent({ name: identity.name })};`, values: [] })),
|
|
3400
|
+
protectedFields: protectedFields.map((field) => field.name),
|
|
3401
|
+
...gateField === undefined ? {} : { gateField: gateField.name }
|
|
3402
|
+
};
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
// packages/sql/src/dialects/mysql-row-rules.ts
|
|
3406
|
+
import { MYSQL_ROW_RULE_MESSAGE_MAX_BYTES, VibeError as VibeError19, mysqlRowRuleMessageByteLength, unsupportedCapability as unsupportedCapability21 } from "@vibeorm/schema";
|
|
3407
|
+
function diagnosticLiteral({ name, context, dialect }) {
|
|
3408
|
+
const message = `Check constraint '${name}' is violated.`;
|
|
3409
|
+
if (mysqlRowRuleMessageByteLength({ triggerName: name }) > MYSQL_ROW_RULE_MESSAGE_MAX_BYTES)
|
|
3410
|
+
throw new VibeError19({ code: "VIBE_SCHEMA_INVALID", message: `Row rule ${context.rule.name} exceeds MySQL's ${MYSQL_ROW_RULE_MESSAGE_MAX_BYTES}-byte diagnostic limit; supply a shorter name stem`, meta: { model: context.model, rule: context.rule.name } });
|
|
3411
|
+
if (message.includes("\\")) {
|
|
3412
|
+
const hex = Array.from(new TextEncoder().encode(message), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
3413
|
+
return `_utf8mb4 X'${hex}'`;
|
|
3414
|
+
}
|
|
3415
|
+
return dialect.quoteStringLiteral({ value: message });
|
|
3416
|
+
}
|
|
3417
|
+
function renderMysqlRowRule({ dialect, context }) {
|
|
3418
|
+
if (dialect.name !== "mysql")
|
|
3419
|
+
throw unsupportedCapability21({ dialect: dialect.name, feature: "mysqlRowRules" });
|
|
3420
|
+
const { rule, table, triggers, protectedFields, gateField, placement } = context;
|
|
3421
|
+
for (const name of Object.keys(placement ?? {}))
|
|
3422
|
+
if (!triggers.some(({ identity }) => identity.name === name))
|
|
3423
|
+
throw new VibeError19({ code: "VIBE_INTERNAL", message: "A MySQL trigger placement names a trigger this rule does not own", meta: { model: context.model, rule: rule.name, trigger: name } });
|
|
3424
|
+
const position = ({ name }) => {
|
|
3425
|
+
const entry = placement?.[name];
|
|
3426
|
+
if (entry === undefined)
|
|
3427
|
+
return "";
|
|
3428
|
+
return "follows" in entry ? ` FOLLOWS ${dialect.quoteIdent({ name: entry.follows })}` : ` PRECEDES ${dialect.quoteIdent({ name: entry.precedes })}`;
|
|
3429
|
+
};
|
|
3430
|
+
const qualify = ({ name }) => table.namespace === "public" ? dialect.quoteIdent({ name }) : `${dialect.quoteIdent({ name: table.namespace })}.${dialect.quoteIdent({ name })}`;
|
|
3431
|
+
const terms = protectedFields.map((field) => {
|
|
3432
|
+
const column = dialect.quoteIdent({ name: field.dbName ?? field.name });
|
|
3433
|
+
return `NOT (OLD.${column} <=> NEW.${column})`;
|
|
3434
|
+
});
|
|
3435
|
+
if (rule.kind !== "appendOnly" && terms.length === 0)
|
|
3436
|
+
throw new VibeError19({ code: "VIBE_SCHEMA_INVALID", message: `Row rule ${rule.name} has no protected fields`, meta: { model: context.model, rule: rule.name } });
|
|
3437
|
+
const condition = gateField === undefined ? terms.join(`
|
|
3438
|
+
OR `) : `OLD.${dialect.quoteIdent({ name: gateField.dbName ?? gateField.name })} IS NOT NULL AND (
|
|
3439
|
+
${terms.join(`
|
|
3440
|
+
OR `)}
|
|
3441
|
+
)`;
|
|
3442
|
+
return {
|
|
3443
|
+
identities: triggers.map(({ identity }) => identity),
|
|
3444
|
+
create: triggers.map(({ event, identity }) => {
|
|
3445
|
+
const signal = `SIGNAL SQLSTATE 'HY000' SET MYSQL_ERRNO = 3819, MESSAGE_TEXT = ${diagnosticLiteral({ name: identity.name, context, dialect })};`;
|
|
3446
|
+
const body = rule.kind === "appendOnly" ? ` ${signal}` : ` IF ${condition} THEN
|
|
3447
|
+
${signal}
|
|
3448
|
+
END IF;`;
|
|
3449
|
+
return { text: `CREATE TRIGGER ${qualify({ name: identity.name })} BEFORE ${event.toUpperCase()} ON ${qualify({ name: table.name })}
|
|
3450
|
+
FOR EACH ROW${position({ name: identity.name })}
|
|
3451
|
+
BEGIN
|
|
3452
|
+
${body}
|
|
3453
|
+
END;`, values: [] };
|
|
3454
|
+
}),
|
|
3455
|
+
drop: [...triggers].reverse().map(({ identity }) => ({ text: `DROP TRIGGER ${qualify({ name: identity.name })};`, values: [] })),
|
|
3456
|
+
protectedFields: protectedFields.map((field) => field.name),
|
|
3457
|
+
...gateField === undefined ? {} : { gateField: gateField.name }
|
|
3458
|
+
};
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
// packages/sql/src/row-rules.ts
|
|
3462
|
+
function renderRowRule({ schema, model, rule, dialect, placement }) {
|
|
3463
|
+
const issue = rowRuleShapeIssue({ value: rule });
|
|
3464
|
+
if (issue !== undefined) {
|
|
3465
|
+
throw new VibeError20({ code: "VIBE_SCHEMA_INVALID", message: issue, meta: { model: model.name } });
|
|
3466
|
+
}
|
|
3467
|
+
if (placement !== undefined && Object.keys(placement).length > 0 && dialect.name !== "mysql")
|
|
3468
|
+
throw unsupportedCapability22({ dialect: dialect.name, feature: "rowRuleTriggerPlacement" });
|
|
3469
|
+
const normalized = normalizeRowRules({ schema, model, rules: [rule], dialect: dialect.name })[0];
|
|
3470
|
+
switch (dialect.name) {
|
|
3471
|
+
case "postgres": {
|
|
3472
|
+
const objects = rowRuleIdentities({ schema, model, rule: normalized, dialect: dialect.name });
|
|
3473
|
+
const functionIdentity = objects.find((object) => object.role === "function")?.identity;
|
|
3474
|
+
const triggerIdentity = objects.find((object) => object.role === "trigger")?.identity;
|
|
3475
|
+
if (functionIdentity?.kind !== "function" || triggerIdentity?.kind !== "trigger")
|
|
3476
|
+
throw internalError2({ message: "PostgreSQL row rules own one function and one trigger", meta: { model: model.name, rule: normalized.name } });
|
|
3477
|
+
const gate = rowRuleGateField({ rule: normalized });
|
|
3478
|
+
const gateField = gate === undefined ? undefined : model.fields.find((field) => field.name === gate);
|
|
3479
|
+
return renderPostgresRowRule({
|
|
3480
|
+
dialect,
|
|
3481
|
+
context: {
|
|
3482
|
+
model: model.name,
|
|
3483
|
+
table: triggerIdentity.table,
|
|
3484
|
+
rule: normalized,
|
|
3485
|
+
functionIdentity,
|
|
3486
|
+
triggerIdentity: { kind: "trigger", table: triggerIdentity.table, name: triggerIdentity.name },
|
|
3487
|
+
protectedFields: rowRuleProtectedFields({ schema, model, rule: normalized }),
|
|
3488
|
+
...gateField === undefined ? {} : { gateField }
|
|
3489
|
+
}
|
|
3490
|
+
});
|
|
3491
|
+
}
|
|
3492
|
+
case "sqlite":
|
|
3493
|
+
case "mysql": {
|
|
3494
|
+
const objects = rowRuleIdentities({ schema, model, rule: normalized, dialect: dialect.name });
|
|
3495
|
+
const triggers = objects.map(({ role, identity }) => {
|
|
3496
|
+
if (role !== "trigger" || identity.kind !== "trigger")
|
|
3497
|
+
throw internalError2({ message: "Event row rules own only triggers", meta: { model: model.name, rule: normalized.name } });
|
|
3498
|
+
const event = ["update", "delete"].find((event) => identity.name === rowRuleTriggerName({ rule: normalized, event }));
|
|
3499
|
+
if (event === undefined)
|
|
3500
|
+
throw internalError2({ message: "Row-rule identity has no declared event", meta: { model: model.name, rule: normalized.name, trigger: identity.name } });
|
|
3501
|
+
return { event, identity: { kind: "trigger", table: identity.table, name: identity.name } };
|
|
3502
|
+
});
|
|
3503
|
+
const table = triggers[0]?.identity.table;
|
|
3504
|
+
if (table === undefined)
|
|
3505
|
+
throw internalError2({ message: "Event row rules require at least one trigger", meta: { model: model.name, rule: normalized.name } });
|
|
3506
|
+
const gate = rowRuleGateField({ rule: normalized });
|
|
3507
|
+
const gateField = gate === undefined ? undefined : model.fields.find((field) => field.name === gate);
|
|
3508
|
+
const context = {
|
|
3509
|
+
model: model.name,
|
|
3510
|
+
table,
|
|
3511
|
+
rule: normalized,
|
|
3512
|
+
triggers,
|
|
3513
|
+
protectedFields: rowRuleProtectedFields({ schema, model, rule: normalized }),
|
|
3514
|
+
...gateField === undefined ? {} : { gateField },
|
|
3515
|
+
...placement === undefined ? {} : { placement }
|
|
3516
|
+
};
|
|
3517
|
+
return dialect.name === "sqlite" ? renderSqliteRowRule({ dialect, context }) : renderMysqlRowRule({ dialect, context });
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
2528
3521
|
// packages/sql/src/dialects/sqlite-json-equality.ts
|
|
2529
3522
|
function renderSqliteJsonEquality(params) {
|
|
2530
3523
|
const q = (name) => params.dialect.quoteIdent({ name });
|
|
@@ -2540,7 +3533,7 @@ function renderSqliteJsonEquality(params) {
|
|
|
2540
3533
|
}
|
|
2541
3534
|
|
|
2542
3535
|
// packages/sql/src/dialects/diagnostics.ts
|
|
2543
|
-
import { unsupportedCapability as
|
|
3536
|
+
import { unsupportedCapability as unsupportedCapability23 } from "@vibeorm/schema";
|
|
2544
3537
|
var QUERIES = Object.freeze({
|
|
2545
3538
|
postgresJsonWire: { dialect: "postgres", text: `SELECT '{"__vibeorm":1}'::jsonb AS "probe"` },
|
|
2546
3539
|
sqliteTriggers: { dialect: "sqlite", text: "SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'trigger'" }
|
|
@@ -2548,7 +3541,7 @@ var QUERIES = Object.freeze({
|
|
|
2548
3541
|
function renderDiagnostic(params) {
|
|
2549
3542
|
const query = QUERIES[params.query];
|
|
2550
3543
|
if (params.dialect.name !== query.dialect) {
|
|
2551
|
-
throw
|
|
3544
|
+
throw unsupportedCapability23({ dialect: params.dialect.name, feature: params.query, hint: `this diagnostic requires ${query.dialect}` });
|
|
2552
3545
|
}
|
|
2553
3546
|
return { text: query.text, values: [], returning: "none" };
|
|
2554
3547
|
}
|
|
@@ -2557,7 +3550,7 @@ function isFixedDiagnostic(params) {
|
|
|
2557
3550
|
}
|
|
2558
3551
|
|
|
2559
3552
|
// packages/sql/src/dialects/postgres-diagnostics.ts
|
|
2560
|
-
import { VibeError as
|
|
3553
|
+
import { VibeError as VibeError21, unsupportedCapability as unsupportedCapability24 } from "@vibeorm/schema";
|
|
2561
3554
|
function renderPostgresExplain(params) {
|
|
2562
3555
|
const { dialect, query, options } = params;
|
|
2563
3556
|
requirePostgres({ dialect, feature: "query plan inspection (EXPLAIN)" });
|
|
@@ -2592,7 +3585,7 @@ var POSTGRES_WORKLOAD_LIMITS = Object.freeze({
|
|
|
2592
3585
|
});
|
|
2593
3586
|
function requirePostgres(params) {
|
|
2594
3587
|
if (params.dialect.name !== "postgres") {
|
|
2595
|
-
throw
|
|
3588
|
+
throw unsupportedCapability24({
|
|
2596
3589
|
dialect: params.dialect.name,
|
|
2597
3590
|
feature: params.feature,
|
|
2598
3591
|
hint: "plan inspection and workload statistics are PostgreSQL-shaped; portable operation tracing works on every dialect"
|
|
@@ -2610,7 +3603,7 @@ function renderPostgresWorkload(params) {
|
|
|
2610
3603
|
if (query === "statementSnapshot") {
|
|
2611
3604
|
const bounded = limit ?? 20;
|
|
2612
3605
|
if (!Number.isInteger(bounded) || bounded < POSTGRES_WORKLOAD_LIMITS.minLimit || bounded > POSTGRES_WORKLOAD_LIMITS.maxLimit) {
|
|
2613
|
-
throw new
|
|
3606
|
+
throw new VibeError21({
|
|
2614
3607
|
code: "VIBE_CONFIG",
|
|
2615
3608
|
message: `workload snapshot limit must be an integer between ${POSTGRES_WORKLOAD_LIMITS.minLimit} and ${POSTGRES_WORKLOAD_LIMITS.maxLimit}, got ${String(limit)}`,
|
|
2616
3609
|
meta: { limit }
|
|
@@ -2629,7 +3622,7 @@ function renderPostgresWorkload(params) {
|
|
|
2629
3622
|
function requireArg(params) {
|
|
2630
3623
|
const { value, name, query } = params;
|
|
2631
3624
|
if (value === undefined || value.length === 0) {
|
|
2632
|
-
throw new
|
|
3625
|
+
throw new VibeError21({
|
|
2633
3626
|
code: "VIBE_CONFIG",
|
|
2634
3627
|
message: `the "${query}" diagnostic read requires a ${name}`,
|
|
2635
3628
|
meta: { query, argument: name }
|
|
@@ -2639,10 +3632,10 @@ function requireArg(params) {
|
|
|
2639
3632
|
}
|
|
2640
3633
|
|
|
2641
3634
|
// packages/sql/src/dialects/postgres-rls-readiness.ts
|
|
2642
|
-
import { unsupportedCapability as
|
|
3635
|
+
import { unsupportedCapability as unsupportedCapability26 } from "@vibeorm/schema";
|
|
2643
3636
|
|
|
2644
3637
|
// packages/sql/src/dialects/postgres-rls.ts
|
|
2645
|
-
import { RlsContextError, VibeError as
|
|
3638
|
+
import { RlsContextError, VibeError as VibeError22, assertNativeRlsAdmission, assertValidSchemaIR, unsupportedCapability as unsupportedCapability25, validateRlsContextIR } from "@vibeorm/schema";
|
|
2646
3639
|
var NATIVE_RLS = Object.freeze({ policyName: "vibeorm_managed", tableSchema: "public", settingPrefix: "vibeorm.", parentAlias: "parent" });
|
|
2647
3640
|
var COLUMN_TYPES = {
|
|
2648
3641
|
String: { default: "text", native: { text: "text", varchar: "varchar", charactervarying: "varchar", uuid: "uuid" } },
|
|
@@ -2655,18 +3648,18 @@ var COLUMN_TYPES = {
|
|
|
2655
3648
|
};
|
|
2656
3649
|
function requireRls(params) {
|
|
2657
3650
|
if (!params.dialect.capabilities.nativeRowLevelSecurity) {
|
|
2658
|
-
throw
|
|
3651
|
+
throw unsupportedCapability25({ dialect: params.dialect.name, feature: "nativeRowLevelSecurity" });
|
|
2659
3652
|
}
|
|
2660
3653
|
}
|
|
2661
3654
|
function identifier2(params) {
|
|
2662
3655
|
if (typeof params.name !== "string" || params.name.length === 0 || params.name.includes("\x00") || new TextEncoder().encode(params.name).length > 63) {
|
|
2663
|
-
throw new
|
|
3656
|
+
throw new VibeError22({ code: "VIBE_SCHEMA_INVALID", message: "Native RLS identifiers must be nonempty PostgreSQL identifiers of at most 63 bytes" });
|
|
2664
3657
|
}
|
|
2665
3658
|
return params.dialect.quoteIdent({ name: params.name });
|
|
2666
3659
|
}
|
|
2667
3660
|
function tableRef(params) {
|
|
2668
3661
|
if (params.schema !== undefined && params.schema !== "public" && params.schema !== "pg_temp") {
|
|
2669
|
-
throw
|
|
3662
|
+
throw unsupportedCapability25({ dialect: params.dialect.name, feature: "nativeRowLevelSecurity", hint: "Only public tables and temporary policy probes are supported" });
|
|
2670
3663
|
}
|
|
2671
3664
|
return `${identifier2({ dialect: params.dialect, name: params.schema ?? NATIVE_RLS.tableSchema })}.${identifier2({ dialect: params.dialect, name: params.table })}`;
|
|
2672
3665
|
}
|
|
@@ -2707,7 +3700,7 @@ function renderRlsColumnType(params) {
|
|
|
2707
3700
|
if (native !== undefined)
|
|
2708
3701
|
return native;
|
|
2709
3702
|
}
|
|
2710
|
-
throw
|
|
3703
|
+
throw unsupportedCapability25({ dialect: params.dialect.name, feature: "nativeRowLevelSecurity", hint: `Unsupported physical ownership type on field ${field.name}` });
|
|
2711
3704
|
}
|
|
2712
3705
|
function protectedModel(params) {
|
|
2713
3706
|
requireRls(params);
|
|
@@ -2715,7 +3708,7 @@ function protectedModel(params) {
|
|
|
2715
3708
|
assertValidSchemaIR({ schema: params.schema });
|
|
2716
3709
|
const model = params.schema.models.find((entry) => entry.name === params.model);
|
|
2717
3710
|
if (model === undefined || model.rls === undefined || model.rls === "unprotected") {
|
|
2718
|
-
throw new
|
|
3711
|
+
throw new VibeError22({ code: "VIBE_SCHEMA_INVALID", message: "Native RLS policy rendering requires a protected model", meta: { model: params.model } });
|
|
2719
3712
|
}
|
|
2720
3713
|
return model;
|
|
2721
3714
|
}
|
|
@@ -2748,7 +3741,7 @@ function predicate(params) {
|
|
|
2748
3741
|
}).join(" AND ");
|
|
2749
3742
|
return `EXISTS (SELECT 1 FROM ${tableRef({ dialect, table: parent.dbName ?? parent.name })} AS ${alias} WHERE ${join})`;
|
|
2750
3743
|
}
|
|
2751
|
-
throw new
|
|
3744
|
+
throw new VibeError22({ code: "VIBE_SCHEMA_INVALID", message: "Native RLS requires a protected policy rule" });
|
|
2752
3745
|
}
|
|
2753
3746
|
function renderRlsPolicy(params) {
|
|
2754
3747
|
const model = protectedModel(params);
|
|
@@ -2782,7 +3775,7 @@ function renderRlsContext(params) {
|
|
|
2782
3775
|
function renderRlsRole(params) {
|
|
2783
3776
|
requireRls(params);
|
|
2784
3777
|
if (typeof params.role !== "string" || params.role.trim().length === 0 || params.role.includes("\x00")) {
|
|
2785
|
-
throw new
|
|
3778
|
+
throw new VibeError22({ code: "VIBE_CONFIG", message: "PGlite native RLS requires a trusted nonempty runtime role name" });
|
|
2786
3779
|
}
|
|
2787
3780
|
const collector = new ParamCollector(params.dialect);
|
|
2788
3781
|
return query({ text: `SELECT set_config('role', ${collector.add(params.role)}, true)`, values: collector.values });
|
|
@@ -2934,7 +3927,7 @@ ORDER BY c.relname, p.polname`,
|
|
|
2934
3927
|
});
|
|
2935
3928
|
function requireRls2(params) {
|
|
2936
3929
|
if (!params.dialect.capabilities.nativeRowLevelSecurity) {
|
|
2937
|
-
throw
|
|
3930
|
+
throw unsupportedCapability26({ dialect: params.dialect.name, feature: "nativeRowLevelSecurity" });
|
|
2938
3931
|
}
|
|
2939
3932
|
}
|
|
2940
3933
|
function renderRlsReadinessQuery(params) {
|
|
@@ -2966,7 +3959,7 @@ function renderRlsPolicyPath(params) {
|
|
|
2966
3959
|
}
|
|
2967
3960
|
|
|
2968
3961
|
// packages/sql/src/render.ts
|
|
2969
|
-
import { assertSqlNamespace as assertSqlNamespace2, internalError as
|
|
3962
|
+
import { assertSqlNamespace as assertSqlNamespace2, internalError as internalError3, unsupportedCapability as unsupportedCapability27 } from "@vibeorm/schema";
|
|
2970
3963
|
|
|
2971
3964
|
// packages/sql/src/like.ts
|
|
2972
3965
|
function escapeLikePattern(params) {
|
|
@@ -2992,6 +3985,9 @@ function strictMatchPattern(params) {
|
|
|
2992
3985
|
}
|
|
2993
3986
|
return op === "contains" ? containsPattern({ raw: value }) : op === "startsWith" ? startsWithPattern({ raw: value }) : endsWithPattern({ raw: value });
|
|
2994
3987
|
}
|
|
3988
|
+
function strictMatchCollationPrefilter(params) {
|
|
3989
|
+
return params.strategy === "binary" && params.op === "startsWith" && !params.negate;
|
|
3990
|
+
}
|
|
2995
3991
|
|
|
2996
3992
|
// packages/sql/src/render.ts
|
|
2997
3993
|
function renderStatement(params) {
|
|
@@ -3051,13 +4047,13 @@ function quoteCol(dialect, column) {
|
|
|
3051
4047
|
const ref = column.table === undefined ? name : `${dialect.quoteIdent({ name: column.table })}.${name}`;
|
|
3052
4048
|
if (column.castReal === true) {
|
|
3053
4049
|
if (dialect.name !== "sqlite") {
|
|
3054
|
-
throw
|
|
4050
|
+
throw internalError3({ message: "ColRef.castReal is a sqlite Decimal construct" });
|
|
3055
4051
|
}
|
|
3056
4052
|
return `CAST(${ref} AS REAL)`;
|
|
3057
4053
|
}
|
|
3058
4054
|
if (column.castJsonb !== undefined) {
|
|
3059
4055
|
if (dialect.name !== "postgres") {
|
|
3060
|
-
throw
|
|
4056
|
+
throw internalError3({ message: "ColRef.castJsonb is a postgres json construct" });
|
|
3061
4057
|
}
|
|
3062
4058
|
return `${ref}::${column.castJsonb}`;
|
|
3063
4059
|
}
|
|
@@ -3074,7 +4070,7 @@ function jsonCellCast(dialect, cell) {
|
|
|
3074
4070
|
if (cell.storage !== "json" || dialect.jsonParamCast === "")
|
|
3075
4071
|
return dialect.jsonParamCast;
|
|
3076
4072
|
if (dialect.name !== "postgres") {
|
|
3077
|
-
throw
|
|
4073
|
+
throw internalError3({ message: "JsonParamCell storage json is a postgres construct" });
|
|
3078
4074
|
}
|
|
3079
4075
|
return "::text::json";
|
|
3080
4076
|
}
|
|
@@ -3095,7 +4091,7 @@ function quoteProjection(dialect, column) {
|
|
|
3095
4091
|
if (column.transform === "castText")
|
|
3096
4092
|
return `${textProjection({ dialect, expression: ref })} AS ${alias}`;
|
|
3097
4093
|
if (dialect.name !== "postgres") {
|
|
3098
|
-
throw
|
|
4094
|
+
throw internalError3({ message: "encodeBase64 is a postgres construct" });
|
|
3099
4095
|
}
|
|
3100
4096
|
return `encode(${ref}, 'base64') AS ${alias}`;
|
|
3101
4097
|
}
|
|
@@ -3117,7 +4113,7 @@ function renderTable(dialect, table) {
|
|
|
3117
4113
|
}
|
|
3118
4114
|
function assertCount(value, what) {
|
|
3119
4115
|
if (!Number.isSafeInteger(value) || value < 0) {
|
|
3120
|
-
throw
|
|
4116
|
+
throw internalError3({ message: `${what} must be a non-negative integer, got ${value}` });
|
|
3121
4117
|
}
|
|
3122
4118
|
}
|
|
3123
4119
|
var COMPARISON_SQL2 = {
|
|
@@ -3149,7 +4145,7 @@ function renderAggRef(dialect, agg) {
|
|
|
3149
4145
|
function assertScalarArrays(dialect, feature) {
|
|
3150
4146
|
if (dialect.capabilities.scalarArrays)
|
|
3151
4147
|
return;
|
|
3152
|
-
throw
|
|
4148
|
+
throw unsupportedCapability27({
|
|
3153
4149
|
dialect: dialect.name,
|
|
3154
4150
|
feature,
|
|
3155
4151
|
hint: "only postgres has native array columns \u2014 model the list as a related table instead"
|
|
@@ -3161,7 +4157,7 @@ function renderExpr(params) {
|
|
|
3161
4157
|
case "cmp": {
|
|
3162
4158
|
const op = COMPARISON_SQL2[expr.op];
|
|
3163
4159
|
if (op === undefined) {
|
|
3164
|
-
throw
|
|
4160
|
+
throw internalError3({ message: `unknown comparison operator "${expr.op}"` });
|
|
3165
4161
|
}
|
|
3166
4162
|
if (isDbNowMarker(expr.value)) {
|
|
3167
4163
|
return `${quoteCol(dialect, expr.column)} ${op} ${dialect.renderDbNow({ offsetSeconds: expr.value.offsetSeconds, collector })}`;
|
|
@@ -3172,7 +4168,7 @@ function renderExpr(params) {
|
|
|
3172
4168
|
case "cmpCol": {
|
|
3173
4169
|
const op = COMPARISON_SQL2[expr.op];
|
|
3174
4170
|
if (op === undefined) {
|
|
3175
|
-
throw
|
|
4171
|
+
throw internalError3({ message: `unknown comparison operator "${expr.op}"` });
|
|
3176
4172
|
}
|
|
3177
4173
|
return `${quoteCol(dialect, expr.left)} ${op} ${quoteCol(dialect, expr.right)}`;
|
|
3178
4174
|
}
|
|
@@ -3196,11 +4192,11 @@ function renderExpr(params) {
|
|
|
3196
4192
|
}
|
|
3197
4193
|
case "tupleIn": {
|
|
3198
4194
|
if (expr.columns.length === 0) {
|
|
3199
|
-
throw
|
|
4195
|
+
throw internalError3({ message: "tupleIn needs at least one column" });
|
|
3200
4196
|
}
|
|
3201
4197
|
for (const row of expr.rows) {
|
|
3202
4198
|
if (row.length !== expr.columns.length) {
|
|
3203
|
-
throw
|
|
4199
|
+
throw internalError3({
|
|
3204
4200
|
message: `tupleIn row has ${row.length} values for ${expr.columns.length} columns`
|
|
3205
4201
|
});
|
|
3206
4202
|
}
|
|
@@ -3221,6 +4217,32 @@ function renderExpr(params) {
|
|
|
3221
4217
|
const base = conjunctions.length === 1 ? conjunctions[0] : `(${conjunctions.join(" OR ")})`;
|
|
3222
4218
|
return expr.negate === true ? `NOT ${base.startsWith("(") ? base : `(${base})`}` : base;
|
|
3223
4219
|
}
|
|
4220
|
+
case "keyList":
|
|
4221
|
+
return renderKeyList({ dialect, expr, collector });
|
|
4222
|
+
case "rowCompare": {
|
|
4223
|
+
const op = COMPARISON_SQL2[expr.op];
|
|
4224
|
+
if (op === undefined || expr.columns.length === 0 || expr.values.length !== expr.columns.length) {
|
|
4225
|
+
throw internalError3({ message: `rowCompare needs a range operator and one value per column (${expr.columns.length} columns, ${expr.values.length} values)` });
|
|
4226
|
+
}
|
|
4227
|
+
const columns = expr.columns.map((column) => quoteCol(dialect, column));
|
|
4228
|
+
const placeholders = expr.values.map((value) => collector.add(value));
|
|
4229
|
+
return `(${columns.join(", ")}) ${op} (${placeholders.join(", ")})`;
|
|
4230
|
+
}
|
|
4231
|
+
case "inSelect": {
|
|
4232
|
+
if (expr.columns.length === 0)
|
|
4233
|
+
throw internalError3({ message: "inSelect needs at least one column" });
|
|
4234
|
+
if (dialect.name === "mysql" && (expr.select.limit !== undefined || expr.select.offset !== undefined)) {
|
|
4235
|
+
throw unsupportedCapability27({
|
|
4236
|
+
dialect: dialect.name,
|
|
4237
|
+
feature: "LIMIT inside an IN subquery",
|
|
4238
|
+
hint: "mysql refuses LIMIT directly inside an IN subquery (error 1235); wrap the limited select in a derived table"
|
|
4239
|
+
});
|
|
4240
|
+
}
|
|
4241
|
+
const columns = expr.columns.map((column) => quoteCol(dialect, column));
|
|
4242
|
+
const tested = columns.length === 1 ? columns[0] : `(${columns.join(", ")})`;
|
|
4243
|
+
const sub = renderSelect({ dialect, statement: expr.select, collector });
|
|
4244
|
+
return `${tested} IN (${sub.text})`;
|
|
4245
|
+
}
|
|
3224
4246
|
case "like": {
|
|
3225
4247
|
const column = quoteCol(dialect, expr.column);
|
|
3226
4248
|
const placeholder = collector.add(expr.pattern);
|
|
@@ -3249,10 +4271,17 @@ function renderExpr(params) {
|
|
|
3249
4271
|
if (dialect.strictMatchStrategy === "glob") {
|
|
3250
4272
|
base = `${column} GLOB ${collector.add(pattern)}`;
|
|
3251
4273
|
} else {
|
|
4274
|
+
const prefilter = strictMatchCollationPrefilter({
|
|
4275
|
+
strategy: dialect.strictMatchStrategy,
|
|
4276
|
+
op: expr.op,
|
|
4277
|
+
negate: expr.negate === true
|
|
4278
|
+
}) ? `${column} LIKE ${collector.add(pattern)}${dialect.likeEscapeClause ?? ""}` : null;
|
|
3252
4279
|
const placeholder = collector.add(pattern);
|
|
3253
4280
|
base = dialect.strictMatchStrategy === "binary" ? `${column} LIKE CAST(${placeholder} AS BINARY)` : `${column} LIKE ${placeholder}`;
|
|
3254
4281
|
if (dialect.likeEscapeClause !== null)
|
|
3255
4282
|
base = `${base}${dialect.likeEscapeClause}`;
|
|
4283
|
+
if (prefilter !== null)
|
|
4284
|
+
base = `(${prefilter} AND ${base})`;
|
|
3256
4285
|
}
|
|
3257
4286
|
}
|
|
3258
4287
|
return expr.negate === true ? `NOT (${base})` : base;
|
|
@@ -3274,7 +4303,7 @@ function renderExpr(params) {
|
|
|
3274
4303
|
case "aggCmp": {
|
|
3275
4304
|
const op = COMPARISON_SQL2[expr.op];
|
|
3276
4305
|
if (op === undefined) {
|
|
3277
|
-
throw
|
|
4306
|
+
throw internalError3({ message: `unknown comparison operator "${expr.op}"` });
|
|
3278
4307
|
}
|
|
3279
4308
|
return `${renderAggRef(dialect, expr.agg)} ${op} ${collector.add(expr.value)}`;
|
|
3280
4309
|
}
|
|
@@ -3352,10 +4381,146 @@ function listComparison(params) {
|
|
|
3352
4381
|
}
|
|
3353
4382
|
return { column: quoteCol(dialect, column), operand: collector.add(value) };
|
|
3354
4383
|
}
|
|
4384
|
+
function renderKeyList(params) {
|
|
4385
|
+
const { dialect, expr, collector } = params;
|
|
4386
|
+
const arity = expr.columns.length;
|
|
4387
|
+
if (arity === 0)
|
|
4388
|
+
throw internalError3({ message: "keyList needs at least one column" });
|
|
4389
|
+
if (expr.members.length !== arity) {
|
|
4390
|
+
throw internalError3({ message: `keyList has ${expr.members.length} members for ${arity} columns` });
|
|
4391
|
+
}
|
|
4392
|
+
for (const row of expr.rows) {
|
|
4393
|
+
if (row.length !== arity)
|
|
4394
|
+
throw internalError3({ message: `keyList row has ${row.length} values for ${arity} columns` });
|
|
4395
|
+
}
|
|
4396
|
+
if (expr.rows.length === 0)
|
|
4397
|
+
return "1 = 0";
|
|
4398
|
+
const columns = expr.columns.map((column) => quoteCol(dialect, column));
|
|
4399
|
+
const tuple = arity === 1 ? columns[0] : `(${columns.join(", ")})`;
|
|
4400
|
+
if (dialect.keyListStrategy === "unnest") {
|
|
4401
|
+
const types = expr.members.map((member) => postgresKeyListElementType({ dialect, member }));
|
|
4402
|
+
if (!keyListBindsUntypedRows({ members: expr.members })) {
|
|
4403
|
+
const arrays = types.map((type, index) => `${collector.add(expr.rows.map((row) => row[index]))}::${type}[]`);
|
|
4404
|
+
return `${tuple} IN (SELECT * FROM unnest(${arrays.join(", ")}))`;
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
if (dialect.timeOfDayInStrategy === "equalities" && expr.members.some((member) => member.timeOfDay === true)) {
|
|
4408
|
+
const conjunctions = expr.rows.map((row) => {
|
|
4409
|
+
const terms = row.map((value, index) => `${columns[index]} = ${collector.add(value)}`);
|
|
4410
|
+
return terms.length === 1 ? terms[0] : `(${terms.join(" AND ")})`;
|
|
4411
|
+
});
|
|
4412
|
+
return conjunctions.length === 1 ? conjunctions[0] : `(${conjunctions.join(" OR ")})`;
|
|
4413
|
+
}
|
|
4414
|
+
const tuples = expr.rows.map((row) => `(${collector.addMany(row).join(", ")})`);
|
|
4415
|
+
return dialect.keyListStrategy === "values" ? `${tuple} IN (VALUES ${tuples.join(", ")})` : `${tuple} IN (${tuples.join(", ")})`;
|
|
4416
|
+
}
|
|
4417
|
+
var KEY_PAGE_KEYS = "__vibe_keys";
|
|
4418
|
+
var KEY_PAGE_IDS = "__vibe_ids";
|
|
4419
|
+
var KEY_PAGE_ROWS = "__vibe_page";
|
|
4420
|
+
var KEY_PAGE_SELECT_MEMBERS = new Set(["kind", "table", "columns", "where", "orderBy", "limit", "offset"]);
|
|
4421
|
+
function renderKeyPage(params) {
|
|
4422
|
+
const { dialect, page, collector } = params;
|
|
4423
|
+
const { select } = page;
|
|
4424
|
+
const arity = page.keyColumns.length;
|
|
4425
|
+
if (arity === 0 || page.members.length !== arity) {
|
|
4426
|
+
throw internalError3({ message: `keyPage has ${page.members.length} members for ${arity} key columns` });
|
|
4427
|
+
}
|
|
4428
|
+
if (page.rows.length === 0)
|
|
4429
|
+
throw internalError3({ message: "keyPage needs at least one key" });
|
|
4430
|
+
for (const row of page.rows) {
|
|
4431
|
+
if (row.length !== arity)
|
|
4432
|
+
throw internalError3({ message: `keyPage row has ${row.length} values for ${arity} key columns` });
|
|
4433
|
+
}
|
|
4434
|
+
if (page.identity.length === 0)
|
|
4435
|
+
throw internalError3({ message: "keyPage needs the child's identity columns" });
|
|
4436
|
+
const order = select.orderBy ?? [];
|
|
4437
|
+
if (order.length === 0 || order.some((term) => !("column" in term))) {
|
|
4438
|
+
throw internalError3({ message: "keyPage needs a non-empty order of plain column terms" });
|
|
4439
|
+
}
|
|
4440
|
+
if (select.table.alias !== undefined || Object.keys(select).some((member) => !KEY_PAGE_SELECT_MEMBERS.has(member))) {
|
|
4441
|
+
throw internalError3({ message: "keyPage select carries a member the per-key page cannot render" });
|
|
4442
|
+
}
|
|
4443
|
+
if (dialect.keyPageStrategy === "window") {
|
|
4444
|
+
throw unsupportedCapability27({
|
|
4445
|
+
dialect: dialect.name,
|
|
4446
|
+
feature: "a per-key page (keyPage)",
|
|
4447
|
+
hint: "the runtime keeps the ROW_NUMBER window on this dialect"
|
|
4448
|
+
});
|
|
4449
|
+
}
|
|
4450
|
+
const pageRef = (column) => ({ ...column, table: KEY_PAGE_ROWS });
|
|
4451
|
+
const projection = select.columns.map((column) => quoteProjection(dialect, pageRef(column)));
|
|
4452
|
+
const withKey = (keyRef) => {
|
|
4453
|
+
const equalities = page.keyColumns.map((column, index) => ({ kind: "cmpCol", op: "eq", left: column, right: keyRef(index) }));
|
|
4454
|
+
const operands = select.where === undefined ? equalities : [...equalities, select.where];
|
|
4455
|
+
return operands.length === 1 ? operands[0] : { kind: "and", operands };
|
|
4456
|
+
};
|
|
4457
|
+
const keyPage = (where, rank) => ({
|
|
4458
|
+
kind: "select",
|
|
4459
|
+
table: select.table,
|
|
4460
|
+
columns: page.identity,
|
|
4461
|
+
...rank ? { rowNumber: { partitionBy: page.keyColumns, orderBy: order, alias: page.rankAlias } } : {},
|
|
4462
|
+
where,
|
|
4463
|
+
orderBy: order,
|
|
4464
|
+
...select.limit === undefined ? {} : { limit: select.limit },
|
|
4465
|
+
...select.offset === undefined ? {} : { offset: select.offset }
|
|
4466
|
+
});
|
|
4467
|
+
const rows = renderTable(dialect, { ...select.table, alias: KEY_PAGE_ROWS });
|
|
4468
|
+
const keyAlias = select.table.name === KEY_PAGE_KEYS ? `${KEY_PAGE_KEYS}_1` : KEY_PAGE_KEYS;
|
|
4469
|
+
const keys = dialect.quoteIdent({ name: keyAlias });
|
|
4470
|
+
if (dialect.keyPageStrategy === "lateral") {
|
|
4471
|
+
if (keyListBindsUntypedRows({ members: page.members })) {
|
|
4472
|
+
throw unsupportedCapability27({
|
|
4473
|
+
dialect: dialect.name,
|
|
4474
|
+
feature: "a per-key page over a DateTime key without a declared native type",
|
|
4475
|
+
hint: "the runtime keeps the ROW_NUMBER window for such keys"
|
|
4476
|
+
});
|
|
4477
|
+
}
|
|
4478
|
+
const types = page.members.map((member) => postgresKeyListElementType({ dialect, member }));
|
|
4479
|
+
const arrays = types.map((type, index) => `${collector.add(page.rows.map((row) => row[index]))}::${type}[]`);
|
|
4480
|
+
const names = page.keyColumns.map((_, index) => dialect.quoteIdent({ name: `__vibe_k${index}` }));
|
|
4481
|
+
const ids = renderSelect({ dialect, statement: keyPage(withKey((index) => ({ table: keyAlias, name: `__vibe_k${index}` })), true), collector });
|
|
4482
|
+
const on = page.identity.map((column) => `${quoteCol(dialect, pageRef(column))} = ${quoteCol(dialect, { table: KEY_PAGE_IDS, name: column.name })}`).join(" AND ");
|
|
4483
|
+
const rank = quoteCol(dialect, { table: KEY_PAGE_IDS, name: page.rankAlias });
|
|
4484
|
+
return `SELECT ${[...projection, rank].join(", ")} FROM unnest(${arrays.join(", ")}) AS ${keys} (${names.join(", ")}) CROSS JOIN LATERAL (${ids.text}) AS ${dialect.quoteIdent({ name: KEY_PAGE_IDS })} INNER JOIN ${rows} ON ${on}`;
|
|
4485
|
+
}
|
|
4486
|
+
const partition = page.keyColumns.map((column) => quoteCol(dialect, pageRef(column))).join(", ");
|
|
4487
|
+
const rankOrder = order.map((term) => renderOrderTerm({ dialect, term: { ...term, column: pageRef(term.column) }, collector })).join(", ");
|
|
4488
|
+
const rank = `ROW_NUMBER() OVER (PARTITION BY ${partition} ORDER BY ${rankOrder}) AS ${dialect.quoteIdent({ name: page.rankAlias })}`;
|
|
4489
|
+
const tuples = page.rows.map((row) => `(${collector.addMany(row).join(", ")})`);
|
|
4490
|
+
const membership = renderExpr({
|
|
4491
|
+
dialect,
|
|
4492
|
+
expr: { kind: "inSelect", columns: page.identity.map(pageRef), select: keyPage(withKey((index) => ({ table: keyAlias, name: `column${index + 1}` })), false) },
|
|
4493
|
+
collector
|
|
4494
|
+
});
|
|
4495
|
+
return `SELECT ${[...projection, rank].join(", ")} FROM (VALUES ${tuples.join(", ")}) AS ${keys} INNER JOIN ${rows} ON ${membership}`;
|
|
4496
|
+
}
|
|
4497
|
+
var UNION_BRANCH_MEMBERS = new Set(["kind", "table", "columns", "where", "orderBy", "limit"]);
|
|
4498
|
+
function renderUnionPage(params) {
|
|
4499
|
+
const { dialect, statement, collector } = params;
|
|
4500
|
+
const branches = statement.fromUnion ?? [];
|
|
4501
|
+
if (branches.length < 2)
|
|
4502
|
+
throw internalError3({ message: "fromUnion needs at least two branches" });
|
|
4503
|
+
if (statement.fromSelect !== undefined || statement.keyPage !== undefined || statement.table.alias !== undefined) {
|
|
4504
|
+
throw internalError3({ message: "fromUnion uses table.name as the derived-table alias \u2014 fromSelect, keyPage and table.alias must be unset" });
|
|
4505
|
+
}
|
|
4506
|
+
if (statement.rowLock !== undefined)
|
|
4507
|
+
throw internalError3({ message: "a locking read cannot read from a union page" });
|
|
4508
|
+
const outputNames = (branch) => branch.columns.map((column) => column.alias ?? column.name).join("\x00");
|
|
4509
|
+
const names = outputNames(branches[0]);
|
|
4510
|
+
return branches.map((branch, index) => {
|
|
4511
|
+
if (branch.columns.length === 0 || Object.keys(branch).some((member) => !UNION_BRANCH_MEMBERS.has(member))) {
|
|
4512
|
+
throw internalError3({ message: "a fromUnion branch must be a plain SELECT <columns> FROM <table> [WHERE] [ORDER BY] [LIMIT]" });
|
|
4513
|
+
}
|
|
4514
|
+
if (outputNames(branch) !== names)
|
|
4515
|
+
throw internalError3({ message: "fromUnion branches must project the same output names in the same order" });
|
|
4516
|
+
const inner = renderSelect({ dialect, statement: branch, collector });
|
|
4517
|
+
return `SELECT * FROM (${inner.text}) AS ${dialect.quoteIdent({ name: `__vibe_u${index}` })}`;
|
|
4518
|
+
}).join(" UNION ALL ");
|
|
4519
|
+
}
|
|
3355
4520
|
function jsonStringPattern(params) {
|
|
3356
4521
|
const { expr } = params;
|
|
3357
4522
|
if (typeof expr.value !== "string") {
|
|
3358
|
-
throw
|
|
4523
|
+
throw internalError3({ message: `json ${expr.op} expects a string operand` });
|
|
3359
4524
|
}
|
|
3360
4525
|
if (expr.op === "string_contains")
|
|
3361
4526
|
return containsPattern({ raw: expr.value });
|
|
@@ -3379,7 +4544,7 @@ function renderJsonLike(params) {
|
|
|
3379
4544
|
function renderJsonFilter(params) {
|
|
3380
4545
|
const { dialect, expr, collector } = params;
|
|
3381
4546
|
if (!dialect.supportedJsonOps.includes(expr.op)) {
|
|
3382
|
-
throw
|
|
4547
|
+
throw unsupportedCapability27({
|
|
3383
4548
|
dialect: dialect.name,
|
|
3384
4549
|
feature: `the json filter "${expr.op}"`,
|
|
3385
4550
|
hint: `this dialect can express: ${dialect.supportedJsonOps.join(", ")}`
|
|
@@ -3446,7 +4611,7 @@ function renderJsonFilter(params) {
|
|
|
3446
4611
|
const elements = Array.isArray(expr.value) ? expr.value : [expr.value];
|
|
3447
4612
|
const tests = elements.map((element) => {
|
|
3448
4613
|
if (element === null || typeof element === "object" && element !== null) {
|
|
3449
|
-
throw
|
|
4614
|
+
throw unsupportedCapability27({
|
|
3450
4615
|
dialect: dialect.name,
|
|
3451
4616
|
feature: "json array_contains with object/array/null elements",
|
|
3452
4617
|
hint: "sqlite expresses scalar-element containment only \u2014 deep structural containment needs postgres or mysql"
|
|
@@ -3460,7 +4625,7 @@ function renderJsonFilter(params) {
|
|
|
3460
4625
|
return tests.length === 0 ? guard : `(${[guard, ...tests].join(" AND ")})`;
|
|
3461
4626
|
}
|
|
3462
4627
|
default:
|
|
3463
|
-
throw
|
|
4628
|
+
throw internalError3({ message: `json op "${expr.op}" leaked past supportedJsonOps on sqlite` });
|
|
3464
4629
|
}
|
|
3465
4630
|
}
|
|
3466
4631
|
const column = quoteCol(dialect, expr.column);
|
|
@@ -3515,11 +4680,11 @@ function renderSelectExpr(params) {
|
|
|
3515
4680
|
function renderPositionalRow(params) {
|
|
3516
4681
|
const { dialect, join, subAlias } = params;
|
|
3517
4682
|
if (join.select.columns.length === 0 || join.select.selectExpr !== undefined || (join.select.aggregates?.length ?? 0) > 0 || join.select.rowNumber !== undefined) {
|
|
3518
|
-
throw
|
|
4683
|
+
throw internalError3({ message: "positional lateral rows require an explicit column-only projection" });
|
|
3519
4684
|
}
|
|
3520
4685
|
const names = join.select.columns.map((column) => projectionOutputName({ column }));
|
|
3521
4686
|
if (new Set(names).size !== names.length) {
|
|
3522
|
-
throw
|
|
4687
|
+
throw internalError3({ message: "positional lateral projection has duplicate output names" });
|
|
3523
4688
|
}
|
|
3524
4689
|
const values = names.map((name) => `${subAlias}.${dialect.quoteIdent({ name })}`);
|
|
3525
4690
|
const chunks = [];
|
|
@@ -3527,7 +4692,7 @@ function renderPositionalRow(params) {
|
|
|
3527
4692
|
chunks.push(`json_build_array(${values.slice(index, index + JSON_TRANSPORT.chunkSize).join(", ")})`);
|
|
3528
4693
|
}
|
|
3529
4694
|
if (chunks.length > JSON_TRANSPORT.chunkSize) {
|
|
3530
|
-
throw
|
|
4695
|
+
throw internalError3({ message: "positional lateral projection exceeds the transport width limit" });
|
|
3531
4696
|
}
|
|
3532
4697
|
return chunks.length === 1 ? chunks[0] : `json_build_array(${chunks.join(", ")})`;
|
|
3533
4698
|
}
|
|
@@ -3535,8 +4700,8 @@ function renderLateralJoin(params) {
|
|
|
3535
4700
|
const { dialect, join, collector } = params;
|
|
3536
4701
|
const inner = renderSelect({ dialect, statement: join.select, collector });
|
|
3537
4702
|
if (join.agg === "count") {
|
|
3538
|
-
if (join.rowFormat === "positional" || join.select.columns.length !== 0 || join.select.selectExpr?.kind !== "countAll" || join.select.selectExpr.alias !== join.columnAlias || join.select.groupBy !== undefined || join.select.having !== undefined || join.select.limit !== undefined || join.select.offset !== undefined || join.select.distinctOn !== undefined) {
|
|
3539
|
-
throw
|
|
4703
|
+
if (join.rowFormat === "positional" || join.select.columns.length !== 0 || join.select.selectExpr?.kind !== "countAll" || join.select.selectExpr.alias !== join.columnAlias || join.select.groupBy !== undefined || join.select.having !== undefined || join.select.limit !== undefined || join.select.offset !== undefined || join.select.distinctOn !== undefined || join.select.distinct !== undefined) {
|
|
4704
|
+
throw internalError3({ message: "count lateral must return one ungrouped COUNT(*) under its column alias" });
|
|
3540
4705
|
}
|
|
3541
4706
|
return `LEFT JOIN LATERAL (${inner.text}) AS ${dialect.quoteIdent({ name: join.alias })} ON true`;
|
|
3542
4707
|
}
|
|
@@ -3567,8 +4732,8 @@ function renderLateralJoin(params) {
|
|
|
3567
4732
|
function renderCountSubquery(params) {
|
|
3568
4733
|
const { dialect, subquery, collector } = params;
|
|
3569
4734
|
const select = subquery.select;
|
|
3570
|
-
if (select.columns.length !== 0 || select.selectExpr?.kind !== "countAll" || (select.aggregates?.length ?? 0) > 0 || select.rowNumber !== undefined || select.groupBy !== undefined || select.having !== undefined || select.limit !== undefined || select.offset !== undefined || select.distinctOn !== undefined || select.rowLock !== undefined || select.countSubqueries !== undefined) {
|
|
3571
|
-
throw
|
|
4735
|
+
if (select.columns.length !== 0 || select.selectExpr?.kind !== "countAll" || (select.aggregates?.length ?? 0) > 0 || select.rowNumber !== undefined || select.groupBy !== undefined || select.having !== undefined || select.limit !== undefined || select.offset !== undefined || select.distinctOn !== undefined || select.distinct !== undefined || select.rowLock !== undefined || select.countSubqueries !== undefined) {
|
|
4736
|
+
throw internalError3({ message: "count subquery must be one ungrouped COUNT(*)" });
|
|
3572
4737
|
}
|
|
3573
4738
|
const inner = renderSelect({ dialect, statement: select, collector });
|
|
3574
4739
|
return `(${inner.text}) AS ${dialect.quoteIdent({ name: subquery.alias })}`;
|
|
@@ -3598,27 +4763,27 @@ function renderKeyTableJoin(params) {
|
|
|
3598
4763
|
const postgresMember = (member) => member.kind === "citext" || member.kind === "text" || member.kind === "integer";
|
|
3599
4764
|
const admitted = dialect.name === "mysql" ? join.members.every((member) => member.kind !== "citext" && member.kind !== "text") : dialect.name === "postgres" && join.members.every(postgresMember);
|
|
3600
4765
|
if (!admitted) {
|
|
3601
|
-
throw
|
|
4766
|
+
throw unsupportedCapability27({
|
|
3602
4767
|
dialect: dialect.name,
|
|
3603
4768
|
feature: "relation key table",
|
|
3604
4769
|
hint: "the key-table join is mysql's relation association \u2014 this dialect compares relation keys by value"
|
|
3605
4770
|
});
|
|
3606
4771
|
}
|
|
3607
4772
|
if (join.columns.length === 0 || join.columns.length !== join.members.length) {
|
|
3608
|
-
throw
|
|
4773
|
+
throw internalError3({ message: `key table needs one member per column, got ${join.members.length} for ${join.columns.length}` });
|
|
3609
4774
|
}
|
|
3610
4775
|
if (join.rows.length === 0)
|
|
3611
|
-
throw
|
|
4776
|
+
throw internalError3({ message: "key table needs at least one key row" });
|
|
3612
4777
|
for (const member of join.members) {
|
|
3613
4778
|
if (member.kind === "decimal" && !(Number.isInteger(member.scale) && member.scale >= 0 && member.scale <= 30)) {
|
|
3614
|
-
throw
|
|
4779
|
+
throw internalError3({ message: `key table decimal scale must be an integer 0-30, got ${String(member.scale)}` });
|
|
3615
4780
|
}
|
|
3616
4781
|
}
|
|
3617
4782
|
const alias = dialect.quoteIdent({ name: join.alias });
|
|
3618
4783
|
const memberNames = join.members.map((_, index) => dialect.quoteIdent({ name: `__vibe_k${index}` }));
|
|
3619
4784
|
const rows = join.rows.map((row, ordinal) => {
|
|
3620
4785
|
if (row.length !== join.members.length) {
|
|
3621
|
-
throw
|
|
4786
|
+
throw internalError3({ message: `key table row has ${row.length} values for ${join.members.length} members` });
|
|
3622
4787
|
}
|
|
3623
4788
|
const cells = row.map((value, index) => {
|
|
3624
4789
|
const placeholder = collector.add(value);
|
|
@@ -3645,13 +4810,25 @@ function renderKeyTableJoin(params) {
|
|
|
3645
4810
|
const on = join.columns.map((column, index) => `${quoteCol(dialect, column)} = ${alias}.${memberNames[index]}`).join(" AND ");
|
|
3646
4811
|
return `INNER JOIN (VALUES ${rows.join(", ")}) AS ${alias} (${[...memberNames, dialect.quoteIdent({ name: join.ordinalAlias })].join(", ")}) ON ${on}`;
|
|
3647
4812
|
}
|
|
4813
|
+
function renderLeftJoin(params) {
|
|
4814
|
+
const { dialect, join, collector } = params;
|
|
4815
|
+
if (join.on.length === 0) {
|
|
4816
|
+
throw internalError3({ message: "left join needs at least one ON pair" });
|
|
4817
|
+
}
|
|
4818
|
+
if (join.select.columns.length === 0 || join.select.columns.some((column) => column.alias === undefined)) {
|
|
4819
|
+
throw internalError3({ message: "a left join's derived select must alias every projected column" });
|
|
4820
|
+
}
|
|
4821
|
+
const inner = renderSelect({ dialect, statement: join.select, collector });
|
|
4822
|
+
const on = join.on.map((pair) => `${quoteCol(dialect, pair.left)} = ${quoteCol(dialect, pair.right)}`).join(" AND ");
|
|
4823
|
+
return `LEFT JOIN (${inner.text}) AS ${dialect.quoteIdent({ name: join.alias })} ON ${on}`;
|
|
4824
|
+
}
|
|
3648
4825
|
function renderSelect(params) {
|
|
3649
4826
|
const { dialect, statement, collector } = params;
|
|
3650
4827
|
if (statement.selectExpr !== undefined && statement.columns.length > 0 && statement.selectExpr.kind !== "countAll") {
|
|
3651
|
-
throw
|
|
4828
|
+
throw internalError3({ message: "only countAll can combine with a column list" });
|
|
3652
4829
|
}
|
|
3653
4830
|
if (statement.selectExpr !== undefined && statement.aggregates !== undefined && statement.aggregates.length > 0) {
|
|
3654
|
-
throw
|
|
4831
|
+
throw internalError3({ message: "selectExpr and aggregates cannot combine" });
|
|
3655
4832
|
}
|
|
3656
4833
|
const columnList = statement.columns.map((column) => quoteProjection(dialect, column));
|
|
3657
4834
|
if (statement.selectExpr !== undefined) {
|
|
@@ -3672,16 +4849,19 @@ function renderSelect(params) {
|
|
|
3672
4849
|
if (statement.rowNumber !== undefined) {
|
|
3673
4850
|
const rowNumber = statement.rowNumber;
|
|
3674
4851
|
if (rowNumber.partitionBy.length === 0) {
|
|
3675
|
-
throw
|
|
4852
|
+
throw internalError3({ message: "rowNumber needs at least one partition column" });
|
|
3676
4853
|
}
|
|
3677
4854
|
const partition = rowNumber.partitionBy.map((column) => quoteCol(dialect, column)).join(", ");
|
|
3678
4855
|
const order = rowNumber.orderBy.length === 0 ? "" : ` ORDER BY ${rowNumber.orderBy.map((term) => renderOrderTerm({ dialect, term, collector })).join(", ")}`;
|
|
3679
4856
|
columnList.push(`ROW_NUMBER() OVER (PARTITION BY ${partition}${order}) AS ${dialect.quoteIdent({ name: rowNumber.alias })}`);
|
|
3680
4857
|
}
|
|
3681
|
-
let distinctPrefix = "";
|
|
4858
|
+
let distinctPrefix = statement.distinct === true ? "DISTINCT " : "";
|
|
4859
|
+
if (statement.distinct === true && statement.distinctOn !== undefined) {
|
|
4860
|
+
throw internalError3({ message: "distinct and distinctOn cannot combine" });
|
|
4861
|
+
}
|
|
3682
4862
|
if (statement.distinctOn !== undefined && statement.distinctOn.length > 0) {
|
|
3683
4863
|
if (!dialect.capabilities.distinctOn) {
|
|
3684
|
-
throw
|
|
4864
|
+
throw unsupportedCapability27({
|
|
3685
4865
|
dialect: dialect.name,
|
|
3686
4866
|
feature: "SELECT DISTINCT ON",
|
|
3687
4867
|
hint: "postgres-only \u2014 the runtime emulates distinct client-side on this dialect"
|
|
@@ -3691,9 +4871,16 @@ function renderSelect(params) {
|
|
|
3691
4871
|
}
|
|
3692
4872
|
const columns = columnList.length === 0 ? "*" : columnList.join(", ");
|
|
3693
4873
|
let from;
|
|
3694
|
-
if (statement.
|
|
4874
|
+
if (statement.fromUnion !== undefined) {
|
|
4875
|
+
from = `(${renderUnionPage({ dialect, statement, collector })}) AS ${dialect.quoteIdent({ name: statement.table.name })}`;
|
|
4876
|
+
} else if (statement.keyPage !== undefined) {
|
|
4877
|
+
if (statement.fromSelect !== undefined || statement.table.alias !== undefined) {
|
|
4878
|
+
throw internalError3({ message: "keyPage uses table.name as the derived-table alias \u2014 fromSelect and table.alias must be unset" });
|
|
4879
|
+
}
|
|
4880
|
+
from = `(${renderKeyPage({ dialect, page: statement.keyPage, collector })}) AS ${dialect.quoteIdent({ name: statement.table.name })}`;
|
|
4881
|
+
} else if (statement.fromSelect !== undefined) {
|
|
3695
4882
|
if (statement.table.alias !== undefined) {
|
|
3696
|
-
throw
|
|
4883
|
+
throw internalError3({ message: "fromSelect uses table.name as the subquery alias \u2014 table.alias must be unset" });
|
|
3697
4884
|
}
|
|
3698
4885
|
const inner = renderSelect({ dialect, statement: statement.fromSelect, collector });
|
|
3699
4886
|
from = `(${inner.text}) AS ${dialect.quoteIdent({ name: statement.table.name })}`;
|
|
@@ -3704,7 +4891,7 @@ function renderSelect(params) {
|
|
|
3704
4891
|
if (statement.innerJoins !== undefined && statement.innerJoins.length > 0) {
|
|
3705
4892
|
for (const join of statement.innerJoins) {
|
|
3706
4893
|
if (join.on.length === 0) {
|
|
3707
|
-
throw
|
|
4894
|
+
throw internalError3({ message: "inner join needs at least one ON pair" });
|
|
3708
4895
|
}
|
|
3709
4896
|
const on = join.on.map((pair) => `${quoteCol(dialect, pair.left)} = ${quoteCol(dialect, pair.right)}`).join(" AND ");
|
|
3710
4897
|
text += ` INNER JOIN ${renderTable(dialect, join.table)} ON ${on}`;
|
|
@@ -3713,9 +4900,17 @@ function renderSelect(params) {
|
|
|
3713
4900
|
if (statement.keyTable !== undefined) {
|
|
3714
4901
|
text += ` ${renderKeyTableJoin({ dialect, join: statement.keyTable, collector })}`;
|
|
3715
4902
|
}
|
|
4903
|
+
if (statement.leftJoins !== undefined && statement.leftJoins.length > 0) {
|
|
4904
|
+
if (statement.rowLock !== undefined) {
|
|
4905
|
+
throw internalError3({ message: "a locking read cannot carry a left join \u2014 postgres refuses FOR UPDATE on the nullable side of an outer join" });
|
|
4906
|
+
}
|
|
4907
|
+
for (const join of statement.leftJoins) {
|
|
4908
|
+
text += ` ${renderLeftJoin({ dialect, join, collector })}`;
|
|
4909
|
+
}
|
|
4910
|
+
}
|
|
3716
4911
|
if (statement.lateralJoins !== undefined && statement.lateralJoins.length > 0) {
|
|
3717
4912
|
if (!dialect.capabilities.lateralJoin) {
|
|
3718
|
-
throw
|
|
4913
|
+
throw unsupportedCapability27({
|
|
3719
4914
|
dialect: dialect.name,
|
|
3720
4915
|
feature: "LATERAL JOIN relation loading",
|
|
3721
4916
|
hint: 'use the portable "query" relation strategy on this dialect'
|
|
@@ -3756,17 +4951,79 @@ function renderSelect(params) {
|
|
|
3756
4951
|
}
|
|
3757
4952
|
return { text, values: collector.values, returning: "none" };
|
|
3758
4953
|
}
|
|
4954
|
+
var UNNEST_ROWS_ALIAS = "__vibe_rows";
|
|
4955
|
+
var UNNEST_ORDINALITY = "ord";
|
|
4956
|
+
function unnestElement(params) {
|
|
4957
|
+
const { value, column, jsonStorage } = params;
|
|
4958
|
+
if (value === null || value === undefined)
|
|
4959
|
+
return null;
|
|
4960
|
+
if (isJsonParamCell(value)) {
|
|
4961
|
+
if (jsonStorage === undefined || (value.storage ?? "jsonb") !== jsonStorage) {
|
|
4962
|
+
throw internalError3({ message: `unnest insert column "${column}" got a Json cell its declared type does not take` });
|
|
4963
|
+
}
|
|
4964
|
+
return value.text;
|
|
4965
|
+
}
|
|
4966
|
+
if (jsonStorage === undefined) {
|
|
4967
|
+
switch (typeof value) {
|
|
4968
|
+
case "string":
|
|
4969
|
+
case "number":
|
|
4970
|
+
case "boolean":
|
|
4971
|
+
case "bigint":
|
|
4972
|
+
return value;
|
|
4973
|
+
case "object":
|
|
4974
|
+
if (value instanceof Date || value instanceof Uint8Array)
|
|
4975
|
+
return value;
|
|
4976
|
+
}
|
|
4977
|
+
}
|
|
4978
|
+
throw internalError3({ message: `unnest insert column "${column}" got a cell that is not a plain value (DEFAULT, the database clock, a document) \u2014 such rows keep VALUES` });
|
|
4979
|
+
}
|
|
4980
|
+
function renderUnnestRows(params) {
|
|
4981
|
+
const { dialect, statement, members, collector } = params;
|
|
4982
|
+
if (dialect.bulkInsertForm !== "unnest") {
|
|
4983
|
+
throw unsupportedCapability27({
|
|
4984
|
+
dialect: dialect.name,
|
|
4985
|
+
feature: "a bulk insert bound as typed arrays (INSERT \u2026 SELECT FROM unnest)",
|
|
4986
|
+
hint: "this dialect binds createMany rows as VALUES"
|
|
4987
|
+
});
|
|
4988
|
+
}
|
|
4989
|
+
if (statement.columns.length === 0)
|
|
4990
|
+
throw internalError3({ message: "an unnest insert needs at least one column" });
|
|
4991
|
+
if (members.length !== statement.columns.length) {
|
|
4992
|
+
throw internalError3({ message: `unnest insert has ${members.length} column types for ${statement.columns.length} columns` });
|
|
4993
|
+
}
|
|
4994
|
+
if (statement.onConflict !== undefined && !("doNothing" in statement.onConflict)) {
|
|
4995
|
+
throw internalError3({ message: "an unnest insert takes no DO UPDATE arm \u2014 upserts bind VALUES" });
|
|
4996
|
+
}
|
|
4997
|
+
const aliases = statement.columns.map((_column, index) => dialect.quoteIdent({ name: `c${index + 1}` }));
|
|
4998
|
+
const arrays = members.map((member, index) => {
|
|
4999
|
+
const cast = postgresBulkInsertArrayCast({ dialect, member });
|
|
5000
|
+
const jsonStorage = member.type !== "Json" ? undefined : cast === "text::json" ? "json" : "jsonb";
|
|
5001
|
+
const column = statement.columns[index];
|
|
5002
|
+
const values = statement.rows.map((row) => unnestElement({ value: row[index], column, jsonStorage }));
|
|
5003
|
+
return `${collector.add(values)}::${cast}[]`;
|
|
5004
|
+
});
|
|
5005
|
+
const ord = dialect.quoteIdent({ name: UNNEST_ORDINALITY });
|
|
5006
|
+
return `SELECT ${aliases.join(", ")} FROM unnest(${arrays.join(", ")}) WITH ORDINALITY AS ${dialect.quoteIdent({ name: UNNEST_ROWS_ALIAS })} (${aliases.join(", ")}, ${ord}) ORDER BY ${ord}`;
|
|
5007
|
+
}
|
|
3759
5008
|
function renderInsert(params) {
|
|
3760
5009
|
const { dialect, statement, collector } = params;
|
|
3761
|
-
if (statement.columns.length === 0) {
|
|
3762
|
-
throw internalError2({ message: "insert requires at least one column" });
|
|
3763
|
-
}
|
|
3764
5010
|
if (statement.rows.length === 0) {
|
|
3765
|
-
throw
|
|
5011
|
+
throw internalError3({ message: "insert requires at least one row" });
|
|
5012
|
+
}
|
|
5013
|
+
const defaultValues = statement.columns.length === 0 && dialect.emptyInsert.form === "defaultValues";
|
|
5014
|
+
if (defaultValues && statement.rows.length !== 1) {
|
|
5015
|
+
throw internalError3({ message: `${dialect.name} inserts an all-default row as DEFAULT VALUES, one row per statement` });
|
|
5016
|
+
}
|
|
5017
|
+
if (defaultValues && statement.onConflict !== undefined && !(dialect.emptyInsert.form === "defaultValues" && dialect.emptyInsert.conflictClause)) {
|
|
5018
|
+
throw unsupportedCapability27({
|
|
5019
|
+
dialect: dialect.name,
|
|
5020
|
+
feature: "a conflict clause on an all-default row (DEFAULT VALUES)",
|
|
5021
|
+
hint: "supply at least one column value, or drop the conflict clause"
|
|
5022
|
+
});
|
|
3766
5023
|
}
|
|
3767
5024
|
for (const row of statement.rows) {
|
|
3768
5025
|
if (row.length !== statement.columns.length) {
|
|
3769
|
-
throw
|
|
5026
|
+
throw internalError3({
|
|
3770
5027
|
message: `insert row has ${row.length} values but ${statement.columns.length} columns`
|
|
3771
5028
|
});
|
|
3772
5029
|
}
|
|
@@ -3775,7 +5032,8 @@ function renderInsert(params) {
|
|
|
3775
5032
|
const skipConflicts = onConflict !== undefined && "doNothing" in onConflict;
|
|
3776
5033
|
const insertKeyword = skipConflicts && dialect.doNothingForm === "insertIgnore" ? "INSERT IGNORE INTO" : "INSERT INTO";
|
|
3777
5034
|
const columnList = statement.columns.map((name) => dialect.quoteIdent({ name })).join(", ");
|
|
3778
|
-
const
|
|
5035
|
+
const unnestSource = statement.unnest === undefined ? undefined : renderUnnestRows({ dialect, statement, members: statement.unnest, collector });
|
|
5036
|
+
const rowsSql = unnestSource !== undefined ? "" : statement.rows.map((row) => `(${row.map((value) => {
|
|
3779
5037
|
if (isJsonParamCell(value))
|
|
3780
5038
|
return `${collector.add(value.text)}${jsonCellCast(dialect, value)}`;
|
|
3781
5039
|
if (isDbNowMarker(value)) {
|
|
@@ -3784,13 +5042,13 @@ function renderInsert(params) {
|
|
|
3784
5042
|
if (!isSqlDefaultCell(value))
|
|
3785
5043
|
return collector.add(value);
|
|
3786
5044
|
if (!dialect.rowDefaultKeyword) {
|
|
3787
|
-
throw
|
|
5045
|
+
throw internalError3({
|
|
3788
5046
|
message: `${dialect.name} cannot render a per-row DEFAULT cell \u2014 rows must be grouped by column set`
|
|
3789
5047
|
});
|
|
3790
5048
|
}
|
|
3791
5049
|
return "DEFAULT";
|
|
3792
5050
|
}).join(", ")})`).join(", ");
|
|
3793
|
-
let text = `${insertKeyword} ${renderTable(dialect, statement.table)} (${columnList}) VALUES ${rowsSql}`;
|
|
5051
|
+
let text = defaultValues ? `${insertKeyword} ${renderTable(dialect, statement.table)} DEFAULT VALUES` : `${insertKeyword} ${renderTable(dialect, statement.table)} (${columnList}) ${unnestSource ?? `VALUES ${rowsSql}`}`;
|
|
3794
5052
|
if (onConflict !== undefined && !("doNothing" in onConflict) && dialect.excludedRowRef.form === "rowAlias" && Object.values(onConflict.update).some((value) => isExcludedRef(value) || isSqlFragment(value) && sqlUsesIncoming({ fragment: value }))) {
|
|
3795
5053
|
text += ` AS ${dialect.quoteIdent({ name: dialect.excludedRowRef.alias })}`;
|
|
3796
5054
|
}
|
|
@@ -3800,7 +5058,7 @@ function renderInsert(params) {
|
|
|
3800
5058
|
let target = "";
|
|
3801
5059
|
if (onConflict.constraint !== undefined) {
|
|
3802
5060
|
if (dialect.name !== "postgres") {
|
|
3803
|
-
throw
|
|
5061
|
+
throw unsupportedCapability27({
|
|
3804
5062
|
dialect: dialect.name,
|
|
3805
5063
|
feature: "a named-constraint conflict target (ON CONFLICT ON CONSTRAINT)",
|
|
3806
5064
|
hint: "only postgres names constraints in conflict targets \u2014 use a column-list target"
|
|
@@ -3815,7 +5073,7 @@ function renderInsert(params) {
|
|
|
3815
5073
|
}
|
|
3816
5074
|
text += ` ON CONFLICT${target} DO NOTHING`;
|
|
3817
5075
|
} else if (onConflict.constraint !== undefined || onConflict.conflictWhere !== undefined) {
|
|
3818
|
-
throw
|
|
5076
|
+
throw unsupportedCapability27({
|
|
3819
5077
|
dialect: dialect.name,
|
|
3820
5078
|
feature: "a refined conflict target (constraint / partial-index predicate) on INSERT IGNORE",
|
|
3821
5079
|
hint: "mysql skips on any duplicate key \u2014 drop the refinement or gate the call per dialect"
|
|
@@ -3824,23 +5082,23 @@ function renderInsert(params) {
|
|
|
3824
5082
|
} else {
|
|
3825
5083
|
const updates = Object.entries(onConflict.update);
|
|
3826
5084
|
if (updates.length === 0) {
|
|
3827
|
-
throw
|
|
5085
|
+
throw internalError3({ message: "upsert update must set at least one column" });
|
|
3828
5086
|
}
|
|
3829
5087
|
const selfRef = dialect.quoteIdent({ name: statement.table.name });
|
|
3830
5088
|
const assignments = renderAssignments({ dialect, entries: updates, collector, selfRef, allowExcluded: true, tableName: statement.table.name });
|
|
3831
5089
|
if (dialect.upsertForm === "onConflict") {
|
|
3832
5090
|
if (onConflict.lastInsertIdColumn !== undefined) {
|
|
3833
|
-
throw
|
|
5091
|
+
throw internalError3({ message: "lastInsertIdColumn is a mysql (onDuplicateKey) construct" });
|
|
3834
5092
|
}
|
|
3835
5093
|
if (onConflict.constraint !== undefined && dialect.name !== "postgres") {
|
|
3836
|
-
throw
|
|
5094
|
+
throw unsupportedCapability27({
|
|
3837
5095
|
dialect: dialect.name,
|
|
3838
5096
|
feature: "a named-constraint conflict target (ON CONFLICT ON CONSTRAINT)",
|
|
3839
5097
|
hint: "sqlite conflict targets are column lists \u2014 name the indexed columns instead"
|
|
3840
5098
|
});
|
|
3841
5099
|
}
|
|
3842
5100
|
if (onConflict.constraint !== undefined && onConflict.conflictWhere !== undefined) {
|
|
3843
|
-
throw
|
|
5101
|
+
throw internalError3({
|
|
3844
5102
|
message: "a named-constraint conflict target already identifies the index \u2014 it cannot carry a partial predicate too"
|
|
3845
5103
|
});
|
|
3846
5104
|
}
|
|
@@ -3849,14 +5107,14 @@ function renderInsert(params) {
|
|
|
3849
5107
|
text += ` ON CONFLICT${target} DO UPDATE SET ${assignments}`;
|
|
3850
5108
|
} else {
|
|
3851
5109
|
if (onConflict.constraint !== undefined) {
|
|
3852
|
-
throw
|
|
5110
|
+
throw unsupportedCapability27({
|
|
3853
5111
|
dialect: dialect.name,
|
|
3854
5112
|
feature: "a named-constraint conflict target on ON DUPLICATE KEY UPDATE",
|
|
3855
5113
|
hint: "mysql's ODKU fires on any duplicate key \u2014 no target can be named"
|
|
3856
5114
|
});
|
|
3857
5115
|
}
|
|
3858
5116
|
if (onConflict.conflictWhere !== undefined) {
|
|
3859
|
-
throw
|
|
5117
|
+
throw unsupportedCapability27({
|
|
3860
5118
|
dialect: dialect.name,
|
|
3861
5119
|
feature: "a partial-index conflict target on ON DUPLICATE KEY UPDATE",
|
|
3862
5120
|
hint: "mysql's ODKU fires on any duplicate key \u2014 a partial arbiter cannot be selected; gate the call per dialect"
|
|
@@ -3874,7 +5132,7 @@ function renderUpdate(params) {
|
|
|
3874
5132
|
const { dialect, statement, collector } = params;
|
|
3875
5133
|
const assignments = Object.entries(statement.set);
|
|
3876
5134
|
if (assignments.length === 0) {
|
|
3877
|
-
throw
|
|
5135
|
+
throw internalError3({ message: "update must set at least one column" });
|
|
3878
5136
|
}
|
|
3879
5137
|
const setSql = renderAssignments({ dialect, entries: assignments, collector });
|
|
3880
5138
|
let text = `UPDATE ${renderTable(dialect, statement.table)} SET ${setSql}`;
|
|
@@ -3897,12 +5155,12 @@ function renderAssignments(params) {
|
|
|
3897
5155
|
const read = selfRef === undefined ? column : `${selfRef}.${column}`;
|
|
3898
5156
|
if (isSqlFragment(value)) {
|
|
3899
5157
|
if (params.allowExcluded !== true || selfRef === undefined)
|
|
3900
|
-
throw
|
|
5158
|
+
throw internalError3({ message: "SQL assignment fragments require a conflict update" });
|
|
3901
5159
|
return `${column} = ${renderSqlFragment({ dialect, fragment: value, collector, conflict: { table: params.tableName } })}`;
|
|
3902
5160
|
}
|
|
3903
5161
|
if (isExcludedRef(value)) {
|
|
3904
5162
|
if (params.allowExcluded !== true) {
|
|
3905
|
-
throw
|
|
5163
|
+
throw internalError3({
|
|
3906
5164
|
message: `an incoming-row reference (${value.column}) is only meaningful inside an upsert's update arm`
|
|
3907
5165
|
});
|
|
3908
5166
|
}
|
|
@@ -3943,19 +5201,19 @@ var CHAIN_ONE_ALIAS = "__vibe_one";
|
|
|
3943
5201
|
function renderChain(params) {
|
|
3944
5202
|
const { dialect, statement, collector } = params;
|
|
3945
5203
|
if (dialect.statementChain === null) {
|
|
3946
|
-
throw
|
|
5204
|
+
throw unsupportedCapability27({
|
|
3947
5205
|
dialect: dialect.name,
|
|
3948
5206
|
feature: "a statement chain (data-modifying WITH steps)",
|
|
3949
5207
|
hint: "this dialect accepts only SELECT inside WITH \u2014 gate on SqlDialect.statementChain and keep the multi-statement path"
|
|
3950
5208
|
});
|
|
3951
5209
|
}
|
|
3952
5210
|
if (statement.steps.length === 0)
|
|
3953
|
-
throw
|
|
5211
|
+
throw internalError3({ message: "a statement chain needs at least one step" });
|
|
3954
5212
|
const defined = new Set;
|
|
3955
5213
|
const steps = [];
|
|
3956
5214
|
for (const step of statement.steps) {
|
|
3957
5215
|
if (step.name === CHAIN_ROWS_ALIAS || step.name === CHAIN_ONE_ALIAS || defined.has(step.name)) {
|
|
3958
|
-
throw
|
|
5216
|
+
throw internalError3({ message: `chain step name "${step.name}" is reserved or repeated` });
|
|
3959
5217
|
}
|
|
3960
5218
|
const body = renderChainStep({ dialect, statement: step.statement, collector, earlier: defined });
|
|
3961
5219
|
steps.push(`${dialect.quoteIdent({ name: step.name })} AS (${body})`);
|
|
@@ -3963,11 +5221,11 @@ function renderChain(params) {
|
|
|
3963
5221
|
}
|
|
3964
5222
|
const result = statement.result;
|
|
3965
5223
|
if (result.rowsFrom.length === 0 || result.columns.length === 0) {
|
|
3966
|
-
throw
|
|
5224
|
+
throw internalError3({ message: "a chain result needs at least one source step and one column" });
|
|
3967
5225
|
}
|
|
3968
5226
|
for (const name of result.rowsFrom) {
|
|
3969
5227
|
if (!defined.has(name))
|
|
3970
|
-
throw
|
|
5228
|
+
throw internalError3({ message: `chain result reads undefined step "${name}"` });
|
|
3971
5229
|
}
|
|
3972
5230
|
const columnList = result.columns.map((name) => dialect.quoteIdent({ name })).join(", ");
|
|
3973
5231
|
const union = result.rowsFrom.map((name) => `SELECT ${columnList} FROM ${dialect.quoteIdent({ name })}`).join(" UNION ALL ");
|
|
@@ -3976,7 +5234,7 @@ function renderChain(params) {
|
|
|
3976
5234
|
const extra = [];
|
|
3977
5235
|
for (const alias of [result.verdict?.alias, result.tally?.alias]) {
|
|
3978
5236
|
if (alias !== undefined && result.columns.includes(alias)) {
|
|
3979
|
-
throw
|
|
5237
|
+
throw internalError3({ message: `chain result alias "${alias}" collides with a result column` });
|
|
3980
5238
|
}
|
|
3981
5239
|
}
|
|
3982
5240
|
const rows = dialect.quoteIdent({ name: CHAIN_ROWS_ALIAS });
|
|
@@ -4013,21 +5271,21 @@ function renderSelectCell(params) {
|
|
|
4013
5271
|
if (isDbNowMarker(value))
|
|
4014
5272
|
return dialect.renderDbNow({ offsetSeconds: value.offsetSeconds, collector });
|
|
4015
5273
|
if (isSqlDefaultCell(value)) {
|
|
4016
|
-
throw
|
|
5274
|
+
throw internalError3({ message: "DEFAULT is not allowed in an INSERT \u2026 SELECT list \u2014 omit the column instead" });
|
|
4017
5275
|
}
|
|
4018
5276
|
return collector.add(value);
|
|
4019
5277
|
}
|
|
4020
5278
|
function renderRowSource(params) {
|
|
4021
5279
|
const { dialect, source, collector } = params;
|
|
4022
5280
|
if (source.columns.length === 0 || source.rows.length === 0) {
|
|
4023
|
-
throw
|
|
5281
|
+
throw internalError3({ message: "a row source needs at least one column and one row" });
|
|
4024
5282
|
}
|
|
4025
5283
|
const typedBy = renderTable(dialect, source.typedBy);
|
|
4026
5284
|
const columns = source.columns.map((name) => dialect.quoteIdent({ name }));
|
|
4027
5285
|
const rows = [`(${columns.map((column) => `(SELECT ${column} FROM ${typedBy} WHERE FALSE)`).join(", ")})`];
|
|
4028
5286
|
for (const row of source.rows) {
|
|
4029
5287
|
if (row.length !== source.columns.length) {
|
|
4030
|
-
throw
|
|
5288
|
+
throw internalError3({ message: `row source row has ${row.length} values but ${source.columns.length} columns` });
|
|
4031
5289
|
}
|
|
4032
5290
|
rows.push(`(${row.map((value) => renderSelectCell({ dialect, value, collector })).join(", ")})`);
|
|
4033
5291
|
}
|
|
@@ -4039,12 +5297,12 @@ function renderTallyColumn(params) {
|
|
|
4039
5297
|
const { dialect, result, defined, collector } = params;
|
|
4040
5298
|
const tally = result.tally;
|
|
4041
5299
|
if (tally === undefined)
|
|
4042
|
-
throw
|
|
5300
|
+
throw internalError3({ message: "a chain veto needs a tally column to guard" });
|
|
4043
5301
|
if (tally.steps.length === 0)
|
|
4044
|
-
throw
|
|
5302
|
+
throw internalError3({ message: "a chain tally needs at least one step" });
|
|
4045
5303
|
const step = (name) => {
|
|
4046
5304
|
if (!defined.has(name))
|
|
4047
|
-
throw
|
|
5305
|
+
throw internalError3({ message: `chain tally or veto reads undefined step "${name}"` });
|
|
4048
5306
|
return dialect.quoteIdent({ name });
|
|
4049
5307
|
};
|
|
4050
5308
|
const count = (name) => `(SELECT count(*) FROM ${step(name)})`;
|
|
@@ -4054,12 +5312,12 @@ function renderTallyColumn(params) {
|
|
|
4054
5312
|
return `${sum} AS ${dialect.quoteIdent({ name: tally.alias })}`;
|
|
4055
5313
|
const arms = vetoes.map((veto) => {
|
|
4056
5314
|
if (!CHAIN_VETO_TOKEN_PATTERN.test(veto.token))
|
|
4057
|
-
throw
|
|
5315
|
+
throw internalError3({ message: `chain veto token "${veto.token}" is not a lower-case word` });
|
|
4058
5316
|
if (typeof veto.nonce !== "string" || veto.nonce === "")
|
|
4059
|
-
throw
|
|
5317
|
+
throw internalError3({ message: `chain veto "${veto.token}" needs a non-empty nonce` });
|
|
4060
5318
|
const minRows = veto.minRows ?? 1;
|
|
4061
5319
|
if (!Number.isSafeInteger(minRows) || minRows < 1)
|
|
4062
|
-
throw
|
|
5320
|
+
throw internalError3({ message: `chain veto "${veto.token}" needs a positive integer minRows` });
|
|
4063
5321
|
const when = minRows === 1 ? `WHEN NOT EXISTS (SELECT 1 FROM ${step(veto.step)})` : `WHEN ${count(veto.step)} < ${minRows}`;
|
|
4064
5322
|
return `${when} THEN CAST('${CHAIN_VETO_PREFIX}${veto.token}:' || ${collector.add(veto.nonce)} || ':' || ${count(veto.step)} AS integer)`;
|
|
4065
5323
|
});
|
|
@@ -4068,25 +5326,25 @@ function renderTallyColumn(params) {
|
|
|
4068
5326
|
function renderInsertSelect(params) {
|
|
4069
5327
|
const { dialect, statement, collector } = params;
|
|
4070
5328
|
if (statement.columns.length === 0)
|
|
4071
|
-
throw
|
|
5329
|
+
throw internalError3({ message: "insert requires at least one column" });
|
|
4072
5330
|
if (statement.cells.length !== statement.columns.length) {
|
|
4073
|
-
throw
|
|
5331
|
+
throw internalError3({
|
|
4074
5332
|
message: `insert row has ${statement.cells.length} values but ${statement.columns.length} columns`
|
|
4075
5333
|
});
|
|
4076
5334
|
}
|
|
4077
5335
|
const from = statement.from ?? [];
|
|
4078
5336
|
for (const name of from) {
|
|
4079
5337
|
if (!params.earlier.has(name))
|
|
4080
|
-
throw
|
|
5338
|
+
throw internalError3({ message: `INSERT \u2026 SELECT reads step "${name}" before it is defined` });
|
|
4081
5339
|
}
|
|
4082
5340
|
const source = statement.source;
|
|
4083
5341
|
if (source !== undefined && (params.earlier.has(source.alias) || from.includes(source.alias))) {
|
|
4084
|
-
throw
|
|
5342
|
+
throw internalError3({ message: `row source alias "${source.alias}" collides with a chain step` });
|
|
4085
5343
|
}
|
|
4086
5344
|
const cells = statement.cells.map((value) => {
|
|
4087
5345
|
if (isStepColumnRef(value)) {
|
|
4088
5346
|
if (!from.includes(value.step) && value.step !== source?.alias) {
|
|
4089
|
-
throw
|
|
5347
|
+
throw internalError3({ message: `cell reads step "${value.step}", which the INSERT \u2026 SELECT does not name in FROM` });
|
|
4090
5348
|
}
|
|
4091
5349
|
return `${dialect.quoteIdent({ name: value.step })}.${dialect.quoteIdent({ name: value.column })}`;
|
|
4092
5350
|
}
|
|
@@ -4105,7 +5363,7 @@ function renderInsertSelect(params) {
|
|
|
4105
5363
|
return text + applyReturning({ dialect, requested: statement.returning }).suffix;
|
|
4106
5364
|
}
|
|
4107
5365
|
// packages/sql/src/dialects/postgres-type-identity.ts
|
|
4108
|
-
import { unsupportedCapability as
|
|
5366
|
+
import { unsupportedCapability as unsupportedCapability28 } from "@vibeorm/schema";
|
|
4109
5367
|
var IDENTIFIER = '(?:"(?:[^"\\x00]|"")+"|[a-zA-Z_][a-zA-Z_0-9$]*)';
|
|
4110
5368
|
var MODIFIER_VALUE = `(?:[+-]?\\d+|${IDENTIFIER}|'(?:[^'\\x00]|'')*')`;
|
|
4111
5369
|
var TYPE_NAME = new RegExp(`^\\s*(${IDENTIFIER})(?:\\s*\\.\\s*(${IDENTIFIER}))?\\s*(\\(\\s*${MODIFIER_VALUE}(?:\\s*,\\s*${MODIFIER_VALUE})*\\s*\\))?\\s*((?:\\[\\s*\\]\\s*)*)$`);
|
|
@@ -4132,13 +5390,13 @@ function parsePostgresTypeName(params) {
|
|
|
4132
5390
|
}
|
|
4133
5391
|
function renderPostgresTypeName(params) {
|
|
4134
5392
|
if (params.dialect.name !== "postgres")
|
|
4135
|
-
throw
|
|
5393
|
+
throw unsupportedCapability28({ dialect: params.dialect.name, feature: "postgresTypeIdentity" });
|
|
4136
5394
|
const { dialect, type } = params;
|
|
4137
5395
|
return `${type.namespace === undefined ? "" : `${dialect.quoteIdent({ name: type.namespace })}.`}${dialect.quoteIdent({ name: type.name })}${type.modifier}${type.array ? "[]" : ""}`;
|
|
4138
5396
|
}
|
|
4139
5397
|
function renderPostgresTypeIdentity(params) {
|
|
4140
5398
|
if (params.dialect.name !== "postgres")
|
|
4141
|
-
throw
|
|
5399
|
+
throw unsupportedCapability28({ dialect: params.dialect.name, feature: "postgresTypeIdentity" });
|
|
4142
5400
|
const collector = new ParamCollector(params.dialect);
|
|
4143
5401
|
const { source } = params;
|
|
4144
5402
|
let text;
|
|
@@ -4151,11 +5409,11 @@ function renderPostgresTypeIdentity(params) {
|
|
|
4151
5409
|
return { text, values: collector.values, returning: "none" };
|
|
4152
5410
|
}
|
|
4153
5411
|
// packages/sql/src/dialects/postgres-default-identity.ts
|
|
4154
|
-
import { unsupportedCapability as
|
|
5412
|
+
import { unsupportedCapability as unsupportedCapability29 } from "@vibeorm/schema";
|
|
4155
5413
|
function renderPostgresDefaultFunctionIdentity(params) {
|
|
4156
5414
|
const { dialect, table, column } = params;
|
|
4157
5415
|
if (dialect.name !== "postgres")
|
|
4158
|
-
throw
|
|
5416
|
+
throw unsupportedCapability29({ dialect: dialect.name, feature: "defaultFunctionIdentity" });
|
|
4159
5417
|
const collector = new ParamCollector(dialect);
|
|
4160
5418
|
const namespace = collector.add(table.namespace);
|
|
4161
5419
|
const name = collector.add(table.name);
|
|
@@ -4179,36 +5437,36 @@ WHERE n.nspname = ${namespace} AND c.relname = ${name} AND a.attname = ${field}
|
|
|
4179
5437
|
};
|
|
4180
5438
|
}
|
|
4181
5439
|
// packages/sql/src/dialects/constraint-owned-index.ts
|
|
4182
|
-
import { unsupportedCapability as
|
|
5440
|
+
import { unsupportedCapability as unsupportedCapability30 } from "@vibeorm/schema";
|
|
4183
5441
|
function renderConstraintOwnedIndexDrop(params) {
|
|
4184
5442
|
if (params.dialect.name !== "postgres")
|
|
4185
|
-
throw
|
|
5443
|
+
throw unsupportedCapability30({ dialect: params.dialect.name, feature: "constraintOwnedIndexDrop" });
|
|
4186
5444
|
const quote = (name) => params.dialect.quoteIdent({ name });
|
|
4187
5445
|
return { text: `ALTER TABLE ${quote(params.table)} DROP CONSTRAINT ${quote(params.constraint)};`, values: [], returning: "none" };
|
|
4188
5446
|
}
|
|
4189
5447
|
// packages/sql/src/dialects/mysql-check-probe.ts
|
|
4190
|
-
import { unsupportedCapability as
|
|
5448
|
+
import { unsupportedCapability as unsupportedCapability31 } from "@vibeorm/schema";
|
|
4191
5449
|
function renderMysqlCheckProbe(params) {
|
|
4192
5450
|
if (params.dialect.name !== "mysql")
|
|
4193
|
-
throw
|
|
5451
|
+
throw unsupportedCapability31({ dialect: params.dialect.name, feature: "temporaryCheckProbe" });
|
|
4194
5452
|
const q = (name) => params.dialect.quoteIdent({ name });
|
|
4195
5453
|
const probe = q(params.probe);
|
|
4196
5454
|
switch (params.operation) {
|
|
4197
5455
|
case "clone":
|
|
4198
5456
|
if (params.table === undefined)
|
|
4199
|
-
throw
|
|
5457
|
+
throw unsupportedCapability31({ dialect: "mysql", feature: "temporaryCheckProbe", hint: "A source table is required" });
|
|
4200
5458
|
return { text: `CREATE TEMPORARY TABLE ${probe} LIKE ${q(params.table.namespace)}.${q(params.table.name)}`, values: [], returning: "none" };
|
|
4201
5459
|
case "add": {
|
|
4202
5460
|
if (params.expression === undefined || params.definition === undefined)
|
|
4203
|
-
throw
|
|
4204
|
-
const parts = tableDefinitionParts({ sql: params.definition });
|
|
5461
|
+
throw unsupportedCapability31({ dialect: "mysql", feature: "temporaryCheckProbe", hint: "An authored expression and temporary clone definition are required" });
|
|
5462
|
+
const parts = tableDefinitionParts({ sql: params.definition, backslashEscapes: true });
|
|
4205
5463
|
return { text: `CREATE TEMPORARY TABLE ${probe} (
|
|
4206
5464
|
${[...parts.clauses, `CONSTRAINT ${probe} CHECK (${params.expression})`].join(`,
|
|
4207
5465
|
`)}
|
|
4208
5466
|
)${parts.suffix}`, values: [], returning: "none" };
|
|
4209
5467
|
}
|
|
4210
5468
|
case "read":
|
|
4211
|
-
return { text: `SHOW CREATE TABLE ${probe}`, values: [], returning: "none" };
|
|
5469
|
+
return { text: `SHOW CREATE TABLE ${params.table === undefined ? probe : `${q(params.table.namespace)}.${q(params.table.name)}`}`, values: [], returning: "none" };
|
|
4212
5470
|
case "drop":
|
|
4213
5471
|
return { text: `DROP TEMPORARY TABLE ${probe}`, values: [], returning: "none" };
|
|
4214
5472
|
}
|
|
@@ -4246,31 +5504,31 @@ function sqliteTableConstraints(params) {
|
|
|
4246
5504
|
return result;
|
|
4247
5505
|
}
|
|
4248
5506
|
// packages/sql/src/dialects/migration-visibility.ts
|
|
4249
|
-
import { unsupportedCapability as
|
|
5507
|
+
import { unsupportedCapability as unsupportedCapability32 } from "@vibeorm/schema";
|
|
4250
5508
|
function renderMigrationVisibility(params) {
|
|
4251
5509
|
if (params.dialect.name !== "mysql")
|
|
4252
|
-
throw
|
|
5510
|
+
throw unsupportedCapability32({ dialect: params.dialect.name, feature: "mysqlCatalogVisibility" });
|
|
4253
5511
|
const collector = new ParamCollector(params.dialect);
|
|
4254
5512
|
const grantee = "CONCAT(QUOTE(SUBSTRING_INDEX(CURRENT_USER(), '@', 1)), '@', QUOTE(SUBSTRING_INDEX(CURRENT_USER(), '@', -1)))";
|
|
4255
5513
|
return { text: `SELECT PRIVILEGE_TYPE AS privilege FROM information_schema.USER_PRIVILEGES WHERE GRANTEE = ${grantee} UNION SELECT PRIVILEGE_TYPE AS privilege FROM information_schema.SCHEMA_PRIVILEGES WHERE GRANTEE = ${grantee} AND ${collector.add(params.namespace)} LIKE TABLE_SCHEMA`, values: collector.values, returning: "none" };
|
|
4256
5514
|
}
|
|
4257
5515
|
// packages/sql/src/dialects/migration-inventory.ts
|
|
4258
|
-
import { unsupportedCapability as
|
|
5516
|
+
import { unsupportedCapability as unsupportedCapability33 } from "@vibeorm/schema";
|
|
4259
5517
|
function renderSqliteColumnInventory(params) {
|
|
4260
5518
|
if (params.dialect.name !== "sqlite")
|
|
4261
|
-
throw
|
|
5519
|
+
throw unsupportedCapability33({ dialect: params.dialect.name, feature: "sqliteColumnInventory" });
|
|
4262
5520
|
const collector = new ParamCollector(params.dialect);
|
|
4263
5521
|
return { text: `SELECT name, hidden, pk, dflt_value AS definition FROM pragma_table_xinfo(${collector.add(params.table)}, ${collector.add(params.namespace)}) ORDER BY cid`, values: collector.values, returning: "none" };
|
|
4264
5522
|
}
|
|
4265
5523
|
function renderSqliteForeignKeyInventory(params) {
|
|
4266
5524
|
if (params.dialect.name !== "sqlite")
|
|
4267
|
-
throw
|
|
5525
|
+
throw unsupportedCapability33({ dialect: params.dialect.name, feature: "sqliteForeignKeyInventory" });
|
|
4268
5526
|
const collector = new ParamCollector(params.dialect);
|
|
4269
5527
|
return { text: `SELECT id, seq, "from", "to", "table", on_update, on_delete, match FROM pragma_foreign_key_list(${collector.add(params.table)}, ${collector.add(params.namespace)}) ORDER BY id, seq`, values: collector.values, returning: "none" };
|
|
4270
5528
|
}
|
|
4271
5529
|
function renderSqliteRebuildObjects(params) {
|
|
4272
5530
|
if (params.dialect.name !== "sqlite")
|
|
4273
|
-
throw
|
|
5531
|
+
throw unsupportedCapability33({ dialect: params.dialect.name, feature: "sqliteRebuildObjects" });
|
|
4274
5532
|
return { text: "SELECT name, tbl_name, type, sql FROM main.sqlite_schema WHERE type IN ('index', 'trigger') AND sql IS NOT NULL ORDER BY type, name", values: [], returning: "none" };
|
|
4275
5533
|
}
|
|
4276
5534
|
function renderMigrationInventory(params) {
|
|
@@ -4309,7 +5567,7 @@ ORDER BY kind, parent, name`, values: collector.values, returning: "none" };
|
|
|
4309
5567
|
return { text: `SELECT type AS kind, name, tbl_name AS parent, sql AS definition FROM ${schema}.sqlite_schema WHERE name NOT LIKE 'sqlite\\_%' ESCAPE '\\' AND name <> '_vibe_migrations' ORDER BY type, tbl_name, name`, values: [], returning: "none" };
|
|
4310
5568
|
}
|
|
4311
5569
|
// packages/sql/src/dialects/migration-objects.ts
|
|
4312
|
-
import { internalError as
|
|
5570
|
+
import { internalError as internalError4, unsupportedCapability as unsupportedCapability34 } from "@vibeorm/schema";
|
|
4313
5571
|
function renderMigrationObjectQuery(params) {
|
|
4314
5572
|
return objectQuery({ ...params, collector: new ParamCollector(params.dialect) });
|
|
4315
5573
|
}
|
|
@@ -4317,7 +5575,7 @@ function objectQuery(params) {
|
|
|
4317
5575
|
const { dialect, identity: object, collector } = params;
|
|
4318
5576
|
if (object.kind === "namespace") {
|
|
4319
5577
|
if (dialect.name !== "postgres")
|
|
4320
|
-
throw
|
|
5578
|
+
throw unsupportedCapability34({ dialect: dialect.name, feature: "fixedNamespaces" });
|
|
4321
5579
|
return { text: `SELECT n.nspname::text AS "name" FROM pg_catalog.pg_namespace n WHERE n.nspname = ${collector.add(object.name)}`, values: collector.values, returning: "none" };
|
|
4322
5580
|
}
|
|
4323
5581
|
const physical = "table" in object ? object.table : object.name;
|
|
@@ -4381,7 +5639,7 @@ function objectQuery(params) {
|
|
|
4381
5639
|
break;
|
|
4382
5640
|
case "constraint":
|
|
4383
5641
|
if (object.constraintType === "exclusion")
|
|
4384
|
-
throw
|
|
5642
|
+
throw unsupportedCapability34({ dialect: "mysql", feature: "exclusionConstraints" });
|
|
4385
5643
|
text = `SELECT tc.CONSTRAINT_TYPE AS type, tc.ENFORCED AS enforced, cc.CHECK_CLAUSE AS definition, rc.UPDATE_RULE AS onUpdate, rc.DELETE_RULE AS onDelete, rc.MATCH_OPTION AS matchOption FROM information_schema.TABLE_CONSTRAINTS tc LEFT JOIN information_schema.CHECK_CONSTRAINTS cc ON cc.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA AND cc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME LEFT JOIN information_schema.REFERENTIAL_CONSTRAINTS rc ON rc.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA AND rc.TABLE_NAME = tc.TABLE_NAME AND rc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME WHERE tc.TABLE_SCHEMA = ${namespace} AND tc.TABLE_NAME = ${name} AND tc.CONSTRAINT_NAME = ${collector.add(object.name)}`;
|
|
4386
5644
|
break;
|
|
4387
5645
|
case "index":
|
|
@@ -4391,22 +5649,22 @@ function objectQuery(params) {
|
|
|
4391
5649
|
text = `SELECT TABLE_TYPE AS type, ENGINE AS engine, TABLE_COLLATION AS collation FROM information_schema.TABLES WHERE TABLE_SCHEMA = ${namespace} AND TABLE_NAME = ${name}`;
|
|
4392
5650
|
break;
|
|
4393
5651
|
default:
|
|
4394
|
-
throw
|
|
5652
|
+
throw unsupportedCapability34({ dialect: "mysql", feature: `inspect:${object.kind}` });
|
|
4395
5653
|
}
|
|
4396
5654
|
} else {
|
|
4397
5655
|
if (physical.namespace !== "main" && physical.namespace !== "temp")
|
|
4398
|
-
throw
|
|
5656
|
+
throw unsupportedCapability34({ dialect: "sqlite", feature: "attachedSchemaInspection" });
|
|
4399
5657
|
const catalog = `${dialect.quoteIdent({ name: physical.namespace })}.sqlite_schema`;
|
|
4400
5658
|
const kind = object.kind === "table" || object.kind === "view" ? object.kind : object.kind === "trigger" ? "trigger" : object.kind === "index" ? "index" : "table";
|
|
4401
5659
|
if (["function", "sequence", "postgresExtension", "enum"].includes(object.kind) || object.kind === "constraint" && object.constraintType === "exclusion")
|
|
4402
|
-
throw
|
|
5660
|
+
throw unsupportedCapability34({ dialect: "sqlite", feature: `inspect:${object.kind}` });
|
|
4403
5661
|
const sqliteParams = new ParamCollector(dialect);
|
|
4404
5662
|
if (object.kind === "column" || object.kind === "columnDefault" || object.kind === "generatedColumn") {
|
|
4405
5663
|
text = `SELECT x.type AS type, x."notnull" AS "notNull", x.pk AS primaryKeyPosition, x.hidden AS generated, x.dflt_value AS definition, (SELECT sql FROM ${catalog} WHERE name = ${sqliteParams.add(physical.name)} AND type = 'table') AS tableDefinition FROM pragma_table_xinfo(${sqliteParams.add(physical.name)}, ${sqliteParams.add(physical.namespace)}) x WHERE x.name = ${sqliteParams.add(object.column)}`;
|
|
4406
5664
|
return { text, values: sqliteParams.values, returning: "none" };
|
|
4407
5665
|
}
|
|
4408
5666
|
const objectName = object.kind === "trigger" || object.kind === "index" ? object.name : physical.name;
|
|
4409
|
-
text = `SELECT sql AS definition, type AS type FROM ${catalog} WHERE name = ${sqliteParams.add(objectName)} AND type = ${sqliteParams.add(kind)}`;
|
|
5667
|
+
text = `SELECT sql AS definition, type AS type FROM ${catalog} WHERE name = ${sqliteParams.add(objectName)} AND type = ${sqliteParams.add(kind)}${object.kind === "trigger" ? ` AND tbl_name = ${sqliteParams.add(physical.name)}` : ""}`;
|
|
4410
5668
|
return { text, values: sqliteParams.values, returning: "none" };
|
|
4411
5669
|
}
|
|
4412
5670
|
return { text, values: collector.values, returning: "none" };
|
|
@@ -4429,7 +5687,7 @@ function renderMigrationDependencies(params) {
|
|
|
4429
5687
|
}
|
|
4430
5688
|
function dependencyQuery(params) {
|
|
4431
5689
|
if (params.dialect.name !== "postgres")
|
|
4432
|
-
throw
|
|
5690
|
+
throw unsupportedCapability34({ dialect: params.dialect.name, feature: "catalogDependencies" });
|
|
4433
5691
|
const object = params.identity;
|
|
4434
5692
|
const collector = params.collector;
|
|
4435
5693
|
if (object.kind === "namespace") {
|
|
@@ -4486,7 +5744,7 @@ function inspectionReferences(params) {
|
|
|
4486
5744
|
function renderMigrationObjectBatch(params) {
|
|
4487
5745
|
const { dialect } = params;
|
|
4488
5746
|
if (dialect.name !== "postgres")
|
|
4489
|
-
throw
|
|
5747
|
+
throw unsupportedCapability34({ dialect: dialect.name, feature: "batchedMigrationInspection" });
|
|
4490
5748
|
const groups = new Map;
|
|
4491
5749
|
for (const [position, identity] of params.identities.entries()) {
|
|
4492
5750
|
const kind = identity.kind === "constraint" ? `${identity.kind}:${identity.constraintType}` : identity.kind === "generatedColumn" ? "columnDefault" : identity.kind;
|
|
@@ -4499,7 +5757,7 @@ function renderMigrationObjectBatch(params) {
|
|
|
4499
5757
|
const facts = objectQuery({ dialect, identity, collector: inspectionReferences({ dialect }) });
|
|
4500
5758
|
const dependencies = dependencyQuery({ dialect, identity, collector: inspectionReferences({ dialect }) });
|
|
4501
5759
|
if (JSON.stringify(dependencies.values) !== JSON.stringify(facts.values))
|
|
4502
|
-
throw
|
|
5760
|
+
throw internalError4({ message: "Batched catalog queries bind their identity in different orders", meta: { kind: identity.kind } });
|
|
4503
5761
|
const columns = facts.values.map((value, index) => `${dialect.quoteIdent({ name: `p${index + 1}` })} ${Array.isArray(value) ? "text[]" : "text"}`);
|
|
4504
5762
|
const requests = group.map(({ identity, position }) => ({ position, ...Object.fromEntries(renderMigrationObjectQuery({ dialect, identity }).values.map((value, index) => [`p${index + 1}`, value])) }));
|
|
4505
5763
|
const collector = new ParamCollector(dialect);
|
|
@@ -4519,16 +5777,16 @@ function renderMigrationNamespace(params) {
|
|
|
4519
5777
|
}
|
|
4520
5778
|
function renderCheckProbe(params) {
|
|
4521
5779
|
if (params.dialect.name !== "postgres")
|
|
4522
|
-
throw
|
|
5780
|
+
throw unsupportedCapability34({ dialect: params.dialect.name, feature: "rollbackCheckProbe" });
|
|
4523
5781
|
const q = (name) => params.dialect.quoteIdent({ name });
|
|
4524
5782
|
return { text: `ALTER TABLE ${q(params.table.namespace)}.${q(params.table.name)} ADD CONSTRAINT ${q(params.name)} CHECK (${params.expression}) NOT VALID`, values: [], returning: "none" };
|
|
4525
5783
|
}
|
|
4526
5784
|
// packages/sql/src/dialects/postgres-migration.ts
|
|
4527
|
-
import { VibeError as
|
|
5785
|
+
import { VibeError as VibeError23, unsupportedCapability as unsupportedCapability35 } from "@vibeorm/schema";
|
|
4528
5786
|
var SETTING_VALUE = /^[0-9]+(?:\.[0-9]+)?\s*(?:us|ms|s|min|h|d)?$/;
|
|
4529
5787
|
function requirePostgres2(params) {
|
|
4530
5788
|
if (params.dialect.name !== "postgres") {
|
|
4531
|
-
throw
|
|
5789
|
+
throw unsupportedCapability35({ dialect: params.dialect.name, feature: params.feature });
|
|
4532
5790
|
}
|
|
4533
5791
|
}
|
|
4534
5792
|
function renderMigrationSessionRead(params) {
|
|
@@ -4543,7 +5801,7 @@ function renderMigrationSessionSet(params) {
|
|
|
4543
5801
|
requirePostgres2({ dialect: params.dialect, feature: "migrationSessionSettings" });
|
|
4544
5802
|
const { milliseconds, setting } = params;
|
|
4545
5803
|
if (!Number.isSafeInteger(milliseconds) || milliseconds < 0) {
|
|
4546
|
-
throw new
|
|
5804
|
+
throw new VibeError23({
|
|
4547
5805
|
code: "VIBE_MIGRATION",
|
|
4548
5806
|
message: `${setting} must be a non-negative integer number of milliseconds (got ${String(milliseconds)})`,
|
|
4549
5807
|
meta: { reason: "execution-metadata-invalid", setting, value: milliseconds }
|
|
@@ -4661,6 +5919,7 @@ export {
|
|
|
4661
5919
|
SQLITE_SESSION_SQL,
|
|
4662
5920
|
SQL_DEFAULT_CELL,
|
|
4663
5921
|
SQL_DIALECTS,
|
|
5922
|
+
assertIndexExpressionVersion,
|
|
4664
5923
|
assertSqlParamBuilding,
|
|
4665
5924
|
chainVetoToken,
|
|
4666
5925
|
cloneTimestampDate,
|
|
@@ -4688,6 +5947,9 @@ export {
|
|
|
4688
5947
|
escapeGlobPattern,
|
|
4689
5948
|
escapeLikePattern,
|
|
4690
5949
|
excludedRef,
|
|
5950
|
+
extractIndexKeys,
|
|
5951
|
+
extractMysqlGeneratedColumn,
|
|
5952
|
+
extractSqliteGeneratedColumn,
|
|
4691
5953
|
float32Codec,
|
|
4692
5954
|
getSqlDialect,
|
|
4693
5955
|
isDbNowMarker,
|
|
@@ -4709,6 +5971,8 @@ export {
|
|
|
4709
5971
|
isStepColumnRef,
|
|
4710
5972
|
jsonListParameter,
|
|
4711
5973
|
jsonParamCell,
|
|
5974
|
+
keyListBindsUntypedRows,
|
|
5975
|
+
localDateCodec,
|
|
4712
5976
|
mapConflictColumns,
|
|
4713
5977
|
mysqlDialect,
|
|
4714
5978
|
nativeEnumArrayParameter,
|
|
@@ -4718,7 +5982,9 @@ export {
|
|
|
4718
5982
|
parsePgArrayLiteralNested,
|
|
4719
5983
|
parsePostgresTypeName,
|
|
4720
5984
|
postgresArrayElementFromText,
|
|
5985
|
+
postgresBulkInsertArrayCastOf,
|
|
4721
5986
|
postgresDialect,
|
|
5987
|
+
postgresKeyListElementTypeOf,
|
|
4722
5988
|
projectionOutputName,
|
|
4723
5989
|
recordSqlBinding,
|
|
4724
5990
|
recordSqlBindingCheck,
|
|
@@ -4737,8 +6003,12 @@ export {
|
|
|
4737
6003
|
renderExternalDependents,
|
|
4738
6004
|
renderExternalOwnershipCatalog,
|
|
4739
6005
|
renderFragment,
|
|
6006
|
+
renderGeneratedColumnClause,
|
|
4740
6007
|
renderIndexColumns,
|
|
6008
|
+
renderIndexExpressionProbe,
|
|
6009
|
+
renderIndexKeys,
|
|
4741
6010
|
renderIndexStateQuery,
|
|
6011
|
+
renderLocalDateCheck,
|
|
4742
6012
|
renderMigrationDependencies,
|
|
4743
6013
|
renderMigrationHistoryQuery,
|
|
4744
6014
|
renderMigrationInventory,
|
|
@@ -4753,10 +6023,15 @@ export {
|
|
|
4753
6023
|
renderModuleDependencyQuery,
|
|
4754
6024
|
renderModuleJournal,
|
|
4755
6025
|
renderMysqlCheckProbe,
|
|
6026
|
+
renderMysqlGeneratedColumnProbe,
|
|
4756
6027
|
renderMysqlObjectMembers,
|
|
4757
6028
|
renderPostgresDefaultFunctionIdentity,
|
|
6029
|
+
renderPostgresDropGeneratedExpression,
|
|
4758
6030
|
renderPostgresExplain,
|
|
6031
|
+
renderPostgresGeneratedColumnProbe,
|
|
4759
6032
|
renderPostgresIndexKeys,
|
|
6033
|
+
renderPostgresJitRestore,
|
|
6034
|
+
renderPostgresJitSuspend,
|
|
4760
6035
|
renderPostgresPhysicalCatalog,
|
|
4761
6036
|
renderPostgresTypeIdentity,
|
|
4762
6037
|
renderPostgresTypeName,
|
|
@@ -4779,6 +6054,8 @@ export {
|
|
|
4779
6054
|
renderRlsReadinessQuery,
|
|
4780
6055
|
renderRlsRole,
|
|
4781
6056
|
renderRowLockClause,
|
|
6057
|
+
renderRowRule,
|
|
6058
|
+
renderSchemaExpression,
|
|
4782
6059
|
renderShadowOperation,
|
|
4783
6060
|
renderSqlFragment,
|
|
4784
6061
|
renderSqliteColumnInventory,
|
|
@@ -4786,6 +6063,7 @@ export {
|
|
|
4786
6063
|
renderSqliteFtsUpdateTrigger,
|
|
4787
6064
|
renderSqliteRebuildObjects,
|
|
4788
6065
|
renderStatement,
|
|
6066
|
+
resolveIndexKeys,
|
|
4789
6067
|
resultScalarContract,
|
|
4790
6068
|
returningColumnName,
|
|
4791
6069
|
sameResultDecoder,
|
|
@@ -4802,6 +6080,7 @@ export {
|
|
|
4802
6080
|
sqliteTableConstraints,
|
|
4803
6081
|
startsWithPattern,
|
|
4804
6082
|
stepColumnRef,
|
|
6083
|
+
strictMatchCollationPrefilter,
|
|
4805
6084
|
strictMatchPattern,
|
|
4806
6085
|
tableDefinitionParts,
|
|
4807
6086
|
timeOfDayCodec,
|
|
@@ -4810,4 +6089,4 @@ export {
|
|
|
4810
6089
|
toPgArrayLiteral
|
|
4811
6090
|
};
|
|
4812
6091
|
|
|
4813
|
-
//# debugId=
|
|
6092
|
+
//# debugId=75BF4FE272330AE564756E2164756E21
|