@vibeorm/sql 4.2.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 +103 -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 +18 -5
- package/dist/index.js +2055 -787
- 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,159 +1219,1078 @@ 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
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
+
});
|
|
1343
|
+
}
|
|
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 }
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
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
|
+
});
|
|
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: "*"
|
|
1400
|
+
};
|
|
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" });
|
|
1416
|
+
}
|
|
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 };
|
|
1419
|
+
}
|
|
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 };
|
|
1428
|
+
}
|
|
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 };
|
|
1440
|
+
}
|
|
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
|
+
});
|
|
1480
|
+
}
|
|
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);
|
|
1516
|
+
}
|
|
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
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
return value;
|
|
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`);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
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
|
+
});
|
|
1573
|
+
}
|
|
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;
|
|
1584
|
+
}
|
|
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 });
|
|
1596
|
+
}
|
|
1597
|
+
function describe(params) {
|
|
1598
|
+
const { value } = params;
|
|
1599
|
+
return typeof value === "string" ? `the string ${JSON.stringify(value)}` : `the ${typeof value} ${String(value)}`;
|
|
1600
|
+
}
|
|
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++;
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
if (char === "-" && sql[index + 1] === "-" || char === "#") {
|
|
1617
|
+
const end = sql.indexOf(`
|
|
1618
|
+
`, index + 1);
|
|
1619
|
+
index = end < 0 ? sql.length : end + 1;
|
|
1620
|
+
continue;
|
|
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;
|
|
1627
|
+
continue;
|
|
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 });
|
|
1655
|
+
continue;
|
|
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++;
|
|
1672
|
+
}
|
|
1673
|
+
if (depth !== 0)
|
|
1674
|
+
throw unavailable();
|
|
1675
|
+
return tokens;
|
|
1676
|
+
}
|
|
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) };
|
|
1702
|
+
}
|
|
1703
|
+
function unavailable() {
|
|
1704
|
+
return new VibeError8({ code: "VIBE_MIGRATION", message: "Cannot preserve table definition", meta: { reason: "table-definition-unavailable" } });
|
|
1705
|
+
}
|
|
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 });
|
|
1717
|
+
}
|
|
1718
|
+
return columns;
|
|
1719
|
+
}
|
|
1720
|
+
function sqliteColumnClauses({ sql }) {
|
|
1721
|
+
return tableColumnClauses({ sql });
|
|
1722
|
+
}
|
|
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 });
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
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;
|
|
1777
|
+
};
|
|
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;
|
|
1822
|
+
}
|
|
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;
|
|
1828
|
+
}
|
|
1829
|
+
function extractSqliteIndexKeys({ definition, index }) {
|
|
1830
|
+
return extractStandaloneIndexKeys({ definition, index });
|
|
1831
|
+
}
|
|
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" }];
|
|
1857
|
+
}
|
|
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;
|
|
1907
|
+
}
|
|
1908
|
+
function extractSqliteGeneratedColumn({ definition, column }) {
|
|
1909
|
+
return extractGenerationFromDefinition({ definition, column });
|
|
1910
|
+
}
|
|
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" };
|
|
1965
|
+
}
|
|
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 } });
|
|
1968
|
+
}
|
|
1969
|
+
function extractMysqlGeneratedColumn({ definition, column, backslashEscapes }) {
|
|
1970
|
+
return extractGenerationFromDefinition({ definition, column, backslashEscapes, mysql: true });
|
|
1971
|
+
}
|
|
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" }];
|
|
2039
|
+
}
|
|
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
|
+
});
|
|
2103
|
+
}
|
|
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)}` });
|
|
2137
|
+
}
|
|
2138
|
+
function invalid3({ site, reason }) {
|
|
2139
|
+
return new VibeError11({ code: "VIBE_SCHEMA_INVALID", message: `Invalid index keys at ${site}: ${reason}`, meta: { rule: reason, site } });
|
|
2140
|
+
}
|
|
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
|
+
};
|
|
2166
|
+
}
|
|
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" };
|
|
2185
|
+
}
|
|
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
|
+
}
|
|
2274
|
+
}
|
|
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;
|
|
1305
2294
|
case "insert":
|
|
1306
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(", ")})`;
|
|
1307
2296
|
break;
|
|
@@ -1320,10 +2309,10 @@ import {
|
|
|
1320
2309
|
} from "@vibeorm/schema";
|
|
1321
2310
|
|
|
1322
2311
|
// packages/sql/src/identifiers.ts
|
|
1323
|
-
import { VibeError as
|
|
2312
|
+
import { VibeError as VibeError12 } from "@vibeorm/schema";
|
|
1324
2313
|
function assertNoNul(params) {
|
|
1325
2314
|
if (params.name.includes("\x00")) {
|
|
1326
|
-
throw new
|
|
2315
|
+
throw new VibeError12({
|
|
1327
2316
|
code: "VIBE_VALIDATION",
|
|
1328
2317
|
message: "an SQL identifier cannot contain a NUL character \u2014 the server would stop reading it there and address a different object",
|
|
1329
2318
|
meta: { name: params.name.replaceAll("\x00", "\\0") }
|
|
@@ -1338,6 +2327,13 @@ var sqliteDialect = {
|
|
|
1338
2327
|
capabilities: SQLITE_CAPABILITIES,
|
|
1339
2328
|
inArrayStrategy: "expand",
|
|
1340
2329
|
tupleInStrategy: "rowValue",
|
|
2330
|
+
keyListStrategy: "values",
|
|
2331
|
+
keysetSeekStrategy: "rowValue",
|
|
2332
|
+
keyPageStrategy: "inLimit",
|
|
2333
|
+
relationFilterStrategy: "inSelect",
|
|
2334
|
+
deferredJoinPage: null,
|
|
2335
|
+
nativeNullsOrder: "low",
|
|
2336
|
+
nullsUnionPage: false,
|
|
1341
2337
|
timeOfDayInStrategy: "in",
|
|
1342
2338
|
upsertForm: "onConflict",
|
|
1343
2339
|
conflictKeyEquality: {
|
|
@@ -1369,6 +2365,7 @@ var sqliteDialect = {
|
|
|
1369
2365
|
rowLockSyntax: null,
|
|
1370
2366
|
advisoryLockFunctions: null,
|
|
1371
2367
|
rowDefaultKeyword: false,
|
|
2368
|
+
bulkInsertForm: "values",
|
|
1372
2369
|
emptyInsert: { form: "defaultValues", conflictClause: false },
|
|
1373
2370
|
statementChain: null,
|
|
1374
2371
|
paramBudget: 900,
|
|
@@ -1392,84 +2389,6 @@ var sqliteDialect = {
|
|
|
1392
2389
|
}
|
|
1393
2390
|
};
|
|
1394
2391
|
|
|
1395
|
-
// packages/sql/src/dialects/sqlite-table-definition.ts
|
|
1396
|
-
import { VibeError as VibeError8 } from "@vibeorm/schema";
|
|
1397
|
-
function tableDefinitionParts(params) {
|
|
1398
|
-
const sql = params.sql;
|
|
1399
|
-
if (!/^\s*CREATE\s+(?:TEMP(?:ORARY)?\s+)?TABLE\b/i.test(sql))
|
|
1400
|
-
throw unavailable();
|
|
1401
|
-
const parts = [];
|
|
1402
|
-
let start = -1;
|
|
1403
|
-
let depth = 0;
|
|
1404
|
-
let quote;
|
|
1405
|
-
for (let index = 0;index < sql.length; index++) {
|
|
1406
|
-
const char = sql[index];
|
|
1407
|
-
if (quote !== undefined) {
|
|
1408
|
-
if (char === quote) {
|
|
1409
|
-
if (quote !== "]" && sql[index + 1] === quote)
|
|
1410
|
-
index++;
|
|
1411
|
-
else
|
|
1412
|
-
quote = undefined;
|
|
1413
|
-
}
|
|
1414
|
-
continue;
|
|
1415
|
-
}
|
|
1416
|
-
if (char === "'" || char === '"' || char === "`") {
|
|
1417
|
-
quote = char;
|
|
1418
|
-
continue;
|
|
1419
|
-
}
|
|
1420
|
-
if (char === "[") {
|
|
1421
|
-
quote = "]";
|
|
1422
|
-
continue;
|
|
1423
|
-
}
|
|
1424
|
-
if (char === "-" && sql[index + 1] === "-") {
|
|
1425
|
-
const end = sql.indexOf(`
|
|
1426
|
-
`, index + 2);
|
|
1427
|
-
if (end < 0)
|
|
1428
|
-
break;
|
|
1429
|
-
index = end;
|
|
1430
|
-
continue;
|
|
1431
|
-
}
|
|
1432
|
-
if (char === "/" && sql[index + 1] === "*") {
|
|
1433
|
-
const end = sql.indexOf("*/", index + 2);
|
|
1434
|
-
if (end < 0)
|
|
1435
|
-
break;
|
|
1436
|
-
index = end + 1;
|
|
1437
|
-
continue;
|
|
1438
|
-
}
|
|
1439
|
-
if (char === "(") {
|
|
1440
|
-
if (depth === 0)
|
|
1441
|
-
start = index + 1;
|
|
1442
|
-
depth++;
|
|
1443
|
-
} else if (char === ")") {
|
|
1444
|
-
depth--;
|
|
1445
|
-
if (depth === 0 && start >= 0) {
|
|
1446
|
-
parts.push(sql.slice(start, index).trim());
|
|
1447
|
-
return { clauses: parts, suffix: sql.slice(index + 1) };
|
|
1448
|
-
}
|
|
1449
|
-
} else if (char === "," && depth === 1) {
|
|
1450
|
-
parts.push(sql.slice(start, index).trim());
|
|
1451
|
-
start = index + 1;
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
throw unavailable();
|
|
1455
|
-
}
|
|
1456
|
-
function unavailable() {
|
|
1457
|
-
return new VibeError8({ code: "VIBE_MIGRATION", message: "Cannot preserve table definition", meta: { reason: "table-definition-unavailable" } });
|
|
1458
|
-
}
|
|
1459
|
-
function sqliteColumnClauses(params) {
|
|
1460
|
-
const columns = [];
|
|
1461
|
-
for (const part of tableDefinitionParts(params).clauses) {
|
|
1462
|
-
const token = /^(?:"((?:[^"]|"")+)"|`((?:[^`]|``)+)`|\[([^\]]+)\]|([A-Za-z_][A-Za-z_0-9]*))/.exec(part);
|
|
1463
|
-
if (token === null)
|
|
1464
|
-
throw unavailable();
|
|
1465
|
-
if (token[4] !== undefined && /^(?:CONSTRAINT|PRIMARY|UNIQUE|CHECK|FOREIGN)$/i.test(token[4]))
|
|
1466
|
-
continue;
|
|
1467
|
-
const name = token[1]?.replaceAll('""', '"') ?? token[2]?.replaceAll("``", "`") ?? token[3] ?? token[4];
|
|
1468
|
-
columns.push({ name, sql: part });
|
|
1469
|
-
}
|
|
1470
|
-
return columns;
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
2392
|
// packages/sql/src/dialects/sqlite-fts.ts
|
|
1474
2393
|
function updateStatements(params) {
|
|
1475
2394
|
const quote = (name) => sqliteDialect.quoteIdent({ name });
|
|
@@ -1531,11 +2450,11 @@ var SQLITE_SESSION_SQL = Object.freeze({
|
|
|
1531
2450
|
foreignKeysOn: "PRAGMA foreign_keys = ON"
|
|
1532
2451
|
});
|
|
1533
2452
|
// packages/sql/src/composable.ts
|
|
1534
|
-
import { VibeError as
|
|
2453
|
+
import { VibeError as VibeError13 } from "@vibeorm/schema";
|
|
1535
2454
|
var fragments = new WeakMap;
|
|
1536
2455
|
registerSqlHandleCheck({ check: (value) => fragments.has(value) });
|
|
1537
|
-
function
|
|
1538
|
-
throw new
|
|
2456
|
+
function invalid4(message) {
|
|
2457
|
+
throw new VibeError13({ code: "VIBE_VALIDATION", message });
|
|
1539
2458
|
}
|
|
1540
2459
|
function construct(parts) {
|
|
1541
2460
|
const snapshot = Object.freeze([...parts]);
|
|
@@ -1549,19 +2468,19 @@ function isSqlFragment(value) {
|
|
|
1549
2468
|
return typeof value === "object" && value !== null && fragments.has(value);
|
|
1550
2469
|
}
|
|
1551
2470
|
function partsOf(value) {
|
|
1552
|
-
return fragments.get(value) ??
|
|
2471
|
+
return fragments.get(value) ?? invalid4("Expected a constructed SQL fragment");
|
|
1553
2472
|
}
|
|
1554
2473
|
function containsGreatest(fragment) {
|
|
1555
2474
|
return partsOf(fragment).some((part) => typeof part !== "string" && ("greatest" in part));
|
|
1556
2475
|
}
|
|
1557
2476
|
function identifier(parts) {
|
|
1558
2477
|
if (!Array.isArray(parts) || parts.length === 0 || parts.some((part) => typeof part !== "string" || part.length === 0 || part.includes("\x00"))) {
|
|
1559
|
-
|
|
2478
|
+
invalid4("SQL identifiers require non-empty, NUL-free name components");
|
|
1560
2479
|
}
|
|
1561
2480
|
}
|
|
1562
2481
|
function template(strings, ...values) {
|
|
1563
2482
|
if (!Array.isArray(strings) || !Array.isArray(strings.raw) || strings.length !== values.length + 1 || strings.some((text) => typeof text !== "string")) {
|
|
1564
|
-
|
|
2483
|
+
invalid4("Use sql as a template tag");
|
|
1565
2484
|
}
|
|
1566
2485
|
const parts = [];
|
|
1567
2486
|
for (let index = 0;index < strings.length; index++) {
|
|
@@ -1579,7 +2498,7 @@ function template(strings, ...values) {
|
|
|
1579
2498
|
var sql = Object.freeze(Object.assign(template, {
|
|
1580
2499
|
text: ({ text }) => {
|
|
1581
2500
|
if (typeof text !== "string" || text.includes("\x00"))
|
|
1582
|
-
|
|
2501
|
+
invalid4("SQL text must be a NUL-free string");
|
|
1583
2502
|
return construct([text]);
|
|
1584
2503
|
},
|
|
1585
2504
|
ident: ({ parts }) => {
|
|
@@ -1589,7 +2508,7 @@ var sql = Object.freeze(Object.assign(template, {
|
|
|
1589
2508
|
value: ({ value, decoder }) => construct([Object.freeze({ value: snapshotSqlValue(value), ...decoder === undefined ? {} : { decoder } })]),
|
|
1590
2509
|
join: ({ items, separator }) => {
|
|
1591
2510
|
if (!Array.isArray(items))
|
|
1592
|
-
|
|
2511
|
+
invalid4("sql.join requires an array");
|
|
1593
2512
|
const between = separator === undefined ? [", "] : partsOf(separator);
|
|
1594
2513
|
const parts = [];
|
|
1595
2514
|
items.forEach((value, index) => {
|
|
@@ -1609,7 +2528,7 @@ var sql = Object.freeze(Object.assign(template, {
|
|
|
1609
2528
|
},
|
|
1610
2529
|
greatest: ({ values }) => {
|
|
1611
2530
|
if (!Array.isArray(values) || values.length !== 2 || values.some((value) => !isSqlFragment(value)))
|
|
1612
|
-
|
|
2531
|
+
invalid4("sql.greatest requires two SQL fragments");
|
|
1613
2532
|
return construct([Object.freeze({ greatest: Object.freeze([...values]) })]);
|
|
1614
2533
|
}
|
|
1615
2534
|
}));
|
|
@@ -1618,7 +2537,7 @@ function renderSqlFragment(params) {
|
|
|
1618
2537
|
const render = (fragment) => renderSqlFragment({ ...params, fragment });
|
|
1619
2538
|
const encode = (part) => {
|
|
1620
2539
|
if (part.value === undefined)
|
|
1621
|
-
|
|
2540
|
+
invalid4("SQL parameters cannot be undefined; use null explicitly");
|
|
1622
2541
|
return snapshotSqlValue(part.decoder === undefined ? part.value : part.decoder.encode({ value: snapshotSqlValue(part.value), dialect: dialect.name }));
|
|
1623
2542
|
};
|
|
1624
2543
|
const recording = isSqlBindingCollector({ collector });
|
|
@@ -1633,12 +2552,12 @@ function renderSqlFragment(params) {
|
|
|
1633
2552
|
const only = parts.length === 1 ? parts[0] : undefined;
|
|
1634
2553
|
if (recording && only !== undefined && typeof only !== "string" && "value" in only) {
|
|
1635
2554
|
if (only.decoder !== undefined && resultScalarContract({ decoder: only.decoder }) === undefined)
|
|
1636
|
-
throw new
|
|
2555
|
+
throw new VibeError13({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Reusable SQL cannot compile an opaque encoder whose output controls a greatest NULL frame" });
|
|
1637
2556
|
if (only.value === null) {
|
|
1638
2557
|
if (only.decoder !== undefined)
|
|
1639
2558
|
recordSqlBindingCheck({ collector, check: () => {
|
|
1640
2559
|
if (encode({ ...only, value: snapshotSqlValue(only.value) }) !== null)
|
|
1641
|
-
throw new
|
|
2560
|
+
throw new VibeError13({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Reusable SQL NULL frame encoding changed its declared structure" });
|
|
1642
2561
|
} });
|
|
1643
2562
|
return "NULL";
|
|
1644
2563
|
}
|
|
@@ -1660,10 +2579,10 @@ function renderSqlFragment(params) {
|
|
|
1660
2579
|
return part.ident.map((name) => dialect.quoteIdent({ name })).join(".");
|
|
1661
2580
|
if ("row" in part) {
|
|
1662
2581
|
if (conflict === undefined)
|
|
1663
|
-
|
|
2582
|
+
invalid4("Existing/incoming row references require a conflict update");
|
|
1664
2583
|
const name = conflict.columns === undefined ? part.column : conflict.columns[part.column];
|
|
1665
2584
|
if (name === undefined)
|
|
1666
|
-
|
|
2585
|
+
invalid4(`Unknown conflict-expression field "${part.column}"`);
|
|
1667
2586
|
const ref = dialect.excludedRowRef;
|
|
1668
2587
|
const qualifier = part.row === "existing" ? dialect.quoteIdent({ name: conflict.table }) : ref.form === "keyword" ? ref.name : dialect.quoteIdent({ name: ref.alias });
|
|
1669
2588
|
return `${qualifier}.${dialect.quoteIdent({ name })}`;
|
|
@@ -1685,7 +2604,7 @@ function mapConflictColumns(params) {
|
|
|
1685
2604
|
if ("row" in part) {
|
|
1686
2605
|
const column = Object.hasOwn(params.columns, part.column) ? params.columns[part.column] : undefined;
|
|
1687
2606
|
if (column === undefined)
|
|
1688
|
-
|
|
2607
|
+
invalid4(`Unknown conflict-expression field "${part.column}"`);
|
|
1689
2608
|
return Object.freeze({ ...part, column });
|
|
1690
2609
|
}
|
|
1691
2610
|
if ("greatest" in part)
|
|
@@ -1702,25 +2621,25 @@ function sqlUsesIncoming(params) {
|
|
|
1702
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 }))));
|
|
1703
2622
|
}
|
|
1704
2623
|
// packages/sql/src/query-plan.ts
|
|
1705
|
-
import { VibeError as
|
|
2624
|
+
import { VibeError as VibeError14 } from "@vibeorm/schema";
|
|
1706
2625
|
var queries = new WeakMap;
|
|
1707
2626
|
var commands = new WeakMap;
|
|
1708
2627
|
var reusableQueries = new WeakMap;
|
|
1709
2628
|
var reusableCommands = new WeakMap;
|
|
1710
2629
|
registerSqlHandleCheck({ check: (value) => queries.has(value) || commands.has(value) || reusableQueries.has(value) || reusableCommands.has(value) });
|
|
1711
|
-
function
|
|
1712
|
-
throw new
|
|
2630
|
+
function invalid5(message) {
|
|
2631
|
+
throw new VibeError14({ code: "VIBE_VALIDATION", message });
|
|
1713
2632
|
}
|
|
1714
2633
|
function createSqlQuery(params) {
|
|
1715
2634
|
if (typeof params.render !== "function" || typeof params.decoder?.decode !== "function")
|
|
1716
|
-
|
|
2635
|
+
invalid5("SQL queries require a renderer and result decoder");
|
|
1717
2636
|
const query = Object.freeze(Object.create(null));
|
|
1718
2637
|
queries.set(query, Object.freeze({ render: params.render, decoder: params.decoder }));
|
|
1719
2638
|
return query;
|
|
1720
2639
|
}
|
|
1721
2640
|
function createSqlCommand(params) {
|
|
1722
2641
|
if (typeof params.render !== "function")
|
|
1723
|
-
|
|
2642
|
+
invalid5("SQL commands require a renderer");
|
|
1724
2643
|
const command = Object.freeze(Object.create(null));
|
|
1725
2644
|
commands.set(command, params.render);
|
|
1726
2645
|
return command;
|
|
@@ -1738,14 +2657,14 @@ function isReusableSqlCommand(value) {
|
|
|
1738
2657
|
return typeof value === "object" && value !== null && reusableCommands.has(value);
|
|
1739
2658
|
}
|
|
1740
2659
|
function createReusableSqlQuery(params) {
|
|
1741
|
-
const state = queries.get(params.query) ??
|
|
2660
|
+
const state = queries.get(params.query) ?? invalid5("Expected an authentic SQL row query");
|
|
1742
2661
|
sqlInputScopeKeys({ scope: params.scope });
|
|
1743
2662
|
const query = Object.freeze(Object.create(null));
|
|
1744
2663
|
reusableQueries.set(query, { ...state, scope: params.scope, cache: new WeakMap });
|
|
1745
2664
|
return query;
|
|
1746
2665
|
}
|
|
1747
2666
|
function createReusableSqlCommand(params) {
|
|
1748
|
-
const render = commands.get(params.query) ??
|
|
2667
|
+
const render = commands.get(params.query) ?? invalid5("Expected an authentic SQL command");
|
|
1749
2668
|
sqlInputScopeKeys({ scope: params.scope });
|
|
1750
2669
|
const query = Object.freeze(Object.create(null));
|
|
1751
2670
|
reusableCommands.set(query, { render, scope: params.scope, cache: new WeakMap });
|
|
@@ -1759,7 +2678,7 @@ function compiledTemplate(params) {
|
|
|
1759
2678
|
startSqlBindingCollection({ collector, dialect: params.dialect, scope: params.state.scope });
|
|
1760
2679
|
const text = params.state.render({ dialect: params.dialect, collector });
|
|
1761
2680
|
if (collector.count() > params.dialect.paramBudget)
|
|
1762
|
-
|
|
2681
|
+
invalid5("SQL query exceeds this dialect's parameter budget; statements are never split implicitly");
|
|
1763
2682
|
const program = finishSqlBindingCollection({ collector });
|
|
1764
2683
|
const compiled = Object.freeze({ text, program });
|
|
1765
2684
|
params.state.cache.set(params.dialect, compiled);
|
|
@@ -1769,385 +2688,55 @@ function executionHandle(params) {
|
|
|
1769
2688
|
const { value } = params;
|
|
1770
2689
|
return queries.has(value) || commands.has(value) || reusableQueries.has(value) || reusableCommands.has(value) || isSqlFragment(value) || isSqlInput(value) || sqlInputScopeHandle(params);
|
|
1771
2690
|
}
|
|
1772
|
-
function bindTemplate(params) {
|
|
1773
|
-
const { scope } = params.state;
|
|
1774
|
-
return bindSqlBindingProgram({ program: params.compiled.program, params: params.inputs, isHandle: (value) => executionHandle({ scope, value }) });
|
|
1775
|
-
}
|
|
1776
|
-
function compileReusableSqlQuery(params) {
|
|
1777
|
-
const state = reusableQueries.get(params.query) ?? invalid4("Expected an authentic reusable SQL row query");
|
|
1778
|
-
const compiled = compiledTemplate({ dialect: params.dialect, state });
|
|
1779
|
-
return { text: compiled.text, values: bindTemplate({ compiled, state, inputs: params.params }), decoder: state.decoder };
|
|
1780
|
-
}
|
|
1781
|
-
function compileReusableSqlCommand(params) {
|
|
1782
|
-
const state = reusableCommands.get(params.query) ?? invalid4("Expected an authentic reusable SQL command");
|
|
1783
|
-
const compiled = compiledTemplate({ dialect: params.dialect, state });
|
|
1784
|
-
return { text: compiled.text, values: bindTemplate({ compiled, state, inputs: params.params }) };
|
|
1785
|
-
}
|
|
1786
|
-
function compileSqlQuery(params) {
|
|
1787
|
-
const state = queries.get(params.query) ?? invalid4("Expected an authentic SQL row query");
|
|
1788
|
-
const collector = new ParamCollector(params.dialect);
|
|
1789
|
-
const text = state.render({ dialect: params.dialect, collector });
|
|
1790
|
-
if (collector.values.length > params.dialect.paramBudget)
|
|
1791
|
-
invalid4("SQL query exceeds this dialect's parameter budget; statements are never split implicitly");
|
|
1792
|
-
return { text, values: collector.values, decoder: state.decoder };
|
|
1793
|
-
}
|
|
1794
|
-
function compileSqlCommand(params) {
|
|
1795
|
-
const render = commands.get(params.query) ?? invalid4("Expected an authentic SQL command");
|
|
1796
|
-
const collector = new ParamCollector(params.dialect);
|
|
1797
|
-
const text = render({ dialect: params.dialect, collector });
|
|
1798
|
-
if (collector.values.length > params.dialect.paramBudget)
|
|
1799
|
-
invalid4("SQL command exceeds this dialect's parameter budget; statements are never split implicitly");
|
|
1800
|
-
return { text, values: collector.values };
|
|
1801
|
-
}
|
|
1802
|
-
// packages/sql/src/exclusion.ts
|
|
1803
|
-
import { VibeError as VibeError12, fieldTypeIdentity as fieldTypeIdentity2, isEnumType as isEnumType2, unsupportedCapability as unsupportedCapability10 } from "@vibeorm/schema";
|
|
1804
|
-
|
|
1805
|
-
// packages/sql/src/predicate.ts
|
|
1806
|
-
import { VibeError as VibeError11, assertPredicateCapabilities, fieldTypeIdentity, isEnumType, unsupportedCapability as unsupportedCapability9 } from "@vibeorm/schema";
|
|
1807
|
-
function renderPredicate(params) {
|
|
1808
|
-
assertPredicateCapabilities({ predicate: params.predicate, dialect: params.dialect.name, usage: params.usage });
|
|
1809
|
-
if (params.usage === "exclusionPredicate" && !params.dialect.capabilities.exclusionConstraints)
|
|
1810
|
-
throw unsupportedCapability9({ dialect: params.dialect.name, feature: "exclusionConstraints" });
|
|
1811
|
-
if (params.usage === "indexPredicate" && !params.dialect.capabilities.partialIndexes) {
|
|
1812
|
-
throw unsupportedCapability9({ dialect: params.dialect.name, feature: "partialIndexes" });
|
|
1813
|
-
}
|
|
1814
|
-
const context = {
|
|
1815
|
-
model: params.model,
|
|
1816
|
-
dialect: params.dialect,
|
|
1817
|
-
enums: params.enums ?? [],
|
|
1818
|
-
usage: params.usage,
|
|
1819
|
-
site: params.site
|
|
1820
|
-
};
|
|
1821
|
-
return renderNode({ node: params.predicate, context });
|
|
1822
|
-
}
|
|
1823
|
-
var PRECEDENCE = {
|
|
1824
|
-
or: 1,
|
|
1825
|
-
and: 2,
|
|
1826
|
-
not: 3,
|
|
1827
|
-
compare: 4,
|
|
1828
|
-
inList: 4,
|
|
1829
|
-
isNull: 4,
|
|
1830
|
-
isTrue: 4
|
|
1831
|
-
};
|
|
1832
|
-
var COMPARISON_SQL = {
|
|
1833
|
-
eq: "=",
|
|
1834
|
-
ne: "<>",
|
|
1835
|
-
gt: ">",
|
|
1836
|
-
gte: ">=",
|
|
1837
|
-
lt: "<",
|
|
1838
|
-
lte: "<="
|
|
1839
|
-
};
|
|
1840
|
-
var EXACT_INTEGER = /^-?(0|[1-9]\d*)$/;
|
|
1841
|
-
var EXACT_DECIMAL = /^-?(0|[1-9]\d*)(\.\d+)?$/;
|
|
1842
|
-
var INT32_MIN = -2147483648n;
|
|
1843
|
-
var INT32_MAX = 2147483647n;
|
|
1844
|
-
var INT64_MIN = -(2n ** 63n);
|
|
1845
|
-
var INT64_MAX = 2n ** 63n - 1n;
|
|
1846
|
-
function refuse2(params) {
|
|
1847
|
-
const { context, rule, message } = params;
|
|
1848
|
-
throw new VibeError11({
|
|
1849
|
-
code: "VIBE_SCHEMA_INVALID",
|
|
1850
|
-
message: `${context.site} on "${context.model.name}": ${message}`,
|
|
1851
|
-
meta: { rule, model: context.model.name, site: context.site, ...params.meta ?? {} }
|
|
1852
|
-
});
|
|
1853
|
-
}
|
|
1854
|
-
function renderChild(params) {
|
|
1855
|
-
const { node, parent, context } = params;
|
|
1856
|
-
if ((node.kind === "and" || node.kind === "or") && node.operands.length === 1) {
|
|
1857
|
-
return renderChild({ node: node.operands[0], parent, context });
|
|
1858
|
-
}
|
|
1859
|
-
const text = renderNode({ node, context });
|
|
1860
|
-
return PRECEDENCE[node.kind] < PRECEDENCE[parent] ? `(${text})` : text;
|
|
1861
|
-
}
|
|
1862
|
-
function renderNode(params) {
|
|
1863
|
-
const { node, context } = params;
|
|
1864
|
-
switch (node.kind) {
|
|
1865
|
-
case "and":
|
|
1866
|
-
case "or": {
|
|
1867
|
-
if (node.operands.length === 0) {
|
|
1868
|
-
refuse2({
|
|
1869
|
-
context,
|
|
1870
|
-
rule: "predicate-empty",
|
|
1871
|
-
message: `${node.kind === "and" ? "and()" : "or()"} has no operands \u2014 an empty boolean group has no honest SQL spelling`
|
|
1872
|
-
});
|
|
1873
|
-
}
|
|
1874
|
-
if (node.operands.length === 1)
|
|
1875
|
-
return renderNode({ node: node.operands[0], context });
|
|
1876
|
-
const separator = node.kind === "and" ? " AND " : " OR ";
|
|
1877
|
-
return node.operands.map((operand) => renderChild({ node: operand, parent: node.kind, context })).join(separator);
|
|
1878
|
-
}
|
|
1879
|
-
case "not":
|
|
1880
|
-
return `NOT ${renderChild({ node: node.operand, parent: "not", context })}`;
|
|
1881
|
-
case "isTrue":
|
|
1882
|
-
return `COALESCE(${renderNode({ node: node.operand, context })}, FALSE)`;
|
|
1883
|
-
case "isNull": {
|
|
1884
|
-
const field = resolveField({ name: node.field.name, context, nullTest: true });
|
|
1885
|
-
return `${quoteColumn({ field, context })} IS ${node.negate === true ? "NOT NULL" : "NULL"}`;
|
|
1886
|
-
}
|
|
1887
|
-
case "compare": {
|
|
1888
|
-
const left = renderExpression({ expression: node.left, context });
|
|
1889
|
-
const right = renderOperand({ operand: node.right, field: left.field, context });
|
|
1890
|
-
return `${left.text} ${COMPARISON_SQL[node.op]} ${right}`;
|
|
1891
|
-
}
|
|
1892
|
-
case "inList": {
|
|
1893
|
-
if (node.values.length === 0) {
|
|
1894
|
-
refuse2({
|
|
1895
|
-
context,
|
|
1896
|
-
rule: "predicate-empty",
|
|
1897
|
-
message: `inList("${node.field.name}") has no values \u2014 an empty IN list is UNKNOWN on every row and never what an author means`,
|
|
1898
|
-
meta: { field: node.field.name }
|
|
1899
|
-
});
|
|
1900
|
-
}
|
|
1901
|
-
const field = resolveField({ name: node.field.name, context });
|
|
1902
|
-
const values = node.values.map((value) => renderLiteral({ value, field, context })).join(", ");
|
|
1903
|
-
return `${compareColumn({ field, context })} ${node.negate === true ? "NOT IN" : "IN"} (${values})`;
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
function resolveField(params) {
|
|
1908
|
-
const { name, context } = params;
|
|
1909
|
-
const field = context.model.fields.find((candidate) => candidate.name === name);
|
|
1910
|
-
if (field === undefined) {
|
|
1911
|
-
refuse2({
|
|
1912
|
-
context,
|
|
1913
|
-
rule: "predicate-unknown-field",
|
|
1914
|
-
message: `references field "${name}", which the model does not declare`,
|
|
1915
|
-
meta: { field: name }
|
|
1916
|
-
});
|
|
1917
|
-
}
|
|
1918
|
-
if (field.isList) {
|
|
1919
|
-
refuse2({
|
|
1920
|
-
context,
|
|
1921
|
-
rule: "predicate-list-field",
|
|
1922
|
-
message: `compares scalar list "${name}" \u2014 array operators differ per dialect; use a raw expression for them`,
|
|
1923
|
-
meta: { field: name }
|
|
1924
|
-
});
|
|
1925
|
-
}
|
|
1926
|
-
if (field.isUnsupported === true) {
|
|
1927
|
-
refuse2({
|
|
1928
|
-
context,
|
|
1929
|
-
rule: "predicate-unsupported-type",
|
|
1930
|
-
message: `references "${name}", an Unsupported("${field.nativeType ?? "?"}") column whose operators VibeORM does not model \u2014 use a raw expression`,
|
|
1931
|
-
meta: { field: name }
|
|
1932
|
-
});
|
|
1933
|
-
}
|
|
1934
|
-
if (params.nullTest !== true && (field.type === "Json" || field.type === "Bytes")) {
|
|
1935
|
-
refuse2({
|
|
1936
|
-
context,
|
|
1937
|
-
rule: "predicate-unsupported-type",
|
|
1938
|
-
message: `references ${field.type} column "${name}" \u2014 ${field.type} comparison operators differ per dialect; use a raw expression`,
|
|
1939
|
-
meta: { field: name, type: field.type }
|
|
1940
|
-
});
|
|
1941
|
-
}
|
|
1942
|
-
if (context.usage === "check" && context.dialect.name === "mysql" && field.default?.kind === "autoincrement") {
|
|
1943
|
-
throw unsupportedCapability9({
|
|
1944
|
-
dialect: "mysql",
|
|
1945
|
-
feature: `CHECK constraint referencing auto-increment column "${context.model.name}.${name}"`,
|
|
1946
|
-
hint: "mysql refuses this with error 3818 \u2014 enforce the rule in application code or on a non-generated column"
|
|
1947
|
-
});
|
|
1948
|
-
}
|
|
1949
|
-
return field;
|
|
1950
|
-
}
|
|
1951
|
-
function quoteColumn(params) {
|
|
1952
|
-
const { field, context } = params;
|
|
1953
|
-
return context.dialect.quoteIdent({ name: field.dbName ?? field.name });
|
|
1954
|
-
}
|
|
1955
|
-
function compareColumn(params) {
|
|
1956
|
-
const { field, context } = params;
|
|
1957
|
-
const quoted = quoteColumn({ field, context });
|
|
1958
|
-
return context.dialect.name === "sqlite" && field.type === "Decimal" ? `CAST(${quoted} AS REAL)` : quoted;
|
|
1959
|
-
}
|
|
1960
|
-
function renderExpression(params) {
|
|
1961
|
-
const { expression, context } = params;
|
|
1962
|
-
if (expression.kind === "field") {
|
|
1963
|
-
const field = resolveField({ name: expression.name, context });
|
|
1964
|
-
return { text: compareColumn({ field, context }), field };
|
|
1965
|
-
}
|
|
1966
|
-
if (expression.kind === "jsonText") {
|
|
1967
|
-
const field = resolveField({ name: expression.field.name, context, nullTest: true });
|
|
1968
|
-
if (field.type !== "Json")
|
|
1969
|
-
refuse2({ context, rule: "predicate-type-mismatch", message: `jsonText requires a scalar Json field, not "${field.name}"` });
|
|
1970
|
-
if (!Array.isArray(expression.path) || Array.from(expression.path).some((segment) => typeof segment !== "string" || segment.includes("\x00"))) {
|
|
1971
|
-
refuse2({ context, rule: "predicate-json-path", message: "jsonText requires a string-only path without NUL bytes" });
|
|
1972
|
-
}
|
|
1973
|
-
const path = expression.path.map((value) => context.dialect.quoteStringLiteral({ value })).join(", ");
|
|
1974
|
-
return { text: `(${quoteColumn({ field, context })} #>> ARRAY[${path}]::text[])`, field: { ...field, type: "String" } };
|
|
1975
|
-
}
|
|
1976
|
-
const inner = renderExpression({ expression: expression.expression, context });
|
|
1977
|
-
if (inner.field.type !== "String")
|
|
1978
|
-
refuse2({ context, rule: "predicate-type-mismatch", message: "length requires a text expression" });
|
|
1979
|
-
return { text: `length(${inner.text})`, field: { ...inner.field, type: "Int" } };
|
|
1980
|
-
}
|
|
1981
|
-
function renderOperand(params) {
|
|
1982
|
-
const { operand, field, context } = params;
|
|
1983
|
-
if (operand.kind === "value")
|
|
1984
|
-
return renderLiteral({ value: operand, field, context });
|
|
1985
|
-
const other = renderExpression({ expression: operand, context });
|
|
1986
|
-
const leftType = fieldTypeIdentity({ type: field.type });
|
|
1987
|
-
const rightType = fieldTypeIdentity({ type: other.field.type });
|
|
1988
|
-
if (leftType !== rightType) {
|
|
1989
|
-
refuse2({
|
|
1990
|
-
context,
|
|
1991
|
-
rule: "predicate-type-mismatch",
|
|
1992
|
-
message: `compares "${field.name}" (${leftType}) with "${other.field.name}" (${rightType}) \u2014 both sides of a comparison need the same scalar type`,
|
|
1993
|
-
meta: { field: field.name, other: other.field.name, fieldType: leftType, otherType: rightType }
|
|
1994
|
-
});
|
|
1995
|
-
}
|
|
1996
|
-
return other.text;
|
|
2691
|
+
function bindTemplate(params) {
|
|
2692
|
+
const { scope } = params.state;
|
|
2693
|
+
return bindSqlBindingProgram({ program: params.compiled.program, params: params.inputs, isHandle: (value) => executionHandle({ scope, value }) });
|
|
1997
2694
|
}
|
|
1998
|
-
function
|
|
1999
|
-
const
|
|
2000
|
-
const
|
|
2001
|
-
|
|
2002
|
-
const wrongCarrier = (expected) => refuse2({
|
|
2003
|
-
context,
|
|
2004
|
-
rule: "predicate-type-mismatch",
|
|
2005
|
-
message: `compares ${fieldTypeIdentity({ type })} field "${field.name}" with ${describe({ value })} \u2014 ${expected}`,
|
|
2006
|
-
meta: { field: field.name, fieldType: fieldTypeIdentity({ type }), value }
|
|
2007
|
-
});
|
|
2008
|
-
if (isEnumType(type)) {
|
|
2009
|
-
if (typeof value !== "string")
|
|
2010
|
-
return wrongCarrier(`enum ${type.enum} takes one of its member names as a string`);
|
|
2011
|
-
const declared = context.enums.find((entry) => entry.name === type.enum);
|
|
2012
|
-
if (declared !== undefined && !declared.values.includes(value)) {
|
|
2013
|
-
refuse2({
|
|
2014
|
-
context,
|
|
2015
|
-
rule: "predicate-enum-member",
|
|
2016
|
-
message: `compares "${field.name}" with "${value}", which is not a member of enum ${type.enum} (${declared.values.join(", ")})`,
|
|
2017
|
-
meta: { field: field.name, enum: type.enum, value, members: [...declared.values] }
|
|
2018
|
-
});
|
|
2019
|
-
}
|
|
2020
|
-
return stringLiteral({ value, context, field });
|
|
2021
|
-
}
|
|
2022
|
-
switch (type) {
|
|
2023
|
-
case "String":
|
|
2024
|
-
if (typeof value !== "string")
|
|
2025
|
-
return wrongCarrier("a String column takes a string literal");
|
|
2026
|
-
return stringLiteral({ value, context, field });
|
|
2027
|
-
case "Boolean":
|
|
2028
|
-
if (typeof value !== "boolean")
|
|
2029
|
-
return wrongCarrier("a Boolean column takes `true` or `false`");
|
|
2030
|
-
return value ? context.dialect.booleanLiteral.true : context.dialect.booleanLiteral.false;
|
|
2031
|
-
case "Int":
|
|
2032
|
-
return integerLiteral({ value, field, context, min: INT32_MIN, max: INT32_MAX, label: "Int" });
|
|
2033
|
-
case "BigInt":
|
|
2034
|
-
return integerLiteral({ value, field, context, min: INT64_MIN, max: INT64_MAX, label: "BigInt" });
|
|
2035
|
-
case "Float": {
|
|
2036
|
-
if (typeof value !== "number")
|
|
2037
|
-
return wrongCarrier("a Float column takes a number");
|
|
2038
|
-
if (!Number.isFinite(value)) {
|
|
2039
|
-
refuse2({
|
|
2040
|
-
context,
|
|
2041
|
-
rule: "predicate-literal-invalid",
|
|
2042
|
-
message: `compares "${field.name}" with ${String(value)} \u2014 a Float literal must be finite`,
|
|
2043
|
-
meta: { field: field.name, value: String(value) }
|
|
2044
|
-
});
|
|
2045
|
-
}
|
|
2046
|
-
return String(value);
|
|
2047
|
-
}
|
|
2048
|
-
case "Decimal": {
|
|
2049
|
-
if (typeof value !== "string") {
|
|
2050
|
-
return wrongCarrier('a Decimal column takes exact decimal TEXT ("0", "19.99") \u2014 a JS number cannot carry an exact decimal');
|
|
2051
|
-
}
|
|
2052
|
-
if (!EXACT_DECIMAL.test(value)) {
|
|
2053
|
-
refuse2({
|
|
2054
|
-
context,
|
|
2055
|
-
rule: "predicate-literal-invalid",
|
|
2056
|
-
message: `compares "${field.name}" with "${value}", which is not an exact decimal literal`,
|
|
2057
|
-
meta: { field: field.name, value }
|
|
2058
|
-
});
|
|
2059
|
-
}
|
|
2060
|
-
return value;
|
|
2061
|
-
}
|
|
2062
|
-
case "DateTime":
|
|
2063
|
-
refuse2({
|
|
2064
|
-
context,
|
|
2065
|
-
rule: "predicate-unsupported-literal",
|
|
2066
|
-
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`,
|
|
2067
|
-
meta: { field: field.name }
|
|
2068
|
-
});
|
|
2069
|
-
case "Json":
|
|
2070
|
-
case "Bytes":
|
|
2071
|
-
return wrongCarrier(`${type} columns take no predicate literal`);
|
|
2072
|
-
}
|
|
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 };
|
|
2073
2699
|
}
|
|
2074
|
-
function
|
|
2075
|
-
const
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
if (!Number.isSafeInteger(value)) {
|
|
2079
|
-
refuse2({
|
|
2080
|
-
context,
|
|
2081
|
-
rule: "predicate-literal-invalid",
|
|
2082
|
-
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`,
|
|
2083
|
-
meta: { field: field.name, value: String(value) }
|
|
2084
|
-
});
|
|
2085
|
-
}
|
|
2086
|
-
text = String(value);
|
|
2087
|
-
} else if (typeof value === "string") {
|
|
2088
|
-
if (!EXACT_INTEGER.test(value)) {
|
|
2089
|
-
refuse2({
|
|
2090
|
-
context,
|
|
2091
|
-
rule: "predicate-literal-invalid",
|
|
2092
|
-
message: `compares "${field.name}" with "${value}", which is not an exact integer literal`,
|
|
2093
|
-
meta: { field: field.name, value }
|
|
2094
|
-
});
|
|
2095
|
-
}
|
|
2096
|
-
text = value;
|
|
2097
|
-
} else {
|
|
2098
|
-
refuse2({
|
|
2099
|
-
context,
|
|
2100
|
-
rule: "predicate-type-mismatch",
|
|
2101
|
-
message: `compares ${label} field "${field.name}" with ${describe({ value })} \u2014 an ${label} column takes an integer`,
|
|
2102
|
-
meta: { field: field.name, value }
|
|
2103
|
-
});
|
|
2104
|
-
}
|
|
2105
|
-
const parsed = BigInt(text);
|
|
2106
|
-
if (parsed < min || parsed > max) {
|
|
2107
|
-
refuse2({
|
|
2108
|
-
context,
|
|
2109
|
-
rule: "predicate-literal-invalid",
|
|
2110
|
-
message: `compares "${field.name}" with ${text}, which is outside the range an ${label} column can hold (${min}\u2026${max})`,
|
|
2111
|
-
meta: { field: field.name, value: text }
|
|
2112
|
-
});
|
|
2113
|
-
}
|
|
2114
|
-
return text;
|
|
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 }) };
|
|
2115
2704
|
}
|
|
2116
|
-
function
|
|
2117
|
-
const
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
meta: { field: field.name }
|
|
2124
|
-
});
|
|
2125
|
-
}
|
|
2126
|
-
return context.dialect.quoteStringLiteral({ value });
|
|
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 };
|
|
2127
2712
|
}
|
|
2128
|
-
function
|
|
2129
|
-
const
|
|
2130
|
-
|
|
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 };
|
|
2131
2720
|
}
|
|
2132
|
-
|
|
2133
2721
|
// packages/sql/src/exclusion.ts
|
|
2134
|
-
|
|
2135
|
-
|
|
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}` } });
|
|
2136
2725
|
}
|
|
2137
2726
|
function compileExclusion(params) {
|
|
2138
2727
|
const { exclusion: spec, model, dialect } = params;
|
|
2139
2728
|
if (!dialect.capabilities.exclusionConstraints)
|
|
2140
|
-
throw
|
|
2729
|
+
throw unsupportedCapability14({ dialect: dialect.name, feature: "exclusionConstraints" });
|
|
2141
2730
|
if (!spec.name || spec.name.includes("\x00"))
|
|
2142
|
-
|
|
2731
|
+
invalid6("name");
|
|
2143
2732
|
if (!["int8range", "tsrange", "tstzrange"].includes(spec.range.type))
|
|
2144
|
-
|
|
2733
|
+
invalid6("range-type");
|
|
2145
2734
|
if (!["[)", "[]", "()", "(]"].includes(spec.range.bounds))
|
|
2146
|
-
|
|
2735
|
+
invalid6("bounds");
|
|
2147
2736
|
const field = (name) => {
|
|
2148
2737
|
const value = model.fields.find((entry) => entry.name === name);
|
|
2149
2738
|
if (value === undefined || value.isList || value.isUnsupported)
|
|
2150
|
-
|
|
2739
|
+
invalid6("field");
|
|
2151
2740
|
return value;
|
|
2152
2741
|
};
|
|
2153
2742
|
const endpoint = (value) => {
|
|
@@ -2155,17 +2744,17 @@ function compileExclusion(params) {
|
|
|
2155
2744
|
return "NULL";
|
|
2156
2745
|
const infinity = "infinity" in value ? value.infinity : value.nullAs;
|
|
2157
2746
|
if (infinity !== undefined && (spec.range.type === "int8range" || !["infinity", "-infinity"].includes(infinity)))
|
|
2158
|
-
|
|
2747
|
+
invalid6("infinity");
|
|
2159
2748
|
const literal = infinity === undefined ? undefined : `${dialect.quoteStringLiteral({ value: infinity })}::${spec.range.type === "tsrange" ? "timestamp" : "timestamptz"}`;
|
|
2160
2749
|
if ("infinity" in value)
|
|
2161
2750
|
return literal;
|
|
2162
2751
|
const column = field(value.field);
|
|
2163
2752
|
if (fieldTypeIdentity2({ type: column.type }) !== (spec.range.type === "int8range" ? "BigInt" : "DateTime"))
|
|
2164
|
-
|
|
2753
|
+
invalid6("range-field-type");
|
|
2165
2754
|
if (column.type === "DateTime") {
|
|
2166
2755
|
const timezone = /timestamptz|timestamp.*with time zone/i.test(column.nativeType ?? "timestamptz");
|
|
2167
2756
|
if (timezone !== (spec.range.type === "tstzrange"))
|
|
2168
|
-
|
|
2757
|
+
invalid6("range-timezone");
|
|
2169
2758
|
}
|
|
2170
2759
|
const sql = dialect.quoteIdent({ name: column.dbName ?? column.name });
|
|
2171
2760
|
return literal === undefined ? sql : `COALESCE(${sql}, ${literal})`;
|
|
@@ -2173,15 +2762,15 @@ function compileExclusion(params) {
|
|
|
2173
2762
|
const keys = spec.keys.map((key) => {
|
|
2174
2763
|
if ("expression" in key) {
|
|
2175
2764
|
if (!key.expression.trim() || key.expression.includes("\x00") || key.fields.length === 0)
|
|
2176
|
-
|
|
2765
|
+
invalid6("expression");
|
|
2177
2766
|
key.fields.forEach(field);
|
|
2178
2767
|
return `(${key.expression}) WITH =`;
|
|
2179
2768
|
}
|
|
2180
2769
|
const column = field(key.field);
|
|
2181
2770
|
if (column.isOptional)
|
|
2182
|
-
|
|
2771
|
+
invalid6("nullable-key-requires-expression");
|
|
2183
2772
|
if (!isEnumType2(column.type) && !["String", "Int", "BigInt", "Float", "Decimal", "Boolean", "DateTime"].includes(fieldTypeIdentity2({ type: column.type })))
|
|
2184
|
-
|
|
2773
|
+
invalid6("key-type");
|
|
2185
2774
|
return `${dialect.quoteIdent({ name: column.dbName ?? column.name })} WITH =`;
|
|
2186
2775
|
});
|
|
2187
2776
|
keys.push(`${spec.range.type}(${endpoint(spec.range.lower)}, ${endpoint(spec.range.upper)}, ${dialect.quoteStringLiteral({ value: spec.range.bounds })}) WITH &&`);
|
|
@@ -2190,13 +2779,13 @@ function compileExclusion(params) {
|
|
|
2190
2779
|
}
|
|
2191
2780
|
function renderExclusionSetup(params) {
|
|
2192
2781
|
if (!params.dialect.capabilities.exclusionConstraints)
|
|
2193
|
-
throw
|
|
2782
|
+
throw unsupportedCapability14({ dialect: params.dialect.name, feature: "exclusionConstraints" });
|
|
2194
2783
|
return params.exclusion.requiredExtensions.map((name) => `CREATE EXTENSION IF NOT EXISTS ${params.dialect.quoteIdent({ name })};`);
|
|
2195
2784
|
}
|
|
2196
2785
|
function renderExclusionDdl(params) {
|
|
2197
2786
|
const { dialect, table, exclusion, operation } = params;
|
|
2198
2787
|
if (!dialect.capabilities.exclusionConstraints)
|
|
2199
|
-
throw
|
|
2788
|
+
throw unsupportedCapability14({ dialect: dialect.name, feature: "exclusionConstraints" });
|
|
2200
2789
|
const target = `ALTER TABLE ${dialect.quoteIdent({ name: table })}`;
|
|
2201
2790
|
if (operation === "drop")
|
|
2202
2791
|
return [`${target} DROP CONSTRAINT ${dialect.quoteIdent({ name: exclusion.name })};`];
|
|
@@ -2205,14 +2794,14 @@ function renderExclusionDdl(params) {
|
|
|
2205
2794
|
function renderExclusionCatalog(params) {
|
|
2206
2795
|
const { dialect, probe } = params;
|
|
2207
2796
|
if (!dialect.capabilities.exclusionConstraints)
|
|
2208
|
-
throw
|
|
2797
|
+
throw unsupportedCapability14({ dialect: dialect.name, feature: "exclusionConstraints" });
|
|
2209
2798
|
const scope = probe === undefined ? `n.nspname = ${dialect.quoteStringLiteral({ value: params.namespace ?? "public" })}` : `t.oid = ${dialect.quoteStringLiteral({ value: probe })}::regclass`;
|
|
2210
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`;
|
|
2211
2800
|
}
|
|
2212
2801
|
function renderExclusionProbe(params) {
|
|
2213
2802
|
const { dialect, table, probe, definition } = params;
|
|
2214
2803
|
if (!dialect.capabilities.exclusionConstraints)
|
|
2215
|
-
throw
|
|
2804
|
+
throw unsupportedCapability14({ dialect: dialect.name, feature: "exclusionConstraints" });
|
|
2216
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}`];
|
|
2217
2806
|
}
|
|
2218
2807
|
// packages/sql/src/ast.ts
|
|
@@ -2244,6 +2833,8 @@ function dbNowMarker(params) {
|
|
|
2244
2833
|
function isDbNowMarker(value) {
|
|
2245
2834
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
2246
2835
|
return false;
|
|
2836
|
+
if (!Object.hasOwn(value, "__dbNow"))
|
|
2837
|
+
return false;
|
|
2247
2838
|
const keys = Object.keys(value);
|
|
2248
2839
|
if (keys.length !== 2 || !keys.includes("__dbNow") || !keys.includes("offsetSeconds"))
|
|
2249
2840
|
return false;
|
|
@@ -2291,7 +2882,7 @@ function chainVetoToken(params) {
|
|
|
2291
2882
|
return;
|
|
2292
2883
|
}
|
|
2293
2884
|
// packages/sql/src/locks.ts
|
|
2294
|
-
import { internalError, unsupportedCapability as
|
|
2885
|
+
import { internalError, unsupportedCapability as unsupportedCapability15 } from "@vibeorm/schema";
|
|
2295
2886
|
var ROW_LOCK_PROSE = Object.freeze({
|
|
2296
2887
|
strengths: Object.freeze({
|
|
2297
2888
|
update: "FOR UPDATE",
|
|
@@ -2311,7 +2902,7 @@ function renderRowLockClause(params) {
|
|
|
2311
2902
|
if (syntax === null) {
|
|
2312
2903
|
if (lock.strength === "update" && lock.wait === "wait")
|
|
2313
2904
|
return null;
|
|
2314
|
-
throw
|
|
2905
|
+
throw unsupportedCapability15({
|
|
2315
2906
|
dialect: dialect.name,
|
|
2316
2907
|
feature: `a ${describeRowLock({ lock })} locking read`,
|
|
2317
2908
|
hint: `${dialect.name} has no locking-read clause at all`
|
|
@@ -2319,7 +2910,7 @@ function renderRowLockClause(params) {
|
|
|
2319
2910
|
}
|
|
2320
2911
|
const strengthText = syntax.strengths[lock.strength];
|
|
2321
2912
|
if (strengthText === undefined) {
|
|
2322
|
-
throw
|
|
2913
|
+
throw unsupportedCapability15({
|
|
2323
2914
|
dialect: dialect.name,
|
|
2324
2915
|
feature: `the ${describeRowLock({ lock: { ...lock, wait: "wait" } })} row-lock strength`,
|
|
2325
2916
|
hint: `${dialect.name} can express: ${Object.keys(syntax.strengths).join(", ")}`
|
|
@@ -2327,7 +2918,7 @@ function renderRowLockClause(params) {
|
|
|
2327
2918
|
}
|
|
2328
2919
|
const waitText = syntax.wait[lock.wait];
|
|
2329
2920
|
if (waitText === undefined) {
|
|
2330
|
-
throw
|
|
2921
|
+
throw unsupportedCapability15({
|
|
2331
2922
|
dialect: dialect.name,
|
|
2332
2923
|
feature: `the "${lock.wait}" lock wait policy`,
|
|
2333
2924
|
hint: `${dialect.name} can express: ${Object.keys(syntax.wait).join(", ")}`
|
|
@@ -2346,7 +2937,7 @@ function renderAdvisoryLock(params) {
|
|
|
2346
2937
|
const { dialect, statement, collector } = params;
|
|
2347
2938
|
const functions = dialect.advisoryLockFunctions;
|
|
2348
2939
|
if (functions === null) {
|
|
2349
|
-
throw
|
|
2940
|
+
throw unsupportedCapability15({
|
|
2350
2941
|
dialect: dialect.name,
|
|
2351
2942
|
feature: "advisory locks",
|
|
2352
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`
|
|
@@ -2385,6 +2976,13 @@ var postgresDialect = {
|
|
|
2385
2976
|
capabilities: POSTGRES_CAPABILITIES,
|
|
2386
2977
|
inArrayStrategy: "any",
|
|
2387
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,
|
|
2388
2986
|
timeOfDayInStrategy: "in",
|
|
2389
2987
|
upsertForm: "onConflict",
|
|
2390
2988
|
conflictKeyEquality: {
|
|
@@ -2434,6 +3032,7 @@ var postgresDialect = {
|
|
|
2434
3032
|
keyCast: Object.freeze({ single: "::bigint", pair: "::int4" })
|
|
2435
3033
|
},
|
|
2436
3034
|
rowDefaultKeyword: true,
|
|
3035
|
+
bulkInsertForm: "unnest",
|
|
2437
3036
|
emptyInsert: { form: "defaultValues", conflictClause: true },
|
|
2438
3037
|
statementChain: "dataModifyingCte",
|
|
2439
3038
|
paramBudget: 30000,
|
|
@@ -2462,6 +3061,13 @@ var mysqlDialect = {
|
|
|
2462
3061
|
capabilities: MYSQL_CAPABILITIES,
|
|
2463
3062
|
inArrayStrategy: "expand",
|
|
2464
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,
|
|
2465
3071
|
timeOfDayInStrategy: "equalities",
|
|
2466
3072
|
upsertForm: "onDuplicateKey",
|
|
2467
3073
|
conflictKeyEquality: {
|
|
@@ -2497,6 +3103,7 @@ var mysqlDialect = {
|
|
|
2497
3103
|
},
|
|
2498
3104
|
advisoryLockFunctions: null,
|
|
2499
3105
|
rowDefaultKeyword: true,
|
|
3106
|
+
bulkInsertForm: "values",
|
|
2500
3107
|
emptyInsert: { form: "emptyRow" },
|
|
2501
3108
|
statementChain: null,
|
|
2502
3109
|
paramBudget: 1e4,
|
|
@@ -2528,6 +3135,389 @@ var SQL_DIALECTS = {
|
|
|
2528
3135
|
function getSqlDialect(params) {
|
|
2529
3136
|
return SQL_DIALECTS[params.dialect];
|
|
2530
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
|
+
}
|
|
2531
3521
|
// packages/sql/src/dialects/sqlite-json-equality.ts
|
|
2532
3522
|
function renderSqliteJsonEquality(params) {
|
|
2533
3523
|
const q = (name) => params.dialect.quoteIdent({ name });
|
|
@@ -2543,7 +3533,7 @@ function renderSqliteJsonEquality(params) {
|
|
|
2543
3533
|
}
|
|
2544
3534
|
|
|
2545
3535
|
// packages/sql/src/dialects/diagnostics.ts
|
|
2546
|
-
import { unsupportedCapability as
|
|
3536
|
+
import { unsupportedCapability as unsupportedCapability23 } from "@vibeorm/schema";
|
|
2547
3537
|
var QUERIES = Object.freeze({
|
|
2548
3538
|
postgresJsonWire: { dialect: "postgres", text: `SELECT '{"__vibeorm":1}'::jsonb AS "probe"` },
|
|
2549
3539
|
sqliteTriggers: { dialect: "sqlite", text: "SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'trigger'" }
|
|
@@ -2551,7 +3541,7 @@ var QUERIES = Object.freeze({
|
|
|
2551
3541
|
function renderDiagnostic(params) {
|
|
2552
3542
|
const query = QUERIES[params.query];
|
|
2553
3543
|
if (params.dialect.name !== query.dialect) {
|
|
2554
|
-
throw
|
|
3544
|
+
throw unsupportedCapability23({ dialect: params.dialect.name, feature: params.query, hint: `this diagnostic requires ${query.dialect}` });
|
|
2555
3545
|
}
|
|
2556
3546
|
return { text: query.text, values: [], returning: "none" };
|
|
2557
3547
|
}
|
|
@@ -2560,7 +3550,7 @@ function isFixedDiagnostic(params) {
|
|
|
2560
3550
|
}
|
|
2561
3551
|
|
|
2562
3552
|
// packages/sql/src/dialects/postgres-diagnostics.ts
|
|
2563
|
-
import { VibeError as
|
|
3553
|
+
import { VibeError as VibeError21, unsupportedCapability as unsupportedCapability24 } from "@vibeorm/schema";
|
|
2564
3554
|
function renderPostgresExplain(params) {
|
|
2565
3555
|
const { dialect, query, options } = params;
|
|
2566
3556
|
requirePostgres({ dialect, feature: "query plan inspection (EXPLAIN)" });
|
|
@@ -2595,7 +3585,7 @@ var POSTGRES_WORKLOAD_LIMITS = Object.freeze({
|
|
|
2595
3585
|
});
|
|
2596
3586
|
function requirePostgres(params) {
|
|
2597
3587
|
if (params.dialect.name !== "postgres") {
|
|
2598
|
-
throw
|
|
3588
|
+
throw unsupportedCapability24({
|
|
2599
3589
|
dialect: params.dialect.name,
|
|
2600
3590
|
feature: params.feature,
|
|
2601
3591
|
hint: "plan inspection and workload statistics are PostgreSQL-shaped; portable operation tracing works on every dialect"
|
|
@@ -2613,7 +3603,7 @@ function renderPostgresWorkload(params) {
|
|
|
2613
3603
|
if (query === "statementSnapshot") {
|
|
2614
3604
|
const bounded = limit ?? 20;
|
|
2615
3605
|
if (!Number.isInteger(bounded) || bounded < POSTGRES_WORKLOAD_LIMITS.minLimit || bounded > POSTGRES_WORKLOAD_LIMITS.maxLimit) {
|
|
2616
|
-
throw new
|
|
3606
|
+
throw new VibeError21({
|
|
2617
3607
|
code: "VIBE_CONFIG",
|
|
2618
3608
|
message: `workload snapshot limit must be an integer between ${POSTGRES_WORKLOAD_LIMITS.minLimit} and ${POSTGRES_WORKLOAD_LIMITS.maxLimit}, got ${String(limit)}`,
|
|
2619
3609
|
meta: { limit }
|
|
@@ -2632,7 +3622,7 @@ function renderPostgresWorkload(params) {
|
|
|
2632
3622
|
function requireArg(params) {
|
|
2633
3623
|
const { value, name, query } = params;
|
|
2634
3624
|
if (value === undefined || value.length === 0) {
|
|
2635
|
-
throw new
|
|
3625
|
+
throw new VibeError21({
|
|
2636
3626
|
code: "VIBE_CONFIG",
|
|
2637
3627
|
message: `the "${query}" diagnostic read requires a ${name}`,
|
|
2638
3628
|
meta: { query, argument: name }
|
|
@@ -2642,10 +3632,10 @@ function requireArg(params) {
|
|
|
2642
3632
|
}
|
|
2643
3633
|
|
|
2644
3634
|
// packages/sql/src/dialects/postgres-rls-readiness.ts
|
|
2645
|
-
import { unsupportedCapability as
|
|
3635
|
+
import { unsupportedCapability as unsupportedCapability26 } from "@vibeorm/schema";
|
|
2646
3636
|
|
|
2647
3637
|
// packages/sql/src/dialects/postgres-rls.ts
|
|
2648
|
-
import { RlsContextError, VibeError as
|
|
3638
|
+
import { RlsContextError, VibeError as VibeError22, assertNativeRlsAdmission, assertValidSchemaIR, unsupportedCapability as unsupportedCapability25, validateRlsContextIR } from "@vibeorm/schema";
|
|
2649
3639
|
var NATIVE_RLS = Object.freeze({ policyName: "vibeorm_managed", tableSchema: "public", settingPrefix: "vibeorm.", parentAlias: "parent" });
|
|
2650
3640
|
var COLUMN_TYPES = {
|
|
2651
3641
|
String: { default: "text", native: { text: "text", varchar: "varchar", charactervarying: "varchar", uuid: "uuid" } },
|
|
@@ -2658,18 +3648,18 @@ var COLUMN_TYPES = {
|
|
|
2658
3648
|
};
|
|
2659
3649
|
function requireRls(params) {
|
|
2660
3650
|
if (!params.dialect.capabilities.nativeRowLevelSecurity) {
|
|
2661
|
-
throw
|
|
3651
|
+
throw unsupportedCapability25({ dialect: params.dialect.name, feature: "nativeRowLevelSecurity" });
|
|
2662
3652
|
}
|
|
2663
3653
|
}
|
|
2664
3654
|
function identifier2(params) {
|
|
2665
3655
|
if (typeof params.name !== "string" || params.name.length === 0 || params.name.includes("\x00") || new TextEncoder().encode(params.name).length > 63) {
|
|
2666
|
-
throw new
|
|
3656
|
+
throw new VibeError22({ code: "VIBE_SCHEMA_INVALID", message: "Native RLS identifiers must be nonempty PostgreSQL identifiers of at most 63 bytes" });
|
|
2667
3657
|
}
|
|
2668
3658
|
return params.dialect.quoteIdent({ name: params.name });
|
|
2669
3659
|
}
|
|
2670
3660
|
function tableRef(params) {
|
|
2671
3661
|
if (params.schema !== undefined && params.schema !== "public" && params.schema !== "pg_temp") {
|
|
2672
|
-
throw
|
|
3662
|
+
throw unsupportedCapability25({ dialect: params.dialect.name, feature: "nativeRowLevelSecurity", hint: "Only public tables and temporary policy probes are supported" });
|
|
2673
3663
|
}
|
|
2674
3664
|
return `${identifier2({ dialect: params.dialect, name: params.schema ?? NATIVE_RLS.tableSchema })}.${identifier2({ dialect: params.dialect, name: params.table })}`;
|
|
2675
3665
|
}
|
|
@@ -2710,7 +3700,7 @@ function renderRlsColumnType(params) {
|
|
|
2710
3700
|
if (native !== undefined)
|
|
2711
3701
|
return native;
|
|
2712
3702
|
}
|
|
2713
|
-
throw
|
|
3703
|
+
throw unsupportedCapability25({ dialect: params.dialect.name, feature: "nativeRowLevelSecurity", hint: `Unsupported physical ownership type on field ${field.name}` });
|
|
2714
3704
|
}
|
|
2715
3705
|
function protectedModel(params) {
|
|
2716
3706
|
requireRls(params);
|
|
@@ -2718,7 +3708,7 @@ function protectedModel(params) {
|
|
|
2718
3708
|
assertValidSchemaIR({ schema: params.schema });
|
|
2719
3709
|
const model = params.schema.models.find((entry) => entry.name === params.model);
|
|
2720
3710
|
if (model === undefined || model.rls === undefined || model.rls === "unprotected") {
|
|
2721
|
-
throw new
|
|
3711
|
+
throw new VibeError22({ code: "VIBE_SCHEMA_INVALID", message: "Native RLS policy rendering requires a protected model", meta: { model: params.model } });
|
|
2722
3712
|
}
|
|
2723
3713
|
return model;
|
|
2724
3714
|
}
|
|
@@ -2751,7 +3741,7 @@ function predicate(params) {
|
|
|
2751
3741
|
}).join(" AND ");
|
|
2752
3742
|
return `EXISTS (SELECT 1 FROM ${tableRef({ dialect, table: parent.dbName ?? parent.name })} AS ${alias} WHERE ${join})`;
|
|
2753
3743
|
}
|
|
2754
|
-
throw new
|
|
3744
|
+
throw new VibeError22({ code: "VIBE_SCHEMA_INVALID", message: "Native RLS requires a protected policy rule" });
|
|
2755
3745
|
}
|
|
2756
3746
|
function renderRlsPolicy(params) {
|
|
2757
3747
|
const model = protectedModel(params);
|
|
@@ -2785,7 +3775,7 @@ function renderRlsContext(params) {
|
|
|
2785
3775
|
function renderRlsRole(params) {
|
|
2786
3776
|
requireRls(params);
|
|
2787
3777
|
if (typeof params.role !== "string" || params.role.trim().length === 0 || params.role.includes("\x00")) {
|
|
2788
|
-
throw new
|
|
3778
|
+
throw new VibeError22({ code: "VIBE_CONFIG", message: "PGlite native RLS requires a trusted nonempty runtime role name" });
|
|
2789
3779
|
}
|
|
2790
3780
|
const collector = new ParamCollector(params.dialect);
|
|
2791
3781
|
return query({ text: `SELECT set_config('role', ${collector.add(params.role)}, true)`, values: collector.values });
|
|
@@ -2937,7 +3927,7 @@ ORDER BY c.relname, p.polname`,
|
|
|
2937
3927
|
});
|
|
2938
3928
|
function requireRls2(params) {
|
|
2939
3929
|
if (!params.dialect.capabilities.nativeRowLevelSecurity) {
|
|
2940
|
-
throw
|
|
3930
|
+
throw unsupportedCapability26({ dialect: params.dialect.name, feature: "nativeRowLevelSecurity" });
|
|
2941
3931
|
}
|
|
2942
3932
|
}
|
|
2943
3933
|
function renderRlsReadinessQuery(params) {
|
|
@@ -2969,7 +3959,7 @@ function renderRlsPolicyPath(params) {
|
|
|
2969
3959
|
}
|
|
2970
3960
|
|
|
2971
3961
|
// packages/sql/src/render.ts
|
|
2972
|
-
import { assertSqlNamespace as assertSqlNamespace2, internalError as
|
|
3962
|
+
import { assertSqlNamespace as assertSqlNamespace2, internalError as internalError3, unsupportedCapability as unsupportedCapability27 } from "@vibeorm/schema";
|
|
2973
3963
|
|
|
2974
3964
|
// packages/sql/src/like.ts
|
|
2975
3965
|
function escapeLikePattern(params) {
|
|
@@ -2995,6 +3985,9 @@ function strictMatchPattern(params) {
|
|
|
2995
3985
|
}
|
|
2996
3986
|
return op === "contains" ? containsPattern({ raw: value }) : op === "startsWith" ? startsWithPattern({ raw: value }) : endsWithPattern({ raw: value });
|
|
2997
3987
|
}
|
|
3988
|
+
function strictMatchCollationPrefilter(params) {
|
|
3989
|
+
return params.strategy === "binary" && params.op === "startsWith" && !params.negate;
|
|
3990
|
+
}
|
|
2998
3991
|
|
|
2999
3992
|
// packages/sql/src/render.ts
|
|
3000
3993
|
function renderStatement(params) {
|
|
@@ -3054,13 +4047,13 @@ function quoteCol(dialect, column) {
|
|
|
3054
4047
|
const ref = column.table === undefined ? name : `${dialect.quoteIdent({ name: column.table })}.${name}`;
|
|
3055
4048
|
if (column.castReal === true) {
|
|
3056
4049
|
if (dialect.name !== "sqlite") {
|
|
3057
|
-
throw
|
|
4050
|
+
throw internalError3({ message: "ColRef.castReal is a sqlite Decimal construct" });
|
|
3058
4051
|
}
|
|
3059
4052
|
return `CAST(${ref} AS REAL)`;
|
|
3060
4053
|
}
|
|
3061
4054
|
if (column.castJsonb !== undefined) {
|
|
3062
4055
|
if (dialect.name !== "postgres") {
|
|
3063
|
-
throw
|
|
4056
|
+
throw internalError3({ message: "ColRef.castJsonb is a postgres json construct" });
|
|
3064
4057
|
}
|
|
3065
4058
|
return `${ref}::${column.castJsonb}`;
|
|
3066
4059
|
}
|
|
@@ -3077,7 +4070,7 @@ function jsonCellCast(dialect, cell) {
|
|
|
3077
4070
|
if (cell.storage !== "json" || dialect.jsonParamCast === "")
|
|
3078
4071
|
return dialect.jsonParamCast;
|
|
3079
4072
|
if (dialect.name !== "postgres") {
|
|
3080
|
-
throw
|
|
4073
|
+
throw internalError3({ message: "JsonParamCell storage json is a postgres construct" });
|
|
3081
4074
|
}
|
|
3082
4075
|
return "::text::json";
|
|
3083
4076
|
}
|
|
@@ -3098,7 +4091,7 @@ function quoteProjection(dialect, column) {
|
|
|
3098
4091
|
if (column.transform === "castText")
|
|
3099
4092
|
return `${textProjection({ dialect, expression: ref })} AS ${alias}`;
|
|
3100
4093
|
if (dialect.name !== "postgres") {
|
|
3101
|
-
throw
|
|
4094
|
+
throw internalError3({ message: "encodeBase64 is a postgres construct" });
|
|
3102
4095
|
}
|
|
3103
4096
|
return `encode(${ref}, 'base64') AS ${alias}`;
|
|
3104
4097
|
}
|
|
@@ -3120,7 +4113,7 @@ function renderTable(dialect, table) {
|
|
|
3120
4113
|
}
|
|
3121
4114
|
function assertCount(value, what) {
|
|
3122
4115
|
if (!Number.isSafeInteger(value) || value < 0) {
|
|
3123
|
-
throw
|
|
4116
|
+
throw internalError3({ message: `${what} must be a non-negative integer, got ${value}` });
|
|
3124
4117
|
}
|
|
3125
4118
|
}
|
|
3126
4119
|
var COMPARISON_SQL2 = {
|
|
@@ -3152,7 +4145,7 @@ function renderAggRef(dialect, agg) {
|
|
|
3152
4145
|
function assertScalarArrays(dialect, feature) {
|
|
3153
4146
|
if (dialect.capabilities.scalarArrays)
|
|
3154
4147
|
return;
|
|
3155
|
-
throw
|
|
4148
|
+
throw unsupportedCapability27({
|
|
3156
4149
|
dialect: dialect.name,
|
|
3157
4150
|
feature,
|
|
3158
4151
|
hint: "only postgres has native array columns \u2014 model the list as a related table instead"
|
|
@@ -3164,7 +4157,7 @@ function renderExpr(params) {
|
|
|
3164
4157
|
case "cmp": {
|
|
3165
4158
|
const op = COMPARISON_SQL2[expr.op];
|
|
3166
4159
|
if (op === undefined) {
|
|
3167
|
-
throw
|
|
4160
|
+
throw internalError3({ message: `unknown comparison operator "${expr.op}"` });
|
|
3168
4161
|
}
|
|
3169
4162
|
if (isDbNowMarker(expr.value)) {
|
|
3170
4163
|
return `${quoteCol(dialect, expr.column)} ${op} ${dialect.renderDbNow({ offsetSeconds: expr.value.offsetSeconds, collector })}`;
|
|
@@ -3175,7 +4168,7 @@ function renderExpr(params) {
|
|
|
3175
4168
|
case "cmpCol": {
|
|
3176
4169
|
const op = COMPARISON_SQL2[expr.op];
|
|
3177
4170
|
if (op === undefined) {
|
|
3178
|
-
throw
|
|
4171
|
+
throw internalError3({ message: `unknown comparison operator "${expr.op}"` });
|
|
3179
4172
|
}
|
|
3180
4173
|
return `${quoteCol(dialect, expr.left)} ${op} ${quoteCol(dialect, expr.right)}`;
|
|
3181
4174
|
}
|
|
@@ -3199,11 +4192,11 @@ function renderExpr(params) {
|
|
|
3199
4192
|
}
|
|
3200
4193
|
case "tupleIn": {
|
|
3201
4194
|
if (expr.columns.length === 0) {
|
|
3202
|
-
throw
|
|
4195
|
+
throw internalError3({ message: "tupleIn needs at least one column" });
|
|
3203
4196
|
}
|
|
3204
4197
|
for (const row of expr.rows) {
|
|
3205
4198
|
if (row.length !== expr.columns.length) {
|
|
3206
|
-
throw
|
|
4199
|
+
throw internalError3({
|
|
3207
4200
|
message: `tupleIn row has ${row.length} values for ${expr.columns.length} columns`
|
|
3208
4201
|
});
|
|
3209
4202
|
}
|
|
@@ -3224,6 +4217,32 @@ function renderExpr(params) {
|
|
|
3224
4217
|
const base = conjunctions.length === 1 ? conjunctions[0] : `(${conjunctions.join(" OR ")})`;
|
|
3225
4218
|
return expr.negate === true ? `NOT ${base.startsWith("(") ? base : `(${base})`}` : base;
|
|
3226
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
|
+
}
|
|
3227
4246
|
case "like": {
|
|
3228
4247
|
const column = quoteCol(dialect, expr.column);
|
|
3229
4248
|
const placeholder = collector.add(expr.pattern);
|
|
@@ -3252,10 +4271,17 @@ function renderExpr(params) {
|
|
|
3252
4271
|
if (dialect.strictMatchStrategy === "glob") {
|
|
3253
4272
|
base = `${column} GLOB ${collector.add(pattern)}`;
|
|
3254
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;
|
|
3255
4279
|
const placeholder = collector.add(pattern);
|
|
3256
4280
|
base = dialect.strictMatchStrategy === "binary" ? `${column} LIKE CAST(${placeholder} AS BINARY)` : `${column} LIKE ${placeholder}`;
|
|
3257
4281
|
if (dialect.likeEscapeClause !== null)
|
|
3258
4282
|
base = `${base}${dialect.likeEscapeClause}`;
|
|
4283
|
+
if (prefilter !== null)
|
|
4284
|
+
base = `(${prefilter} AND ${base})`;
|
|
3259
4285
|
}
|
|
3260
4286
|
}
|
|
3261
4287
|
return expr.negate === true ? `NOT (${base})` : base;
|
|
@@ -3277,7 +4303,7 @@ function renderExpr(params) {
|
|
|
3277
4303
|
case "aggCmp": {
|
|
3278
4304
|
const op = COMPARISON_SQL2[expr.op];
|
|
3279
4305
|
if (op === undefined) {
|
|
3280
|
-
throw
|
|
4306
|
+
throw internalError3({ message: `unknown comparison operator "${expr.op}"` });
|
|
3281
4307
|
}
|
|
3282
4308
|
return `${renderAggRef(dialect, expr.agg)} ${op} ${collector.add(expr.value)}`;
|
|
3283
4309
|
}
|
|
@@ -3355,10 +4381,146 @@ function listComparison(params) {
|
|
|
3355
4381
|
}
|
|
3356
4382
|
return { column: quoteCol(dialect, column), operand: collector.add(value) };
|
|
3357
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
|
+
}
|
|
3358
4520
|
function jsonStringPattern(params) {
|
|
3359
4521
|
const { expr } = params;
|
|
3360
4522
|
if (typeof expr.value !== "string") {
|
|
3361
|
-
throw
|
|
4523
|
+
throw internalError3({ message: `json ${expr.op} expects a string operand` });
|
|
3362
4524
|
}
|
|
3363
4525
|
if (expr.op === "string_contains")
|
|
3364
4526
|
return containsPattern({ raw: expr.value });
|
|
@@ -3382,7 +4544,7 @@ function renderJsonLike(params) {
|
|
|
3382
4544
|
function renderJsonFilter(params) {
|
|
3383
4545
|
const { dialect, expr, collector } = params;
|
|
3384
4546
|
if (!dialect.supportedJsonOps.includes(expr.op)) {
|
|
3385
|
-
throw
|
|
4547
|
+
throw unsupportedCapability27({
|
|
3386
4548
|
dialect: dialect.name,
|
|
3387
4549
|
feature: `the json filter "${expr.op}"`,
|
|
3388
4550
|
hint: `this dialect can express: ${dialect.supportedJsonOps.join(", ")}`
|
|
@@ -3449,7 +4611,7 @@ function renderJsonFilter(params) {
|
|
|
3449
4611
|
const elements = Array.isArray(expr.value) ? expr.value : [expr.value];
|
|
3450
4612
|
const tests = elements.map((element) => {
|
|
3451
4613
|
if (element === null || typeof element === "object" && element !== null) {
|
|
3452
|
-
throw
|
|
4614
|
+
throw unsupportedCapability27({
|
|
3453
4615
|
dialect: dialect.name,
|
|
3454
4616
|
feature: "json array_contains with object/array/null elements",
|
|
3455
4617
|
hint: "sqlite expresses scalar-element containment only \u2014 deep structural containment needs postgres or mysql"
|
|
@@ -3463,7 +4625,7 @@ function renderJsonFilter(params) {
|
|
|
3463
4625
|
return tests.length === 0 ? guard : `(${[guard, ...tests].join(" AND ")})`;
|
|
3464
4626
|
}
|
|
3465
4627
|
default:
|
|
3466
|
-
throw
|
|
4628
|
+
throw internalError3({ message: `json op "${expr.op}" leaked past supportedJsonOps on sqlite` });
|
|
3467
4629
|
}
|
|
3468
4630
|
}
|
|
3469
4631
|
const column = quoteCol(dialect, expr.column);
|
|
@@ -3518,11 +4680,11 @@ function renderSelectExpr(params) {
|
|
|
3518
4680
|
function renderPositionalRow(params) {
|
|
3519
4681
|
const { dialect, join, subAlias } = params;
|
|
3520
4682
|
if (join.select.columns.length === 0 || join.select.selectExpr !== undefined || (join.select.aggregates?.length ?? 0) > 0 || join.select.rowNumber !== undefined) {
|
|
3521
|
-
throw
|
|
4683
|
+
throw internalError3({ message: "positional lateral rows require an explicit column-only projection" });
|
|
3522
4684
|
}
|
|
3523
4685
|
const names = join.select.columns.map((column) => projectionOutputName({ column }));
|
|
3524
4686
|
if (new Set(names).size !== names.length) {
|
|
3525
|
-
throw
|
|
4687
|
+
throw internalError3({ message: "positional lateral projection has duplicate output names" });
|
|
3526
4688
|
}
|
|
3527
4689
|
const values = names.map((name) => `${subAlias}.${dialect.quoteIdent({ name })}`);
|
|
3528
4690
|
const chunks = [];
|
|
@@ -3530,7 +4692,7 @@ function renderPositionalRow(params) {
|
|
|
3530
4692
|
chunks.push(`json_build_array(${values.slice(index, index + JSON_TRANSPORT.chunkSize).join(", ")})`);
|
|
3531
4693
|
}
|
|
3532
4694
|
if (chunks.length > JSON_TRANSPORT.chunkSize) {
|
|
3533
|
-
throw
|
|
4695
|
+
throw internalError3({ message: "positional lateral projection exceeds the transport width limit" });
|
|
3534
4696
|
}
|
|
3535
4697
|
return chunks.length === 1 ? chunks[0] : `json_build_array(${chunks.join(", ")})`;
|
|
3536
4698
|
}
|
|
@@ -3538,8 +4700,8 @@ function renderLateralJoin(params) {
|
|
|
3538
4700
|
const { dialect, join, collector } = params;
|
|
3539
4701
|
const inner = renderSelect({ dialect, statement: join.select, collector });
|
|
3540
4702
|
if (join.agg === "count") {
|
|
3541
|
-
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) {
|
|
3542
|
-
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" });
|
|
3543
4705
|
}
|
|
3544
4706
|
return `LEFT JOIN LATERAL (${inner.text}) AS ${dialect.quoteIdent({ name: join.alias })} ON true`;
|
|
3545
4707
|
}
|
|
@@ -3570,8 +4732,8 @@ function renderLateralJoin(params) {
|
|
|
3570
4732
|
function renderCountSubquery(params) {
|
|
3571
4733
|
const { dialect, subquery, collector } = params;
|
|
3572
4734
|
const select = subquery.select;
|
|
3573
|
-
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) {
|
|
3574
|
-
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(*)" });
|
|
3575
4737
|
}
|
|
3576
4738
|
const inner = renderSelect({ dialect, statement: select, collector });
|
|
3577
4739
|
return `(${inner.text}) AS ${dialect.quoteIdent({ name: subquery.alias })}`;
|
|
@@ -3601,27 +4763,27 @@ function renderKeyTableJoin(params) {
|
|
|
3601
4763
|
const postgresMember = (member) => member.kind === "citext" || member.kind === "text" || member.kind === "integer";
|
|
3602
4764
|
const admitted = dialect.name === "mysql" ? join.members.every((member) => member.kind !== "citext" && member.kind !== "text") : dialect.name === "postgres" && join.members.every(postgresMember);
|
|
3603
4765
|
if (!admitted) {
|
|
3604
|
-
throw
|
|
4766
|
+
throw unsupportedCapability27({
|
|
3605
4767
|
dialect: dialect.name,
|
|
3606
4768
|
feature: "relation key table",
|
|
3607
4769
|
hint: "the key-table join is mysql's relation association \u2014 this dialect compares relation keys by value"
|
|
3608
4770
|
});
|
|
3609
4771
|
}
|
|
3610
4772
|
if (join.columns.length === 0 || join.columns.length !== join.members.length) {
|
|
3611
|
-
throw
|
|
4773
|
+
throw internalError3({ message: `key table needs one member per column, got ${join.members.length} for ${join.columns.length}` });
|
|
3612
4774
|
}
|
|
3613
4775
|
if (join.rows.length === 0)
|
|
3614
|
-
throw
|
|
4776
|
+
throw internalError3({ message: "key table needs at least one key row" });
|
|
3615
4777
|
for (const member of join.members) {
|
|
3616
4778
|
if (member.kind === "decimal" && !(Number.isInteger(member.scale) && member.scale >= 0 && member.scale <= 30)) {
|
|
3617
|
-
throw
|
|
4779
|
+
throw internalError3({ message: `key table decimal scale must be an integer 0-30, got ${String(member.scale)}` });
|
|
3618
4780
|
}
|
|
3619
4781
|
}
|
|
3620
4782
|
const alias = dialect.quoteIdent({ name: join.alias });
|
|
3621
4783
|
const memberNames = join.members.map((_, index) => dialect.quoteIdent({ name: `__vibe_k${index}` }));
|
|
3622
4784
|
const rows = join.rows.map((row, ordinal) => {
|
|
3623
4785
|
if (row.length !== join.members.length) {
|
|
3624
|
-
throw
|
|
4786
|
+
throw internalError3({ message: `key table row has ${row.length} values for ${join.members.length} members` });
|
|
3625
4787
|
}
|
|
3626
4788
|
const cells = row.map((value, index) => {
|
|
3627
4789
|
const placeholder = collector.add(value);
|
|
@@ -3648,13 +4810,25 @@ function renderKeyTableJoin(params) {
|
|
|
3648
4810
|
const on = join.columns.map((column, index) => `${quoteCol(dialect, column)} = ${alias}.${memberNames[index]}`).join(" AND ");
|
|
3649
4811
|
return `INNER JOIN (VALUES ${rows.join(", ")}) AS ${alias} (${[...memberNames, dialect.quoteIdent({ name: join.ordinalAlias })].join(", ")}) ON ${on}`;
|
|
3650
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
|
+
}
|
|
3651
4825
|
function renderSelect(params) {
|
|
3652
4826
|
const { dialect, statement, collector } = params;
|
|
3653
4827
|
if (statement.selectExpr !== undefined && statement.columns.length > 0 && statement.selectExpr.kind !== "countAll") {
|
|
3654
|
-
throw
|
|
4828
|
+
throw internalError3({ message: "only countAll can combine with a column list" });
|
|
3655
4829
|
}
|
|
3656
4830
|
if (statement.selectExpr !== undefined && statement.aggregates !== undefined && statement.aggregates.length > 0) {
|
|
3657
|
-
throw
|
|
4831
|
+
throw internalError3({ message: "selectExpr and aggregates cannot combine" });
|
|
3658
4832
|
}
|
|
3659
4833
|
const columnList = statement.columns.map((column) => quoteProjection(dialect, column));
|
|
3660
4834
|
if (statement.selectExpr !== undefined) {
|
|
@@ -3675,16 +4849,19 @@ function renderSelect(params) {
|
|
|
3675
4849
|
if (statement.rowNumber !== undefined) {
|
|
3676
4850
|
const rowNumber = statement.rowNumber;
|
|
3677
4851
|
if (rowNumber.partitionBy.length === 0) {
|
|
3678
|
-
throw
|
|
4852
|
+
throw internalError3({ message: "rowNumber needs at least one partition column" });
|
|
3679
4853
|
}
|
|
3680
4854
|
const partition = rowNumber.partitionBy.map((column) => quoteCol(dialect, column)).join(", ");
|
|
3681
4855
|
const order = rowNumber.orderBy.length === 0 ? "" : ` ORDER BY ${rowNumber.orderBy.map((term) => renderOrderTerm({ dialect, term, collector })).join(", ")}`;
|
|
3682
4856
|
columnList.push(`ROW_NUMBER() OVER (PARTITION BY ${partition}${order}) AS ${dialect.quoteIdent({ name: rowNumber.alias })}`);
|
|
3683
4857
|
}
|
|
3684
|
-
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
|
+
}
|
|
3685
4862
|
if (statement.distinctOn !== undefined && statement.distinctOn.length > 0) {
|
|
3686
4863
|
if (!dialect.capabilities.distinctOn) {
|
|
3687
|
-
throw
|
|
4864
|
+
throw unsupportedCapability27({
|
|
3688
4865
|
dialect: dialect.name,
|
|
3689
4866
|
feature: "SELECT DISTINCT ON",
|
|
3690
4867
|
hint: "postgres-only \u2014 the runtime emulates distinct client-side on this dialect"
|
|
@@ -3694,9 +4871,16 @@ function renderSelect(params) {
|
|
|
3694
4871
|
}
|
|
3695
4872
|
const columns = columnList.length === 0 ? "*" : columnList.join(", ");
|
|
3696
4873
|
let from;
|
|
3697
|
-
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) {
|
|
3698
4882
|
if (statement.table.alias !== undefined) {
|
|
3699
|
-
throw
|
|
4883
|
+
throw internalError3({ message: "fromSelect uses table.name as the subquery alias \u2014 table.alias must be unset" });
|
|
3700
4884
|
}
|
|
3701
4885
|
const inner = renderSelect({ dialect, statement: statement.fromSelect, collector });
|
|
3702
4886
|
from = `(${inner.text}) AS ${dialect.quoteIdent({ name: statement.table.name })}`;
|
|
@@ -3707,7 +4891,7 @@ function renderSelect(params) {
|
|
|
3707
4891
|
if (statement.innerJoins !== undefined && statement.innerJoins.length > 0) {
|
|
3708
4892
|
for (const join of statement.innerJoins) {
|
|
3709
4893
|
if (join.on.length === 0) {
|
|
3710
|
-
throw
|
|
4894
|
+
throw internalError3({ message: "inner join needs at least one ON pair" });
|
|
3711
4895
|
}
|
|
3712
4896
|
const on = join.on.map((pair) => `${quoteCol(dialect, pair.left)} = ${quoteCol(dialect, pair.right)}`).join(" AND ");
|
|
3713
4897
|
text += ` INNER JOIN ${renderTable(dialect, join.table)} ON ${on}`;
|
|
@@ -3716,9 +4900,17 @@ function renderSelect(params) {
|
|
|
3716
4900
|
if (statement.keyTable !== undefined) {
|
|
3717
4901
|
text += ` ${renderKeyTableJoin({ dialect, join: statement.keyTable, collector })}`;
|
|
3718
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
|
+
}
|
|
3719
4911
|
if (statement.lateralJoins !== undefined && statement.lateralJoins.length > 0) {
|
|
3720
4912
|
if (!dialect.capabilities.lateralJoin) {
|
|
3721
|
-
throw
|
|
4913
|
+
throw unsupportedCapability27({
|
|
3722
4914
|
dialect: dialect.name,
|
|
3723
4915
|
feature: "LATERAL JOIN relation loading",
|
|
3724
4916
|
hint: 'use the portable "query" relation strategy on this dialect'
|
|
@@ -3759,17 +4951,71 @@ function renderSelect(params) {
|
|
|
3759
4951
|
}
|
|
3760
4952
|
return { text, values: collector.values, returning: "none" };
|
|
3761
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
|
+
}
|
|
3762
5008
|
function renderInsert(params) {
|
|
3763
5009
|
const { dialect, statement, collector } = params;
|
|
3764
5010
|
if (statement.rows.length === 0) {
|
|
3765
|
-
throw
|
|
5011
|
+
throw internalError3({ message: "insert requires at least one row" });
|
|
3766
5012
|
}
|
|
3767
5013
|
const defaultValues = statement.columns.length === 0 && dialect.emptyInsert.form === "defaultValues";
|
|
3768
5014
|
if (defaultValues && statement.rows.length !== 1) {
|
|
3769
|
-
throw
|
|
5015
|
+
throw internalError3({ message: `${dialect.name} inserts an all-default row as DEFAULT VALUES, one row per statement` });
|
|
3770
5016
|
}
|
|
3771
5017
|
if (defaultValues && statement.onConflict !== undefined && !(dialect.emptyInsert.form === "defaultValues" && dialect.emptyInsert.conflictClause)) {
|
|
3772
|
-
throw
|
|
5018
|
+
throw unsupportedCapability27({
|
|
3773
5019
|
dialect: dialect.name,
|
|
3774
5020
|
feature: "a conflict clause on an all-default row (DEFAULT VALUES)",
|
|
3775
5021
|
hint: "supply at least one column value, or drop the conflict clause"
|
|
@@ -3777,7 +5023,7 @@ function renderInsert(params) {
|
|
|
3777
5023
|
}
|
|
3778
5024
|
for (const row of statement.rows) {
|
|
3779
5025
|
if (row.length !== statement.columns.length) {
|
|
3780
|
-
throw
|
|
5026
|
+
throw internalError3({
|
|
3781
5027
|
message: `insert row has ${row.length} values but ${statement.columns.length} columns`
|
|
3782
5028
|
});
|
|
3783
5029
|
}
|
|
@@ -3786,7 +5032,8 @@ function renderInsert(params) {
|
|
|
3786
5032
|
const skipConflicts = onConflict !== undefined && "doNothing" in onConflict;
|
|
3787
5033
|
const insertKeyword = skipConflicts && dialect.doNothingForm === "insertIgnore" ? "INSERT IGNORE INTO" : "INSERT INTO";
|
|
3788
5034
|
const columnList = statement.columns.map((name) => dialect.quoteIdent({ name })).join(", ");
|
|
3789
|
-
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) => {
|
|
3790
5037
|
if (isJsonParamCell(value))
|
|
3791
5038
|
return `${collector.add(value.text)}${jsonCellCast(dialect, value)}`;
|
|
3792
5039
|
if (isDbNowMarker(value)) {
|
|
@@ -3795,13 +5042,13 @@ function renderInsert(params) {
|
|
|
3795
5042
|
if (!isSqlDefaultCell(value))
|
|
3796
5043
|
return collector.add(value);
|
|
3797
5044
|
if (!dialect.rowDefaultKeyword) {
|
|
3798
|
-
throw
|
|
5045
|
+
throw internalError3({
|
|
3799
5046
|
message: `${dialect.name} cannot render a per-row DEFAULT cell \u2014 rows must be grouped by column set`
|
|
3800
5047
|
});
|
|
3801
5048
|
}
|
|
3802
5049
|
return "DEFAULT";
|
|
3803
5050
|
}).join(", ")})`).join(", ");
|
|
3804
|
-
let text = defaultValues ? `${insertKeyword} ${renderTable(dialect, statement.table)} DEFAULT VALUES` : `${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}`}`;
|
|
3805
5052
|
if (onConflict !== undefined && !("doNothing" in onConflict) && dialect.excludedRowRef.form === "rowAlias" && Object.values(onConflict.update).some((value) => isExcludedRef(value) || isSqlFragment(value) && sqlUsesIncoming({ fragment: value }))) {
|
|
3806
5053
|
text += ` AS ${dialect.quoteIdent({ name: dialect.excludedRowRef.alias })}`;
|
|
3807
5054
|
}
|
|
@@ -3811,7 +5058,7 @@ function renderInsert(params) {
|
|
|
3811
5058
|
let target = "";
|
|
3812
5059
|
if (onConflict.constraint !== undefined) {
|
|
3813
5060
|
if (dialect.name !== "postgres") {
|
|
3814
|
-
throw
|
|
5061
|
+
throw unsupportedCapability27({
|
|
3815
5062
|
dialect: dialect.name,
|
|
3816
5063
|
feature: "a named-constraint conflict target (ON CONFLICT ON CONSTRAINT)",
|
|
3817
5064
|
hint: "only postgres names constraints in conflict targets \u2014 use a column-list target"
|
|
@@ -3826,7 +5073,7 @@ function renderInsert(params) {
|
|
|
3826
5073
|
}
|
|
3827
5074
|
text += ` ON CONFLICT${target} DO NOTHING`;
|
|
3828
5075
|
} else if (onConflict.constraint !== undefined || onConflict.conflictWhere !== undefined) {
|
|
3829
|
-
throw
|
|
5076
|
+
throw unsupportedCapability27({
|
|
3830
5077
|
dialect: dialect.name,
|
|
3831
5078
|
feature: "a refined conflict target (constraint / partial-index predicate) on INSERT IGNORE",
|
|
3832
5079
|
hint: "mysql skips on any duplicate key \u2014 drop the refinement or gate the call per dialect"
|
|
@@ -3835,23 +5082,23 @@ function renderInsert(params) {
|
|
|
3835
5082
|
} else {
|
|
3836
5083
|
const updates = Object.entries(onConflict.update);
|
|
3837
5084
|
if (updates.length === 0) {
|
|
3838
|
-
throw
|
|
5085
|
+
throw internalError3({ message: "upsert update must set at least one column" });
|
|
3839
5086
|
}
|
|
3840
5087
|
const selfRef = dialect.quoteIdent({ name: statement.table.name });
|
|
3841
5088
|
const assignments = renderAssignments({ dialect, entries: updates, collector, selfRef, allowExcluded: true, tableName: statement.table.name });
|
|
3842
5089
|
if (dialect.upsertForm === "onConflict") {
|
|
3843
5090
|
if (onConflict.lastInsertIdColumn !== undefined) {
|
|
3844
|
-
throw
|
|
5091
|
+
throw internalError3({ message: "lastInsertIdColumn is a mysql (onDuplicateKey) construct" });
|
|
3845
5092
|
}
|
|
3846
5093
|
if (onConflict.constraint !== undefined && dialect.name !== "postgres") {
|
|
3847
|
-
throw
|
|
5094
|
+
throw unsupportedCapability27({
|
|
3848
5095
|
dialect: dialect.name,
|
|
3849
5096
|
feature: "a named-constraint conflict target (ON CONFLICT ON CONSTRAINT)",
|
|
3850
5097
|
hint: "sqlite conflict targets are column lists \u2014 name the indexed columns instead"
|
|
3851
5098
|
});
|
|
3852
5099
|
}
|
|
3853
5100
|
if (onConflict.constraint !== undefined && onConflict.conflictWhere !== undefined) {
|
|
3854
|
-
throw
|
|
5101
|
+
throw internalError3({
|
|
3855
5102
|
message: "a named-constraint conflict target already identifies the index \u2014 it cannot carry a partial predicate too"
|
|
3856
5103
|
});
|
|
3857
5104
|
}
|
|
@@ -3860,14 +5107,14 @@ function renderInsert(params) {
|
|
|
3860
5107
|
text += ` ON CONFLICT${target} DO UPDATE SET ${assignments}`;
|
|
3861
5108
|
} else {
|
|
3862
5109
|
if (onConflict.constraint !== undefined) {
|
|
3863
|
-
throw
|
|
5110
|
+
throw unsupportedCapability27({
|
|
3864
5111
|
dialect: dialect.name,
|
|
3865
5112
|
feature: "a named-constraint conflict target on ON DUPLICATE KEY UPDATE",
|
|
3866
5113
|
hint: "mysql's ODKU fires on any duplicate key \u2014 no target can be named"
|
|
3867
5114
|
});
|
|
3868
5115
|
}
|
|
3869
5116
|
if (onConflict.conflictWhere !== undefined) {
|
|
3870
|
-
throw
|
|
5117
|
+
throw unsupportedCapability27({
|
|
3871
5118
|
dialect: dialect.name,
|
|
3872
5119
|
feature: "a partial-index conflict target on ON DUPLICATE KEY UPDATE",
|
|
3873
5120
|
hint: "mysql's ODKU fires on any duplicate key \u2014 a partial arbiter cannot be selected; gate the call per dialect"
|
|
@@ -3885,7 +5132,7 @@ function renderUpdate(params) {
|
|
|
3885
5132
|
const { dialect, statement, collector } = params;
|
|
3886
5133
|
const assignments = Object.entries(statement.set);
|
|
3887
5134
|
if (assignments.length === 0) {
|
|
3888
|
-
throw
|
|
5135
|
+
throw internalError3({ message: "update must set at least one column" });
|
|
3889
5136
|
}
|
|
3890
5137
|
const setSql = renderAssignments({ dialect, entries: assignments, collector });
|
|
3891
5138
|
let text = `UPDATE ${renderTable(dialect, statement.table)} SET ${setSql}`;
|
|
@@ -3908,12 +5155,12 @@ function renderAssignments(params) {
|
|
|
3908
5155
|
const read = selfRef === undefined ? column : `${selfRef}.${column}`;
|
|
3909
5156
|
if (isSqlFragment(value)) {
|
|
3910
5157
|
if (params.allowExcluded !== true || selfRef === undefined)
|
|
3911
|
-
throw
|
|
5158
|
+
throw internalError3({ message: "SQL assignment fragments require a conflict update" });
|
|
3912
5159
|
return `${column} = ${renderSqlFragment({ dialect, fragment: value, collector, conflict: { table: params.tableName } })}`;
|
|
3913
5160
|
}
|
|
3914
5161
|
if (isExcludedRef(value)) {
|
|
3915
5162
|
if (params.allowExcluded !== true) {
|
|
3916
|
-
throw
|
|
5163
|
+
throw internalError3({
|
|
3917
5164
|
message: `an incoming-row reference (${value.column}) is only meaningful inside an upsert's update arm`
|
|
3918
5165
|
});
|
|
3919
5166
|
}
|
|
@@ -3954,19 +5201,19 @@ var CHAIN_ONE_ALIAS = "__vibe_one";
|
|
|
3954
5201
|
function renderChain(params) {
|
|
3955
5202
|
const { dialect, statement, collector } = params;
|
|
3956
5203
|
if (dialect.statementChain === null) {
|
|
3957
|
-
throw
|
|
5204
|
+
throw unsupportedCapability27({
|
|
3958
5205
|
dialect: dialect.name,
|
|
3959
5206
|
feature: "a statement chain (data-modifying WITH steps)",
|
|
3960
5207
|
hint: "this dialect accepts only SELECT inside WITH \u2014 gate on SqlDialect.statementChain and keep the multi-statement path"
|
|
3961
5208
|
});
|
|
3962
5209
|
}
|
|
3963
5210
|
if (statement.steps.length === 0)
|
|
3964
|
-
throw
|
|
5211
|
+
throw internalError3({ message: "a statement chain needs at least one step" });
|
|
3965
5212
|
const defined = new Set;
|
|
3966
5213
|
const steps = [];
|
|
3967
5214
|
for (const step of statement.steps) {
|
|
3968
5215
|
if (step.name === CHAIN_ROWS_ALIAS || step.name === CHAIN_ONE_ALIAS || defined.has(step.name)) {
|
|
3969
|
-
throw
|
|
5216
|
+
throw internalError3({ message: `chain step name "${step.name}" is reserved or repeated` });
|
|
3970
5217
|
}
|
|
3971
5218
|
const body = renderChainStep({ dialect, statement: step.statement, collector, earlier: defined });
|
|
3972
5219
|
steps.push(`${dialect.quoteIdent({ name: step.name })} AS (${body})`);
|
|
@@ -3974,11 +5221,11 @@ function renderChain(params) {
|
|
|
3974
5221
|
}
|
|
3975
5222
|
const result = statement.result;
|
|
3976
5223
|
if (result.rowsFrom.length === 0 || result.columns.length === 0) {
|
|
3977
|
-
throw
|
|
5224
|
+
throw internalError3({ message: "a chain result needs at least one source step and one column" });
|
|
3978
5225
|
}
|
|
3979
5226
|
for (const name of result.rowsFrom) {
|
|
3980
5227
|
if (!defined.has(name))
|
|
3981
|
-
throw
|
|
5228
|
+
throw internalError3({ message: `chain result reads undefined step "${name}"` });
|
|
3982
5229
|
}
|
|
3983
5230
|
const columnList = result.columns.map((name) => dialect.quoteIdent({ name })).join(", ");
|
|
3984
5231
|
const union = result.rowsFrom.map((name) => `SELECT ${columnList} FROM ${dialect.quoteIdent({ name })}`).join(" UNION ALL ");
|
|
@@ -3987,7 +5234,7 @@ function renderChain(params) {
|
|
|
3987
5234
|
const extra = [];
|
|
3988
5235
|
for (const alias of [result.verdict?.alias, result.tally?.alias]) {
|
|
3989
5236
|
if (alias !== undefined && result.columns.includes(alias)) {
|
|
3990
|
-
throw
|
|
5237
|
+
throw internalError3({ message: `chain result alias "${alias}" collides with a result column` });
|
|
3991
5238
|
}
|
|
3992
5239
|
}
|
|
3993
5240
|
const rows = dialect.quoteIdent({ name: CHAIN_ROWS_ALIAS });
|
|
@@ -4024,21 +5271,21 @@ function renderSelectCell(params) {
|
|
|
4024
5271
|
if (isDbNowMarker(value))
|
|
4025
5272
|
return dialect.renderDbNow({ offsetSeconds: value.offsetSeconds, collector });
|
|
4026
5273
|
if (isSqlDefaultCell(value)) {
|
|
4027
|
-
throw
|
|
5274
|
+
throw internalError3({ message: "DEFAULT is not allowed in an INSERT \u2026 SELECT list \u2014 omit the column instead" });
|
|
4028
5275
|
}
|
|
4029
5276
|
return collector.add(value);
|
|
4030
5277
|
}
|
|
4031
5278
|
function renderRowSource(params) {
|
|
4032
5279
|
const { dialect, source, collector } = params;
|
|
4033
5280
|
if (source.columns.length === 0 || source.rows.length === 0) {
|
|
4034
|
-
throw
|
|
5281
|
+
throw internalError3({ message: "a row source needs at least one column and one row" });
|
|
4035
5282
|
}
|
|
4036
5283
|
const typedBy = renderTable(dialect, source.typedBy);
|
|
4037
5284
|
const columns = source.columns.map((name) => dialect.quoteIdent({ name }));
|
|
4038
5285
|
const rows = [`(${columns.map((column) => `(SELECT ${column} FROM ${typedBy} WHERE FALSE)`).join(", ")})`];
|
|
4039
5286
|
for (const row of source.rows) {
|
|
4040
5287
|
if (row.length !== source.columns.length) {
|
|
4041
|
-
throw
|
|
5288
|
+
throw internalError3({ message: `row source row has ${row.length} values but ${source.columns.length} columns` });
|
|
4042
5289
|
}
|
|
4043
5290
|
rows.push(`(${row.map((value) => renderSelectCell({ dialect, value, collector })).join(", ")})`);
|
|
4044
5291
|
}
|
|
@@ -4050,12 +5297,12 @@ function renderTallyColumn(params) {
|
|
|
4050
5297
|
const { dialect, result, defined, collector } = params;
|
|
4051
5298
|
const tally = result.tally;
|
|
4052
5299
|
if (tally === undefined)
|
|
4053
|
-
throw
|
|
5300
|
+
throw internalError3({ message: "a chain veto needs a tally column to guard" });
|
|
4054
5301
|
if (tally.steps.length === 0)
|
|
4055
|
-
throw
|
|
5302
|
+
throw internalError3({ message: "a chain tally needs at least one step" });
|
|
4056
5303
|
const step = (name) => {
|
|
4057
5304
|
if (!defined.has(name))
|
|
4058
|
-
throw
|
|
5305
|
+
throw internalError3({ message: `chain tally or veto reads undefined step "${name}"` });
|
|
4059
5306
|
return dialect.quoteIdent({ name });
|
|
4060
5307
|
};
|
|
4061
5308
|
const count = (name) => `(SELECT count(*) FROM ${step(name)})`;
|
|
@@ -4065,12 +5312,12 @@ function renderTallyColumn(params) {
|
|
|
4065
5312
|
return `${sum} AS ${dialect.quoteIdent({ name: tally.alias })}`;
|
|
4066
5313
|
const arms = vetoes.map((veto) => {
|
|
4067
5314
|
if (!CHAIN_VETO_TOKEN_PATTERN.test(veto.token))
|
|
4068
|
-
throw
|
|
5315
|
+
throw internalError3({ message: `chain veto token "${veto.token}" is not a lower-case word` });
|
|
4069
5316
|
if (typeof veto.nonce !== "string" || veto.nonce === "")
|
|
4070
|
-
throw
|
|
5317
|
+
throw internalError3({ message: `chain veto "${veto.token}" needs a non-empty nonce` });
|
|
4071
5318
|
const minRows = veto.minRows ?? 1;
|
|
4072
5319
|
if (!Number.isSafeInteger(minRows) || minRows < 1)
|
|
4073
|
-
throw
|
|
5320
|
+
throw internalError3({ message: `chain veto "${veto.token}" needs a positive integer minRows` });
|
|
4074
5321
|
const when = minRows === 1 ? `WHEN NOT EXISTS (SELECT 1 FROM ${step(veto.step)})` : `WHEN ${count(veto.step)} < ${minRows}`;
|
|
4075
5322
|
return `${when} THEN CAST('${CHAIN_VETO_PREFIX}${veto.token}:' || ${collector.add(veto.nonce)} || ':' || ${count(veto.step)} AS integer)`;
|
|
4076
5323
|
});
|
|
@@ -4079,25 +5326,25 @@ function renderTallyColumn(params) {
|
|
|
4079
5326
|
function renderInsertSelect(params) {
|
|
4080
5327
|
const { dialect, statement, collector } = params;
|
|
4081
5328
|
if (statement.columns.length === 0)
|
|
4082
|
-
throw
|
|
5329
|
+
throw internalError3({ message: "insert requires at least one column" });
|
|
4083
5330
|
if (statement.cells.length !== statement.columns.length) {
|
|
4084
|
-
throw
|
|
5331
|
+
throw internalError3({
|
|
4085
5332
|
message: `insert row has ${statement.cells.length} values but ${statement.columns.length} columns`
|
|
4086
5333
|
});
|
|
4087
5334
|
}
|
|
4088
5335
|
const from = statement.from ?? [];
|
|
4089
5336
|
for (const name of from) {
|
|
4090
5337
|
if (!params.earlier.has(name))
|
|
4091
|
-
throw
|
|
5338
|
+
throw internalError3({ message: `INSERT \u2026 SELECT reads step "${name}" before it is defined` });
|
|
4092
5339
|
}
|
|
4093
5340
|
const source = statement.source;
|
|
4094
5341
|
if (source !== undefined && (params.earlier.has(source.alias) || from.includes(source.alias))) {
|
|
4095
|
-
throw
|
|
5342
|
+
throw internalError3({ message: `row source alias "${source.alias}" collides with a chain step` });
|
|
4096
5343
|
}
|
|
4097
5344
|
const cells = statement.cells.map((value) => {
|
|
4098
5345
|
if (isStepColumnRef(value)) {
|
|
4099
5346
|
if (!from.includes(value.step) && value.step !== source?.alias) {
|
|
4100
|
-
throw
|
|
5347
|
+
throw internalError3({ message: `cell reads step "${value.step}", which the INSERT \u2026 SELECT does not name in FROM` });
|
|
4101
5348
|
}
|
|
4102
5349
|
return `${dialect.quoteIdent({ name: value.step })}.${dialect.quoteIdent({ name: value.column })}`;
|
|
4103
5350
|
}
|
|
@@ -4116,7 +5363,7 @@ function renderInsertSelect(params) {
|
|
|
4116
5363
|
return text + applyReturning({ dialect, requested: statement.returning }).suffix;
|
|
4117
5364
|
}
|
|
4118
5365
|
// packages/sql/src/dialects/postgres-type-identity.ts
|
|
4119
|
-
import { unsupportedCapability as
|
|
5366
|
+
import { unsupportedCapability as unsupportedCapability28 } from "@vibeorm/schema";
|
|
4120
5367
|
var IDENTIFIER = '(?:"(?:[^"\\x00]|"")+"|[a-zA-Z_][a-zA-Z_0-9$]*)';
|
|
4121
5368
|
var MODIFIER_VALUE = `(?:[+-]?\\d+|${IDENTIFIER}|'(?:[^'\\x00]|'')*')`;
|
|
4122
5369
|
var TYPE_NAME = new RegExp(`^\\s*(${IDENTIFIER})(?:\\s*\\.\\s*(${IDENTIFIER}))?\\s*(\\(\\s*${MODIFIER_VALUE}(?:\\s*,\\s*${MODIFIER_VALUE})*\\s*\\))?\\s*((?:\\[\\s*\\]\\s*)*)$`);
|
|
@@ -4143,13 +5390,13 @@ function parsePostgresTypeName(params) {
|
|
|
4143
5390
|
}
|
|
4144
5391
|
function renderPostgresTypeName(params) {
|
|
4145
5392
|
if (params.dialect.name !== "postgres")
|
|
4146
|
-
throw
|
|
5393
|
+
throw unsupportedCapability28({ dialect: params.dialect.name, feature: "postgresTypeIdentity" });
|
|
4147
5394
|
const { dialect, type } = params;
|
|
4148
5395
|
return `${type.namespace === undefined ? "" : `${dialect.quoteIdent({ name: type.namespace })}.`}${dialect.quoteIdent({ name: type.name })}${type.modifier}${type.array ? "[]" : ""}`;
|
|
4149
5396
|
}
|
|
4150
5397
|
function renderPostgresTypeIdentity(params) {
|
|
4151
5398
|
if (params.dialect.name !== "postgres")
|
|
4152
|
-
throw
|
|
5399
|
+
throw unsupportedCapability28({ dialect: params.dialect.name, feature: "postgresTypeIdentity" });
|
|
4153
5400
|
const collector = new ParamCollector(params.dialect);
|
|
4154
5401
|
const { source } = params;
|
|
4155
5402
|
let text;
|
|
@@ -4162,11 +5409,11 @@ function renderPostgresTypeIdentity(params) {
|
|
|
4162
5409
|
return { text, values: collector.values, returning: "none" };
|
|
4163
5410
|
}
|
|
4164
5411
|
// packages/sql/src/dialects/postgres-default-identity.ts
|
|
4165
|
-
import { unsupportedCapability as
|
|
5412
|
+
import { unsupportedCapability as unsupportedCapability29 } from "@vibeorm/schema";
|
|
4166
5413
|
function renderPostgresDefaultFunctionIdentity(params) {
|
|
4167
5414
|
const { dialect, table, column } = params;
|
|
4168
5415
|
if (dialect.name !== "postgres")
|
|
4169
|
-
throw
|
|
5416
|
+
throw unsupportedCapability29({ dialect: dialect.name, feature: "defaultFunctionIdentity" });
|
|
4170
5417
|
const collector = new ParamCollector(dialect);
|
|
4171
5418
|
const namespace = collector.add(table.namespace);
|
|
4172
5419
|
const name = collector.add(table.name);
|
|
@@ -4190,36 +5437,36 @@ WHERE n.nspname = ${namespace} AND c.relname = ${name} AND a.attname = ${field}
|
|
|
4190
5437
|
};
|
|
4191
5438
|
}
|
|
4192
5439
|
// packages/sql/src/dialects/constraint-owned-index.ts
|
|
4193
|
-
import { unsupportedCapability as
|
|
5440
|
+
import { unsupportedCapability as unsupportedCapability30 } from "@vibeorm/schema";
|
|
4194
5441
|
function renderConstraintOwnedIndexDrop(params) {
|
|
4195
5442
|
if (params.dialect.name !== "postgres")
|
|
4196
|
-
throw
|
|
5443
|
+
throw unsupportedCapability30({ dialect: params.dialect.name, feature: "constraintOwnedIndexDrop" });
|
|
4197
5444
|
const quote = (name) => params.dialect.quoteIdent({ name });
|
|
4198
5445
|
return { text: `ALTER TABLE ${quote(params.table)} DROP CONSTRAINT ${quote(params.constraint)};`, values: [], returning: "none" };
|
|
4199
5446
|
}
|
|
4200
5447
|
// packages/sql/src/dialects/mysql-check-probe.ts
|
|
4201
|
-
import { unsupportedCapability as
|
|
5448
|
+
import { unsupportedCapability as unsupportedCapability31 } from "@vibeorm/schema";
|
|
4202
5449
|
function renderMysqlCheckProbe(params) {
|
|
4203
5450
|
if (params.dialect.name !== "mysql")
|
|
4204
|
-
throw
|
|
5451
|
+
throw unsupportedCapability31({ dialect: params.dialect.name, feature: "temporaryCheckProbe" });
|
|
4205
5452
|
const q = (name) => params.dialect.quoteIdent({ name });
|
|
4206
5453
|
const probe = q(params.probe);
|
|
4207
5454
|
switch (params.operation) {
|
|
4208
5455
|
case "clone":
|
|
4209
5456
|
if (params.table === undefined)
|
|
4210
|
-
throw
|
|
5457
|
+
throw unsupportedCapability31({ dialect: "mysql", feature: "temporaryCheckProbe", hint: "A source table is required" });
|
|
4211
5458
|
return { text: `CREATE TEMPORARY TABLE ${probe} LIKE ${q(params.table.namespace)}.${q(params.table.name)}`, values: [], returning: "none" };
|
|
4212
5459
|
case "add": {
|
|
4213
5460
|
if (params.expression === undefined || params.definition === undefined)
|
|
4214
|
-
throw
|
|
4215
|
-
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 });
|
|
4216
5463
|
return { text: `CREATE TEMPORARY TABLE ${probe} (
|
|
4217
5464
|
${[...parts.clauses, `CONSTRAINT ${probe} CHECK (${params.expression})`].join(`,
|
|
4218
5465
|
`)}
|
|
4219
5466
|
)${parts.suffix}`, values: [], returning: "none" };
|
|
4220
5467
|
}
|
|
4221
5468
|
case "read":
|
|
4222
|
-
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" };
|
|
4223
5470
|
case "drop":
|
|
4224
5471
|
return { text: `DROP TEMPORARY TABLE ${probe}`, values: [], returning: "none" };
|
|
4225
5472
|
}
|
|
@@ -4257,31 +5504,31 @@ function sqliteTableConstraints(params) {
|
|
|
4257
5504
|
return result;
|
|
4258
5505
|
}
|
|
4259
5506
|
// packages/sql/src/dialects/migration-visibility.ts
|
|
4260
|
-
import { unsupportedCapability as
|
|
5507
|
+
import { unsupportedCapability as unsupportedCapability32 } from "@vibeorm/schema";
|
|
4261
5508
|
function renderMigrationVisibility(params) {
|
|
4262
5509
|
if (params.dialect.name !== "mysql")
|
|
4263
|
-
throw
|
|
5510
|
+
throw unsupportedCapability32({ dialect: params.dialect.name, feature: "mysqlCatalogVisibility" });
|
|
4264
5511
|
const collector = new ParamCollector(params.dialect);
|
|
4265
5512
|
const grantee = "CONCAT(QUOTE(SUBSTRING_INDEX(CURRENT_USER(), '@', 1)), '@', QUOTE(SUBSTRING_INDEX(CURRENT_USER(), '@', -1)))";
|
|
4266
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" };
|
|
4267
5514
|
}
|
|
4268
5515
|
// packages/sql/src/dialects/migration-inventory.ts
|
|
4269
|
-
import { unsupportedCapability as
|
|
5516
|
+
import { unsupportedCapability as unsupportedCapability33 } from "@vibeorm/schema";
|
|
4270
5517
|
function renderSqliteColumnInventory(params) {
|
|
4271
5518
|
if (params.dialect.name !== "sqlite")
|
|
4272
|
-
throw
|
|
5519
|
+
throw unsupportedCapability33({ dialect: params.dialect.name, feature: "sqliteColumnInventory" });
|
|
4273
5520
|
const collector = new ParamCollector(params.dialect);
|
|
4274
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" };
|
|
4275
5522
|
}
|
|
4276
5523
|
function renderSqliteForeignKeyInventory(params) {
|
|
4277
5524
|
if (params.dialect.name !== "sqlite")
|
|
4278
|
-
throw
|
|
5525
|
+
throw unsupportedCapability33({ dialect: params.dialect.name, feature: "sqliteForeignKeyInventory" });
|
|
4279
5526
|
const collector = new ParamCollector(params.dialect);
|
|
4280
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" };
|
|
4281
5528
|
}
|
|
4282
5529
|
function renderSqliteRebuildObjects(params) {
|
|
4283
5530
|
if (params.dialect.name !== "sqlite")
|
|
4284
|
-
throw
|
|
5531
|
+
throw unsupportedCapability33({ dialect: params.dialect.name, feature: "sqliteRebuildObjects" });
|
|
4285
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" };
|
|
4286
5533
|
}
|
|
4287
5534
|
function renderMigrationInventory(params) {
|
|
@@ -4320,7 +5567,7 @@ ORDER BY kind, parent, name`, values: collector.values, returning: "none" };
|
|
|
4320
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" };
|
|
4321
5568
|
}
|
|
4322
5569
|
// packages/sql/src/dialects/migration-objects.ts
|
|
4323
|
-
import { internalError as
|
|
5570
|
+
import { internalError as internalError4, unsupportedCapability as unsupportedCapability34 } from "@vibeorm/schema";
|
|
4324
5571
|
function renderMigrationObjectQuery(params) {
|
|
4325
5572
|
return objectQuery({ ...params, collector: new ParamCollector(params.dialect) });
|
|
4326
5573
|
}
|
|
@@ -4328,7 +5575,7 @@ function objectQuery(params) {
|
|
|
4328
5575
|
const { dialect, identity: object, collector } = params;
|
|
4329
5576
|
if (object.kind === "namespace") {
|
|
4330
5577
|
if (dialect.name !== "postgres")
|
|
4331
|
-
throw
|
|
5578
|
+
throw unsupportedCapability34({ dialect: dialect.name, feature: "fixedNamespaces" });
|
|
4332
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" };
|
|
4333
5580
|
}
|
|
4334
5581
|
const physical = "table" in object ? object.table : object.name;
|
|
@@ -4392,7 +5639,7 @@ function objectQuery(params) {
|
|
|
4392
5639
|
break;
|
|
4393
5640
|
case "constraint":
|
|
4394
5641
|
if (object.constraintType === "exclusion")
|
|
4395
|
-
throw
|
|
5642
|
+
throw unsupportedCapability34({ dialect: "mysql", feature: "exclusionConstraints" });
|
|
4396
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)}`;
|
|
4397
5644
|
break;
|
|
4398
5645
|
case "index":
|
|
@@ -4402,22 +5649,22 @@ function objectQuery(params) {
|
|
|
4402
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}`;
|
|
4403
5650
|
break;
|
|
4404
5651
|
default:
|
|
4405
|
-
throw
|
|
5652
|
+
throw unsupportedCapability34({ dialect: "mysql", feature: `inspect:${object.kind}` });
|
|
4406
5653
|
}
|
|
4407
5654
|
} else {
|
|
4408
5655
|
if (physical.namespace !== "main" && physical.namespace !== "temp")
|
|
4409
|
-
throw
|
|
5656
|
+
throw unsupportedCapability34({ dialect: "sqlite", feature: "attachedSchemaInspection" });
|
|
4410
5657
|
const catalog = `${dialect.quoteIdent({ name: physical.namespace })}.sqlite_schema`;
|
|
4411
5658
|
const kind = object.kind === "table" || object.kind === "view" ? object.kind : object.kind === "trigger" ? "trigger" : object.kind === "index" ? "index" : "table";
|
|
4412
5659
|
if (["function", "sequence", "postgresExtension", "enum"].includes(object.kind) || object.kind === "constraint" && object.constraintType === "exclusion")
|
|
4413
|
-
throw
|
|
5660
|
+
throw unsupportedCapability34({ dialect: "sqlite", feature: `inspect:${object.kind}` });
|
|
4414
5661
|
const sqliteParams = new ParamCollector(dialect);
|
|
4415
5662
|
if (object.kind === "column" || object.kind === "columnDefault" || object.kind === "generatedColumn") {
|
|
4416
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)}`;
|
|
4417
5664
|
return { text, values: sqliteParams.values, returning: "none" };
|
|
4418
5665
|
}
|
|
4419
5666
|
const objectName = object.kind === "trigger" || object.kind === "index" ? object.name : physical.name;
|
|
4420
|
-
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)}` : ""}`;
|
|
4421
5668
|
return { text, values: sqliteParams.values, returning: "none" };
|
|
4422
5669
|
}
|
|
4423
5670
|
return { text, values: collector.values, returning: "none" };
|
|
@@ -4440,7 +5687,7 @@ function renderMigrationDependencies(params) {
|
|
|
4440
5687
|
}
|
|
4441
5688
|
function dependencyQuery(params) {
|
|
4442
5689
|
if (params.dialect.name !== "postgres")
|
|
4443
|
-
throw
|
|
5690
|
+
throw unsupportedCapability34({ dialect: params.dialect.name, feature: "catalogDependencies" });
|
|
4444
5691
|
const object = params.identity;
|
|
4445
5692
|
const collector = params.collector;
|
|
4446
5693
|
if (object.kind === "namespace") {
|
|
@@ -4497,7 +5744,7 @@ function inspectionReferences(params) {
|
|
|
4497
5744
|
function renderMigrationObjectBatch(params) {
|
|
4498
5745
|
const { dialect } = params;
|
|
4499
5746
|
if (dialect.name !== "postgres")
|
|
4500
|
-
throw
|
|
5747
|
+
throw unsupportedCapability34({ dialect: dialect.name, feature: "batchedMigrationInspection" });
|
|
4501
5748
|
const groups = new Map;
|
|
4502
5749
|
for (const [position, identity] of params.identities.entries()) {
|
|
4503
5750
|
const kind = identity.kind === "constraint" ? `${identity.kind}:${identity.constraintType}` : identity.kind === "generatedColumn" ? "columnDefault" : identity.kind;
|
|
@@ -4510,7 +5757,7 @@ function renderMigrationObjectBatch(params) {
|
|
|
4510
5757
|
const facts = objectQuery({ dialect, identity, collector: inspectionReferences({ dialect }) });
|
|
4511
5758
|
const dependencies = dependencyQuery({ dialect, identity, collector: inspectionReferences({ dialect }) });
|
|
4512
5759
|
if (JSON.stringify(dependencies.values) !== JSON.stringify(facts.values))
|
|
4513
|
-
throw
|
|
5760
|
+
throw internalError4({ message: "Batched catalog queries bind their identity in different orders", meta: { kind: identity.kind } });
|
|
4514
5761
|
const columns = facts.values.map((value, index) => `${dialect.quoteIdent({ name: `p${index + 1}` })} ${Array.isArray(value) ? "text[]" : "text"}`);
|
|
4515
5762
|
const requests = group.map(({ identity, position }) => ({ position, ...Object.fromEntries(renderMigrationObjectQuery({ dialect, identity }).values.map((value, index) => [`p${index + 1}`, value])) }));
|
|
4516
5763
|
const collector = new ParamCollector(dialect);
|
|
@@ -4530,16 +5777,16 @@ function renderMigrationNamespace(params) {
|
|
|
4530
5777
|
}
|
|
4531
5778
|
function renderCheckProbe(params) {
|
|
4532
5779
|
if (params.dialect.name !== "postgres")
|
|
4533
|
-
throw
|
|
5780
|
+
throw unsupportedCapability34({ dialect: params.dialect.name, feature: "rollbackCheckProbe" });
|
|
4534
5781
|
const q = (name) => params.dialect.quoteIdent({ name });
|
|
4535
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" };
|
|
4536
5783
|
}
|
|
4537
5784
|
// packages/sql/src/dialects/postgres-migration.ts
|
|
4538
|
-
import { VibeError as
|
|
5785
|
+
import { VibeError as VibeError23, unsupportedCapability as unsupportedCapability35 } from "@vibeorm/schema";
|
|
4539
5786
|
var SETTING_VALUE = /^[0-9]+(?:\.[0-9]+)?\s*(?:us|ms|s|min|h|d)?$/;
|
|
4540
5787
|
function requirePostgres2(params) {
|
|
4541
5788
|
if (params.dialect.name !== "postgres") {
|
|
4542
|
-
throw
|
|
5789
|
+
throw unsupportedCapability35({ dialect: params.dialect.name, feature: params.feature });
|
|
4543
5790
|
}
|
|
4544
5791
|
}
|
|
4545
5792
|
function renderMigrationSessionRead(params) {
|
|
@@ -4554,7 +5801,7 @@ function renderMigrationSessionSet(params) {
|
|
|
4554
5801
|
requirePostgres2({ dialect: params.dialect, feature: "migrationSessionSettings" });
|
|
4555
5802
|
const { milliseconds, setting } = params;
|
|
4556
5803
|
if (!Number.isSafeInteger(milliseconds) || milliseconds < 0) {
|
|
4557
|
-
throw new
|
|
5804
|
+
throw new VibeError23({
|
|
4558
5805
|
code: "VIBE_MIGRATION",
|
|
4559
5806
|
message: `${setting} must be a non-negative integer number of milliseconds (got ${String(milliseconds)})`,
|
|
4560
5807
|
meta: { reason: "execution-metadata-invalid", setting, value: milliseconds }
|
|
@@ -4672,6 +5919,7 @@ export {
|
|
|
4672
5919
|
SQLITE_SESSION_SQL,
|
|
4673
5920
|
SQL_DEFAULT_CELL,
|
|
4674
5921
|
SQL_DIALECTS,
|
|
5922
|
+
assertIndexExpressionVersion,
|
|
4675
5923
|
assertSqlParamBuilding,
|
|
4676
5924
|
chainVetoToken,
|
|
4677
5925
|
cloneTimestampDate,
|
|
@@ -4699,6 +5947,9 @@ export {
|
|
|
4699
5947
|
escapeGlobPattern,
|
|
4700
5948
|
escapeLikePattern,
|
|
4701
5949
|
excludedRef,
|
|
5950
|
+
extractIndexKeys,
|
|
5951
|
+
extractMysqlGeneratedColumn,
|
|
5952
|
+
extractSqliteGeneratedColumn,
|
|
4702
5953
|
float32Codec,
|
|
4703
5954
|
getSqlDialect,
|
|
4704
5955
|
isDbNowMarker,
|
|
@@ -4720,6 +5971,8 @@ export {
|
|
|
4720
5971
|
isStepColumnRef,
|
|
4721
5972
|
jsonListParameter,
|
|
4722
5973
|
jsonParamCell,
|
|
5974
|
+
keyListBindsUntypedRows,
|
|
5975
|
+
localDateCodec,
|
|
4723
5976
|
mapConflictColumns,
|
|
4724
5977
|
mysqlDialect,
|
|
4725
5978
|
nativeEnumArrayParameter,
|
|
@@ -4729,7 +5982,9 @@ export {
|
|
|
4729
5982
|
parsePgArrayLiteralNested,
|
|
4730
5983
|
parsePostgresTypeName,
|
|
4731
5984
|
postgresArrayElementFromText,
|
|
5985
|
+
postgresBulkInsertArrayCastOf,
|
|
4732
5986
|
postgresDialect,
|
|
5987
|
+
postgresKeyListElementTypeOf,
|
|
4733
5988
|
projectionOutputName,
|
|
4734
5989
|
recordSqlBinding,
|
|
4735
5990
|
recordSqlBindingCheck,
|
|
@@ -4748,8 +6003,12 @@ export {
|
|
|
4748
6003
|
renderExternalDependents,
|
|
4749
6004
|
renderExternalOwnershipCatalog,
|
|
4750
6005
|
renderFragment,
|
|
6006
|
+
renderGeneratedColumnClause,
|
|
4751
6007
|
renderIndexColumns,
|
|
6008
|
+
renderIndexExpressionProbe,
|
|
6009
|
+
renderIndexKeys,
|
|
4752
6010
|
renderIndexStateQuery,
|
|
6011
|
+
renderLocalDateCheck,
|
|
4753
6012
|
renderMigrationDependencies,
|
|
4754
6013
|
renderMigrationHistoryQuery,
|
|
4755
6014
|
renderMigrationInventory,
|
|
@@ -4764,10 +6023,15 @@ export {
|
|
|
4764
6023
|
renderModuleDependencyQuery,
|
|
4765
6024
|
renderModuleJournal,
|
|
4766
6025
|
renderMysqlCheckProbe,
|
|
6026
|
+
renderMysqlGeneratedColumnProbe,
|
|
4767
6027
|
renderMysqlObjectMembers,
|
|
4768
6028
|
renderPostgresDefaultFunctionIdentity,
|
|
6029
|
+
renderPostgresDropGeneratedExpression,
|
|
4769
6030
|
renderPostgresExplain,
|
|
6031
|
+
renderPostgresGeneratedColumnProbe,
|
|
4770
6032
|
renderPostgresIndexKeys,
|
|
6033
|
+
renderPostgresJitRestore,
|
|
6034
|
+
renderPostgresJitSuspend,
|
|
4771
6035
|
renderPostgresPhysicalCatalog,
|
|
4772
6036
|
renderPostgresTypeIdentity,
|
|
4773
6037
|
renderPostgresTypeName,
|
|
@@ -4790,6 +6054,8 @@ export {
|
|
|
4790
6054
|
renderRlsReadinessQuery,
|
|
4791
6055
|
renderRlsRole,
|
|
4792
6056
|
renderRowLockClause,
|
|
6057
|
+
renderRowRule,
|
|
6058
|
+
renderSchemaExpression,
|
|
4793
6059
|
renderShadowOperation,
|
|
4794
6060
|
renderSqlFragment,
|
|
4795
6061
|
renderSqliteColumnInventory,
|
|
@@ -4797,6 +6063,7 @@ export {
|
|
|
4797
6063
|
renderSqliteFtsUpdateTrigger,
|
|
4798
6064
|
renderSqliteRebuildObjects,
|
|
4799
6065
|
renderStatement,
|
|
6066
|
+
resolveIndexKeys,
|
|
4800
6067
|
resultScalarContract,
|
|
4801
6068
|
returningColumnName,
|
|
4802
6069
|
sameResultDecoder,
|
|
@@ -4813,6 +6080,7 @@ export {
|
|
|
4813
6080
|
sqliteTableConstraints,
|
|
4814
6081
|
startsWithPattern,
|
|
4815
6082
|
stepColumnRef,
|
|
6083
|
+
strictMatchCollationPrefilter,
|
|
4816
6084
|
strictMatchPattern,
|
|
4817
6085
|
tableDefinitionParts,
|
|
4818
6086
|
timeOfDayCodec,
|
|
@@ -4821,4 +6089,4 @@ export {
|
|
|
4821
6089
|
toPgArrayLiteral
|
|
4822
6090
|
};
|
|
4823
6091
|
|
|
4824
|
-
//# debugId=
|
|
6092
|
+
//# debugId=75BF4FE272330AE564756E2164756E21
|