@shapio/local 0.5.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/LICENSE +201 -0
- package/dist/advisoryLocks-COlJQc1r.js +4181 -0
- package/dist/advisoryLocks-COlJQc1r.js.map +1 -0
- package/dist/errors-SK0PwPKC.js +26 -0
- package/dist/errors-SK0PwPKC.js.map +1 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +4481 -0
- package/dist/index.js.map +1 -0
- package/dist/migrator-CIKnDfZy.js +4152 -0
- package/dist/migrator-CIKnDfZy.js.map +1 -0
- package/dist/next.d.ts +10 -0
- package/dist/next.js +40 -0
- package/dist/next.js.map +1 -0
- package/dist/nodeRuntime-DzuiHUhY.js +12 -0
- package/dist/nodeRuntime-DzuiHUhY.js.map +1 -0
- package/dist/s3Adapter-BqCKtI3Y.js +143 -0
- package/dist/s3Adapter-BqCKtI3Y.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,4181 @@
|
|
|
1
|
+
import { AliasNode, AndNode, BinaryOperationNode, ColumnNode, ColumnUpdateNode, IdentifierNode, InsertQueryNode, Kysely, ListNode, MysqlAdapter, MysqlIntrospector, MysqlQueryCompiler, OnDuplicateKeyNode, OperationNodeTransformer, OperatorNode, OrNode, OrderByItemNode, ParensNode, PrimitiveValueListNode, RawNode, ReferenceNode, SelectAllNode, SelectModifierNode, SelectQueryNode, SelectionNode, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler, TableNode, UpdateQueryNode, ValueListNode, ValueNode, ValuesNode, WhereNode } from "kysely";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
|
+
import { EventEmitter } from "node:events";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { DatabaseSync } from "node:sqlite";
|
|
7
|
+
//#region ../../apps/api/src/helpers/appError.ts
|
|
8
|
+
/**
|
|
9
|
+
* An error with a stable machine-readable code, rendered by the central error handler as
|
|
10
|
+
* `{ error: { code, message, details? } }`. Services throw these; they carry no HTTP objects.
|
|
11
|
+
*/
|
|
12
|
+
var AppError = class extends Error {
|
|
13
|
+
statusCode;
|
|
14
|
+
code;
|
|
15
|
+
details;
|
|
16
|
+
constructor(statusCode, code, message, details, options) {
|
|
17
|
+
super(message, options);
|
|
18
|
+
this.name = "AppError";
|
|
19
|
+
this.statusCode = statusCode;
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.details = details;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region ../../apps/api/src/db/mysql/codec.ts
|
|
26
|
+
/**
|
|
27
|
+
* The MySQL value codec (ADR 0001, "MySQL"). Shapio's PostgreSQL types map to MySQL like this, and rows
|
|
28
|
+
* read back with the JavaScript shape `pg` gives them:
|
|
29
|
+
*
|
|
30
|
+
* | PostgreSQL | MySQL | read as |
|
|
31
|
+
* | --------------- | --------------------------------------- | --------------------------- |
|
|
32
|
+
* | uuid | CHAR(36) ascii, case-insensitive | string |
|
|
33
|
+
* | text | VARCHAR(n) (keys) / LONGTEXT, 0900_bin | string |
|
|
34
|
+
* | timestamptz | DATETIME(6), UTC | Date |
|
|
35
|
+
* | date | DATE | `YYYY-MM-DD` string |
|
|
36
|
+
* | jsonb / text[] | JSON | parsed value |
|
|
37
|
+
* | boolean | TINYINT(1) | boolean |
|
|
38
|
+
* | bigint | BIGINT | decimal string |
|
|
39
|
+
* | integer / real | INT / FLOAT | number |
|
|
40
|
+
*
|
|
41
|
+
* Parameters: plain objects and arrays are bound as JSON text (mysql2 would otherwise expand an object into
|
|
42
|
+
* `key = value` pairs and an array into a list), Dates as UTC `DATETIME` text, booleans as 1/0. `undefined`
|
|
43
|
+
* is refused rather than written as NULL.
|
|
44
|
+
*/
|
|
45
|
+
/** A parameter that cannot be bound; never silently written as NULL. */
|
|
46
|
+
var MysqlBindError = class extends TypeError {
|
|
47
|
+
constructor(index, value) {
|
|
48
|
+
const kind = value === void 0 ? "undefined" : value?.constructor?.name ?? typeof value;
|
|
49
|
+
super(`Cannot bind ${kind} to MySQL parameter ${index + 1}`);
|
|
50
|
+
this.name = "MysqlBindError";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const pad = (value, width = 2) => String(value).padStart(width, "0");
|
|
54
|
+
/** A Date as MySQL `DATETIME(6)` text in UTC (millisecond precision, the precision of a Date). */
|
|
55
|
+
const formatDatetime = (date) => `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}.${pad(date.getUTCMilliseconds(), 3)}`;
|
|
56
|
+
const ISO_UTC = /^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})(?:\.(\d{1,6}))?(?:Z|\+00:?00)$/;
|
|
57
|
+
/**
|
|
58
|
+
* A timestamp parameter as `DATETIME(6)` text. ISO-8601 UTC text keeps all its fractional digits (keyset
|
|
59
|
+
* cursors carry microseconds); other text is parsed as a Date. Throws on an invalid time.
|
|
60
|
+
*/
|
|
61
|
+
const toDatetimeText = (value) => {
|
|
62
|
+
if (typeof value === "string") {
|
|
63
|
+
const match = ISO_UTC.exec(value);
|
|
64
|
+
if (match) return `${match[1]} ${match[2]}.${(match[3] ?? "").padEnd(6, "0")}`;
|
|
65
|
+
}
|
|
66
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
67
|
+
if (Number.isNaN(date.getTime())) throw new RangeError(`Invalid timestamp: ${String(value)}`);
|
|
68
|
+
return formatDatetime(date);
|
|
69
|
+
};
|
|
70
|
+
const isPlainObject$1 = (value) => {
|
|
71
|
+
const prototype = Object.getPrototypeOf(value);
|
|
72
|
+
return prototype === Object.prototype || prototype === null;
|
|
73
|
+
};
|
|
74
|
+
/** A JavaScript value as a mysql2 parameter (see the table above). */
|
|
75
|
+
const encodeParameter$1 = (value, index) => {
|
|
76
|
+
switch (typeof value) {
|
|
77
|
+
case "string":
|
|
78
|
+
case "number":
|
|
79
|
+
case "bigint": return value;
|
|
80
|
+
case "boolean": return value ? 1 : 0;
|
|
81
|
+
case "object":
|
|
82
|
+
if (value === null || value instanceof Uint8Array) return value;
|
|
83
|
+
if (value instanceof Date) return formatDatetime(value);
|
|
84
|
+
if (Array.isArray(value) || isPlainObject$1(value)) return JSON.stringify(value);
|
|
85
|
+
throw new MysqlBindError(index, value);
|
|
86
|
+
default: throw new MysqlBindError(index, value);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Decodes result columns: `TINYINT(1)` as boolean (MySQL's boolean), `DATE` as `YYYY-MM-DD` text.
|
|
91
|
+
* DATETIME (UTC Dates), JSON (parsed), BIGINT/DECIMAL (strings) are decoded by the pool options.
|
|
92
|
+
*/
|
|
93
|
+
const typeCast = (field, next) => {
|
|
94
|
+
if (field.type === "TINY" && field.length === 1) {
|
|
95
|
+
const text = field.string();
|
|
96
|
+
return text === null ? null : text !== "0";
|
|
97
|
+
}
|
|
98
|
+
if (field.type === "DATE") return field.string();
|
|
99
|
+
return next();
|
|
100
|
+
};
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region ../../apps/api/src/db/sql/typed.ts
|
|
103
|
+
const MARKED = /* @__PURE__ */ new WeakMap();
|
|
104
|
+
/** The marker on a selection's expression node, if any (read by the SQLite plugin). */
|
|
105
|
+
const markedResultType = (node) => MARKED.get(node);
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region ../../apps/api/src/db/sql/time.ts
|
|
108
|
+
/**
|
|
109
|
+
* Time primitives (dialect boundary, ADR 0001). Only SQL that creates, formats or casts a time value goes
|
|
110
|
+
* through here. Comparing or sorting a timestamp column against a bound JavaScript `Date` stays plain Kysely:
|
|
111
|
+
* it is portable as long as every dialect stores timestamps so that their order is time order.
|
|
112
|
+
*
|
|
113
|
+
* SQLite stores timestamps as fixed-width UTC ISO-8601 text with milliseconds (`toStoredTimestamp`), so text
|
|
114
|
+
* order is time order. PostgreSQL keeps microseconds; SQLite's precision is the millisecond. MySQL stores
|
|
115
|
+
* `DATETIME(6)` in UTC (the session time zone), microseconds like PostgreSQL.
|
|
116
|
+
*/
|
|
117
|
+
/**
|
|
118
|
+
* The text SQLite stores for an instant: `YYYY-MM-DDTHH:MM:SS.sssZ` (24 characters, UTC, milliseconds).
|
|
119
|
+
* Every timestamp written on SQLite has exactly this form (the driver's codec, column defaults and
|
|
120
|
+
* `currentTimestamp`), so comparisons between stored values and bound values are text comparisons.
|
|
121
|
+
* Accepts a Date or any text `Date` parses (sub-millisecond digits are truncated). Throws on an invalid time.
|
|
122
|
+
*/
|
|
123
|
+
const toStoredTimestamp = (value) => {
|
|
124
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
125
|
+
if (Number.isNaN(date.getTime())) throw new RangeError(`Invalid timestamp: ${String(value)}`);
|
|
126
|
+
return date.toISOString();
|
|
127
|
+
};
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region ../../apps/api/src/db/connectionScope.ts
|
|
130
|
+
const scopes$1 = new AsyncLocalStorage();
|
|
131
|
+
var NestedPoolAcquireError = class extends Error {
|
|
132
|
+
constructor() {
|
|
133
|
+
super("A query inside an open transaction asked the pool for a second connection; under load every connection waits for another. Run it on the transaction (trx) instead.");
|
|
134
|
+
this.name = "NestedPoolAcquireError";
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const holdsConnection = (scope) => {
|
|
138
|
+
for (let current = scope; current; current = current.parent) if (current.acquired) return true;
|
|
139
|
+
return false;
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Called by a driver before it takes a pooled connection. The first acquisition of a scope is the
|
|
143
|
+
* transaction's own connection; any later one, or one inside a scope that already holds a connection, is
|
|
144
|
+
* nested.
|
|
145
|
+
*/
|
|
146
|
+
const assertNotNested = () => {
|
|
147
|
+
const scope = scopes$1.getStore();
|
|
148
|
+
if (!scope) return;
|
|
149
|
+
if (holdsConnection(scope)) throw new NestedPoolAcquireError();
|
|
150
|
+
scope.acquired = true;
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Runs `fn` as if no transaction were open: for work that does not belong to the enclosing transaction and
|
|
154
|
+
* that it never waits for (notifications published at commit, a test's independent request).
|
|
155
|
+
*/
|
|
156
|
+
const outsideConnectionScope = (fn) => scopes$1.exit(fn);
|
|
157
|
+
/** Runs every `execute` of a transaction builder (and of the builders it returns) in a connection scope. */
|
|
158
|
+
const withConnectionScope = (builder) => new Proxy(builder, { get(target, property, receiver) {
|
|
159
|
+
if (property === "execute") return (callback) => scopes$1.run({
|
|
160
|
+
acquired: false,
|
|
161
|
+
parent: scopes$1.getStore()
|
|
162
|
+
}, () => target.execute(callback));
|
|
163
|
+
if (property === "setIsolationLevel" || property === "setAccessMode") return (value) => withConnectionScope(target[property](value));
|
|
164
|
+
return Reflect.get(target, property, receiver);
|
|
165
|
+
} });
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region ../../apps/api/src/db/mysql/tables.ts
|
|
168
|
+
const MYSQL_TABLES = {
|
|
169
|
+
admin_invitations: {
|
|
170
|
+
primaryKey: ["id"],
|
|
171
|
+
generated: { id: "uuid" },
|
|
172
|
+
columns: [
|
|
173
|
+
"id",
|
|
174
|
+
"email",
|
|
175
|
+
"token_hash",
|
|
176
|
+
"invited_by",
|
|
177
|
+
"expires_at",
|
|
178
|
+
"accepted_at",
|
|
179
|
+
"revoked_at",
|
|
180
|
+
"created_at",
|
|
181
|
+
"updated_at",
|
|
182
|
+
"role_assignments"
|
|
183
|
+
],
|
|
184
|
+
nullable: [
|
|
185
|
+
"token_hash",
|
|
186
|
+
"invited_by",
|
|
187
|
+
"accepted_at",
|
|
188
|
+
"revoked_at"
|
|
189
|
+
],
|
|
190
|
+
timestamps: [
|
|
191
|
+
"expires_at",
|
|
192
|
+
"accepted_at",
|
|
193
|
+
"revoked_at",
|
|
194
|
+
"created_at",
|
|
195
|
+
"updated_at"
|
|
196
|
+
],
|
|
197
|
+
uniqueKeys: [{
|
|
198
|
+
name: "admin_invitations_pkey",
|
|
199
|
+
columns: ["id"]
|
|
200
|
+
}, {
|
|
201
|
+
name: "admin_invitations_token_hash_key",
|
|
202
|
+
columns: ["token_hash"]
|
|
203
|
+
}]
|
|
204
|
+
},
|
|
205
|
+
admin_role_permissions: {
|
|
206
|
+
primaryKey: ["id"],
|
|
207
|
+
generated: { id: "uuid" },
|
|
208
|
+
columns: [
|
|
209
|
+
"id",
|
|
210
|
+
"role_id",
|
|
211
|
+
"action",
|
|
212
|
+
"model_id",
|
|
213
|
+
"condition",
|
|
214
|
+
"field_ids",
|
|
215
|
+
"created_at",
|
|
216
|
+
"updated_at"
|
|
217
|
+
],
|
|
218
|
+
nullable: [
|
|
219
|
+
"model_id",
|
|
220
|
+
"condition",
|
|
221
|
+
"field_ids"
|
|
222
|
+
],
|
|
223
|
+
timestamps: ["created_at", "updated_at"],
|
|
224
|
+
uniqueKeys: [{
|
|
225
|
+
name: "admin_role_permissions_grant_uq",
|
|
226
|
+
columns: [
|
|
227
|
+
"role_id",
|
|
228
|
+
"action",
|
|
229
|
+
"model_id"
|
|
230
|
+
]
|
|
231
|
+
}, {
|
|
232
|
+
name: "admin_role_permissions_pkey",
|
|
233
|
+
columns: ["id"]
|
|
234
|
+
}]
|
|
235
|
+
},
|
|
236
|
+
admin_roles: {
|
|
237
|
+
primaryKey: ["id"],
|
|
238
|
+
generated: { id: "uuid" },
|
|
239
|
+
columns: [
|
|
240
|
+
"id",
|
|
241
|
+
"key",
|
|
242
|
+
"name",
|
|
243
|
+
"description",
|
|
244
|
+
"kind",
|
|
245
|
+
"is_system",
|
|
246
|
+
"version",
|
|
247
|
+
"created_at",
|
|
248
|
+
"updated_at"
|
|
249
|
+
],
|
|
250
|
+
nullable: [],
|
|
251
|
+
timestamps: ["created_at", "updated_at"],
|
|
252
|
+
uniqueKeys: [{
|
|
253
|
+
name: "admin_roles_key_key",
|
|
254
|
+
columns: ["key"]
|
|
255
|
+
}, {
|
|
256
|
+
name: "admin_roles_pkey",
|
|
257
|
+
columns: ["id"]
|
|
258
|
+
}]
|
|
259
|
+
},
|
|
260
|
+
admin_sessions: {
|
|
261
|
+
primaryKey: ["id"],
|
|
262
|
+
generated: { id: "uuid" },
|
|
263
|
+
columns: [
|
|
264
|
+
"id",
|
|
265
|
+
"admin_user_id",
|
|
266
|
+
"token_hash",
|
|
267
|
+
"csrf_secret",
|
|
268
|
+
"ip",
|
|
269
|
+
"user_agent",
|
|
270
|
+
"last_seen_at",
|
|
271
|
+
"expires_at",
|
|
272
|
+
"revoked_at",
|
|
273
|
+
"rotation_required",
|
|
274
|
+
"created_at",
|
|
275
|
+
"updated_at"
|
|
276
|
+
],
|
|
277
|
+
nullable: [
|
|
278
|
+
"ip",
|
|
279
|
+
"user_agent",
|
|
280
|
+
"revoked_at"
|
|
281
|
+
],
|
|
282
|
+
timestamps: [
|
|
283
|
+
"last_seen_at",
|
|
284
|
+
"expires_at",
|
|
285
|
+
"revoked_at",
|
|
286
|
+
"created_at",
|
|
287
|
+
"updated_at"
|
|
288
|
+
],
|
|
289
|
+
uniqueKeys: [{
|
|
290
|
+
name: "admin_sessions_pkey",
|
|
291
|
+
columns: ["id"]
|
|
292
|
+
}, {
|
|
293
|
+
name: "admin_sessions_token_hash_key",
|
|
294
|
+
columns: ["token_hash"]
|
|
295
|
+
}]
|
|
296
|
+
},
|
|
297
|
+
admin_user_roles: {
|
|
298
|
+
primaryKey: ["id"],
|
|
299
|
+
generated: { id: "uuid" },
|
|
300
|
+
columns: [
|
|
301
|
+
"admin_user_id",
|
|
302
|
+
"role_id",
|
|
303
|
+
"created_at",
|
|
304
|
+
"updated_at",
|
|
305
|
+
"site_id",
|
|
306
|
+
"id"
|
|
307
|
+
],
|
|
308
|
+
nullable: ["site_id"],
|
|
309
|
+
timestamps: ["created_at", "updated_at"],
|
|
310
|
+
uniqueKeys: [{
|
|
311
|
+
name: "admin_user_roles_assignment_uq",
|
|
312
|
+
columns: [
|
|
313
|
+
"admin_user_id",
|
|
314
|
+
"role_id",
|
|
315
|
+
"site_id"
|
|
316
|
+
]
|
|
317
|
+
}, {
|
|
318
|
+
name: "admin_user_roles_pk",
|
|
319
|
+
columns: ["id"]
|
|
320
|
+
}]
|
|
321
|
+
},
|
|
322
|
+
admin_users: {
|
|
323
|
+
primaryKey: ["id"],
|
|
324
|
+
generated: { id: "uuid" },
|
|
325
|
+
columns: [
|
|
326
|
+
"id",
|
|
327
|
+
"email",
|
|
328
|
+
"name",
|
|
329
|
+
"password_hash",
|
|
330
|
+
"status",
|
|
331
|
+
"last_login_at",
|
|
332
|
+
"password_changed_at",
|
|
333
|
+
"created_at",
|
|
334
|
+
"updated_at"
|
|
335
|
+
],
|
|
336
|
+
nullable: ["last_login_at"],
|
|
337
|
+
timestamps: [
|
|
338
|
+
"last_login_at",
|
|
339
|
+
"password_changed_at",
|
|
340
|
+
"created_at",
|
|
341
|
+
"updated_at"
|
|
342
|
+
],
|
|
343
|
+
uniqueKeys: [{
|
|
344
|
+
name: "admin_users_email_key",
|
|
345
|
+
columns: ["email"]
|
|
346
|
+
}, {
|
|
347
|
+
name: "admin_users_pkey",
|
|
348
|
+
columns: ["id"]
|
|
349
|
+
}]
|
|
350
|
+
},
|
|
351
|
+
api_tokens: {
|
|
352
|
+
primaryKey: ["id"],
|
|
353
|
+
generated: { id: "uuid" },
|
|
354
|
+
columns: [
|
|
355
|
+
"id",
|
|
356
|
+
"name",
|
|
357
|
+
"token_hash",
|
|
358
|
+
"token_prefix",
|
|
359
|
+
"role_id",
|
|
360
|
+
"created_by",
|
|
361
|
+
"expires_at",
|
|
362
|
+
"last_used_at",
|
|
363
|
+
"revoked_at",
|
|
364
|
+
"created_at",
|
|
365
|
+
"updated_at",
|
|
366
|
+
"site_id"
|
|
367
|
+
],
|
|
368
|
+
nullable: [
|
|
369
|
+
"created_by",
|
|
370
|
+
"expires_at",
|
|
371
|
+
"last_used_at",
|
|
372
|
+
"revoked_at",
|
|
373
|
+
"site_id"
|
|
374
|
+
],
|
|
375
|
+
timestamps: [
|
|
376
|
+
"expires_at",
|
|
377
|
+
"last_used_at",
|
|
378
|
+
"revoked_at",
|
|
379
|
+
"created_at",
|
|
380
|
+
"updated_at"
|
|
381
|
+
],
|
|
382
|
+
uniqueKeys: [{
|
|
383
|
+
name: "api_tokens_pkey",
|
|
384
|
+
columns: ["id"]
|
|
385
|
+
}, {
|
|
386
|
+
name: "api_tokens_token_hash_key",
|
|
387
|
+
columns: ["token_hash"]
|
|
388
|
+
}]
|
|
389
|
+
},
|
|
390
|
+
app_login_codes: {
|
|
391
|
+
primaryKey: ["id"],
|
|
392
|
+
generated: { id: "uuid" },
|
|
393
|
+
columns: [
|
|
394
|
+
"id",
|
|
395
|
+
"app_user_id",
|
|
396
|
+
"code_hash",
|
|
397
|
+
"expires_at",
|
|
398
|
+
"used_at",
|
|
399
|
+
"created_at",
|
|
400
|
+
"code_challenge"
|
|
401
|
+
],
|
|
402
|
+
nullable: ["used_at"],
|
|
403
|
+
timestamps: [
|
|
404
|
+
"expires_at",
|
|
405
|
+
"used_at",
|
|
406
|
+
"created_at"
|
|
407
|
+
],
|
|
408
|
+
uniqueKeys: [{
|
|
409
|
+
name: "app_login_codes_code_hash_key",
|
|
410
|
+
columns: ["code_hash"]
|
|
411
|
+
}, {
|
|
412
|
+
name: "app_login_codes_pkey",
|
|
413
|
+
columns: ["id"]
|
|
414
|
+
}]
|
|
415
|
+
},
|
|
416
|
+
app_oauth_accounts: {
|
|
417
|
+
primaryKey: ["id"],
|
|
418
|
+
generated: { id: "uuid" },
|
|
419
|
+
columns: [
|
|
420
|
+
"id",
|
|
421
|
+
"app_user_id",
|
|
422
|
+
"provider",
|
|
423
|
+
"provider_user_id",
|
|
424
|
+
"email",
|
|
425
|
+
"created_at",
|
|
426
|
+
"updated_at",
|
|
427
|
+
"site_id"
|
|
428
|
+
],
|
|
429
|
+
nullable: ["email"],
|
|
430
|
+
timestamps: ["created_at", "updated_at"],
|
|
431
|
+
uniqueKeys: [{
|
|
432
|
+
name: "app_oauth_accounts_identity_uq",
|
|
433
|
+
columns: [
|
|
434
|
+
"site_id",
|
|
435
|
+
"provider",
|
|
436
|
+
"provider_user_id"
|
|
437
|
+
]
|
|
438
|
+
}, {
|
|
439
|
+
name: "app_oauth_accounts_pkey",
|
|
440
|
+
columns: ["id"]
|
|
441
|
+
}]
|
|
442
|
+
},
|
|
443
|
+
app_password_resets: {
|
|
444
|
+
primaryKey: ["id"],
|
|
445
|
+
generated: { id: "uuid" },
|
|
446
|
+
columns: [
|
|
447
|
+
"id",
|
|
448
|
+
"app_user_id",
|
|
449
|
+
"email",
|
|
450
|
+
"token_hash",
|
|
451
|
+
"expires_at",
|
|
452
|
+
"used_at",
|
|
453
|
+
"created_at",
|
|
454
|
+
"updated_at"
|
|
455
|
+
],
|
|
456
|
+
nullable: ["token_hash", "used_at"],
|
|
457
|
+
timestamps: [
|
|
458
|
+
"expires_at",
|
|
459
|
+
"used_at",
|
|
460
|
+
"created_at",
|
|
461
|
+
"updated_at"
|
|
462
|
+
],
|
|
463
|
+
uniqueKeys: [{
|
|
464
|
+
name: "app_password_resets_pkey",
|
|
465
|
+
columns: ["id"]
|
|
466
|
+
}, {
|
|
467
|
+
name: "app_password_resets_token_hash_key",
|
|
468
|
+
columns: ["token_hash"]
|
|
469
|
+
}]
|
|
470
|
+
},
|
|
471
|
+
app_refresh_tokens: {
|
|
472
|
+
primaryKey: ["id"],
|
|
473
|
+
generated: { id: "uuid" },
|
|
474
|
+
columns: [
|
|
475
|
+
"id",
|
|
476
|
+
"app_user_id",
|
|
477
|
+
"family_id",
|
|
478
|
+
"token_hash",
|
|
479
|
+
"expires_at",
|
|
480
|
+
"used_at",
|
|
481
|
+
"reuse_grace_used_at",
|
|
482
|
+
"revoked_at",
|
|
483
|
+
"revoked_reason",
|
|
484
|
+
"ip",
|
|
485
|
+
"user_agent",
|
|
486
|
+
"created_at"
|
|
487
|
+
],
|
|
488
|
+
nullable: [
|
|
489
|
+
"used_at",
|
|
490
|
+
"reuse_grace_used_at",
|
|
491
|
+
"revoked_at",
|
|
492
|
+
"revoked_reason",
|
|
493
|
+
"ip",
|
|
494
|
+
"user_agent"
|
|
495
|
+
],
|
|
496
|
+
timestamps: [
|
|
497
|
+
"expires_at",
|
|
498
|
+
"used_at",
|
|
499
|
+
"reuse_grace_used_at",
|
|
500
|
+
"revoked_at",
|
|
501
|
+
"created_at"
|
|
502
|
+
],
|
|
503
|
+
uniqueKeys: [{
|
|
504
|
+
name: "app_refresh_tokens_pkey",
|
|
505
|
+
columns: ["id"]
|
|
506
|
+
}, {
|
|
507
|
+
name: "app_refresh_tokens_token_hash_key",
|
|
508
|
+
columns: ["token_hash"]
|
|
509
|
+
}]
|
|
510
|
+
},
|
|
511
|
+
app_role_permissions: {
|
|
512
|
+
primaryKey: ["id"],
|
|
513
|
+
generated: { id: "uuid" },
|
|
514
|
+
columns: [
|
|
515
|
+
"id",
|
|
516
|
+
"role_id",
|
|
517
|
+
"action",
|
|
518
|
+
"model_id",
|
|
519
|
+
"condition",
|
|
520
|
+
"field_ids",
|
|
521
|
+
"created_at",
|
|
522
|
+
"updated_at"
|
|
523
|
+
],
|
|
524
|
+
nullable: [
|
|
525
|
+
"model_id",
|
|
526
|
+
"condition",
|
|
527
|
+
"field_ids"
|
|
528
|
+
],
|
|
529
|
+
timestamps: ["created_at", "updated_at"],
|
|
530
|
+
uniqueKeys: [{
|
|
531
|
+
name: "app_role_permissions_grant_uq",
|
|
532
|
+
columns: [
|
|
533
|
+
"role_id",
|
|
534
|
+
"action",
|
|
535
|
+
"model_id"
|
|
536
|
+
]
|
|
537
|
+
}, {
|
|
538
|
+
name: "app_role_permissions_pkey",
|
|
539
|
+
columns: ["id"]
|
|
540
|
+
}]
|
|
541
|
+
},
|
|
542
|
+
app_roles: {
|
|
543
|
+
primaryKey: ["id"],
|
|
544
|
+
generated: { id: "uuid" },
|
|
545
|
+
columns: [
|
|
546
|
+
"id",
|
|
547
|
+
"key",
|
|
548
|
+
"name",
|
|
549
|
+
"description",
|
|
550
|
+
"is_system",
|
|
551
|
+
"version",
|
|
552
|
+
"created_at",
|
|
553
|
+
"updated_at"
|
|
554
|
+
],
|
|
555
|
+
nullable: [],
|
|
556
|
+
timestamps: ["created_at", "updated_at"],
|
|
557
|
+
uniqueKeys: [{
|
|
558
|
+
name: "app_roles_key_key",
|
|
559
|
+
columns: ["key"]
|
|
560
|
+
}, {
|
|
561
|
+
name: "app_roles_pkey",
|
|
562
|
+
columns: ["id"]
|
|
563
|
+
}]
|
|
564
|
+
},
|
|
565
|
+
app_user_roles: {
|
|
566
|
+
primaryKey: ["app_user_id", "role_id"],
|
|
567
|
+
columns: [
|
|
568
|
+
"app_user_id",
|
|
569
|
+
"role_id",
|
|
570
|
+
"created_at",
|
|
571
|
+
"site_id"
|
|
572
|
+
],
|
|
573
|
+
nullable: [],
|
|
574
|
+
timestamps: ["created_at"],
|
|
575
|
+
uniqueKeys: [{
|
|
576
|
+
name: "app_user_roles_pk",
|
|
577
|
+
columns: ["app_user_id", "role_id"]
|
|
578
|
+
}]
|
|
579
|
+
},
|
|
580
|
+
app_users: {
|
|
581
|
+
primaryKey: ["id"],
|
|
582
|
+
generated: { id: "uuid" },
|
|
583
|
+
columns: [
|
|
584
|
+
"id",
|
|
585
|
+
"email",
|
|
586
|
+
"name",
|
|
587
|
+
"password_hash",
|
|
588
|
+
"confirmed_at",
|
|
589
|
+
"blocked_at",
|
|
590
|
+
"last_login_at",
|
|
591
|
+
"password_changed_at",
|
|
592
|
+
"deleted_at",
|
|
593
|
+
"created_at",
|
|
594
|
+
"updated_at",
|
|
595
|
+
"token_version",
|
|
596
|
+
"site_id"
|
|
597
|
+
],
|
|
598
|
+
nullable: [
|
|
599
|
+
"password_hash",
|
|
600
|
+
"confirmed_at",
|
|
601
|
+
"blocked_at",
|
|
602
|
+
"last_login_at",
|
|
603
|
+
"password_changed_at",
|
|
604
|
+
"deleted_at"
|
|
605
|
+
],
|
|
606
|
+
timestamps: [
|
|
607
|
+
"confirmed_at",
|
|
608
|
+
"blocked_at",
|
|
609
|
+
"last_login_at",
|
|
610
|
+
"password_changed_at",
|
|
611
|
+
"deleted_at",
|
|
612
|
+
"created_at",
|
|
613
|
+
"updated_at"
|
|
614
|
+
],
|
|
615
|
+
uniqueKeys: [{
|
|
616
|
+
name: "app_users_email_uq",
|
|
617
|
+
columns: ["site_id", "email"]
|
|
618
|
+
}, {
|
|
619
|
+
name: "app_users_pkey",
|
|
620
|
+
columns: ["id"]
|
|
621
|
+
}]
|
|
622
|
+
},
|
|
623
|
+
assist_runs: {
|
|
624
|
+
primaryKey: ["id"],
|
|
625
|
+
generated: { id: "uuid" },
|
|
626
|
+
columns: [
|
|
627
|
+
"id",
|
|
628
|
+
"site_id",
|
|
629
|
+
"actor_type",
|
|
630
|
+
"actor_id",
|
|
631
|
+
"action",
|
|
632
|
+
"rule",
|
|
633
|
+
"provider",
|
|
634
|
+
"model",
|
|
635
|
+
"input_tokens",
|
|
636
|
+
"output_tokens",
|
|
637
|
+
"duration_ms",
|
|
638
|
+
"status",
|
|
639
|
+
"error_code",
|
|
640
|
+
"job_id",
|
|
641
|
+
"created_at",
|
|
642
|
+
"finished_at"
|
|
643
|
+
],
|
|
644
|
+
nullable: [
|
|
645
|
+
"rule",
|
|
646
|
+
"error_code",
|
|
647
|
+
"job_id",
|
|
648
|
+
"finished_at"
|
|
649
|
+
],
|
|
650
|
+
timestamps: ["created_at", "finished_at"],
|
|
651
|
+
uniqueKeys: [{
|
|
652
|
+
name: "assist_runs_pkey",
|
|
653
|
+
columns: ["id"]
|
|
654
|
+
}]
|
|
655
|
+
},
|
|
656
|
+
audit_events: {
|
|
657
|
+
primaryKey: ["id"],
|
|
658
|
+
identity: "seq",
|
|
659
|
+
generated: { id: "uuid" },
|
|
660
|
+
columns: [
|
|
661
|
+
"id",
|
|
662
|
+
"occurred_at",
|
|
663
|
+
"actor_type",
|
|
664
|
+
"actor_id",
|
|
665
|
+
"action",
|
|
666
|
+
"target_type",
|
|
667
|
+
"target_id",
|
|
668
|
+
"outcome",
|
|
669
|
+
"request_id",
|
|
670
|
+
"ip",
|
|
671
|
+
"metadata",
|
|
672
|
+
"site_id",
|
|
673
|
+
"seq"
|
|
674
|
+
],
|
|
675
|
+
nullable: [
|
|
676
|
+
"actor_id",
|
|
677
|
+
"target_type",
|
|
678
|
+
"target_id",
|
|
679
|
+
"request_id",
|
|
680
|
+
"ip",
|
|
681
|
+
"site_id"
|
|
682
|
+
],
|
|
683
|
+
timestamps: ["occurred_at"],
|
|
684
|
+
uniqueKeys: [{
|
|
685
|
+
name: "audit_events_pkey",
|
|
686
|
+
columns: ["id"]
|
|
687
|
+
}, {
|
|
688
|
+
name: "audit_events_seq_key",
|
|
689
|
+
columns: ["seq"]
|
|
690
|
+
}]
|
|
691
|
+
},
|
|
692
|
+
change_set_items: {
|
|
693
|
+
primaryKey: ["id"],
|
|
694
|
+
generated: { id: "uuid" },
|
|
695
|
+
columns: [
|
|
696
|
+
"id",
|
|
697
|
+
"change_set_id",
|
|
698
|
+
"kind",
|
|
699
|
+
"position",
|
|
700
|
+
"entry_id",
|
|
701
|
+
"model_id",
|
|
702
|
+
"locale",
|
|
703
|
+
"action",
|
|
704
|
+
"source_revision_id",
|
|
705
|
+
"schema_draft_id",
|
|
706
|
+
"status",
|
|
707
|
+
"error",
|
|
708
|
+
"ship_state",
|
|
709
|
+
"created_at",
|
|
710
|
+
"site_id"
|
|
711
|
+
],
|
|
712
|
+
nullable: [
|
|
713
|
+
"entry_id",
|
|
714
|
+
"model_id",
|
|
715
|
+
"locale",
|
|
716
|
+
"action",
|
|
717
|
+
"source_revision_id",
|
|
718
|
+
"schema_draft_id",
|
|
719
|
+
"error",
|
|
720
|
+
"ship_state"
|
|
721
|
+
],
|
|
722
|
+
timestamps: ["created_at"],
|
|
723
|
+
uniqueKeys: [
|
|
724
|
+
{
|
|
725
|
+
name: "change_set_items_draft_uq",
|
|
726
|
+
columns: ["schema_draft_id"]
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
name: "change_set_items_entry_uq",
|
|
730
|
+
columns: [
|
|
731
|
+
"change_set_id",
|
|
732
|
+
"entry_id",
|
|
733
|
+
"locale"
|
|
734
|
+
]
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
name: "change_set_items_pkey",
|
|
738
|
+
columns: ["id"]
|
|
739
|
+
}
|
|
740
|
+
]
|
|
741
|
+
},
|
|
742
|
+
change_sets: {
|
|
743
|
+
primaryKey: ["id"],
|
|
744
|
+
generated: { id: "uuid" },
|
|
745
|
+
columns: [
|
|
746
|
+
"id",
|
|
747
|
+
"title",
|
|
748
|
+
"description",
|
|
749
|
+
"status",
|
|
750
|
+
"ship_phase",
|
|
751
|
+
"source",
|
|
752
|
+
"restore_of_seq",
|
|
753
|
+
"scheduled_for",
|
|
754
|
+
"schedule_job_id",
|
|
755
|
+
"scheduled_by",
|
|
756
|
+
"scheduled_by_token",
|
|
757
|
+
"ship_job_id",
|
|
758
|
+
"ship_mode",
|
|
759
|
+
"ship_requested_by",
|
|
760
|
+
"ship_requested_by_token",
|
|
761
|
+
"acknowledge_breaking",
|
|
762
|
+
"acknowledge_destructive",
|
|
763
|
+
"deployment_connection_id",
|
|
764
|
+
"deployment_run_id",
|
|
765
|
+
"shipped_at",
|
|
766
|
+
"shipped_seq",
|
|
767
|
+
"schema_version_after",
|
|
768
|
+
"error",
|
|
769
|
+
"restore_report",
|
|
770
|
+
"created_by",
|
|
771
|
+
"created_by_token",
|
|
772
|
+
"version",
|
|
773
|
+
"created_at",
|
|
774
|
+
"updated_at",
|
|
775
|
+
"site_id"
|
|
776
|
+
],
|
|
777
|
+
nullable: [
|
|
778
|
+
"ship_phase",
|
|
779
|
+
"restore_of_seq",
|
|
780
|
+
"scheduled_for",
|
|
781
|
+
"schedule_job_id",
|
|
782
|
+
"scheduled_by",
|
|
783
|
+
"scheduled_by_token",
|
|
784
|
+
"ship_job_id",
|
|
785
|
+
"ship_mode",
|
|
786
|
+
"ship_requested_by",
|
|
787
|
+
"ship_requested_by_token",
|
|
788
|
+
"deployment_connection_id",
|
|
789
|
+
"deployment_run_id",
|
|
790
|
+
"shipped_at",
|
|
791
|
+
"shipped_seq",
|
|
792
|
+
"schema_version_after",
|
|
793
|
+
"error",
|
|
794
|
+
"created_by",
|
|
795
|
+
"created_by_token"
|
|
796
|
+
],
|
|
797
|
+
timestamps: [
|
|
798
|
+
"scheduled_for",
|
|
799
|
+
"shipped_at",
|
|
800
|
+
"created_at",
|
|
801
|
+
"updated_at"
|
|
802
|
+
],
|
|
803
|
+
uniqueKeys: [{
|
|
804
|
+
name: "change_sets_id_site_uq",
|
|
805
|
+
columns: ["id", "site_id"]
|
|
806
|
+
}, {
|
|
807
|
+
name: "change_sets_pkey",
|
|
808
|
+
columns: ["id"]
|
|
809
|
+
}]
|
|
810
|
+
},
|
|
811
|
+
content_health_findings: {
|
|
812
|
+
primaryKey: ["id"],
|
|
813
|
+
generated: { id: "uuid" },
|
|
814
|
+
columns: [
|
|
815
|
+
"id",
|
|
816
|
+
"entry_id",
|
|
817
|
+
"model_id",
|
|
818
|
+
"locale",
|
|
819
|
+
"rule",
|
|
820
|
+
"subject",
|
|
821
|
+
"severity",
|
|
822
|
+
"path",
|
|
823
|
+
"details",
|
|
824
|
+
"first_seen_at",
|
|
825
|
+
"last_seen_at",
|
|
826
|
+
"resolved_at",
|
|
827
|
+
"site_id"
|
|
828
|
+
],
|
|
829
|
+
nullable: ["path", "resolved_at"],
|
|
830
|
+
timestamps: [
|
|
831
|
+
"first_seen_at",
|
|
832
|
+
"last_seen_at",
|
|
833
|
+
"resolved_at"
|
|
834
|
+
],
|
|
835
|
+
uniqueKeys: [{
|
|
836
|
+
name: "content_health_findings_entry_id_locale_rule_subject_key",
|
|
837
|
+
columns: [
|
|
838
|
+
"entry_id",
|
|
839
|
+
"locale",
|
|
840
|
+
"rule",
|
|
841
|
+
"subject"
|
|
842
|
+
]
|
|
843
|
+
}, {
|
|
844
|
+
name: "content_health_findings_pkey",
|
|
845
|
+
columns: ["id"]
|
|
846
|
+
}]
|
|
847
|
+
},
|
|
848
|
+
content_revisions: {
|
|
849
|
+
primaryKey: ["id"],
|
|
850
|
+
generated: { id: "uuid" },
|
|
851
|
+
columns: [
|
|
852
|
+
"id",
|
|
853
|
+
"entry_id",
|
|
854
|
+
"locale",
|
|
855
|
+
"schema_revision_id",
|
|
856
|
+
"data",
|
|
857
|
+
"reason",
|
|
858
|
+
"author_type",
|
|
859
|
+
"author_id",
|
|
860
|
+
"parent_revision_id",
|
|
861
|
+
"created_at"
|
|
862
|
+
],
|
|
863
|
+
nullable: ["author_id", "parent_revision_id"],
|
|
864
|
+
timestamps: ["created_at"],
|
|
865
|
+
uniqueKeys: [{
|
|
866
|
+
name: "content_revisions_pkey",
|
|
867
|
+
columns: ["id"]
|
|
868
|
+
}]
|
|
869
|
+
},
|
|
870
|
+
deployment_connections: {
|
|
871
|
+
primaryKey: ["id"],
|
|
872
|
+
generated: { id: "uuid" },
|
|
873
|
+
columns: [
|
|
874
|
+
"id",
|
|
875
|
+
"name",
|
|
876
|
+
"provider",
|
|
877
|
+
"settings",
|
|
878
|
+
"secrets_encrypted",
|
|
879
|
+
"secret_env_refs",
|
|
880
|
+
"delivery_role_id",
|
|
881
|
+
"preview_url_template",
|
|
882
|
+
"trigger_policy",
|
|
883
|
+
"debounce_seconds",
|
|
884
|
+
"allow_private_network",
|
|
885
|
+
"enabled",
|
|
886
|
+
"version",
|
|
887
|
+
"created_by",
|
|
888
|
+
"created_at",
|
|
889
|
+
"updated_at",
|
|
890
|
+
"site_id"
|
|
891
|
+
],
|
|
892
|
+
nullable: [
|
|
893
|
+
"delivery_role_id",
|
|
894
|
+
"preview_url_template",
|
|
895
|
+
"created_by"
|
|
896
|
+
],
|
|
897
|
+
timestamps: ["created_at", "updated_at"],
|
|
898
|
+
uniqueKeys: [{
|
|
899
|
+
name: "deployment_connections_pkey",
|
|
900
|
+
columns: ["id"]
|
|
901
|
+
}]
|
|
902
|
+
},
|
|
903
|
+
deployment_runs: {
|
|
904
|
+
primaryKey: ["id"],
|
|
905
|
+
generated: { id: "uuid" },
|
|
906
|
+
columns: [
|
|
907
|
+
"id",
|
|
908
|
+
"connection_id",
|
|
909
|
+
"status",
|
|
910
|
+
"status_rank",
|
|
911
|
+
"trigger",
|
|
912
|
+
"snapshot_seq",
|
|
913
|
+
"schema_version",
|
|
914
|
+
"retry_of",
|
|
915
|
+
"provider_ref",
|
|
916
|
+
"log_url",
|
|
917
|
+
"site_url",
|
|
918
|
+
"error",
|
|
919
|
+
"timeline",
|
|
920
|
+
"job_id",
|
|
921
|
+
"triggered_at",
|
|
922
|
+
"finished_at",
|
|
923
|
+
"created_by",
|
|
924
|
+
"created_at",
|
|
925
|
+
"updated_at",
|
|
926
|
+
"change_set_id"
|
|
927
|
+
],
|
|
928
|
+
nullable: [
|
|
929
|
+
"snapshot_seq",
|
|
930
|
+
"schema_version",
|
|
931
|
+
"retry_of",
|
|
932
|
+
"provider_ref",
|
|
933
|
+
"log_url",
|
|
934
|
+
"site_url",
|
|
935
|
+
"error",
|
|
936
|
+
"job_id",
|
|
937
|
+
"triggered_at",
|
|
938
|
+
"finished_at",
|
|
939
|
+
"created_by",
|
|
940
|
+
"change_set_id"
|
|
941
|
+
],
|
|
942
|
+
timestamps: [
|
|
943
|
+
"triggered_at",
|
|
944
|
+
"finished_at",
|
|
945
|
+
"created_at",
|
|
946
|
+
"updated_at"
|
|
947
|
+
],
|
|
948
|
+
uniqueKeys: [{
|
|
949
|
+
name: "deployment_runs_pkey",
|
|
950
|
+
columns: ["id"]
|
|
951
|
+
}, {
|
|
952
|
+
name: "deployment_runs_queued_uq",
|
|
953
|
+
columns: ["connection_id"]
|
|
954
|
+
}]
|
|
955
|
+
},
|
|
956
|
+
editor_presence: {
|
|
957
|
+
primaryKey: [
|
|
958
|
+
"entry_id",
|
|
959
|
+
"admin_user_id",
|
|
960
|
+
"tab_id"
|
|
961
|
+
],
|
|
962
|
+
columns: [
|
|
963
|
+
"entry_id",
|
|
964
|
+
"model_id",
|
|
965
|
+
"admin_user_id",
|
|
966
|
+
"tab_id",
|
|
967
|
+
"locale",
|
|
968
|
+
"started_at",
|
|
969
|
+
"last_seen_at"
|
|
970
|
+
],
|
|
971
|
+
nullable: ["locale"],
|
|
972
|
+
timestamps: ["started_at", "last_seen_at"],
|
|
973
|
+
uniqueKeys: [{
|
|
974
|
+
name: "editor_presence_pkey",
|
|
975
|
+
columns: [
|
|
976
|
+
"entry_id",
|
|
977
|
+
"admin_user_id",
|
|
978
|
+
"tab_id"
|
|
979
|
+
]
|
|
980
|
+
}]
|
|
981
|
+
},
|
|
982
|
+
email_confirmations: {
|
|
983
|
+
primaryKey: ["id"],
|
|
984
|
+
generated: { id: "uuid" },
|
|
985
|
+
columns: [
|
|
986
|
+
"id",
|
|
987
|
+
"app_user_id",
|
|
988
|
+
"email",
|
|
989
|
+
"token_hash",
|
|
990
|
+
"expires_at",
|
|
991
|
+
"used_at",
|
|
992
|
+
"created_at",
|
|
993
|
+
"updated_at"
|
|
994
|
+
],
|
|
995
|
+
nullable: ["token_hash", "used_at"],
|
|
996
|
+
timestamps: [
|
|
997
|
+
"expires_at",
|
|
998
|
+
"used_at",
|
|
999
|
+
"created_at",
|
|
1000
|
+
"updated_at"
|
|
1001
|
+
],
|
|
1002
|
+
uniqueKeys: [{
|
|
1003
|
+
name: "email_confirmations_pkey",
|
|
1004
|
+
columns: ["id"]
|
|
1005
|
+
}, {
|
|
1006
|
+
name: "email_confirmations_token_hash_key",
|
|
1007
|
+
columns: ["token_hash"]
|
|
1008
|
+
}]
|
|
1009
|
+
},
|
|
1010
|
+
entries: {
|
|
1011
|
+
primaryKey: ["id"],
|
|
1012
|
+
generated: { id: "uuid" },
|
|
1013
|
+
columns: [
|
|
1014
|
+
"id",
|
|
1015
|
+
"model_id",
|
|
1016
|
+
"owner_app_user_id",
|
|
1017
|
+
"created_by_admin_id",
|
|
1018
|
+
"created_at",
|
|
1019
|
+
"updated_at",
|
|
1020
|
+
"deleted_at",
|
|
1021
|
+
"site_id"
|
|
1022
|
+
],
|
|
1023
|
+
nullable: [
|
|
1024
|
+
"owner_app_user_id",
|
|
1025
|
+
"created_by_admin_id",
|
|
1026
|
+
"deleted_at"
|
|
1027
|
+
],
|
|
1028
|
+
timestamps: [
|
|
1029
|
+
"created_at",
|
|
1030
|
+
"updated_at",
|
|
1031
|
+
"deleted_at"
|
|
1032
|
+
],
|
|
1033
|
+
uniqueKeys: [{
|
|
1034
|
+
name: "entries_id_site_uq",
|
|
1035
|
+
columns: ["id", "site_id"]
|
|
1036
|
+
}, {
|
|
1037
|
+
name: "entries_pkey",
|
|
1038
|
+
columns: ["id"]
|
|
1039
|
+
}]
|
|
1040
|
+
},
|
|
1041
|
+
entry_heads: {
|
|
1042
|
+
primaryKey: [
|
|
1043
|
+
"entry_id",
|
|
1044
|
+
"locale",
|
|
1045
|
+
"state"
|
|
1046
|
+
],
|
|
1047
|
+
generated: { change_seq: "entry_heads_change_seq" },
|
|
1048
|
+
columns: [
|
|
1049
|
+
"entry_id",
|
|
1050
|
+
"model_id",
|
|
1051
|
+
"locale",
|
|
1052
|
+
"state",
|
|
1053
|
+
"revision_id",
|
|
1054
|
+
"data",
|
|
1055
|
+
"autosaved_at",
|
|
1056
|
+
"version",
|
|
1057
|
+
"change_seq",
|
|
1058
|
+
"created_at",
|
|
1059
|
+
"updated_at",
|
|
1060
|
+
"site_id",
|
|
1061
|
+
"entry_created_at"
|
|
1062
|
+
],
|
|
1063
|
+
nullable: ["autosaved_at", "entry_created_at"],
|
|
1064
|
+
timestamps: [
|
|
1065
|
+
"autosaved_at",
|
|
1066
|
+
"created_at",
|
|
1067
|
+
"updated_at",
|
|
1068
|
+
"entry_created_at"
|
|
1069
|
+
],
|
|
1070
|
+
uniqueKeys: [{
|
|
1071
|
+
name: "entry_heads_pkey",
|
|
1072
|
+
columns: [
|
|
1073
|
+
"entry_id",
|
|
1074
|
+
"locale",
|
|
1075
|
+
"state"
|
|
1076
|
+
]
|
|
1077
|
+
}]
|
|
1078
|
+
},
|
|
1079
|
+
extension_hook_runs: {
|
|
1080
|
+
primaryKey: ["event_id", "hook"],
|
|
1081
|
+
columns: [
|
|
1082
|
+
"event_id",
|
|
1083
|
+
"hook",
|
|
1084
|
+
"job_id",
|
|
1085
|
+
"completed_at"
|
|
1086
|
+
],
|
|
1087
|
+
nullable: ["job_id"],
|
|
1088
|
+
timestamps: ["completed_at"],
|
|
1089
|
+
uniqueKeys: [{
|
|
1090
|
+
name: "extension_hook_runs_pkey",
|
|
1091
|
+
columns: ["event_id", "hook"]
|
|
1092
|
+
}]
|
|
1093
|
+
},
|
|
1094
|
+
field_reads: {
|
|
1095
|
+
primaryKey: [
|
|
1096
|
+
"day",
|
|
1097
|
+
"site_id",
|
|
1098
|
+
"model_id",
|
|
1099
|
+
"field_path",
|
|
1100
|
+
"principal_key",
|
|
1101
|
+
"selection"
|
|
1102
|
+
],
|
|
1103
|
+
columns: [
|
|
1104
|
+
"day",
|
|
1105
|
+
"model_id",
|
|
1106
|
+
"field_path",
|
|
1107
|
+
"principal_key",
|
|
1108
|
+
"selection",
|
|
1109
|
+
"reads",
|
|
1110
|
+
"last_read_at",
|
|
1111
|
+
"site_id"
|
|
1112
|
+
],
|
|
1113
|
+
nullable: [],
|
|
1114
|
+
timestamps: ["last_read_at"],
|
|
1115
|
+
uniqueKeys: [{
|
|
1116
|
+
name: "field_reads_pkey",
|
|
1117
|
+
columns: [
|
|
1118
|
+
"day",
|
|
1119
|
+
"site_id",
|
|
1120
|
+
"model_id",
|
|
1121
|
+
"field_path",
|
|
1122
|
+
"principal_key",
|
|
1123
|
+
"selection"
|
|
1124
|
+
]
|
|
1125
|
+
}]
|
|
1126
|
+
},
|
|
1127
|
+
jobs: {
|
|
1128
|
+
primaryKey: ["id"],
|
|
1129
|
+
generated: { id: "uuid" },
|
|
1130
|
+
columns: [
|
|
1131
|
+
"id",
|
|
1132
|
+
"type",
|
|
1133
|
+
"payload",
|
|
1134
|
+
"status",
|
|
1135
|
+
"priority",
|
|
1136
|
+
"run_at",
|
|
1137
|
+
"attempts",
|
|
1138
|
+
"max_attempts",
|
|
1139
|
+
"idempotency_key",
|
|
1140
|
+
"locked_by",
|
|
1141
|
+
"locked_until",
|
|
1142
|
+
"checkpoint",
|
|
1143
|
+
"result",
|
|
1144
|
+
"last_error",
|
|
1145
|
+
"created_at",
|
|
1146
|
+
"updated_at",
|
|
1147
|
+
"finished_at"
|
|
1148
|
+
],
|
|
1149
|
+
nullable: [
|
|
1150
|
+
"idempotency_key",
|
|
1151
|
+
"locked_by",
|
|
1152
|
+
"locked_until",
|
|
1153
|
+
"checkpoint",
|
|
1154
|
+
"result",
|
|
1155
|
+
"last_error",
|
|
1156
|
+
"finished_at"
|
|
1157
|
+
],
|
|
1158
|
+
timestamps: [
|
|
1159
|
+
"run_at",
|
|
1160
|
+
"locked_until",
|
|
1161
|
+
"created_at",
|
|
1162
|
+
"updated_at",
|
|
1163
|
+
"finished_at"
|
|
1164
|
+
],
|
|
1165
|
+
uniqueKeys: [{
|
|
1166
|
+
name: "jobs_idempotency_key_uq",
|
|
1167
|
+
columns: ["idempotency_key"]
|
|
1168
|
+
}, {
|
|
1169
|
+
name: "jobs_pkey",
|
|
1170
|
+
columns: ["id"]
|
|
1171
|
+
}]
|
|
1172
|
+
},
|
|
1173
|
+
locales: {
|
|
1174
|
+
primaryKey: ["code"],
|
|
1175
|
+
columns: [
|
|
1176
|
+
"code",
|
|
1177
|
+
"label",
|
|
1178
|
+
"is_default",
|
|
1179
|
+
"fallbacks",
|
|
1180
|
+
"created_at",
|
|
1181
|
+
"updated_at"
|
|
1182
|
+
],
|
|
1183
|
+
nullable: [],
|
|
1184
|
+
timestamps: ["created_at", "updated_at"],
|
|
1185
|
+
uniqueKeys: [{
|
|
1186
|
+
name: "locales_pkey",
|
|
1187
|
+
columns: ["code"]
|
|
1188
|
+
}, {
|
|
1189
|
+
name: "locales_single_default_uq",
|
|
1190
|
+
columns: ["is_default"]
|
|
1191
|
+
}]
|
|
1192
|
+
},
|
|
1193
|
+
media_assets: {
|
|
1194
|
+
primaryKey: ["id"],
|
|
1195
|
+
columns: [
|
|
1196
|
+
"id",
|
|
1197
|
+
"folder_id",
|
|
1198
|
+
"storage_driver",
|
|
1199
|
+
"storage_key",
|
|
1200
|
+
"original_filename",
|
|
1201
|
+
"mime_type",
|
|
1202
|
+
"size_bytes",
|
|
1203
|
+
"width",
|
|
1204
|
+
"height",
|
|
1205
|
+
"checksum_sha256",
|
|
1206
|
+
"alt",
|
|
1207
|
+
"caption",
|
|
1208
|
+
"focal_x",
|
|
1209
|
+
"focal_y",
|
|
1210
|
+
"visibility",
|
|
1211
|
+
"status",
|
|
1212
|
+
"processing_error",
|
|
1213
|
+
"created_by",
|
|
1214
|
+
"version",
|
|
1215
|
+
"deleted_at",
|
|
1216
|
+
"created_at",
|
|
1217
|
+
"updated_at",
|
|
1218
|
+
"site_id"
|
|
1219
|
+
],
|
|
1220
|
+
nullable: [
|
|
1221
|
+
"folder_id",
|
|
1222
|
+
"width",
|
|
1223
|
+
"height",
|
|
1224
|
+
"checksum_sha256",
|
|
1225
|
+
"focal_x",
|
|
1226
|
+
"focal_y",
|
|
1227
|
+
"processing_error",
|
|
1228
|
+
"created_by",
|
|
1229
|
+
"deleted_at"
|
|
1230
|
+
],
|
|
1231
|
+
timestamps: [
|
|
1232
|
+
"deleted_at",
|
|
1233
|
+
"created_at",
|
|
1234
|
+
"updated_at"
|
|
1235
|
+
],
|
|
1236
|
+
uniqueKeys: [{
|
|
1237
|
+
name: "media_assets_pkey",
|
|
1238
|
+
columns: ["id"]
|
|
1239
|
+
}, {
|
|
1240
|
+
name: "media_assets_storage_key_uq",
|
|
1241
|
+
columns: ["storage_key"]
|
|
1242
|
+
}]
|
|
1243
|
+
},
|
|
1244
|
+
media_folders: {
|
|
1245
|
+
primaryKey: ["id"],
|
|
1246
|
+
generated: { id: "uuid" },
|
|
1247
|
+
columns: [
|
|
1248
|
+
"id",
|
|
1249
|
+
"parent_id",
|
|
1250
|
+
"name",
|
|
1251
|
+
"created_by",
|
|
1252
|
+
"version",
|
|
1253
|
+
"created_at",
|
|
1254
|
+
"updated_at",
|
|
1255
|
+
"site_id"
|
|
1256
|
+
],
|
|
1257
|
+
nullable: ["parent_id", "created_by"],
|
|
1258
|
+
timestamps: ["created_at", "updated_at"],
|
|
1259
|
+
uniqueKeys: [
|
|
1260
|
+
{
|
|
1261
|
+
name: "media_folders_id_site_uq",
|
|
1262
|
+
columns: ["id", "site_id"]
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
name: "media_folders_pkey",
|
|
1266
|
+
columns: ["id"]
|
|
1267
|
+
},
|
|
1268
|
+
{
|
|
1269
|
+
name: "media_folders_sibling_name_uq",
|
|
1270
|
+
columns: [
|
|
1271
|
+
"site_id",
|
|
1272
|
+
"parent_id",
|
|
1273
|
+
"name"
|
|
1274
|
+
]
|
|
1275
|
+
}
|
|
1276
|
+
]
|
|
1277
|
+
},
|
|
1278
|
+
media_references: {
|
|
1279
|
+
primaryKey: [
|
|
1280
|
+
"asset_id",
|
|
1281
|
+
"entry_id",
|
|
1282
|
+
"locale",
|
|
1283
|
+
"state",
|
|
1284
|
+
"field_id"
|
|
1285
|
+
],
|
|
1286
|
+
columns: [
|
|
1287
|
+
"asset_id",
|
|
1288
|
+
"entry_id",
|
|
1289
|
+
"model_id",
|
|
1290
|
+
"field_id",
|
|
1291
|
+
"locale",
|
|
1292
|
+
"state",
|
|
1293
|
+
"created_at"
|
|
1294
|
+
],
|
|
1295
|
+
nullable: [],
|
|
1296
|
+
timestamps: ["created_at"],
|
|
1297
|
+
uniqueKeys: [{
|
|
1298
|
+
name: "media_references_pk",
|
|
1299
|
+
columns: [
|
|
1300
|
+
"asset_id",
|
|
1301
|
+
"entry_id",
|
|
1302
|
+
"locale",
|
|
1303
|
+
"state",
|
|
1304
|
+
"field_id"
|
|
1305
|
+
]
|
|
1306
|
+
}]
|
|
1307
|
+
},
|
|
1308
|
+
media_upload_grants: {
|
|
1309
|
+
primaryKey: ["id"],
|
|
1310
|
+
generated: { id: "uuid" },
|
|
1311
|
+
columns: [
|
|
1312
|
+
"id",
|
|
1313
|
+
"asset_id",
|
|
1314
|
+
"kind",
|
|
1315
|
+
"storage_driver",
|
|
1316
|
+
"storage_key",
|
|
1317
|
+
"original_filename",
|
|
1318
|
+
"declared_mime_type",
|
|
1319
|
+
"max_size_bytes",
|
|
1320
|
+
"folder_id",
|
|
1321
|
+
"visibility",
|
|
1322
|
+
"created_by",
|
|
1323
|
+
"created_by_principal",
|
|
1324
|
+
"status",
|
|
1325
|
+
"expires_at",
|
|
1326
|
+
"consumed_at",
|
|
1327
|
+
"created_at",
|
|
1328
|
+
"site_id"
|
|
1329
|
+
],
|
|
1330
|
+
nullable: [
|
|
1331
|
+
"folder_id",
|
|
1332
|
+
"created_by",
|
|
1333
|
+
"consumed_at"
|
|
1334
|
+
],
|
|
1335
|
+
timestamps: [
|
|
1336
|
+
"expires_at",
|
|
1337
|
+
"consumed_at",
|
|
1338
|
+
"created_at"
|
|
1339
|
+
],
|
|
1340
|
+
uniqueKeys: [{
|
|
1341
|
+
name: "media_upload_grants_pkey",
|
|
1342
|
+
columns: ["id"]
|
|
1343
|
+
}, {
|
|
1344
|
+
name: "media_upload_grants_storage_key_key",
|
|
1345
|
+
columns: ["storage_key"]
|
|
1346
|
+
}]
|
|
1347
|
+
},
|
|
1348
|
+
media_variants: {
|
|
1349
|
+
primaryKey: ["id"],
|
|
1350
|
+
generated: { id: "uuid" },
|
|
1351
|
+
columns: [
|
|
1352
|
+
"id",
|
|
1353
|
+
"asset_id",
|
|
1354
|
+
"name",
|
|
1355
|
+
"width",
|
|
1356
|
+
"height",
|
|
1357
|
+
"format",
|
|
1358
|
+
"mime_type",
|
|
1359
|
+
"storage_key",
|
|
1360
|
+
"size_bytes",
|
|
1361
|
+
"checksum_sha256",
|
|
1362
|
+
"status",
|
|
1363
|
+
"error",
|
|
1364
|
+
"created_at",
|
|
1365
|
+
"updated_at"
|
|
1366
|
+
],
|
|
1367
|
+
nullable: [
|
|
1368
|
+
"width",
|
|
1369
|
+
"height",
|
|
1370
|
+
"storage_key",
|
|
1371
|
+
"size_bytes",
|
|
1372
|
+
"checksum_sha256",
|
|
1373
|
+
"error"
|
|
1374
|
+
],
|
|
1375
|
+
timestamps: ["created_at", "updated_at"],
|
|
1376
|
+
uniqueKeys: [
|
|
1377
|
+
{
|
|
1378
|
+
name: "media_variants_asset_name_uq",
|
|
1379
|
+
columns: ["asset_id", "name"]
|
|
1380
|
+
},
|
|
1381
|
+
{
|
|
1382
|
+
name: "media_variants_pkey",
|
|
1383
|
+
columns: ["id"]
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
name: "media_variants_storage_key_uq",
|
|
1387
|
+
columns: ["storage_key"]
|
|
1388
|
+
}
|
|
1389
|
+
]
|
|
1390
|
+
},
|
|
1391
|
+
model_active_versions: {
|
|
1392
|
+
primaryKey: ["model_id"],
|
|
1393
|
+
columns: [
|
|
1394
|
+
"model_id",
|
|
1395
|
+
"revision_id",
|
|
1396
|
+
"version",
|
|
1397
|
+
"schema_version",
|
|
1398
|
+
"activated_at"
|
|
1399
|
+
],
|
|
1400
|
+
nullable: [],
|
|
1401
|
+
timestamps: ["activated_at"],
|
|
1402
|
+
uniqueKeys: [{
|
|
1403
|
+
name: "model_active_versions_pkey",
|
|
1404
|
+
columns: ["model_id"]
|
|
1405
|
+
}]
|
|
1406
|
+
},
|
|
1407
|
+
models: {
|
|
1408
|
+
primaryKey: ["id"],
|
|
1409
|
+
columns: [
|
|
1410
|
+
"id",
|
|
1411
|
+
"kind",
|
|
1412
|
+
"api_key",
|
|
1413
|
+
"created_at",
|
|
1414
|
+
"updated_at",
|
|
1415
|
+
"deleted_at",
|
|
1416
|
+
"site_id"
|
|
1417
|
+
],
|
|
1418
|
+
nullable: ["deleted_at", "site_id"],
|
|
1419
|
+
timestamps: [
|
|
1420
|
+
"created_at",
|
|
1421
|
+
"updated_at",
|
|
1422
|
+
"deleted_at"
|
|
1423
|
+
],
|
|
1424
|
+
uniqueKeys: [{
|
|
1425
|
+
name: "models_api_key_uq",
|
|
1426
|
+
columns: ["site_id", "api_key"]
|
|
1427
|
+
}, {
|
|
1428
|
+
name: "models_pkey",
|
|
1429
|
+
columns: ["id"]
|
|
1430
|
+
}]
|
|
1431
|
+
},
|
|
1432
|
+
outbox_events: {
|
|
1433
|
+
primaryKey: ["id"],
|
|
1434
|
+
identity: "id",
|
|
1435
|
+
generated: { event_id: "uuid" },
|
|
1436
|
+
columns: [
|
|
1437
|
+
"id",
|
|
1438
|
+
"event_id",
|
|
1439
|
+
"type",
|
|
1440
|
+
"aggregate_type",
|
|
1441
|
+
"aggregate_id",
|
|
1442
|
+
"payload",
|
|
1443
|
+
"created_at",
|
|
1444
|
+
"dispatched_at",
|
|
1445
|
+
"dispatch_attempts",
|
|
1446
|
+
"last_dispatch_error",
|
|
1447
|
+
"site_id"
|
|
1448
|
+
],
|
|
1449
|
+
nullable: [
|
|
1450
|
+
"dispatched_at",
|
|
1451
|
+
"last_dispatch_error",
|
|
1452
|
+
"site_id"
|
|
1453
|
+
],
|
|
1454
|
+
timestamps: ["created_at", "dispatched_at"],
|
|
1455
|
+
uniqueKeys: [{
|
|
1456
|
+
name: "outbox_events_event_id_key",
|
|
1457
|
+
columns: ["event_id"]
|
|
1458
|
+
}, {
|
|
1459
|
+
name: "outbox_events_pkey",
|
|
1460
|
+
columns: ["id"]
|
|
1461
|
+
}]
|
|
1462
|
+
},
|
|
1463
|
+
password_resets: {
|
|
1464
|
+
primaryKey: ["id"],
|
|
1465
|
+
generated: { id: "uuid" },
|
|
1466
|
+
columns: [
|
|
1467
|
+
"id",
|
|
1468
|
+
"admin_user_id",
|
|
1469
|
+
"token_hash",
|
|
1470
|
+
"expires_at",
|
|
1471
|
+
"used_at",
|
|
1472
|
+
"created_at",
|
|
1473
|
+
"updated_at"
|
|
1474
|
+
],
|
|
1475
|
+
nullable: ["token_hash", "used_at"],
|
|
1476
|
+
timestamps: [
|
|
1477
|
+
"expires_at",
|
|
1478
|
+
"used_at",
|
|
1479
|
+
"created_at",
|
|
1480
|
+
"updated_at"
|
|
1481
|
+
],
|
|
1482
|
+
uniqueKeys: [{
|
|
1483
|
+
name: "password_resets_pkey",
|
|
1484
|
+
columns: ["id"]
|
|
1485
|
+
}, {
|
|
1486
|
+
name: "password_resets_token_hash_key",
|
|
1487
|
+
columns: ["token_hash"]
|
|
1488
|
+
}]
|
|
1489
|
+
},
|
|
1490
|
+
preview_tokens: {
|
|
1491
|
+
primaryKey: ["id"],
|
|
1492
|
+
generated: { id: "uuid" },
|
|
1493
|
+
columns: [
|
|
1494
|
+
"id",
|
|
1495
|
+
"token_hash",
|
|
1496
|
+
"token_prefix",
|
|
1497
|
+
"model_id",
|
|
1498
|
+
"entry_id",
|
|
1499
|
+
"locale",
|
|
1500
|
+
"connection_id",
|
|
1501
|
+
"delivery_role_id",
|
|
1502
|
+
"created_by",
|
|
1503
|
+
"expires_at",
|
|
1504
|
+
"revoked_at",
|
|
1505
|
+
"last_used_at",
|
|
1506
|
+
"created_at",
|
|
1507
|
+
"site_id"
|
|
1508
|
+
],
|
|
1509
|
+
nullable: [
|
|
1510
|
+
"locale",
|
|
1511
|
+
"connection_id",
|
|
1512
|
+
"delivery_role_id",
|
|
1513
|
+
"revoked_at",
|
|
1514
|
+
"last_used_at"
|
|
1515
|
+
],
|
|
1516
|
+
timestamps: [
|
|
1517
|
+
"expires_at",
|
|
1518
|
+
"revoked_at",
|
|
1519
|
+
"last_used_at",
|
|
1520
|
+
"created_at"
|
|
1521
|
+
],
|
|
1522
|
+
uniqueKeys: [{
|
|
1523
|
+
name: "preview_tokens_pkey",
|
|
1524
|
+
columns: ["id"]
|
|
1525
|
+
}, {
|
|
1526
|
+
name: "preview_tokens_token_hash_key",
|
|
1527
|
+
columns: ["token_hash"]
|
|
1528
|
+
}]
|
|
1529
|
+
},
|
|
1530
|
+
publication_log: {
|
|
1531
|
+
primaryKey: ["id"],
|
|
1532
|
+
identity: "id",
|
|
1533
|
+
columns: [
|
|
1534
|
+
"id",
|
|
1535
|
+
"entry_id",
|
|
1536
|
+
"model_id",
|
|
1537
|
+
"locale",
|
|
1538
|
+
"revision_id",
|
|
1539
|
+
"from_seq",
|
|
1540
|
+
"to_seq",
|
|
1541
|
+
"published_at",
|
|
1542
|
+
"site_id"
|
|
1543
|
+
],
|
|
1544
|
+
nullable: ["to_seq"],
|
|
1545
|
+
timestamps: ["published_at"],
|
|
1546
|
+
uniqueKeys: [{
|
|
1547
|
+
name: "publication_log_open_uq",
|
|
1548
|
+
columns: ["entry_id", "locale"]
|
|
1549
|
+
}, {
|
|
1550
|
+
name: "publication_log_pkey",
|
|
1551
|
+
columns: ["id"]
|
|
1552
|
+
}]
|
|
1553
|
+
},
|
|
1554
|
+
publication_snapshots: {
|
|
1555
|
+
primaryKey: ["site_id", "seq"],
|
|
1556
|
+
columns: [
|
|
1557
|
+
"seq",
|
|
1558
|
+
"schema_version",
|
|
1559
|
+
"source",
|
|
1560
|
+
"change_set_id",
|
|
1561
|
+
"actor_type",
|
|
1562
|
+
"actor_id",
|
|
1563
|
+
"created_at",
|
|
1564
|
+
"site_id"
|
|
1565
|
+
],
|
|
1566
|
+
nullable: [
|
|
1567
|
+
"schema_version",
|
|
1568
|
+
"change_set_id",
|
|
1569
|
+
"actor_type",
|
|
1570
|
+
"actor_id"
|
|
1571
|
+
],
|
|
1572
|
+
timestamps: ["created_at"],
|
|
1573
|
+
uniqueKeys: [{
|
|
1574
|
+
name: "publication_snapshots_pkey",
|
|
1575
|
+
columns: ["site_id", "seq"]
|
|
1576
|
+
}]
|
|
1577
|
+
},
|
|
1578
|
+
publication_state: {
|
|
1579
|
+
primaryKey: ["site_id"],
|
|
1580
|
+
columns: ["last_seq", "site_id"],
|
|
1581
|
+
nullable: [],
|
|
1582
|
+
uniqueKeys: [{
|
|
1583
|
+
name: "publication_state_pkey",
|
|
1584
|
+
columns: ["site_id"]
|
|
1585
|
+
}]
|
|
1586
|
+
},
|
|
1587
|
+
relation_edges: {
|
|
1588
|
+
primaryKey: [
|
|
1589
|
+
"source_entry_id",
|
|
1590
|
+
"locale",
|
|
1591
|
+
"state",
|
|
1592
|
+
"field_id",
|
|
1593
|
+
"position"
|
|
1594
|
+
],
|
|
1595
|
+
columns: [
|
|
1596
|
+
"source_entry_id",
|
|
1597
|
+
"locale",
|
|
1598
|
+
"state",
|
|
1599
|
+
"field_id",
|
|
1600
|
+
"position",
|
|
1601
|
+
"target_entry_id"
|
|
1602
|
+
],
|
|
1603
|
+
nullable: [],
|
|
1604
|
+
uniqueKeys: [{
|
|
1605
|
+
name: "relation_edges_pkey",
|
|
1606
|
+
columns: [
|
|
1607
|
+
"source_entry_id",
|
|
1608
|
+
"locale",
|
|
1609
|
+
"state",
|
|
1610
|
+
"field_id",
|
|
1611
|
+
"position"
|
|
1612
|
+
]
|
|
1613
|
+
}]
|
|
1614
|
+
},
|
|
1615
|
+
scheduled_publications: {
|
|
1616
|
+
primaryKey: ["id"],
|
|
1617
|
+
generated: { id: "uuid" },
|
|
1618
|
+
columns: [
|
|
1619
|
+
"id",
|
|
1620
|
+
"entry_id",
|
|
1621
|
+
"model_id",
|
|
1622
|
+
"locale",
|
|
1623
|
+
"action",
|
|
1624
|
+
"run_at",
|
|
1625
|
+
"status",
|
|
1626
|
+
"created_by_token",
|
|
1627
|
+
"job_id",
|
|
1628
|
+
"snapshot_seq",
|
|
1629
|
+
"error",
|
|
1630
|
+
"executed_at",
|
|
1631
|
+
"created_by",
|
|
1632
|
+
"created_at",
|
|
1633
|
+
"updated_at",
|
|
1634
|
+
"site_id"
|
|
1635
|
+
],
|
|
1636
|
+
nullable: [
|
|
1637
|
+
"created_by_token",
|
|
1638
|
+
"job_id",
|
|
1639
|
+
"snapshot_seq",
|
|
1640
|
+
"error",
|
|
1641
|
+
"executed_at",
|
|
1642
|
+
"created_by"
|
|
1643
|
+
],
|
|
1644
|
+
timestamps: [
|
|
1645
|
+
"run_at",
|
|
1646
|
+
"executed_at",
|
|
1647
|
+
"created_at",
|
|
1648
|
+
"updated_at"
|
|
1649
|
+
],
|
|
1650
|
+
uniqueKeys: [{
|
|
1651
|
+
name: "scheduled_publications_pkey",
|
|
1652
|
+
columns: ["id"]
|
|
1653
|
+
}]
|
|
1654
|
+
},
|
|
1655
|
+
schema_change_jobs: {
|
|
1656
|
+
primaryKey: ["id"],
|
|
1657
|
+
generated: { id: "uuid" },
|
|
1658
|
+
columns: [
|
|
1659
|
+
"id",
|
|
1660
|
+
"target_type",
|
|
1661
|
+
"target_id",
|
|
1662
|
+
"model_id",
|
|
1663
|
+
"from_revision_id",
|
|
1664
|
+
"to_revision_id",
|
|
1665
|
+
"status",
|
|
1666
|
+
"plan",
|
|
1667
|
+
"job_id",
|
|
1668
|
+
"error",
|
|
1669
|
+
"requested_by_type",
|
|
1670
|
+
"requested_by_id",
|
|
1671
|
+
"created_at",
|
|
1672
|
+
"updated_at",
|
|
1673
|
+
"finished_at",
|
|
1674
|
+
"change_set_id"
|
|
1675
|
+
],
|
|
1676
|
+
nullable: [
|
|
1677
|
+
"model_id",
|
|
1678
|
+
"from_revision_id",
|
|
1679
|
+
"to_revision_id",
|
|
1680
|
+
"job_id",
|
|
1681
|
+
"error",
|
|
1682
|
+
"requested_by_id",
|
|
1683
|
+
"finished_at",
|
|
1684
|
+
"change_set_id"
|
|
1685
|
+
],
|
|
1686
|
+
timestamps: [
|
|
1687
|
+
"created_at",
|
|
1688
|
+
"updated_at",
|
|
1689
|
+
"finished_at"
|
|
1690
|
+
],
|
|
1691
|
+
uniqueKeys: [{
|
|
1692
|
+
name: "schema_change_jobs_in_flight_uq",
|
|
1693
|
+
columns: ["target_type", "target_id"]
|
|
1694
|
+
}, {
|
|
1695
|
+
name: "schema_change_jobs_pkey",
|
|
1696
|
+
columns: ["id"]
|
|
1697
|
+
}]
|
|
1698
|
+
},
|
|
1699
|
+
schema_drafts: {
|
|
1700
|
+
primaryKey: ["id"],
|
|
1701
|
+
generated: { id: "uuid" },
|
|
1702
|
+
columns: [
|
|
1703
|
+
"id",
|
|
1704
|
+
"change_set_id",
|
|
1705
|
+
"definition_id",
|
|
1706
|
+
"kind",
|
|
1707
|
+
"api_key",
|
|
1708
|
+
"base_version",
|
|
1709
|
+
"definition",
|
|
1710
|
+
"version",
|
|
1711
|
+
"updated_by_type",
|
|
1712
|
+
"updated_by_id",
|
|
1713
|
+
"created_at",
|
|
1714
|
+
"updated_at",
|
|
1715
|
+
"shared"
|
|
1716
|
+
],
|
|
1717
|
+
nullable: [
|
|
1718
|
+
"base_version",
|
|
1719
|
+
"definition",
|
|
1720
|
+
"updated_by_id"
|
|
1721
|
+
],
|
|
1722
|
+
timestamps: ["created_at", "updated_at"],
|
|
1723
|
+
uniqueKeys: [{
|
|
1724
|
+
name: "schema_drafts_pkey",
|
|
1725
|
+
columns: ["id"]
|
|
1726
|
+
}, {
|
|
1727
|
+
name: "schema_drafts_set_definition_uq",
|
|
1728
|
+
columns: ["change_set_id", "definition_id"]
|
|
1729
|
+
}]
|
|
1730
|
+
},
|
|
1731
|
+
schema_revisions: {
|
|
1732
|
+
primaryKey: ["id"],
|
|
1733
|
+
generated: { id: "uuid" },
|
|
1734
|
+
columns: [
|
|
1735
|
+
"id",
|
|
1736
|
+
"model_id",
|
|
1737
|
+
"version",
|
|
1738
|
+
"definition",
|
|
1739
|
+
"hash",
|
|
1740
|
+
"parent_revision_id",
|
|
1741
|
+
"created_by_type",
|
|
1742
|
+
"created_by_id",
|
|
1743
|
+
"created_at"
|
|
1744
|
+
],
|
|
1745
|
+
nullable: ["parent_revision_id", "created_by_id"],
|
|
1746
|
+
timestamps: ["created_at"],
|
|
1747
|
+
uniqueKeys: [{
|
|
1748
|
+
name: "schema_revisions_model_version_uq",
|
|
1749
|
+
columns: ["model_id", "version"]
|
|
1750
|
+
}, {
|
|
1751
|
+
name: "schema_revisions_pkey",
|
|
1752
|
+
columns: ["id"]
|
|
1753
|
+
}]
|
|
1754
|
+
},
|
|
1755
|
+
schema_settings: {
|
|
1756
|
+
primaryKey: ["id"],
|
|
1757
|
+
columns: [
|
|
1758
|
+
"id",
|
|
1759
|
+
"read_only",
|
|
1760
|
+
"read_only_reason",
|
|
1761
|
+
"updated_by_type",
|
|
1762
|
+
"updated_by_id",
|
|
1763
|
+
"updated_at"
|
|
1764
|
+
],
|
|
1765
|
+
nullable: [
|
|
1766
|
+
"read_only_reason",
|
|
1767
|
+
"updated_by_type",
|
|
1768
|
+
"updated_by_id"
|
|
1769
|
+
],
|
|
1770
|
+
timestamps: ["updated_at"],
|
|
1771
|
+
uniqueKeys: [{
|
|
1772
|
+
name: "schema_settings_pkey",
|
|
1773
|
+
columns: ["id"]
|
|
1774
|
+
}]
|
|
1775
|
+
},
|
|
1776
|
+
setup_tokens: {
|
|
1777
|
+
primaryKey: ["id"],
|
|
1778
|
+
generated: { id: "uuid" },
|
|
1779
|
+
columns: [
|
|
1780
|
+
"id",
|
|
1781
|
+
"token_hash",
|
|
1782
|
+
"used_at",
|
|
1783
|
+
"superseded_at",
|
|
1784
|
+
"created_at",
|
|
1785
|
+
"updated_at"
|
|
1786
|
+
],
|
|
1787
|
+
nullable: ["used_at", "superseded_at"],
|
|
1788
|
+
timestamps: [
|
|
1789
|
+
"used_at",
|
|
1790
|
+
"superseded_at",
|
|
1791
|
+
"created_at",
|
|
1792
|
+
"updated_at"
|
|
1793
|
+
],
|
|
1794
|
+
uniqueKeys: [{
|
|
1795
|
+
name: "setup_tokens_pkey",
|
|
1796
|
+
columns: ["id"]
|
|
1797
|
+
}, {
|
|
1798
|
+
name: "setup_tokens_token_hash_key",
|
|
1799
|
+
columns: ["token_hash"]
|
|
1800
|
+
}]
|
|
1801
|
+
},
|
|
1802
|
+
site_app_roles: {
|
|
1803
|
+
primaryKey: [
|
|
1804
|
+
"site_id",
|
|
1805
|
+
"audience",
|
|
1806
|
+
"role_id"
|
|
1807
|
+
],
|
|
1808
|
+
columns: [
|
|
1809
|
+
"site_id",
|
|
1810
|
+
"audience",
|
|
1811
|
+
"role_id",
|
|
1812
|
+
"created_at"
|
|
1813
|
+
],
|
|
1814
|
+
nullable: [],
|
|
1815
|
+
timestamps: ["created_at"],
|
|
1816
|
+
uniqueKeys: [{
|
|
1817
|
+
name: "site_app_roles_pkey",
|
|
1818
|
+
columns: [
|
|
1819
|
+
"site_id",
|
|
1820
|
+
"audience",
|
|
1821
|
+
"role_id"
|
|
1822
|
+
]
|
|
1823
|
+
}]
|
|
1824
|
+
},
|
|
1825
|
+
sites: {
|
|
1826
|
+
primaryKey: ["id"],
|
|
1827
|
+
generated: { id: "uuid" },
|
|
1828
|
+
columns: [
|
|
1829
|
+
"id",
|
|
1830
|
+
"key",
|
|
1831
|
+
"name",
|
|
1832
|
+
"is_primary",
|
|
1833
|
+
"version",
|
|
1834
|
+
"created_at",
|
|
1835
|
+
"updated_at",
|
|
1836
|
+
"seo_defaults"
|
|
1837
|
+
],
|
|
1838
|
+
nullable: ["seo_defaults"],
|
|
1839
|
+
timestamps: ["created_at", "updated_at"],
|
|
1840
|
+
uniqueKeys: [
|
|
1841
|
+
{
|
|
1842
|
+
name: "sites_key_key",
|
|
1843
|
+
columns: ["key"]
|
|
1844
|
+
},
|
|
1845
|
+
{
|
|
1846
|
+
name: "sites_pkey",
|
|
1847
|
+
columns: ["id"]
|
|
1848
|
+
},
|
|
1849
|
+
{
|
|
1850
|
+
name: "sites_single_primary_uq",
|
|
1851
|
+
columns: ["is_primary"]
|
|
1852
|
+
}
|
|
1853
|
+
]
|
|
1854
|
+
},
|
|
1855
|
+
system_settings: {
|
|
1856
|
+
primaryKey: ["key"],
|
|
1857
|
+
columns: [
|
|
1858
|
+
"key",
|
|
1859
|
+
"value",
|
|
1860
|
+
"updated_at"
|
|
1861
|
+
],
|
|
1862
|
+
nullable: [],
|
|
1863
|
+
timestamps: ["updated_at"],
|
|
1864
|
+
uniqueKeys: [{
|
|
1865
|
+
name: "system_settings_pkey",
|
|
1866
|
+
columns: ["key"]
|
|
1867
|
+
}]
|
|
1868
|
+
},
|
|
1869
|
+
system_versions: {
|
|
1870
|
+
primaryKey: ["id"],
|
|
1871
|
+
columns: [
|
|
1872
|
+
"id",
|
|
1873
|
+
"schema_version",
|
|
1874
|
+
"permissions_version",
|
|
1875
|
+
"updated_at",
|
|
1876
|
+
"release"
|
|
1877
|
+
],
|
|
1878
|
+
nullable: ["release"],
|
|
1879
|
+
timestamps: ["updated_at"],
|
|
1880
|
+
uniqueKeys: [{
|
|
1881
|
+
name: "system_versions_pkey",
|
|
1882
|
+
columns: ["id"]
|
|
1883
|
+
}]
|
|
1884
|
+
},
|
|
1885
|
+
token_reads: {
|
|
1886
|
+
primaryKey: [
|
|
1887
|
+
"day",
|
|
1888
|
+
"site_id",
|
|
1889
|
+
"principal_key"
|
|
1890
|
+
],
|
|
1891
|
+
columns: [
|
|
1892
|
+
"day",
|
|
1893
|
+
"principal_key",
|
|
1894
|
+
"requests",
|
|
1895
|
+
"last_snapshot",
|
|
1896
|
+
"last_read_at",
|
|
1897
|
+
"site_id"
|
|
1898
|
+
],
|
|
1899
|
+
nullable: ["last_snapshot"],
|
|
1900
|
+
timestamps: ["last_read_at"],
|
|
1901
|
+
uniqueKeys: [{
|
|
1902
|
+
name: "token_reads_pkey",
|
|
1903
|
+
columns: [
|
|
1904
|
+
"day",
|
|
1905
|
+
"site_id",
|
|
1906
|
+
"principal_key"
|
|
1907
|
+
]
|
|
1908
|
+
}]
|
|
1909
|
+
},
|
|
1910
|
+
unique_values: {
|
|
1911
|
+
primaryKey: [
|
|
1912
|
+
"site_id",
|
|
1913
|
+
"field_id",
|
|
1914
|
+
"locale",
|
|
1915
|
+
"state",
|
|
1916
|
+
"value_hash"
|
|
1917
|
+
],
|
|
1918
|
+
columns: [
|
|
1919
|
+
"field_id",
|
|
1920
|
+
"locale",
|
|
1921
|
+
"state",
|
|
1922
|
+
"value_hash",
|
|
1923
|
+
"entry_id",
|
|
1924
|
+
"model_id",
|
|
1925
|
+
"site_id"
|
|
1926
|
+
],
|
|
1927
|
+
nullable: [],
|
|
1928
|
+
uniqueKeys: [{
|
|
1929
|
+
name: "unique_values_pkey",
|
|
1930
|
+
columns: [
|
|
1931
|
+
"site_id",
|
|
1932
|
+
"field_id",
|
|
1933
|
+
"locale",
|
|
1934
|
+
"state",
|
|
1935
|
+
"value_hash"
|
|
1936
|
+
]
|
|
1937
|
+
}]
|
|
1938
|
+
},
|
|
1939
|
+
webhook_deliveries: {
|
|
1940
|
+
primaryKey: ["id"],
|
|
1941
|
+
generated: { id: "uuid" },
|
|
1942
|
+
columns: [
|
|
1943
|
+
"id",
|
|
1944
|
+
"webhook_id",
|
|
1945
|
+
"event_id",
|
|
1946
|
+
"event_type",
|
|
1947
|
+
"payload",
|
|
1948
|
+
"is_test",
|
|
1949
|
+
"status",
|
|
1950
|
+
"attempts",
|
|
1951
|
+
"last_response_status",
|
|
1952
|
+
"last_error",
|
|
1953
|
+
"attempt_log",
|
|
1954
|
+
"job_id",
|
|
1955
|
+
"delivered_at",
|
|
1956
|
+
"created_at",
|
|
1957
|
+
"updated_at"
|
|
1958
|
+
],
|
|
1959
|
+
nullable: [
|
|
1960
|
+
"event_id",
|
|
1961
|
+
"last_response_status",
|
|
1962
|
+
"last_error",
|
|
1963
|
+
"job_id",
|
|
1964
|
+
"delivered_at"
|
|
1965
|
+
],
|
|
1966
|
+
timestamps: [
|
|
1967
|
+
"delivered_at",
|
|
1968
|
+
"created_at",
|
|
1969
|
+
"updated_at"
|
|
1970
|
+
],
|
|
1971
|
+
uniqueKeys: [{
|
|
1972
|
+
name: "webhook_deliveries_event_uq",
|
|
1973
|
+
columns: ["webhook_id", "event_id"]
|
|
1974
|
+
}, {
|
|
1975
|
+
name: "webhook_deliveries_pkey",
|
|
1976
|
+
columns: ["id"]
|
|
1977
|
+
}]
|
|
1978
|
+
},
|
|
1979
|
+
webhooks: {
|
|
1980
|
+
primaryKey: ["id"],
|
|
1981
|
+
generated: { id: "uuid" },
|
|
1982
|
+
columns: [
|
|
1983
|
+
"id",
|
|
1984
|
+
"name",
|
|
1985
|
+
"url",
|
|
1986
|
+
"events",
|
|
1987
|
+
"enabled",
|
|
1988
|
+
"secret_encrypted",
|
|
1989
|
+
"allow_private_network",
|
|
1990
|
+
"max_attempts",
|
|
1991
|
+
"version",
|
|
1992
|
+
"created_by",
|
|
1993
|
+
"created_at",
|
|
1994
|
+
"updated_at",
|
|
1995
|
+
"site_id"
|
|
1996
|
+
],
|
|
1997
|
+
nullable: ["created_by", "site_id"],
|
|
1998
|
+
timestamps: ["created_at", "updated_at"],
|
|
1999
|
+
uniqueKeys: [{
|
|
2000
|
+
name: "webhooks_pkey",
|
|
2001
|
+
columns: ["id"]
|
|
2002
|
+
}]
|
|
2003
|
+
},
|
|
2004
|
+
advisory_locks: {
|
|
2005
|
+
primaryKey: ["namespace", "lock_key"],
|
|
2006
|
+
columns: ["namespace", "lock_key"],
|
|
2007
|
+
nullable: [],
|
|
2008
|
+
uniqueKeys: [{
|
|
2009
|
+
name: "advisory_locks_pkey",
|
|
2010
|
+
columns: ["namespace", "lock_key"]
|
|
2011
|
+
}]
|
|
2012
|
+
},
|
|
2013
|
+
notifications: {
|
|
2014
|
+
primaryKey: ["id"],
|
|
2015
|
+
identity: "id",
|
|
2016
|
+
columns: [
|
|
2017
|
+
"id",
|
|
2018
|
+
"channel",
|
|
2019
|
+
"payload",
|
|
2020
|
+
"created_at"
|
|
2021
|
+
],
|
|
2022
|
+
nullable: [],
|
|
2023
|
+
timestamps: ["created_at"],
|
|
2024
|
+
uniqueKeys: [{
|
|
2025
|
+
name: "notifications_pkey",
|
|
2026
|
+
columns: ["id"]
|
|
2027
|
+
}]
|
|
2028
|
+
},
|
|
2029
|
+
sequence_entry_heads_change_seq: {
|
|
2030
|
+
primaryKey: ["value"],
|
|
2031
|
+
identity: "value",
|
|
2032
|
+
columns: ["value"],
|
|
2033
|
+
nullable: [],
|
|
2034
|
+
uniqueKeys: [{
|
|
2035
|
+
name: "sequence_entry_heads_change_seq_pkey",
|
|
2036
|
+
columns: ["value"]
|
|
2037
|
+
}]
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
/** The table's metadata, or undefined for a table Shapio does not own (tests' own tables). */
|
|
2041
|
+
const tableInfo = (table) => MYSQL_TABLES[table];
|
|
2042
|
+
//#endregion
|
|
2043
|
+
//#region ../../apps/api/src/db/mysql/conflicts.ts
|
|
2044
|
+
/** The alias the compiler gives the inserted row in `ON DUPLICATE KEY UPDATE` (PostgreSQL's `excluded`). */
|
|
2045
|
+
const EXCLUDED_ALIAS = "excluded";
|
|
2046
|
+
const sameColumns = (a, b) => a.length === b.length && a.every((column) => b.includes(column));
|
|
2047
|
+
/**
|
|
2048
|
+
* Whether `ON DUPLICATE KEY` (which fires on any unique key) behaves like `ON CONFLICT (target)` for this
|
|
2049
|
+
* insert: every other unique key of the table is made of columns generated for this insert (fresh UUIDs),
|
|
2050
|
+
* so it cannot collide. An untargeted conflict (`ON CONFLICT DO NOTHING`) matches any key, like MySQL.
|
|
2051
|
+
*/
|
|
2052
|
+
const duplicateKeyMatchesTarget = (table, target, supplied) => {
|
|
2053
|
+
const info = tableInfo(table);
|
|
2054
|
+
if (!info) return false;
|
|
2055
|
+
if (target === void 0) return true;
|
|
2056
|
+
const generated = info.generated ?? {};
|
|
2057
|
+
return info.uniqueKeys.every((key) => sameColumns(key.columns, target) || key.columns.every((column) => generated[column] === "uuid" && !supplied.includes(column)));
|
|
2058
|
+
};
|
|
2059
|
+
/**
|
|
2060
|
+
* Tables whose rows a trigger keeps immutable (the baseline's `*_no_update` triggers): a no-op
|
|
2061
|
+
* `ON DUPLICATE KEY UPDATE` is still an UPDATE there, so their `DO NOTHING` goes through the plan.
|
|
2062
|
+
*/
|
|
2063
|
+
const REFUSES_UPDATES = /* @__PURE__ */ new Set(["content_revisions", "schema_revisions"]);
|
|
2064
|
+
const targetOf = (conflict) => conflict.columns?.map((column) => column.column.name);
|
|
2065
|
+
/**
|
|
2066
|
+
* `ON CONFLICT … DO UPDATE` as `ON DUPLICATE KEY UPDATE`, and `ON CONFLICT … DO NOTHING` without RETURNING
|
|
2067
|
+
* as `ON DUPLICATE KEY UPDATE <key> = <key>` (a no-op), when MySQL's any-key behaviour equals the target's
|
|
2068
|
+
* (`duplicateKeyMatchesTarget`). Any other conflict clause (another unique key could fire, a `WHERE` on the
|
|
2069
|
+
* update, RETURNING) is left for the compiler's plan: each row inserted under a savepoint, and a duplicate
|
|
2070
|
+
* on the target either skipped or turned into an UPDATE of the row it found.
|
|
2071
|
+
*/
|
|
2072
|
+
const toOnDuplicateKey = (node, conflict, table) => {
|
|
2073
|
+
const target = targetOf(conflict);
|
|
2074
|
+
const supplied = node.columns?.map((column) => column.column.name) ?? [];
|
|
2075
|
+
const matches = duplicateKeyMatchesTarget(table, target, supplied);
|
|
2076
|
+
if (conflict.doNothing) {
|
|
2077
|
+
if (node.returning || !matches || REFUSES_UPDATES.has(table)) return node;
|
|
2078
|
+
const column = target?.[0] ?? tableInfo(table).primaryKey[0];
|
|
2079
|
+
const self = ReferenceNode.create(ColumnNode.create(column), TableNode.create(table));
|
|
2080
|
+
return Object.freeze({
|
|
2081
|
+
...node,
|
|
2082
|
+
onConflict: void 0,
|
|
2083
|
+
onDuplicateKey: OnDuplicateKeyNode.create([ColumnUpdateNode.create(ColumnNode.create(column), self)])
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
if (conflict.constraint || conflict.indexExpression) throw new Error(`MySQL: ON CONFLICT on ${table} needs a column target`);
|
|
2087
|
+
if (conflict.updateWhere || !matches || node.returning || !node.values || !ValuesNode.is(node.values)) return node;
|
|
2088
|
+
return Object.freeze({
|
|
2089
|
+
...node,
|
|
2090
|
+
onConflict: void 0,
|
|
2091
|
+
onDuplicateKey: OnDuplicateKeyNode.create(conflict.updates ?? [])
|
|
2092
|
+
});
|
|
2093
|
+
};
|
|
2094
|
+
const FOREIGN_KEY_CODES = /* @__PURE__ */ new Set(["ER_ROW_IS_REFERENCED_2", "ER_NO_REFERENCED_ROW_2"]);
|
|
2095
|
+
const fieldsOf = (error) => typeof error === "object" && error !== null ? error : void 0;
|
|
2096
|
+
/** `Duplicate entry '…' for key 'table.index'` → { table, index } (`PRIMARY` → `<table>_pkey`). */
|
|
2097
|
+
const duplicateKeyOf = (message) => {
|
|
2098
|
+
const match = /for key '(?:([^.']+)\.)?([^']+)'$/.exec(message);
|
|
2099
|
+
if (!match) return;
|
|
2100
|
+
const [, table, key] = match;
|
|
2101
|
+
return {
|
|
2102
|
+
table,
|
|
2103
|
+
constraint: key === "PRIMARY" && table ? `${table}_pkey` : key
|
|
2104
|
+
};
|
|
2105
|
+
};
|
|
2106
|
+
/** `… (\`db\`.\`table\`, CONSTRAINT \`name\` FOREIGN KEY …` → { table, constraint }. */
|
|
2107
|
+
const foreignKeyOf = (message) => {
|
|
2108
|
+
const match = /\(`[^`]+`\.`([^`]+)`, CONSTRAINT `([^`]+)`/.exec(message);
|
|
2109
|
+
return match ? {
|
|
2110
|
+
table: match[1],
|
|
2111
|
+
constraint: match[2]
|
|
2112
|
+
} : void 0;
|
|
2113
|
+
};
|
|
2114
|
+
/** Adds `constraint` and `table` to a MySQL constraint error (mutates and returns it). */
|
|
2115
|
+
const annotateMysqlError = (error) => {
|
|
2116
|
+
const fields = fieldsOf(error);
|
|
2117
|
+
if (!fields || typeof fields.sqlMessage !== "string") return error;
|
|
2118
|
+
const found = fields.code === "ER_DUP_ENTRY" ? duplicateKeyOf(fields.sqlMessage) : FOREIGN_KEY_CODES.has(fields.code) ? foreignKeyOf(fields.sqlMessage) : void 0;
|
|
2119
|
+
if (found) {
|
|
2120
|
+
fields.constraint = found.constraint;
|
|
2121
|
+
fields.table = found.table;
|
|
2122
|
+
}
|
|
2123
|
+
return error;
|
|
2124
|
+
};
|
|
2125
|
+
/** Whether `error` is a duplicate key on one of `indexes` (any unique key when `indexes` is undefined). */
|
|
2126
|
+
const isDuplicateKeyOn = (error, indexes) => {
|
|
2127
|
+
const fields = fieldsOf(error);
|
|
2128
|
+
if (fields?.code !== "ER_DUP_ENTRY") return false;
|
|
2129
|
+
return indexes === void 0 || indexes.has(fields.constraint);
|
|
2130
|
+
};
|
|
2131
|
+
//#endregion
|
|
2132
|
+
//#region ../../apps/api/src/db/mysql/planKeys.ts
|
|
2133
|
+
/** Building blocks of statement plans (`plans.ts`): rows found again by their keys. */
|
|
2134
|
+
const ref = (column, table) => ReferenceNode.create(ColumnNode.create(column), table === void 0 ? void 0 : TableNode.create(table));
|
|
2135
|
+
const equals = (left, right) => BinaryOperationNode.create(left, OperatorNode.create("="), right);
|
|
2136
|
+
const valueNode = (value) => value !== null && typeof value === "object" && "kind" in value ? value : ValueNode.create(value);
|
|
2137
|
+
/** `(k1 = v1 and k2 = v2) or (…)` over rows of key values (values may be expressions). */
|
|
2138
|
+
const keyMatch = (columns, keys) => {
|
|
2139
|
+
if (columns.length === 1) {
|
|
2140
|
+
const [column] = columns;
|
|
2141
|
+
const values = keys.map((key) => key[0]);
|
|
2142
|
+
if (values.every((value) => !(value !== null && typeof value === "object" && "kind" in value))) return WhereNode.create(BinaryOperationNode.create(ref(column), OperatorNode.create("in"), PrimitiveValueListNode.create(values)));
|
|
2143
|
+
}
|
|
2144
|
+
const rows = keys.map((key) => ParensNode.create(columns.map((column, index) => equals(ref(column), valueNode(key[index]))).reduce((left, right) => AndNode.create(left, right))));
|
|
2145
|
+
return WhereNode.create(rows.slice(1).reduce((left, right) => OrNode.create(left, right), rows[0]));
|
|
2146
|
+
};
|
|
2147
|
+
const keyText = (values) => JSON.stringify(values.map((value) => String(value).toLowerCase()));
|
|
2148
|
+
/** Rows in the order of `keys`, without the key columns the plan selected (expression keys keep the database's order). */
|
|
2149
|
+
const inKeyOrder = (rows, columns, keys) => {
|
|
2150
|
+
const keyColumns = columns.map((_, index) => `__shapio_key_${index}`);
|
|
2151
|
+
const strip = (row) => Object.fromEntries(Object.entries(row).filter(([name]) => !keyColumns.includes(name)));
|
|
2152
|
+
if (keys.some((key) => key.some((value) => value !== null && typeof value === "object"))) return rows.map(strip);
|
|
2153
|
+
const position = new Map(keys.map((key, index) => [keyText(key), index]));
|
|
2154
|
+
const positionOf = (row) => position.get(keyText(keyColumns.map((column) => row[column]))) ?? 0;
|
|
2155
|
+
return [...rows].sort((a, b) => positionOf(a) - positionOf(b)).map(strip);
|
|
2156
|
+
};
|
|
2157
|
+
/** `select <returning>, <key columns as __shapio_key_i> from <table> where <keys>`. */
|
|
2158
|
+
const selectByKeys = (table, returning, columns, keys) => Object.freeze({
|
|
2159
|
+
...SelectQueryNode.createFrom([TableNode.create(table)]),
|
|
2160
|
+
selections: [...returning.selections, ...columns.map((column, index) => SelectionNode.create(AliasNode.create(ref(column, table), IdentifierNode.create(`__shapio_key_${index}`))))],
|
|
2161
|
+
where: keyMatch(columns, keys)
|
|
2162
|
+
});
|
|
2163
|
+
const infoOf = (table) => {
|
|
2164
|
+
const info = tableInfo(table);
|
|
2165
|
+
if (!info) throw new Error(`MySQL: RETURNING and ON CONFLICT need a Shapio table; ${table} is not one`);
|
|
2166
|
+
return info;
|
|
2167
|
+
};
|
|
2168
|
+
const tableOf = (node, statement) => {
|
|
2169
|
+
const table = node && TableNode.is(node) ? node : void 0;
|
|
2170
|
+
if (!table) throw new Error(`MySQL: ${statement} with RETURNING needs a single table without an alias`);
|
|
2171
|
+
return table.table.identifier.name;
|
|
2172
|
+
};
|
|
2173
|
+
const resultOf = (rows, affected) => ({
|
|
2174
|
+
rows,
|
|
2175
|
+
numAffectedRows: BigInt(affected)
|
|
2176
|
+
});
|
|
2177
|
+
/** The value a VALUES row gives `column` (a primitive or an expression node). */
|
|
2178
|
+
const rowValue = (row, index) => {
|
|
2179
|
+
if (PrimitiveValueListNode.is(row)) return row.values[index];
|
|
2180
|
+
const value = row.values[index];
|
|
2181
|
+
return value && ValueNode.is(value) ? value.value : value;
|
|
2182
|
+
};
|
|
2183
|
+
//#endregion
|
|
2184
|
+
//#region ../../apps/api/src/db/mysql/insertPlan.ts
|
|
2185
|
+
/**
|
|
2186
|
+
* INSERT … RETURNING and INSERT … ON CONFLICT on MySQL (`plans.ts`): the insert, then the returned rows by
|
|
2187
|
+
* key; with a conflict target, each row under a savepoint, a duplicate on the target skipped or turned into
|
|
2188
|
+
* an UPDATE of the row it found.
|
|
2189
|
+
*/
|
|
2190
|
+
/** Replaces `excluded.<column>` references with the values one VALUES row gives those columns. */
|
|
2191
|
+
var ExcludedRowValues = class extends OperationNodeTransformer {
|
|
2192
|
+
#value;
|
|
2193
|
+
constructor(value) {
|
|
2194
|
+
super();
|
|
2195
|
+
this.#value = value;
|
|
2196
|
+
}
|
|
2197
|
+
transformReference(node) {
|
|
2198
|
+
if (node.table?.table.identifier.name === "excluded" && ColumnNode.is(node.column)) return this.#value(node.column.column.name);
|
|
2199
|
+
return node;
|
|
2200
|
+
}
|
|
2201
|
+
};
|
|
2202
|
+
/**
|
|
2203
|
+
* The UPDATE `ON CONFLICT (target) DO UPDATE SET … [WHERE …]` runs on the row the target found, with the
|
|
2204
|
+
* proposed row's values in place of `excluded.*`.
|
|
2205
|
+
*/
|
|
2206
|
+
const conflictUpdate = (table, conflict, target, value) => {
|
|
2207
|
+
const replace = new ExcludedRowValues(value);
|
|
2208
|
+
const match = keyMatch(target, [target.map((column) => value(column))]).where;
|
|
2209
|
+
const where = conflict.updateWhere ? AndNode.create(match, ParensNode.create(replace.transformNode(conflict.updateWhere.where))) : match;
|
|
2210
|
+
return Object.freeze({
|
|
2211
|
+
...UpdateQueryNode.create([TableNode.create(table)]),
|
|
2212
|
+
updates: (conflict.updates ?? []).map((update) => replace.transformNode(update)),
|
|
2213
|
+
where: WhereNode.create(where)
|
|
2214
|
+
});
|
|
2215
|
+
};
|
|
2216
|
+
const insertPlan = (node, compile) => {
|
|
2217
|
+
const table = tableOf(node.into, "INSERT");
|
|
2218
|
+
const info = infoOf(table);
|
|
2219
|
+
if (!node.values || !ValuesNode.is(node.values) || !node.columns) throw new Error(`MySQL: INSERT … SELECT into ${table} cannot use RETURNING or ON CONFLICT DO NOTHING`);
|
|
2220
|
+
const columns = node.columns.map((column) => column.column.name);
|
|
2221
|
+
const rows = node.values.values;
|
|
2222
|
+
const conflict = node.onConflict;
|
|
2223
|
+
const target = conflict?.columns?.map((column) => column.column.name);
|
|
2224
|
+
const targetIndexes = target ? new Set(info.uniqueKeys.filter((key) => sameColumns(key.columns, target)).map((key) => key.name)) : void 0;
|
|
2225
|
+
if (targetIndexes && targetIndexes.size === 0) throw new Error(`MySQL: no unique key on ${table} (${target.join(", ")}) for ON CONFLICT`);
|
|
2226
|
+
const identityKey = info.identity !== void 0 && !columns.includes(info.identity);
|
|
2227
|
+
const perRow = conflict !== void 0 || identityKey && node.returning !== void 0;
|
|
2228
|
+
const base = Object.freeze({
|
|
2229
|
+
...node,
|
|
2230
|
+
onConflict: void 0,
|
|
2231
|
+
returning: void 0
|
|
2232
|
+
});
|
|
2233
|
+
const statements = perRow ? rows.map((row) => compile(Object.freeze({
|
|
2234
|
+
...base,
|
|
2235
|
+
values: ValuesNode.create([row])
|
|
2236
|
+
}))) : [compile(base)];
|
|
2237
|
+
const updates = conflict !== void 0 && !conflict.doNothing;
|
|
2238
|
+
if (updates && !target) throw new Error(`MySQL: ON CONFLICT DO UPDATE on ${table} needs a conflict target`);
|
|
2239
|
+
const keyColumns = updates ? target : identityKey ? [info.identity] : info.primaryKey;
|
|
2240
|
+
const valueOf = (rowIndex) => (column) => {
|
|
2241
|
+
const index = columns.indexOf(column);
|
|
2242
|
+
if (index < 0) throw new Error(`MySQL: ON CONFLICT on ${table} refers to ${column}, which the insert leaves out`);
|
|
2243
|
+
return valueNode(rowValue(rows[rowIndex], index));
|
|
2244
|
+
};
|
|
2245
|
+
const keyOf = (rowIndex, insertId) => identityKey && !updates ? [String(insertId)] : keyColumns.map((column) => rowValue(rows[rowIndex], columns.indexOf(column)));
|
|
2246
|
+
return (context) => context.atomically(async () => {
|
|
2247
|
+
const keys = [];
|
|
2248
|
+
let affected = 0;
|
|
2249
|
+
for (const [index, statement] of statements.entries()) {
|
|
2250
|
+
let result;
|
|
2251
|
+
try {
|
|
2252
|
+
result = conflict ? await context.savepoint(() => context.run(statement)) : await context.run(statement);
|
|
2253
|
+
} catch (error) {
|
|
2254
|
+
if (!conflict || !isDuplicateKeyOn(error, targetIndexes)) throw error;
|
|
2255
|
+
if (!updates) continue;
|
|
2256
|
+
result = await context.run(compile(conflictUpdate(table, conflict, target, valueOf(index))));
|
|
2257
|
+
if (Number(result.numAffectedRows ?? 0n) === 0) continue;
|
|
2258
|
+
}
|
|
2259
|
+
affected += Number(result.numAffectedRows ?? 0n);
|
|
2260
|
+
if (perRow) keys.push(keyOf(index, result.insertId));
|
|
2261
|
+
else keys.push(...rows.map((_, rowIndex) => keyOf(rowIndex, result.insertId)));
|
|
2262
|
+
}
|
|
2263
|
+
if (!node.returning || keys.length === 0) return resultOf([], affected);
|
|
2264
|
+
const selected = await context.run(compile(selectByKeys(table, node.returning, keyColumns, keys)));
|
|
2265
|
+
return resultOf(inKeyOrder(selected.rows, keyColumns, keys), affected);
|
|
2266
|
+
});
|
|
2267
|
+
};
|
|
2268
|
+
//#endregion
|
|
2269
|
+
//#region ../../apps/api/src/db/mysql/subqueries.ts
|
|
2270
|
+
/**
|
|
2271
|
+
* Subquery shapes MySQL rejects, rewritten (ADR 0001, "MySQL"): `IN (… LIMIT n)` (error 1235) and a subquery
|
|
2272
|
+
* reading the table an UPDATE, DELETE or INSERT writes (error 1093) read a derived table instead, which MySQL
|
|
2273
|
+
* materialises first; PostgreSQL's `UPDATE … FROM` becomes a multi-table UPDATE.
|
|
2274
|
+
*/
|
|
2275
|
+
/** The alias of a derived table wrapped around a subquery. */
|
|
2276
|
+
const DERIVED_ALIAS = "shapio_derived";
|
|
2277
|
+
const tableNameOf$1 = (node) => node && TableNode.is(node) ? node.table.identifier.name : void 0;
|
|
2278
|
+
/**
|
|
2279
|
+
* Whether an IN subquery must read a derived table: it has a LIMIT (MySQL rejects `IN (… LIMIT n)`), or it
|
|
2280
|
+
* reads `table`, the target of the enclosing UPDATE, DELETE or INSERT (error 1093).
|
|
2281
|
+
*/
|
|
2282
|
+
const needsDerivedTable = (node, table) => node.limit !== void 0 || table !== void 0 && ((node.from?.froms ?? []).some((item) => tableNameOf$1(AliasNode.is(item) ? item.node : item) === table) || (node.joins ?? []).some((join) => tableNameOf$1(AliasNode.is(join.table) ? join.table.node : join.table) === table));
|
|
2283
|
+
/** `(select * from (<subquery>) as shapio_derived)`: MySQL materialises it first. */
|
|
2284
|
+
const derived = (node) => Object.freeze({
|
|
2285
|
+
...SelectQueryNode.createFrom([AliasNode.create(node, IdentifierNode.create(DERIVED_ALIAS))]),
|
|
2286
|
+
selections: [SelectionNode.create(SelectAllNode.create())]
|
|
2287
|
+
});
|
|
2288
|
+
const IN_OPERATORS = /* @__PURE__ */ new Set(["in", "not in"]);
|
|
2289
|
+
/** Wraps the IN-subqueries of a WHERE tree that need it (see `needsDerivedTable`). */
|
|
2290
|
+
var DerivedTableWrapper = class extends OperationNodeTransformer {
|
|
2291
|
+
#table;
|
|
2292
|
+
constructor(table) {
|
|
2293
|
+
super();
|
|
2294
|
+
this.#table = table;
|
|
2295
|
+
}
|
|
2296
|
+
transformBinaryOperation(node, queryId) {
|
|
2297
|
+
const transformed = super.transformBinaryOperation(node, queryId);
|
|
2298
|
+
const operator = OperatorNode.is(transformed.operator) ? transformed.operator.operator : "";
|
|
2299
|
+
const right = transformed.rightOperand;
|
|
2300
|
+
if (IN_OPERATORS.has(operator) && SelectQueryNode.is(right) && needsDerivedTable(right, this.#table)) return Object.freeze({
|
|
2301
|
+
...transformed,
|
|
2302
|
+
rightOperand: derived(right)
|
|
2303
|
+
});
|
|
2304
|
+
return transformed;
|
|
2305
|
+
}
|
|
2306
|
+
transformSelectQuery(node) {
|
|
2307
|
+
return node;
|
|
2308
|
+
}
|
|
2309
|
+
};
|
|
2310
|
+
const wrapSubqueries = (where, table) => where ? new DerivedTableWrapper(table).transformNode(where) : where;
|
|
2311
|
+
/**
|
|
2312
|
+
* An INSERT whose VALUES read the target table in a scalar subquery (`coalesce(max(position) + 1, 0)`):
|
|
2313
|
+
* MySQL rejects that (error 1093) unless the subquery reads a derived table, which it materialises first.
|
|
2314
|
+
*/
|
|
2315
|
+
const withDerivedValueSubqueries = (node) => {
|
|
2316
|
+
const table = node.into?.table.identifier.name;
|
|
2317
|
+
if (!table || !node.values || !ValuesNode.is(node.values)) return node;
|
|
2318
|
+
let changed = false;
|
|
2319
|
+
const rows = node.values.values.map((row) => {
|
|
2320
|
+
if (!ValueListNode.is(row)) return row;
|
|
2321
|
+
const values = row.values.map((value) => {
|
|
2322
|
+
if (SelectQueryNode.is(value) && needsDerivedTable(value, table)) {
|
|
2323
|
+
changed = true;
|
|
2324
|
+
return derived(value);
|
|
2325
|
+
}
|
|
2326
|
+
return value;
|
|
2327
|
+
});
|
|
2328
|
+
return ValueListNode.create(values);
|
|
2329
|
+
});
|
|
2330
|
+
return changed ? Object.freeze({
|
|
2331
|
+
...node,
|
|
2332
|
+
values: ValuesNode.create(rows)
|
|
2333
|
+
}) : node;
|
|
2334
|
+
};
|
|
2335
|
+
/**
|
|
2336
|
+
* `UPDATE t SET … FROM u WHERE …` (PostgreSQL) as MySQL's multi-table `UPDATE t, u SET … WHERE …`, which
|
|
2337
|
+
* joins the same way: the WHERE relates the tables.
|
|
2338
|
+
*/
|
|
2339
|
+
const withFromAsTables = (node) => node.from && node.table ? Object.freeze({
|
|
2340
|
+
...node,
|
|
2341
|
+
table: ListNode.create([node.table, ...node.from.froms]),
|
|
2342
|
+
from: void 0
|
|
2343
|
+
}) : node;
|
|
2344
|
+
//#endregion
|
|
2345
|
+
//#region ../../apps/api/src/db/mysql/keyedWritePlans.ts
|
|
2346
|
+
/**
|
|
2347
|
+
* UPDATE/DELETE … RETURNING on MySQL (`plans.ts`): lock the matching keys (`SELECT … FOR UPDATE`, or the
|
|
2348
|
+
* statement's own `key IN (subquery)`), run the statement by key, then read the returned rows by key.
|
|
2349
|
+
*/
|
|
2350
|
+
const forUpdate = (node) => node.endModifiers?.some((modifier) => modifier.modifier === "ForUpdate") ? node : Object.freeze({
|
|
2351
|
+
...node,
|
|
2352
|
+
endModifiers: [...node.endModifiers ?? [], SelectModifierNode.create("ForUpdate")]
|
|
2353
|
+
});
|
|
2354
|
+
/** For `key in (subquery)` (possibly wrapped in the plugin's derived table): the subquery. */
|
|
2355
|
+
const keySubquery = (where, key) => {
|
|
2356
|
+
const condition = where?.where;
|
|
2357
|
+
if (key.length !== 1 || !condition || !BinaryOperationNode.is(condition) || !OperatorNode.is(condition.operator) || condition.operator.operator !== "in" || !ReferenceNode.is(condition.leftOperand) || !ColumnNode.is(condition.leftOperand.column) || condition.leftOperand.column.column.name !== key[0] || !SelectQueryNode.is(condition.rightOperand)) return;
|
|
2358
|
+
const subquery = condition.rightOperand;
|
|
2359
|
+
const from = subquery.from?.froms[0];
|
|
2360
|
+
if (from && AliasNode.is(from) && IdentifierNode.is(from.alias) && from.alias.name === "shapio_derived") return SelectQueryNode.is(from.node) ? from.node : void 0;
|
|
2361
|
+
return subquery;
|
|
2362
|
+
};
|
|
2363
|
+
/** `select <key> from <table> where <where> [order/limit] for update` (or the key subquery itself). */
|
|
2364
|
+
const lockingKeySelect = (table, key, node, extra) => {
|
|
2365
|
+
const subquery = extra.length === 0 ? keySubquery(node.where, key) : void 0;
|
|
2366
|
+
if (subquery) return forUpdate(subquery);
|
|
2367
|
+
return forUpdate(Object.freeze({
|
|
2368
|
+
...SelectQueryNode.createFrom([TableNode.create(table)]),
|
|
2369
|
+
selections: [...extra, ...key.map((column, index) => SelectionNode.create(AliasNode.create(ref(column, table), IdentifierNode.create(`__shapio_key_${index}`))))],
|
|
2370
|
+
where: node.where,
|
|
2371
|
+
orderBy: node.orderBy,
|
|
2372
|
+
limit: node.limit
|
|
2373
|
+
}));
|
|
2374
|
+
};
|
|
2375
|
+
const keysOf = (rows, key, bySubquery) => rows.map((row) => bySubquery ? [Object.values(row)[0]] : key.map((_, index) => row[`__shapio_key_${index}`]));
|
|
2376
|
+
const updatePlan = (node, compile) => {
|
|
2377
|
+
const table = tableOf(node.table, "UPDATE");
|
|
2378
|
+
if (node.from || node.joins?.length) throw new Error(`MySQL: UPDATE … FROM/JOIN on ${table} cannot use RETURNING`);
|
|
2379
|
+
const key = infoOf(table).primaryKey;
|
|
2380
|
+
const bySubquery = keySubquery(node.where, key) !== void 0;
|
|
2381
|
+
const lock = compile(lockingKeySelect(table, key, node, []));
|
|
2382
|
+
return (context) => context.atomically(async () => {
|
|
2383
|
+
const keys = keysOf((await context.run(lock)).rows, key, bySubquery);
|
|
2384
|
+
if (keys.length === 0) return resultOf([], 0);
|
|
2385
|
+
await context.run(compile(Object.freeze({
|
|
2386
|
+
...node,
|
|
2387
|
+
where: keyMatch(key, keys),
|
|
2388
|
+
returning: void 0,
|
|
2389
|
+
limit: void 0,
|
|
2390
|
+
orderBy: void 0
|
|
2391
|
+
})));
|
|
2392
|
+
const selected = await context.run(compile(selectByKeys(table, node.returning, key, keys)));
|
|
2393
|
+
return resultOf(inKeyOrder(selected.rows, key, keys), keys.length);
|
|
2394
|
+
});
|
|
2395
|
+
};
|
|
2396
|
+
const deletePlan = (node, compile) => {
|
|
2397
|
+
const table = tableOf(node.from.froms[0], "DELETE");
|
|
2398
|
+
if (node.from.froms.length > 1 || node.using || node.joins?.length) throw new Error(`MySQL: DELETE … USING/JOIN on ${table} cannot use RETURNING`);
|
|
2399
|
+
const key = infoOf(table).primaryKey;
|
|
2400
|
+
const lock = compile(lockingKeySelect(table, key, node, node.returning.selections));
|
|
2401
|
+
return (context) => context.atomically(async () => {
|
|
2402
|
+
const locked = (await context.run(lock)).rows;
|
|
2403
|
+
const keys = keysOf(locked, key, false);
|
|
2404
|
+
if (keys.length === 0) return resultOf([], 0);
|
|
2405
|
+
await context.run(compile(Object.freeze({
|
|
2406
|
+
...node,
|
|
2407
|
+
where: keyMatch(key, keys),
|
|
2408
|
+
returning: void 0,
|
|
2409
|
+
limit: void 0,
|
|
2410
|
+
orderBy: void 0
|
|
2411
|
+
})));
|
|
2412
|
+
return resultOf(inKeyOrder(locked, key, keys), keys.length);
|
|
2413
|
+
});
|
|
2414
|
+
};
|
|
2415
|
+
//#endregion
|
|
2416
|
+
//#region ../../apps/api/src/db/mysql/plans.ts
|
|
2417
|
+
/**
|
|
2418
|
+
* Statement plans (ADR 0001, "MySQL"): MySQL has no `RETURNING` and no `ON CONFLICT (target) DO NOTHING`,
|
|
2419
|
+
* so the compiler turns such a statement into a few statements the connection runs atomically (inside the
|
|
2420
|
+
* caller's transaction under a savepoint, or in a transaction of its own):
|
|
2421
|
+
* - INSERT … RETURNING: the insert, then a SELECT of the returned columns by primary key (keys are known:
|
|
2422
|
+
* the plugin generates UUIDs; an identity key comes from `LAST_INSERT_ID()`, one row per statement).
|
|
2423
|
+
* - INSERT … ON CONFLICT (target) DO NOTHING [RETURNING]: each row inserted under a savepoint; a duplicate
|
|
2424
|
+
* on the target's unique index (any index without a target) rolls that row back and returns nothing for
|
|
2425
|
+
* it, so other unique violations still raise, as on PostgreSQL.
|
|
2426
|
+
* - UPDATE/DELETE … RETURNING: `SELECT <key> … FOR UPDATE` with the statement's WHERE (or, for
|
|
2427
|
+
* `key IN (subquery)`, the subquery itself, keeping its `FOR UPDATE SKIP LOCKED`), the statement by key,
|
|
2428
|
+
* then (UPDATE) a SELECT of the returned columns by key. The rows stay locked, so the result is exact.
|
|
2429
|
+
*/
|
|
2430
|
+
/** The plan a statement needs on MySQL, or undefined when it runs as written. */
|
|
2431
|
+
const planFor = (node, compile) => {
|
|
2432
|
+
switch (node.kind) {
|
|
2433
|
+
case "InsertQueryNode": return node.returning || node.onConflict ? insertPlan(node, compile) : void 0;
|
|
2434
|
+
case "UpdateQueryNode": return node.returning ? updatePlan(node, compile) : void 0;
|
|
2435
|
+
case "DeleteQueryNode": return node.returning ? deletePlan(node, compile) : void 0;
|
|
2436
|
+
default: return;
|
|
2437
|
+
}
|
|
2438
|
+
};
|
|
2439
|
+
/** The statement without the parts a plan replaces (what logs and errors show). */
|
|
2440
|
+
const plannedStatement = (node) => node.kind === "InsertQueryNode" || node.kind === "UpdateQueryNode" || node.kind === "DeleteQueryNode" ? Object.freeze({
|
|
2441
|
+
...node,
|
|
2442
|
+
returning: void 0,
|
|
2443
|
+
...node.kind === "InsertQueryNode" ? { onConflict: void 0 } : {}
|
|
2444
|
+
}) : node;
|
|
2445
|
+
//#endregion
|
|
2446
|
+
//#region ../../apps/api/src/db/mysql/compiler.ts
|
|
2447
|
+
/** The property of a compiled query that carries its plan (`plans.ts`). */
|
|
2448
|
+
const STATEMENT_PLAN = Symbol("shapio.mysql.plan");
|
|
2449
|
+
const planOf = (query) => query[STATEMENT_PLAN];
|
|
2450
|
+
/**
|
|
2451
|
+
* Kysely's MySQL compiler, plus: statements MySQL cannot run as one get a plan (`plans.ts`), and
|
|
2452
|
+
* `ON DUPLICATE KEY UPDATE` names the inserted row `excluded` (MySQL ≥ 8.0.19 row alias), so the
|
|
2453
|
+
* `excluded.<column>` references of a PostgreSQL upsert compile unchanged.
|
|
2454
|
+
*/
|
|
2455
|
+
var ShapioMysqlQueryCompiler = class ShapioMysqlQueryCompiler extends MysqlQueryCompiler {
|
|
2456
|
+
#insertWithValues = false;
|
|
2457
|
+
compileQuery(node, queryId) {
|
|
2458
|
+
const plan = planFor(node, (statement) => new ShapioMysqlQueryCompiler().compileQuery(statement, queryId));
|
|
2459
|
+
if (!plan) return super.compileQuery(node, queryId);
|
|
2460
|
+
const shown = super.compileQuery(plannedStatement(node), queryId);
|
|
2461
|
+
return Object.freeze({
|
|
2462
|
+
...shown,
|
|
2463
|
+
query: node,
|
|
2464
|
+
[STATEMENT_PLAN]: plan
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
visitInsertQuery(node) {
|
|
2468
|
+
const previous = this.#insertWithValues;
|
|
2469
|
+
this.#insertWithValues = node.values !== void 0 && ValuesNode.is(node.values);
|
|
2470
|
+
try {
|
|
2471
|
+
super.visitInsertQuery(node);
|
|
2472
|
+
} finally {
|
|
2473
|
+
this.#insertWithValues = previous;
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
visitOnDuplicateKey(node) {
|
|
2477
|
+
if (this.#insertWithValues) this.append(`as ${this.getLeftIdentifierWrapper()}${EXCLUDED_ALIAS}${this.getRightIdentifierWrapper()} `);
|
|
2478
|
+
super.visitOnDuplicateKey(node);
|
|
2479
|
+
}
|
|
2480
|
+
visitOnConflict(_node) {
|
|
2481
|
+
throw new Error("MySQL has no ON CONFLICT; the MySQL plugin or a statement plan must replace it");
|
|
2482
|
+
}
|
|
2483
|
+
visitReturning(_node) {
|
|
2484
|
+
throw new Error("MySQL has no RETURNING; a statement plan must replace it");
|
|
2485
|
+
}
|
|
2486
|
+
};
|
|
2487
|
+
//#endregion
|
|
2488
|
+
//#region ../../apps/api/src/db/notifyHub.ts
|
|
2489
|
+
/**
|
|
2490
|
+
* In-process notification delivery (ADR 0002 notes notifications are an optimisation; consumers also check
|
|
2491
|
+
* a durable version): an event bus per database. Every handle on the same database in this process shares
|
|
2492
|
+
* one bus, so two app instances in one test process still hear each other.
|
|
2493
|
+
* - SQLite: the only delivery (SQLite deployments are single-process).
|
|
2494
|
+
* - MySQL: the fast path for listeners in the publishing process; other processes poll the notifications
|
|
2495
|
+
* table (`mysql/notificationListener.ts`), and the row ID lets a listener skip what it already heard.
|
|
2496
|
+
*/
|
|
2497
|
+
const HUBS = /* @__PURE__ */ new Map();
|
|
2498
|
+
const hubFor = (databaseKey) => {
|
|
2499
|
+
let hub = HUBS.get(databaseKey);
|
|
2500
|
+
if (!hub) {
|
|
2501
|
+
hub = new EventEmitter();
|
|
2502
|
+
hub.setMaxListeners(0);
|
|
2503
|
+
HUBS.set(databaseKey, hub);
|
|
2504
|
+
}
|
|
2505
|
+
return hub;
|
|
2506
|
+
};
|
|
2507
|
+
/** Delivers on a later turn of the event loop, like a notification arriving from the network. */
|
|
2508
|
+
const publishNotification = (databaseKey, channel, payload, id) => {
|
|
2509
|
+
setImmediate(() => hubFor(databaseKey).emit("notification", channel, payload, id));
|
|
2510
|
+
};
|
|
2511
|
+
//#endregion
|
|
2512
|
+
//#region ../../apps/api/src/db/poolAcquire.ts
|
|
2513
|
+
/**
|
|
2514
|
+
* How long a query waits for a free pooled connection (PostgreSQL and MySQL) before it fails with
|
|
2515
|
+
* `PoolAcquireTimeoutError`. Without a bound, an exhausted pool queues requests forever and the server
|
|
2516
|
+
* stops answering until it restarts. SQLite has no such wait (see `db/sqlite/driver.ts`).
|
|
2517
|
+
*/
|
|
2518
|
+
const DEFAULT_POOL_ACQUIRE_TIMEOUT_MS = 1e4;
|
|
2519
|
+
/**
|
|
2520
|
+
* No pooled connection became free in time. A 503 the client may retry; the central error handler logs it
|
|
2521
|
+
* as a failed request, so an exhausted pool shows in the logs instead of as a silent hang.
|
|
2522
|
+
*/
|
|
2523
|
+
var PoolAcquireTimeoutError = class extends AppError {
|
|
2524
|
+
constructor(timeoutMs, poolMax, options) {
|
|
2525
|
+
super(503, "DATABASE_BUSY", `No database connection became free within ${timeoutMs} ms (pool of ${poolMax}); try again shortly`, void 0, options);
|
|
2526
|
+
this.name = "PoolAcquireTimeoutError";
|
|
2527
|
+
}
|
|
2528
|
+
};
|
|
2529
|
+
/**
|
|
2530
|
+
* Opening a new connection took longer than the acquire timeout: the database is unreachable or overloaded.
|
|
2531
|
+
* A 503 the client may retry, logged by the central error handler like `PoolAcquireTimeoutError`.
|
|
2532
|
+
*/
|
|
2533
|
+
var DatabaseUnavailableError = class extends AppError {
|
|
2534
|
+
constructor(timeoutMs, options) {
|
|
2535
|
+
super(503, "DATABASE_UNAVAILABLE", `Could not connect to the database within ${timeoutMs} ms; try again shortly`, void 0, options);
|
|
2536
|
+
this.name = "DatabaseUnavailableError";
|
|
2537
|
+
}
|
|
2538
|
+
};
|
|
2539
|
+
/** pg-pool's error when `connectionTimeoutMillis` passes while the request waits in the pool's queue. */
|
|
2540
|
+
const PG_QUEUE_TIMEOUT_MESSAGE = "timeout exceeded when trying to connect";
|
|
2541
|
+
/** pg-pool's error when `connectionTimeoutMillis` passes while it opens a new connection. */
|
|
2542
|
+
const PG_CONNECT_TIMEOUT_MESSAGE = "Connection terminated due to connection timeout";
|
|
2543
|
+
/** The 503 for a pg-pool acquisition error, or the error itself when it is not a timeout. */
|
|
2544
|
+
const pgAcquireError = (error, poolMax, timeoutMs) => {
|
|
2545
|
+
if (!(error instanceof Error)) return error;
|
|
2546
|
+
if (error.message === PG_QUEUE_TIMEOUT_MESSAGE) return new PoolAcquireTimeoutError(timeoutMs, poolMax, { cause: error });
|
|
2547
|
+
if (error.message === PG_CONNECT_TIMEOUT_MESSAGE) return new DatabaseUnavailableError(timeoutMs, { cause: error });
|
|
2548
|
+
return error;
|
|
2549
|
+
};
|
|
2550
|
+
//#endregion
|
|
2551
|
+
//#region ../../apps/api/src/db/mysql/module.ts
|
|
2552
|
+
/**
|
|
2553
|
+
* mysql2, loaded on first use. A process on PostgreSQL never loads it, and a bundle of the read path (the
|
|
2554
|
+
* in-process delivery runtime) can leave it out as an optional dependency.
|
|
2555
|
+
*/
|
|
2556
|
+
const loadMysql = async () => (await import("mysql2")).default;
|
|
2557
|
+
//#endregion
|
|
2558
|
+
//#region ../../apps/api/src/db/mysql/parameters.ts
|
|
2559
|
+
/**
|
|
2560
|
+
* Parameters the MySQL connection resolves itself just before a statement runs (ADR 0001, "MySQL").
|
|
2561
|
+
* They let a `db/` primitive keep its single-expression contract where MySQL needs a statement of its own.
|
|
2562
|
+
*/
|
|
2563
|
+
/** The next value of a sequence: allocated from an AUTO_INCREMENT table (no lock held to commit). */
|
|
2564
|
+
var SequenceValue = class {
|
|
2565
|
+
name;
|
|
2566
|
+
constructor(name) {
|
|
2567
|
+
this.name = name;
|
|
2568
|
+
}
|
|
2569
|
+
};
|
|
2570
|
+
/**
|
|
2571
|
+
* A notification's channel. The statement that binds it inserts the notification; the connection also
|
|
2572
|
+
* publishes it to listeners in this process once the transaction commits (or at once outside one).
|
|
2573
|
+
*/
|
|
2574
|
+
var NotificationChannel = class {
|
|
2575
|
+
channel;
|
|
2576
|
+
payload;
|
|
2577
|
+
constructor(channel, payload) {
|
|
2578
|
+
this.channel = channel;
|
|
2579
|
+
this.payload = payload;
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
/** The table that backs a sequence. */
|
|
2583
|
+
const sequenceTableOf = (name) => `sequence_${name}`;
|
|
2584
|
+
//#endregion
|
|
2585
|
+
//#region ../../apps/api/src/db/mysql/driver.ts
|
|
2586
|
+
/**
|
|
2587
|
+
* Shapio's MySQL driver (ADR 0001, "MySQL") on mysql2's pool. Beyond Kysely's own MySQL driver it:
|
|
2588
|
+
* - sets every session up like PostgreSQL behaves: UTC, READ COMMITTED, strict SQL mode, utf8mb4 with the
|
|
2589
|
+
* binary `utf8mb4_0900_bin` collation, a large `group_concat_max_len` (JSON aggregates);
|
|
2590
|
+
* - pins `NOW()` and `CURRENT_TIMESTAMP` defaults to the transaction's start (`SET timestamp`), as
|
|
2591
|
+
* PostgreSQL's `now()` is;
|
|
2592
|
+
* - runs statement plans (`plans.ts`) atomically: inside a transaction under a savepoint, otherwise in a
|
|
2593
|
+
* transaction of their own;
|
|
2594
|
+
* - resolves sequence values and notification channels bound as parameters (`parameters.ts`), and publishes
|
|
2595
|
+
* notifications to listeners in this process once their transaction commits;
|
|
2596
|
+
* - names the constraint of a constraint error (`errors.ts`).
|
|
2597
|
+
*/
|
|
2598
|
+
const SESSION_SETUP = "set names utf8mb4 collate utf8mb4_0900_bin, time_zone = '+00:00', transaction_isolation = 'READ-COMMITTED', sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,ONLY_FULL_GROUP_BY,NO_ENGINE_SUBSTITUTION', group_concat_max_len = 1073741824";
|
|
2599
|
+
const PIN_TIMESTAMP = "set timestamp = unix_timestamp(sysdate(6))";
|
|
2600
|
+
const UNPIN_TIMESTAMP = "set timestamp = default";
|
|
2601
|
+
/** `host:port/database` of a mysql: URL: the key listeners in this process share. */
|
|
2602
|
+
const mysqlDatabaseKey = (url) => {
|
|
2603
|
+
const parsed = new URL(url);
|
|
2604
|
+
return `mysql:${parsed.hostname}:${parsed.port || "3306"}${parsed.pathname}`;
|
|
2605
|
+
};
|
|
2606
|
+
const mysqlPoolOptions = (url, poolMax, applicationName = "shapio") => ({
|
|
2607
|
+
uri: url,
|
|
2608
|
+
connectAttributes: { program_name: applicationName },
|
|
2609
|
+
connectionLimit: poolMax,
|
|
2610
|
+
charset: "UTF8MB4_GENERAL_CI",
|
|
2611
|
+
timezone: "Z",
|
|
2612
|
+
supportBigNumbers: true,
|
|
2613
|
+
bigNumberStrings: true,
|
|
2614
|
+
decimalNumbers: false,
|
|
2615
|
+
typeCast,
|
|
2616
|
+
multipleStatements: false
|
|
2617
|
+
});
|
|
2618
|
+
const isResultHeader = (result) => !Array.isArray(result) && typeof result === "object" && "affectedRows" in result;
|
|
2619
|
+
var MysqlConnection = class {
|
|
2620
|
+
#connection;
|
|
2621
|
+
#databaseKey;
|
|
2622
|
+
#inTransaction = false;
|
|
2623
|
+
#savepoints = 0;
|
|
2624
|
+
#pending = [];
|
|
2625
|
+
constructor(connection, databaseKey) {
|
|
2626
|
+
this.#connection = connection;
|
|
2627
|
+
this.#databaseKey = databaseKey;
|
|
2628
|
+
}
|
|
2629
|
+
get inTransaction() {
|
|
2630
|
+
return this.#inTransaction;
|
|
2631
|
+
}
|
|
2632
|
+
release() {
|
|
2633
|
+
this.#connection.release();
|
|
2634
|
+
}
|
|
2635
|
+
#query(sql, parameters = []) {
|
|
2636
|
+
return new Promise((resolve, reject) => {
|
|
2637
|
+
this.#connection.query(sql, parameters, (error, result) => {
|
|
2638
|
+
if (error) reject(annotateMysqlError(error));
|
|
2639
|
+
else resolve(result);
|
|
2640
|
+
});
|
|
2641
|
+
});
|
|
2642
|
+
}
|
|
2643
|
+
/** Runs a raw control statement (transactions, savepoints, session settings). */
|
|
2644
|
+
async control(sql) {
|
|
2645
|
+
await this.#query(sql);
|
|
2646
|
+
}
|
|
2647
|
+
async begin(settings) {
|
|
2648
|
+
const statements = [];
|
|
2649
|
+
if (settings.isolationLevel || settings.accessMode) {
|
|
2650
|
+
const parts = [...settings.isolationLevel ? [`isolation level ${settings.isolationLevel}`] : [], ...settings.accessMode ? [settings.accessMode] : []];
|
|
2651
|
+
statements.push(`set transaction ${parts.join(", ")}`);
|
|
2652
|
+
}
|
|
2653
|
+
statements.push(PIN_TIMESTAMP, "begin");
|
|
2654
|
+
await Promise.all(statements.map((statement) => this.#query(statement)));
|
|
2655
|
+
this.#inTransaction = true;
|
|
2656
|
+
this.#pending = [];
|
|
2657
|
+
}
|
|
2658
|
+
async end(command) {
|
|
2659
|
+
const pending = this.#pending;
|
|
2660
|
+
this.#pending = [];
|
|
2661
|
+
this.#inTransaction = false;
|
|
2662
|
+
await Promise.all([this.#query(command), this.#query(UNPIN_TIMESTAMP)]);
|
|
2663
|
+
if (command === "commit") outsideConnectionScope(() => {
|
|
2664
|
+
for (const { channel, payload, id } of pending) publishNotification(this.#databaseKey, channel, payload, id);
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
async #nextSequenceValue(name) {
|
|
2668
|
+
const table = sequenceTableOf(name);
|
|
2669
|
+
const inserted = await this.#query(`insert into \`${table}\` () values ()`);
|
|
2670
|
+
const value = isResultHeader(inserted) ? String(inserted.insertId) : void 0;
|
|
2671
|
+
if (value === void 0) throw new Error(`No value from sequence ${name}`);
|
|
2672
|
+
await this.#query(`delete from \`${table}\` where \`value\` = ?`, [value]);
|
|
2673
|
+
return value;
|
|
2674
|
+
}
|
|
2675
|
+
/** Encodes parameters, allocating sequence values and noting notifications. */
|
|
2676
|
+
async #parametersOf(parameters) {
|
|
2677
|
+
const values = [];
|
|
2678
|
+
const notifications = [];
|
|
2679
|
+
for (const [index, parameter] of parameters.entries()) if (parameter instanceof SequenceValue) values.push(await this.#nextSequenceValue(parameter.name));
|
|
2680
|
+
else if (parameter instanceof NotificationChannel) {
|
|
2681
|
+
notifications.push({
|
|
2682
|
+
channel: parameter.channel,
|
|
2683
|
+
payload: parameter.payload
|
|
2684
|
+
});
|
|
2685
|
+
values.push(parameter.channel);
|
|
2686
|
+
} else values.push(encodeParameter$1(parameter, index));
|
|
2687
|
+
return {
|
|
2688
|
+
values,
|
|
2689
|
+
notifications
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
async #execute(query) {
|
|
2693
|
+
const { values, notifications } = await this.#parametersOf(query.parameters);
|
|
2694
|
+
const result = await this.#query(query.sql, values);
|
|
2695
|
+
if (notifications.length > 0) {
|
|
2696
|
+
const id = isResultHeader(result) && result.insertId ? String(result.insertId) : void 0;
|
|
2697
|
+
const published = notifications.map((notification) => ({
|
|
2698
|
+
...notification,
|
|
2699
|
+
id
|
|
2700
|
+
}));
|
|
2701
|
+
if (this.#inTransaction) this.#pending.push(...published);
|
|
2702
|
+
else for (const notification of published) publishNotification(this.#databaseKey, notification.channel, notification.payload, notification.id);
|
|
2703
|
+
}
|
|
2704
|
+
if (!isResultHeader(result)) return { rows: Array.isArray(result) ? result : [] };
|
|
2705
|
+
return {
|
|
2706
|
+
rows: [],
|
|
2707
|
+
numAffectedRows: BigInt(result.affectedRows),
|
|
2708
|
+
...result.insertId ? { insertId: BigInt(result.insertId) } : {}
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
#planContext = {
|
|
2712
|
+
run: (query) => this.#execute(query),
|
|
2713
|
+
atomically: async (fn) => {
|
|
2714
|
+
if (this.#inTransaction) return this.#planContext.savepoint(fn);
|
|
2715
|
+
await this.begin({});
|
|
2716
|
+
try {
|
|
2717
|
+
const result = await fn();
|
|
2718
|
+
await this.end("commit");
|
|
2719
|
+
return result;
|
|
2720
|
+
} catch (error) {
|
|
2721
|
+
await this.end("rollback");
|
|
2722
|
+
throw error;
|
|
2723
|
+
}
|
|
2724
|
+
},
|
|
2725
|
+
savepoint: async (fn) => {
|
|
2726
|
+
const name = `shapio_plan_${this.#savepoints += 1}`;
|
|
2727
|
+
await this.#query(`savepoint ${name}`);
|
|
2728
|
+
try {
|
|
2729
|
+
const result = await fn();
|
|
2730
|
+
await this.#query(`release savepoint ${name}`);
|
|
2731
|
+
return result;
|
|
2732
|
+
} catch (error) {
|
|
2733
|
+
await this.#query(`rollback to savepoint ${name}`);
|
|
2734
|
+
throw error;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
};
|
|
2738
|
+
async executeQuery(query) {
|
|
2739
|
+
const plan = planOf(query);
|
|
2740
|
+
return plan ? await plan(this.#planContext) : await this.#execute(query);
|
|
2741
|
+
}
|
|
2742
|
+
async *streamQuery() {
|
|
2743
|
+
throw new Error("Streaming queries are not supported on MySQL");
|
|
2744
|
+
}
|
|
2745
|
+
};
|
|
2746
|
+
var MysqlDriver = class {
|
|
2747
|
+
#options;
|
|
2748
|
+
databaseKey;
|
|
2749
|
+
get strict() {
|
|
2750
|
+
return this.#options.strict ?? false;
|
|
2751
|
+
}
|
|
2752
|
+
#pool;
|
|
2753
|
+
#connections = /* @__PURE__ */ new WeakMap();
|
|
2754
|
+
constructor(options) {
|
|
2755
|
+
this.#options = options;
|
|
2756
|
+
this.databaseKey = mysqlDatabaseKey(options.url);
|
|
2757
|
+
}
|
|
2758
|
+
async init() {
|
|
2759
|
+
const pool = (await loadMysql()).createPool(mysqlPoolOptions(this.#options.url, this.#options.poolMax, this.#options.applicationName));
|
|
2760
|
+
pool.on("connection", (connection) => {
|
|
2761
|
+
connection.query(SESSION_SETUP, (error) => {
|
|
2762
|
+
if (error) {
|
|
2763
|
+
this.#options.onIdleConnectionError?.(error);
|
|
2764
|
+
connection.destroy();
|
|
2765
|
+
}
|
|
2766
|
+
});
|
|
2767
|
+
connection.on("error", (error) => this.#options.onIdleConnectionError?.(error));
|
|
2768
|
+
});
|
|
2769
|
+
this.#pool = pool;
|
|
2770
|
+
}
|
|
2771
|
+
#pooled() {
|
|
2772
|
+
if (!this.#pool) throw new Error("The MySQL driver is not initialised");
|
|
2773
|
+
return this.#pool;
|
|
2774
|
+
}
|
|
2775
|
+
/**
|
|
2776
|
+
* A pooled connection, or `PoolAcquireTimeoutError` once `acquireTimeoutMs` passes. mysql2 cannot withdraw
|
|
2777
|
+
* a queued request, so a connection that arrives after the timeout goes straight back to the pool.
|
|
2778
|
+
*/
|
|
2779
|
+
#getConnection() {
|
|
2780
|
+
const { acquireTimeoutMs = DEFAULT_POOL_ACQUIRE_TIMEOUT_MS, poolMax } = this.#options;
|
|
2781
|
+
return new Promise((resolve, reject) => {
|
|
2782
|
+
let timedOut = false;
|
|
2783
|
+
const timer = setTimeout(() => {
|
|
2784
|
+
timedOut = true;
|
|
2785
|
+
reject(new PoolAcquireTimeoutError(acquireTimeoutMs, poolMax));
|
|
2786
|
+
}, acquireTimeoutMs);
|
|
2787
|
+
this.#pooled().getConnection((error, connection) => {
|
|
2788
|
+
clearTimeout(timer);
|
|
2789
|
+
if (timedOut) connection?.release();
|
|
2790
|
+
else if (error) reject(error);
|
|
2791
|
+
else resolve(connection);
|
|
2792
|
+
});
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
async acquireConnection() {
|
|
2796
|
+
if (this.#options.strict) assertNotNested();
|
|
2797
|
+
const raw = await this.#getConnection();
|
|
2798
|
+
let connection = this.#connections.get(raw);
|
|
2799
|
+
if (!connection) {
|
|
2800
|
+
connection = new MysqlConnection(raw, this.databaseKey);
|
|
2801
|
+
this.#connections.set(raw, connection);
|
|
2802
|
+
}
|
|
2803
|
+
return connection;
|
|
2804
|
+
}
|
|
2805
|
+
async beginTransaction(connection, settings) {
|
|
2806
|
+
await connection.begin(settings);
|
|
2807
|
+
}
|
|
2808
|
+
async commitTransaction(connection) {
|
|
2809
|
+
await connection.end("commit");
|
|
2810
|
+
}
|
|
2811
|
+
async rollbackTransaction(connection) {
|
|
2812
|
+
await connection.end("rollback");
|
|
2813
|
+
}
|
|
2814
|
+
async savepoint(connection, name) {
|
|
2815
|
+
await connection.control(`savepoint \`${name}\``);
|
|
2816
|
+
}
|
|
2817
|
+
async rollbackToSavepoint(connection, name) {
|
|
2818
|
+
await connection.control(`rollback to savepoint \`${name}\``);
|
|
2819
|
+
}
|
|
2820
|
+
async releaseSavepoint(connection, name) {
|
|
2821
|
+
await connection.control(`release savepoint \`${name}\``);
|
|
2822
|
+
}
|
|
2823
|
+
async releaseConnection(connection) {
|
|
2824
|
+
connection.release();
|
|
2825
|
+
}
|
|
2826
|
+
async destroy() {
|
|
2827
|
+
const pool = this.#pool;
|
|
2828
|
+
this.#pool = void 0;
|
|
2829
|
+
if (pool) await new Promise((resolve, reject) => pool.end((error) => error ? reject(error) : resolve()));
|
|
2830
|
+
}
|
|
2831
|
+
};
|
|
2832
|
+
//#endregion
|
|
2833
|
+
//#region ../../apps/api/src/db/sql/markers.ts
|
|
2834
|
+
/**
|
|
2835
|
+
* Result-type markers of a query's output columns (`typed.ts`), for drivers whose database does not report
|
|
2836
|
+
* the type of a computed column (SQLite) or reports one PostgreSQL would not (MySQL computes comparisons as
|
|
2837
|
+
* integers). Shared by the SQLite and MySQL plugins.
|
|
2838
|
+
*/
|
|
2839
|
+
const selectionsOf = (node) => {
|
|
2840
|
+
switch (node.kind) {
|
|
2841
|
+
case "SelectQueryNode": return node.selections;
|
|
2842
|
+
case "InsertQueryNode":
|
|
2843
|
+
case "UpdateQueryNode":
|
|
2844
|
+
case "DeleteQueryNode": return node.returning?.selections;
|
|
2845
|
+
default: return;
|
|
2846
|
+
}
|
|
2847
|
+
};
|
|
2848
|
+
/** Functions whose result has the type of their (marked) arguments. */
|
|
2849
|
+
const TYPE_PRESERVING_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
2850
|
+
"coalesce",
|
|
2851
|
+
"max",
|
|
2852
|
+
"min",
|
|
2853
|
+
"ifnull"
|
|
2854
|
+
]);
|
|
2855
|
+
/** Aggregates PostgreSQL returns as bigint/numeric, which its driver reads as decimal strings. */
|
|
2856
|
+
const BIGINT_AGGREGATES = /* @__PURE__ */ new Set(["count", "sum"]);
|
|
2857
|
+
/**
|
|
2858
|
+
* The marker of an expression: its own, or one it inherits through `coalesce`/`max`/`min`, parentheses
|
|
2859
|
+
* and scalar subqueries (`coalesce((select asJson(…) …), emptyArray())` needs no second marker).
|
|
2860
|
+
* `count` and `sum` read as decimal strings, like PostgreSQL's bigint and numeric.
|
|
2861
|
+
*/
|
|
2862
|
+
const markerOf = (node) => {
|
|
2863
|
+
if (!node) return;
|
|
2864
|
+
const own = markedResultType(node);
|
|
2865
|
+
if (own) return own;
|
|
2866
|
+
switch (node.kind) {
|
|
2867
|
+
case "AliasNode": return markerOf(node.node);
|
|
2868
|
+
case "ParensNode": return markerOf(node.node);
|
|
2869
|
+
case "SelectionNode": return markerOf(node.selection);
|
|
2870
|
+
case "SelectQueryNode": {
|
|
2871
|
+
const selections = node.selections;
|
|
2872
|
+
return selections?.length === 1 ? markerOf(selections[0]) : void 0;
|
|
2873
|
+
}
|
|
2874
|
+
case "AggregateFunctionNode":
|
|
2875
|
+
case "FunctionNode": {
|
|
2876
|
+
const call = node;
|
|
2877
|
+
if (node.kind === "AggregateFunctionNode" && BIGINT_AGGREGATES.has(String(call.func).toLowerCase())) return "bigint";
|
|
2878
|
+
if (!TYPE_PRESERVING_FUNCTIONS.has(String(call.func).toLowerCase())) return;
|
|
2879
|
+
for (const argument of call.aggregated ?? call.arguments ?? []) {
|
|
2880
|
+
const marker = markerOf(argument);
|
|
2881
|
+
if (marker) return marker;
|
|
2882
|
+
}
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
default: return;
|
|
2886
|
+
}
|
|
2887
|
+
};
|
|
2888
|
+
/** Output column name → marker, for top-level selections that are (or inherit) marked expressions. */
|
|
2889
|
+
const collectMarkers = (node) => {
|
|
2890
|
+
let markers;
|
|
2891
|
+
for (const selection of selectionsOf(node) ?? []) {
|
|
2892
|
+
const aliased = selection.selection;
|
|
2893
|
+
if (!AliasNode.is(aliased) || !IdentifierNode.is(aliased.alias)) continue;
|
|
2894
|
+
const type = markerOf(aliased.node);
|
|
2895
|
+
if (type) {
|
|
2896
|
+
markers ??= /* @__PURE__ */ new Map();
|
|
2897
|
+
markers.set(aliased.alias.name, type);
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
return markers;
|
|
2901
|
+
};
|
|
2902
|
+
//#endregion
|
|
2903
|
+
//#region ../../apps/api/src/db/mysql/insertValues.ts
|
|
2904
|
+
/**
|
|
2905
|
+
* Values MySQL needs written differently (ADR 0001, "MySQL"): columns PostgreSQL defaults that MySQL cannot
|
|
2906
|
+
* (UUIDs generated here, so a planned RETURNING knows the key; the change sequence), and ISO-8601 text bound
|
|
2907
|
+
* to timestamp columns.
|
|
2908
|
+
*/
|
|
2909
|
+
const ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/;
|
|
2910
|
+
/**
|
|
2911
|
+
* Text bound to a timestamp column, as UTC `DATETIME` text: PostgreSQL reads ISO-8601 with a zone
|
|
2912
|
+
* (`…T…Z`, `+02:00`); MySQL would reject it or ignore the zone.
|
|
2913
|
+
*/
|
|
2914
|
+
const timestampText = (value) => typeof value === "string" && ISO_TIMESTAMP.test(value) ? toDatetimeText(value) : value;
|
|
2915
|
+
const isTimestampColumn = (table, column) => table !== void 0 && (tableInfo(table)?.timestamps ?? []).includes(column);
|
|
2916
|
+
/** A value node (or primitive list) bound to a timestamp column, with ISO text normalised. */
|
|
2917
|
+
const timestampValue = (node) => {
|
|
2918
|
+
if (ValueNode.is(node)) {
|
|
2919
|
+
const value = timestampText(node.value);
|
|
2920
|
+
return value === node.value ? node : ValueNode.create(value);
|
|
2921
|
+
}
|
|
2922
|
+
if (PrimitiveValueListNode.is(node)) return PrimitiveValueListNode.create(node.values.map(timestampText));
|
|
2923
|
+
return node;
|
|
2924
|
+
};
|
|
2925
|
+
/** Insert rows with ISO text for timestamp columns normalised. */
|
|
2926
|
+
const withTimestampValues = (node) => {
|
|
2927
|
+
const table = node.into?.table.identifier.name;
|
|
2928
|
+
const positions = (node.columns?.map((column) => column.column.name) ?? []).flatMap((column, index) => isTimestampColumn(table, column) ? [index] : []);
|
|
2929
|
+
if (positions.length === 0 || !node.values || !ValuesNode.is(node.values)) return node;
|
|
2930
|
+
const rows = node.values.values.map((row) => PrimitiveValueListNode.is(row) ? PrimitiveValueListNode.create(row.values.map((value, index) => positions.includes(index) ? timestampText(value) : value)) : ValueListNode.create(row.values.map((value, index) => positions.includes(index) ? timestampValue(value) : value)));
|
|
2931
|
+
return Object.freeze({
|
|
2932
|
+
...node,
|
|
2933
|
+
values: ValuesNode.create(rows)
|
|
2934
|
+
});
|
|
2935
|
+
};
|
|
2936
|
+
/** A fresh value for a column PostgreSQL defaults. */
|
|
2937
|
+
const generatedValue = (kind) => kind === "uuid" ? randomUUID() : new SequenceValue(kind);
|
|
2938
|
+
/** The insert with every generated column the statement leaves out filled in. */
|
|
2939
|
+
const withGeneratedColumns = (node, info) => {
|
|
2940
|
+
const generated = Object.entries(info.generated ?? {});
|
|
2941
|
+
const columns = node.columns?.map((column) => column.column.name) ?? [];
|
|
2942
|
+
const missing = generated.filter(([column]) => !columns.includes(column));
|
|
2943
|
+
if (missing.length === 0 || !node.values || !ValuesNode.is(node.values) || !node.columns) return node;
|
|
2944
|
+
const rows = node.values.values.map((row) => {
|
|
2945
|
+
const extra = missing.map(([, kind]) => generatedValue(kind));
|
|
2946
|
+
return PrimitiveValueListNode.is(row) ? PrimitiveValueListNode.create([...row.values, ...extra]) : ValueListNode.create([...row.values, ...extra.map((value) => ValueNode.create(value))]);
|
|
2947
|
+
});
|
|
2948
|
+
return Object.freeze({
|
|
2949
|
+
...node,
|
|
2950
|
+
columns: [...node.columns, ...missing.map(([column]) => ColumnNode.create(column))],
|
|
2951
|
+
values: ValuesNode.create(rows)
|
|
2952
|
+
});
|
|
2953
|
+
};
|
|
2954
|
+
//#endregion
|
|
2955
|
+
//#region ../../apps/api/src/db/mysql/nullOrdering.ts
|
|
2956
|
+
/**
|
|
2957
|
+
* PostgreSQL's NULL placement on MySQL (ADR 0001, "MySQL"): PostgreSQL sorts NULLs last ascending and first
|
|
2958
|
+
* descending, MySQL the reverse. An ORDER BY item that may be NULL gets an `(expr is null)` item before it;
|
|
2959
|
+
* a NOT NULL column of a table in scope (`tables.ts`) does not, so index-ordered scans stay possible.
|
|
2960
|
+
*/
|
|
2961
|
+
/** `asc`/`desc` from an order item's direction node. */
|
|
2962
|
+
const directionOf$1 = (node) => {
|
|
2963
|
+
const direction = node.direction;
|
|
2964
|
+
if (direction && RawNode.is(direction)) return direction.sqlFragments.join("").trim().toLowerCase() === "desc" ? "desc" : "asc";
|
|
2965
|
+
return "asc";
|
|
2966
|
+
};
|
|
2967
|
+
const addToScope = (scope, node, outer) => {
|
|
2968
|
+
if (AliasNode.is(node) && IdentifierNode.is(node.alias)) {
|
|
2969
|
+
const table = tableNameOf$1(node.node);
|
|
2970
|
+
if (table) scope.set(node.alias.name, {
|
|
2971
|
+
table,
|
|
2972
|
+
outer
|
|
2973
|
+
});
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
const table = tableNameOf$1(node);
|
|
2977
|
+
if (table) scope.set(table, {
|
|
2978
|
+
table,
|
|
2979
|
+
outer
|
|
2980
|
+
});
|
|
2981
|
+
};
|
|
2982
|
+
const scopeOf = (from, joins, extra) => {
|
|
2983
|
+
const scope = /* @__PURE__ */ new Map();
|
|
2984
|
+
for (const item of from?.froms ?? []) addToScope(scope, item, false);
|
|
2985
|
+
for (const join of joins ?? []) addToScope(scope, join.table, join.joinType !== "InnerJoin");
|
|
2986
|
+
if (extra) scope.set(extra, {
|
|
2987
|
+
table: extra,
|
|
2988
|
+
outer: false
|
|
2989
|
+
});
|
|
2990
|
+
return scope;
|
|
2991
|
+
};
|
|
2992
|
+
/** Whether an ORDER BY expression can never be NULL (a NOT NULL column of a table in scope). */
|
|
2993
|
+
const neverNull = (node, scope) => {
|
|
2994
|
+
if (!ReferenceNode.is(node) || !ColumnNode.is(node.column)) return false;
|
|
2995
|
+
const column = node.column.column.name;
|
|
2996
|
+
const qualifier = node.table?.table.identifier.name;
|
|
2997
|
+
const candidates = qualifier ? [scope.get(qualifier)].filter((entry) => entry !== void 0) : [...scope.values()].filter((entry) => tableInfo(entry.table)?.columns.includes(column));
|
|
2998
|
+
if (candidates.length !== 1) return false;
|
|
2999
|
+
const entry = candidates[0];
|
|
3000
|
+
const info = tableInfo(entry.table);
|
|
3001
|
+
return !entry.outer && info !== void 0 && info.columns.includes(column) && !info.nullable.includes(column);
|
|
3002
|
+
};
|
|
3003
|
+
const isNull = (expression) => ParensNode.create(BinaryOperationNode.create(expression, OperatorNode.create("is"), ValueNode.createImmediate(null)));
|
|
3004
|
+
const withDirection = (node, direction) => OrderByItemNode.create(node, RawNode.createWithSql(direction));
|
|
3005
|
+
/** True when a raw ORDER BY item already spells its own direction (the content compiler's SQL). */
|
|
3006
|
+
const spellsDirection = (node) => RawNode.is(node.orderBy) && /\b(asc|desc)\b/i.test(node.orderBy.sqlFragments.join(" "));
|
|
3007
|
+
/** PostgreSQL's NULL placement for one ORDER BY item, as MySQL items. */
|
|
3008
|
+
const orderItems = (item, scope) => {
|
|
3009
|
+
if (spellsDirection(item)) return [item];
|
|
3010
|
+
const direction = directionOf$1(item);
|
|
3011
|
+
const nullsLast = item.nulls ? item.nulls === "last" : direction === "asc";
|
|
3012
|
+
const mysqlNullsLast = direction === "desc";
|
|
3013
|
+
const plain = Object.freeze({
|
|
3014
|
+
...item,
|
|
3015
|
+
nulls: void 0
|
|
3016
|
+
});
|
|
3017
|
+
if (nullsLast === mysqlNullsLast || neverNull(item.orderBy, scope)) return [plain];
|
|
3018
|
+
return [withDirection(isNull(item.orderBy), nullsLast ? "asc" : "desc"), plain];
|
|
3019
|
+
};
|
|
3020
|
+
//#endregion
|
|
3021
|
+
//#region ../../apps/api/src/db/mysql/resultDecoding.ts
|
|
3022
|
+
/** A marked column's value as PostgreSQL's driver would give it (`db/sql/typed.ts`). */
|
|
3023
|
+
const DECODERS = {
|
|
3024
|
+
boolean: (value) => Number(value) !== 0,
|
|
3025
|
+
bigint: (value) => String(value),
|
|
3026
|
+
json: (value) => typeof value === "string" ? JSON.parse(value) : value,
|
|
3027
|
+
timestamp: (value) => value instanceof Date ? value : /* @__PURE__ */ new Date(`${String(value).replace(" ", "T")}Z`),
|
|
3028
|
+
text: (value) => String(value)
|
|
3029
|
+
};
|
|
3030
|
+
const decodeRow = (row, markers) => {
|
|
3031
|
+
const decoded = { ...row };
|
|
3032
|
+
for (const [column, type] of markers) {
|
|
3033
|
+
const value = decoded[column];
|
|
3034
|
+
if (value !== null && value !== void 0) decoded[column] = DECODERS[type](value);
|
|
3035
|
+
}
|
|
3036
|
+
return decoded;
|
|
3037
|
+
};
|
|
3038
|
+
//#endregion
|
|
3039
|
+
//#region ../../apps/api/src/db/mysql/plugin.ts
|
|
3040
|
+
/**
|
|
3041
|
+
* Adapts Kysely queries written for PostgreSQL to MySQL (dialect boundary, ADR 0001, "MySQL"). Rewrites that
|
|
3042
|
+
* stay one statement happen here; statements MySQL cannot run as one (`RETURNING`, most `ON CONFLICT`) are
|
|
3043
|
+
* planned by the compiler (`plans.ts`). This plugin:
|
|
3044
|
+
* - fills the columns PostgreSQL defaults that MySQL cannot, and normalises ISO text bound to timestamp
|
|
3045
|
+
* columns (`insertValues.ts`);
|
|
3046
|
+
* - spells casts MySQL's way (`text` → `char`, `bigint`/`integer` → `signed`) and maps row locks MySQL lacks;
|
|
3047
|
+
* - keeps PostgreSQL's NULL placement in ORDER BY (`nullOrdering.ts`);
|
|
3048
|
+
* - turns `ON CONFLICT` into `ON DUPLICATE KEY UPDATE` where that means the same (`conflicts.ts`);
|
|
3049
|
+
* - rewrites the subquery shapes MySQL rejects and `UPDATE … FROM` (`subqueries.ts`);
|
|
3050
|
+
* - decodes result columns marked with `db/sql/typed.ts` (MySQL computes comparisons as integers).
|
|
3051
|
+
*/
|
|
3052
|
+
const CAST_TYPES = {
|
|
3053
|
+
text: "char",
|
|
3054
|
+
uuid: "char(36)",
|
|
3055
|
+
integer: "signed",
|
|
3056
|
+
bigint: "signed",
|
|
3057
|
+
int4: "signed",
|
|
3058
|
+
int8: "signed",
|
|
3059
|
+
numeric: "decimal(65,30)"
|
|
3060
|
+
};
|
|
3061
|
+
const ROW_LOCKS$1 = {
|
|
3062
|
+
ForNoKeyUpdate: "ForUpdate",
|
|
3063
|
+
ForKeyShare: "ForShare"
|
|
3064
|
+
};
|
|
3065
|
+
const emptyScope = () => /* @__PURE__ */ new Map();
|
|
3066
|
+
var MysqlTransformer = class extends OperationNodeTransformer {
|
|
3067
|
+
#scopes = [];
|
|
3068
|
+
transformSelectQuery(node, queryId) {
|
|
3069
|
+
this.#scopes.push(scopeOf(node.from, node.joins));
|
|
3070
|
+
try {
|
|
3071
|
+
const transformed = super.transformSelectQuery(node, queryId);
|
|
3072
|
+
const where = wrapSubqueries(transformed.where, void 0);
|
|
3073
|
+
return where === transformed.where ? transformed : Object.freeze({
|
|
3074
|
+
...transformed,
|
|
3075
|
+
where
|
|
3076
|
+
});
|
|
3077
|
+
} finally {
|
|
3078
|
+
this.#scopes.pop();
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
transformUpdateQuery(node, queryId) {
|
|
3082
|
+
const table = tableNameOf$1(node.table && AliasNode.is(node.table) ? node.table.node : node.table);
|
|
3083
|
+
this.#scopes.push(scopeOf(node.from, node.joins, table));
|
|
3084
|
+
try {
|
|
3085
|
+
const transformed = super.transformUpdateQuery(node, queryId);
|
|
3086
|
+
const multiTable = withFromAsTables(transformed);
|
|
3087
|
+
const updates = multiTable.updates?.map((update) => ColumnNode.is(update.column) && isTimestampColumn(table, update.column.column.name) ? Object.freeze({
|
|
3088
|
+
...update,
|
|
3089
|
+
value: timestampValue(update.value)
|
|
3090
|
+
}) : update);
|
|
3091
|
+
return Object.freeze({
|
|
3092
|
+
...multiTable,
|
|
3093
|
+
updates,
|
|
3094
|
+
where: wrapSubqueries(multiTable.where, table)
|
|
3095
|
+
});
|
|
3096
|
+
} finally {
|
|
3097
|
+
this.#scopes.pop();
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
transformDeleteQuery(node, queryId) {
|
|
3101
|
+
const table = tableNameOf$1(node.from.froms[0]);
|
|
3102
|
+
this.#scopes.push(scopeOf(node.from, node.joins));
|
|
3103
|
+
try {
|
|
3104
|
+
const transformed = super.transformDeleteQuery(node, queryId);
|
|
3105
|
+
return Object.freeze({
|
|
3106
|
+
...transformed,
|
|
3107
|
+
where: wrapSubqueries(transformed.where, table)
|
|
3108
|
+
});
|
|
3109
|
+
} finally {
|
|
3110
|
+
this.#scopes.pop();
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
transformInsertQuery(node, queryId) {
|
|
3114
|
+
const transformed = withTimestampValues(withDerivedValueSubqueries(super.transformInsertQuery(node, queryId)));
|
|
3115
|
+
const table = transformed.into?.table.identifier.name;
|
|
3116
|
+
const info = table === void 0 ? void 0 : tableInfo(table);
|
|
3117
|
+
let result = transformed;
|
|
3118
|
+
if (table && transformed.onConflict) result = toOnDuplicateKey(result, transformed.onConflict, table);
|
|
3119
|
+
return info ? withGeneratedColumns(result, info) : result;
|
|
3120
|
+
}
|
|
3121
|
+
transformOrderBy(node, queryId) {
|
|
3122
|
+
const transformed = super.transformOrderBy(node, queryId);
|
|
3123
|
+
const scope = this.#scopes.at(-1) ?? emptyScope();
|
|
3124
|
+
return Object.freeze({
|
|
3125
|
+
...transformed,
|
|
3126
|
+
items: transformed.items.flatMap((item) => orderItems(item, scope))
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
3129
|
+
/** `timestamp_column <op> 'ISO text'`: the text as UTC DATETIME text. */
|
|
3130
|
+
transformBinaryOperation(node, queryId) {
|
|
3131
|
+
const transformed = super.transformBinaryOperation(node, queryId);
|
|
3132
|
+
const left = transformed.leftOperand;
|
|
3133
|
+
if (!ReferenceNode.is(left) || !ColumnNode.is(left.column)) return transformed;
|
|
3134
|
+
const column = left.column.column.name;
|
|
3135
|
+
const scope = this.#scopes.at(-1) ?? emptyScope();
|
|
3136
|
+
const qualifier = left.table?.table.identifier.name;
|
|
3137
|
+
const tables = qualifier ? [scope.get(qualifier)?.table] : [...scope.values()].map((entry) => entry.table).filter((table) => tableInfo(table)?.columns.includes(column));
|
|
3138
|
+
if (tables.length !== 1 || !isTimestampColumn(tables[0], column)) return transformed;
|
|
3139
|
+
const right = timestampValue(transformed.rightOperand);
|
|
3140
|
+
return right === transformed.rightOperand ? transformed : Object.freeze({
|
|
3141
|
+
...transformed,
|
|
3142
|
+
rightOperand: right
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
transformCast(node, queryId) {
|
|
3146
|
+
const transformed = super.transformCast(node, queryId);
|
|
3147
|
+
const dataType = transformed.dataType;
|
|
3148
|
+
const mapped = dataType.kind === "DataTypeNode" ? CAST_TYPES[String(dataType.dataType)] : void 0;
|
|
3149
|
+
return mapped ? Object.freeze({
|
|
3150
|
+
...transformed,
|
|
3151
|
+
dataType: RawNode.createWithSql(mapped)
|
|
3152
|
+
}) : transformed;
|
|
3153
|
+
}
|
|
3154
|
+
transformSelectModifier(node, queryId) {
|
|
3155
|
+
const transformed = super.transformSelectModifier(node, queryId);
|
|
3156
|
+
const mapped = transformed.modifier ? ROW_LOCKS$1[transformed.modifier] : void 0;
|
|
3157
|
+
return mapped ? Object.freeze({
|
|
3158
|
+
...transformed,
|
|
3159
|
+
modifier: mapped
|
|
3160
|
+
}) : transformed;
|
|
3161
|
+
}
|
|
3162
|
+
};
|
|
3163
|
+
var MysqlPlugin = class {
|
|
3164
|
+
#markers = /* @__PURE__ */ new WeakMap();
|
|
3165
|
+
transformQuery({ node, queryId }) {
|
|
3166
|
+
const markers = collectMarkers(node);
|
|
3167
|
+
if (markers) this.#markers.set(queryId, markers);
|
|
3168
|
+
return new MysqlTransformer().transformNode(node, queryId);
|
|
3169
|
+
}
|
|
3170
|
+
/** Decodes columns marked with a result type (MySQL computes `a is not null` as an integer). */
|
|
3171
|
+
async transformResult({ result, queryId }) {
|
|
3172
|
+
const markers = this.#markers.get(queryId);
|
|
3173
|
+
if (!markers || result.rows.length === 0) return result;
|
|
3174
|
+
return {
|
|
3175
|
+
...result,
|
|
3176
|
+
rows: result.rows.map((row) => decodeRow(row, markers))
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
};
|
|
3180
|
+
//#endregion
|
|
3181
|
+
//#region ../../apps/api/src/db/mysql/index.ts
|
|
3182
|
+
/**
|
|
3183
|
+
* Kysely on MySQL 8.4 (ADR 0001, "MySQL"). RETURNING is emulated by statement plans. Migration locking is Shapio's own session lock
|
|
3184
|
+
* (`db/migrator.ts`), scoped to the database, so Kysely's server-wide `GET_LOCK` name is not taken.
|
|
3185
|
+
*/
|
|
3186
|
+
var ShapioMysqlAdapter = class extends MysqlAdapter {
|
|
3187
|
+
/** Statement plans give MySQL RETURNING (`plans.ts`), so builders return the rows. */
|
|
3188
|
+
get supportsReturning() {
|
|
3189
|
+
return true;
|
|
3190
|
+
}
|
|
3191
|
+
async acquireMigrationLock() {}
|
|
3192
|
+
async releaseMigrationLock() {}
|
|
3193
|
+
};
|
|
3194
|
+
var ShapioMysqlDialect = class {
|
|
3195
|
+
driver;
|
|
3196
|
+
constructor(driver) {
|
|
3197
|
+
this.driver = driver;
|
|
3198
|
+
}
|
|
3199
|
+
createAdapter() {
|
|
3200
|
+
return new ShapioMysqlAdapter();
|
|
3201
|
+
}
|
|
3202
|
+
createDriver() {
|
|
3203
|
+
return this.driver;
|
|
3204
|
+
}
|
|
3205
|
+
createIntrospector(db) {
|
|
3206
|
+
return new MysqlIntrospector(db);
|
|
3207
|
+
}
|
|
3208
|
+
createQueryCompiler() {
|
|
3209
|
+
return new ShapioMysqlQueryCompiler();
|
|
3210
|
+
}
|
|
3211
|
+
};
|
|
3212
|
+
/** A Kysely handle on MySQL; the driver is reachable for the database key listeners share. */
|
|
3213
|
+
var MysqlKysely = class extends Kysely {
|
|
3214
|
+
mysqlDriver;
|
|
3215
|
+
constructor(driver) {
|
|
3216
|
+
super({
|
|
3217
|
+
dialect: new ShapioMysqlDialect(driver),
|
|
3218
|
+
plugins: [new MysqlPlugin()]
|
|
3219
|
+
});
|
|
3220
|
+
this.mysqlDriver = driver;
|
|
3221
|
+
}
|
|
3222
|
+
/** Strict mode runs transactions in a connection scope (`db/connectionScope.ts`). */
|
|
3223
|
+
transaction() {
|
|
3224
|
+
const builder = super.transaction();
|
|
3225
|
+
return this.mysqlDriver.strict ? withConnectionScope(builder) : builder;
|
|
3226
|
+
}
|
|
3227
|
+
};
|
|
3228
|
+
const createMysqlDb = (options) => new MysqlKysely(new MysqlDriver(options));
|
|
3229
|
+
/** The MySQL driver behind a handle, or undefined for another dialect. */
|
|
3230
|
+
const mysqlDriverOf = (db) => db instanceof MysqlKysely ? db.mysqlDriver : void 0;
|
|
3231
|
+
//#endregion
|
|
3232
|
+
//#region ../../apps/api/src/db/sqlite/codec.ts
|
|
3233
|
+
/**
|
|
3234
|
+
* The SQLite value codec (ADR 0001, "Dialect boundary"). SQLite has five storage classes, so Shapio's
|
|
3235
|
+
* PostgreSQL types are declared with names that keep both the storage affinity right (`text_*` → TEXT,
|
|
3236
|
+
* `integer_*`/`bigint` → INTEGER) and the original type readable; the driver decodes every result column by
|
|
3237
|
+
* its declared type (`StatementSync.columns()` reports it even through subqueries, CTEs and RETURNING), so
|
|
3238
|
+
* rows have the same JavaScript shape as on PostgreSQL:
|
|
3239
|
+
*
|
|
3240
|
+
* | PostgreSQL | declared | stored | read as |
|
|
3241
|
+
* | -------------- | ------------------- | ------------------------------- | ------------------ |
|
|
3242
|
+
* | timestamptz | text_timestamptz | `YYYY-MM-DDTHH:MM:SS.sssZ` | Date |
|
|
3243
|
+
* | date | text_date | `YYYY-MM-DD` | string |
|
|
3244
|
+
* | jsonb / json | text_jsonb / _json | JSON text | parsed value |
|
|
3245
|
+
* | text[] / uuid[]| text_array / _uuid_array | JSON array text | string[] |
|
|
3246
|
+
* | uuid | text_uuid | lowercase text | string |
|
|
3247
|
+
* | boolean | integer_boolean | 0 / 1 | boolean |
|
|
3248
|
+
* | bigint | bigint | integer | decimal string |
|
|
3249
|
+
* | integer | integer | integer | number |
|
|
3250
|
+
* | real / double | real / double | real | number |
|
|
3251
|
+
*/
|
|
3252
|
+
const DECLARED_TYPES = {
|
|
3253
|
+
timestamptz: "text_timestamptz",
|
|
3254
|
+
date: "text_date",
|
|
3255
|
+
jsonb: "text_jsonb",
|
|
3256
|
+
json: "text_json",
|
|
3257
|
+
textArray: "text_array",
|
|
3258
|
+
uuidArray: "text_uuid_array",
|
|
3259
|
+
uuid: "text_uuid",
|
|
3260
|
+
boolean: "integer_boolean",
|
|
3261
|
+
bigint: "bigint",
|
|
3262
|
+
integer: "integer",
|
|
3263
|
+
real: "real",
|
|
3264
|
+
double: "double",
|
|
3265
|
+
text: "text"
|
|
3266
|
+
};
|
|
3267
|
+
/**
|
|
3268
|
+
* `bigint` identity primary keys. SQLite only makes a column its auto-incrementing row ID when it is
|
|
3269
|
+
* declared exactly `INTEGER PRIMARY KEY`, so these read back as strings by name instead of by type.
|
|
3270
|
+
*/
|
|
3271
|
+
const BIGINT_ROWID_COLUMNS = /* @__PURE__ */ new Set(["outbox_events.id", "publication_log.id"]);
|
|
3272
|
+
/** A parameter that cannot be stored; never silently written as NULL. */
|
|
3273
|
+
var SqliteBindError = class extends TypeError {
|
|
3274
|
+
constructor(index, value) {
|
|
3275
|
+
const kind = value === void 0 ? "undefined" : value?.constructor?.name ?? typeof value;
|
|
3276
|
+
super(`Cannot bind ${kind} to SQLite parameter ${index + 1}`);
|
|
3277
|
+
this.name = "SqliteBindError";
|
|
3278
|
+
}
|
|
3279
|
+
};
|
|
3280
|
+
const isPlainObject = (value) => {
|
|
3281
|
+
const prototype = Object.getPrototypeOf(value);
|
|
3282
|
+
return prototype === Object.prototype || prototype === null;
|
|
3283
|
+
};
|
|
3284
|
+
/** A JavaScript value as SQLite stores it (see the table above). Throws on values with no column type. */
|
|
3285
|
+
const encodeParameter = (value, index) => {
|
|
3286
|
+
switch (typeof value) {
|
|
3287
|
+
case "string":
|
|
3288
|
+
case "number":
|
|
3289
|
+
case "bigint": return value;
|
|
3290
|
+
case "boolean": return value ? 1 : 0;
|
|
3291
|
+
case "object":
|
|
3292
|
+
if (value === null) return null;
|
|
3293
|
+
if (value instanceof Date) return toStoredTimestamp(value);
|
|
3294
|
+
if (value instanceof Uint8Array) return value;
|
|
3295
|
+
if (Array.isArray(value) || isPlainObject(value)) return JSON.stringify(value);
|
|
3296
|
+
throw new SqliteBindError(index, value);
|
|
3297
|
+
default: throw new SqliteBindError(index, value);
|
|
3298
|
+
}
|
|
3299
|
+
};
|
|
3300
|
+
const toNumber = (value) => typeof value === "bigint" ? Number(value) : value;
|
|
3301
|
+
const toDecimalString = (value) => typeof value === "bigint" || typeof value === "number" ? String(value) : value;
|
|
3302
|
+
const toDate = (value) => typeof value === "string" ? new Date(value) : value;
|
|
3303
|
+
const toBoolean = (value) => value === null ? null : Number(value) !== 0;
|
|
3304
|
+
const parseJson = (value) => typeof value === "string" ? JSON.parse(value) : value;
|
|
3305
|
+
const passText = (value) => typeof value === "bigint" ? String(value) : value;
|
|
3306
|
+
/** Integers arrive as bigint (`setReadBigInts`); an untyped one is a number when it fits, else a string. */
|
|
3307
|
+
const untyped = (value) => {
|
|
3308
|
+
if (typeof value !== "bigint") return value;
|
|
3309
|
+
return value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : String(value);
|
|
3310
|
+
};
|
|
3311
|
+
const BY_DECLARED_TYPE = {
|
|
3312
|
+
[DECLARED_TYPES.timestamptz]: toDate,
|
|
3313
|
+
[DECLARED_TYPES.date]: passText,
|
|
3314
|
+
[DECLARED_TYPES.jsonb]: parseJson,
|
|
3315
|
+
[DECLARED_TYPES.json]: parseJson,
|
|
3316
|
+
[DECLARED_TYPES.textArray]: parseJson,
|
|
3317
|
+
[DECLARED_TYPES.uuidArray]: parseJson,
|
|
3318
|
+
[DECLARED_TYPES.uuid]: passText,
|
|
3319
|
+
[DECLARED_TYPES.boolean]: toBoolean,
|
|
3320
|
+
[DECLARED_TYPES.bigint]: toDecimalString,
|
|
3321
|
+
[DECLARED_TYPES.integer]: toNumber,
|
|
3322
|
+
[DECLARED_TYPES.real]: toNumber,
|
|
3323
|
+
[DECLARED_TYPES.double]: toNumber,
|
|
3324
|
+
[DECLARED_TYPES.text]: passText
|
|
3325
|
+
};
|
|
3326
|
+
const BY_MARKER = {
|
|
3327
|
+
json: parseJson,
|
|
3328
|
+
timestamp: toDate,
|
|
3329
|
+
boolean: toBoolean,
|
|
3330
|
+
bigint: toDecimalString,
|
|
3331
|
+
text: passText
|
|
3332
|
+
};
|
|
3333
|
+
/** The decoder of one result column: a marker wins, then the origin column, then the declared type. */
|
|
3334
|
+
const decoderFor = (metadata, marker) => {
|
|
3335
|
+
if (marker) return BY_MARKER[marker];
|
|
3336
|
+
if (metadata.table !== null && BIGINT_ROWID_COLUMNS.has(`${metadata.table}.${metadata.column ?? ""}`)) return toDecimalString;
|
|
3337
|
+
const declared = metadata.type?.toLowerCase();
|
|
3338
|
+
return (declared === void 0 ? void 0 : BY_DECLARED_TYPE[declared]) ?? untyped;
|
|
3339
|
+
};
|
|
3340
|
+
const STORED_TIMESTAMP = /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/;
|
|
3341
|
+
/**
|
|
3342
|
+
* Strict mode (tests): an unmarked computed column whose text looks like a stored timestamp or a JSON
|
|
3343
|
+
* document was almost certainly meant to be decoded; fail loudly so the query gets a `db/sql/typed` marker.
|
|
3344
|
+
*/
|
|
3345
|
+
const assertNotUndecoded = (name, value) => {
|
|
3346
|
+
if (typeof value !== "string") return;
|
|
3347
|
+
if (STORED_TIMESTAMP.test(value)) throw new Error(`SQLite result column "${name}" is a computed timestamp without a type marker; wrap it in asTimestamp() (db/sql/typed.ts)`);
|
|
3348
|
+
const first = value[0];
|
|
3349
|
+
if (first === "{" || first === "[") {
|
|
3350
|
+
try {
|
|
3351
|
+
JSON.parse(value);
|
|
3352
|
+
} catch {
|
|
3353
|
+
return;
|
|
3354
|
+
}
|
|
3355
|
+
throw new Error(`SQLite result column "${name}" is computed JSON without a type marker; wrap it in asJson() (db/sql/typed.ts)`);
|
|
3356
|
+
}
|
|
3357
|
+
};
|
|
3358
|
+
//#endregion
|
|
3359
|
+
//#region ../../apps/api/src/db/sqlite/errors.ts
|
|
3360
|
+
/**
|
|
3361
|
+
* SQLite errors in the shape `db/sql/errors.ts` classifies: `code` is the extended result code's name and,
|
|
3362
|
+
* like PostgreSQL's error fields, `constraint` names the violated unique index and `table` the table a
|
|
3363
|
+
* failed foreign-key write targeted (SQLite reports no constraint name for foreign keys).
|
|
3364
|
+
*/
|
|
3365
|
+
var SqliteDatabaseError = class extends Error {
|
|
3366
|
+
code;
|
|
3367
|
+
errcode;
|
|
3368
|
+
constraint;
|
|
3369
|
+
table;
|
|
3370
|
+
/** The failed statement's SQL (values are bound separately and never included). */
|
|
3371
|
+
sql;
|
|
3372
|
+
constructor(message, fields, options) {
|
|
3373
|
+
super(message, options);
|
|
3374
|
+
this.name = "SqliteDatabaseError";
|
|
3375
|
+
this.code = fields.code;
|
|
3376
|
+
this.errcode = fields.errcode;
|
|
3377
|
+
this.constraint = fields.constraint;
|
|
3378
|
+
this.table = fields.table;
|
|
3379
|
+
this.sql = fields.sql;
|
|
3380
|
+
}
|
|
3381
|
+
};
|
|
3382
|
+
/** Extended result codes Shapio distinguishes (sqlite.org/rescode.html). */
|
|
3383
|
+
const CODE_NAMES = {
|
|
3384
|
+
5: "SQLITE_BUSY",
|
|
3385
|
+
19: "SQLITE_CONSTRAINT",
|
|
3386
|
+
275: "SQLITE_CONSTRAINT_CHECK",
|
|
3387
|
+
787: "SQLITE_CONSTRAINT_FOREIGNKEY",
|
|
3388
|
+
1299: "SQLITE_CONSTRAINT_NOTNULL",
|
|
3389
|
+
1555: "SQLITE_CONSTRAINT_PRIMARYKEY",
|
|
3390
|
+
1811: "SQLITE_CONSTRAINT_TRIGGER",
|
|
3391
|
+
2067: "SQLITE_CONSTRAINT_UNIQUE"
|
|
3392
|
+
};
|
|
3393
|
+
const NAMED_INDEX = /UNIQUE constraint failed: index '([^']+)'/;
|
|
3394
|
+
const COLUMN_LIST = /(?:UNIQUE) constraint failed: (.+)$/;
|
|
3395
|
+
/** The unique index on exactly `columns` of `table` (PostgreSQL names the primary key `<table>_pkey`). */
|
|
3396
|
+
const uniqueIndexOn = (database, table, columns) => {
|
|
3397
|
+
const indexes = database.prepare(`select name, origin from pragma_index_list(?) where "unique" = 1`).all(table);
|
|
3398
|
+
for (const index of indexes) {
|
|
3399
|
+
const indexColumns = database.prepare("select name from pragma_index_info(?) order by seqno").all(index.name).map((row) => row.name);
|
|
3400
|
+
if (indexColumns.length === columns.length && indexColumns.every((name, at) => name === columns[at])) return index.origin === "pk" ? `${table}_pkey` : index.name;
|
|
3401
|
+
}
|
|
3402
|
+
};
|
|
3403
|
+
/** `t.a, t.b` → table `t`, columns `[a, b]`. */
|
|
3404
|
+
const parseColumnList = (list) => {
|
|
3405
|
+
const parts = list.split(",").map((part) => part.trim().split("."));
|
|
3406
|
+
const table = parts[0]?.[0];
|
|
3407
|
+
if (!table || parts.some((part) => part.length !== 2 || part[0] !== table)) return;
|
|
3408
|
+
return {
|
|
3409
|
+
table,
|
|
3410
|
+
columns: parts.map((part) => part[1] ?? "")
|
|
3411
|
+
};
|
|
3412
|
+
};
|
|
3413
|
+
const uniqueConstraintOf = (database, message) => {
|
|
3414
|
+
const named = NAMED_INDEX.exec(message);
|
|
3415
|
+
if (named) return named[1];
|
|
3416
|
+
const list = COLUMN_LIST.exec(message)?.[1];
|
|
3417
|
+
const parsed = list === void 0 ? void 0 : parseColumnList(list);
|
|
3418
|
+
return parsed && uniqueIndexOn(database, parsed.table, parsed.columns);
|
|
3419
|
+
};
|
|
3420
|
+
/**
|
|
3421
|
+
* Wraps a `node:sqlite` error with the constraint details services ask about. `writtenTable` is the table an
|
|
3422
|
+
* INSERT or UPDATE targeted (attached to foreign-key failures). Other errors pass through unchanged.
|
|
3423
|
+
*/
|
|
3424
|
+
const translateSqliteError = (error, database, writtenTable, statement) => {
|
|
3425
|
+
const errcode = error?.errcode;
|
|
3426
|
+
if (!(error instanceof Error) || typeof errcode !== "number") return error;
|
|
3427
|
+
const code = CODE_NAMES[errcode] ?? `SQLITE_${errcode}`;
|
|
3428
|
+
const fields = {
|
|
3429
|
+
code,
|
|
3430
|
+
errcode,
|
|
3431
|
+
...statement === void 0 ? {} : { sql: statement }
|
|
3432
|
+
};
|
|
3433
|
+
if (code === "SQLITE_CONSTRAINT_UNIQUE" || code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
3434
|
+
const constraint = uniqueConstraintOf(database, error.message);
|
|
3435
|
+
if (constraint) fields.constraint = constraint;
|
|
3436
|
+
}
|
|
3437
|
+
if (code === "SQLITE_CONSTRAINT_FOREIGNKEY" && writtenTable) fields.table = writtenTable;
|
|
3438
|
+
return new SqliteDatabaseError(error.message, fields, { cause: error });
|
|
3439
|
+
};
|
|
3440
|
+
//#endregion
|
|
3441
|
+
//#region ../../apps/api/src/db/sqlite/functions.ts
|
|
3442
|
+
/** Unicode-aware case folding (SQLite's own `lower`/`upper`/`LIKE` only fold ASCII). */
|
|
3443
|
+
const fold = (value) => typeof value === "string" ? value.toLowerCase() : value;
|
|
3444
|
+
const upper = (value) => typeof value === "string" ? value.toUpperCase() : value;
|
|
3445
|
+
const PATTERNS = /* @__PURE__ */ new Map();
|
|
3446
|
+
const compiled = (pattern) => {
|
|
3447
|
+
let regexp = PATTERNS.get(pattern);
|
|
3448
|
+
if (!regexp) {
|
|
3449
|
+
regexp = new RegExp(pattern, "u");
|
|
3450
|
+
PATTERNS.set(pattern, regexp);
|
|
3451
|
+
}
|
|
3452
|
+
return regexp;
|
|
3453
|
+
};
|
|
3454
|
+
/** `value REGEXP pattern` (PostgreSQL `~`), which SQLite rewrites to `regexp(pattern, value)`. */
|
|
3455
|
+
const regexp = (pattern, value) => {
|
|
3456
|
+
if (pattern === null || value === null) return null;
|
|
3457
|
+
return compiled(String(pattern)).test(String(value)) ? 1 : 0;
|
|
3458
|
+
};
|
|
3459
|
+
/** PostgreSQL's `split_part(text, delimiter, n)` for n ≥ 1: empty text past the last part. */
|
|
3460
|
+
const splitPart = (value, delimiter, position) => {
|
|
3461
|
+
if (value === null || delimiter === null || position === null) return null;
|
|
3462
|
+
const index = Number(position);
|
|
3463
|
+
if (!Number.isInteger(index) || index < 1) throw new Error("split_part: field position must be a positive integer");
|
|
3464
|
+
return String(value).split(String(delimiter))[index - 1] ?? "";
|
|
3465
|
+
};
|
|
3466
|
+
const DETERMINISTIC = { deterministic: true };
|
|
3467
|
+
const registerFunctions = (database, hooks) => {
|
|
3468
|
+
database.function("shapio_fold", DETERMINISTIC, fold);
|
|
3469
|
+
database.function("lower", DETERMINISTIC, fold);
|
|
3470
|
+
database.function("upper", DETERMINISTIC, upper);
|
|
3471
|
+
database.function("regexp", DETERMINISTIC, regexp);
|
|
3472
|
+
database.function("split_part", DETERMINISTIC, splitPart);
|
|
3473
|
+
database.function("gen_random_uuid", () => randomUUID());
|
|
3474
|
+
database.function("shapio_now", () => hooks.now());
|
|
3475
|
+
database.function("shapio_nextval", (name) => hooks.nextval(String(name)));
|
|
3476
|
+
database.function("shapio_assert_write_transaction", () => {
|
|
3477
|
+
hooks.assertWriteTransaction();
|
|
3478
|
+
return 1;
|
|
3479
|
+
});
|
|
3480
|
+
database.function("shapio_notify", (channel, payload) => {
|
|
3481
|
+
hooks.notify(String(channel), payload === null ? "" : String(payload));
|
|
3482
|
+
return null;
|
|
3483
|
+
});
|
|
3484
|
+
};
|
|
3485
|
+
//#endregion
|
|
3486
|
+
//#region ../../apps/api/src/db/sqlite/connection.ts
|
|
3487
|
+
const STATEMENT_CACHE_SIZE = 500;
|
|
3488
|
+
/**
|
|
3489
|
+
* One `node:sqlite` connection. The writer runs every write transaction; readers run read-only
|
|
3490
|
+
* transactions and autocommit reads (WAL lets them read while the writer works). Calls are synchronous:
|
|
3491
|
+
* a statement blocks the event loop while it runs.
|
|
3492
|
+
*/
|
|
3493
|
+
var SqliteConnection = class {
|
|
3494
|
+
role;
|
|
3495
|
+
#database;
|
|
3496
|
+
#options;
|
|
3497
|
+
#statements = /* @__PURE__ */ new Map();
|
|
3498
|
+
#transactionStart;
|
|
3499
|
+
#statementStart;
|
|
3500
|
+
#pendingNotifications = [];
|
|
3501
|
+
constructor(options) {
|
|
3502
|
+
this.#options = options;
|
|
3503
|
+
this.role = options.role;
|
|
3504
|
+
this.#database = new DatabaseSync(options.location, { enableForeignKeyConstraints: true });
|
|
3505
|
+
this.#database.exec(`pragma busy_timeout = ${options.busyTimeoutMs}`);
|
|
3506
|
+
if (options.location !== ":memory:") {
|
|
3507
|
+
if (options.role === "writer") this.#database.exec("pragma journal_mode = wal");
|
|
3508
|
+
this.#database.exec("pragma synchronous = normal");
|
|
3509
|
+
}
|
|
3510
|
+
if (options.role === "reader") this.#database.exec("pragma query_only = 1");
|
|
3511
|
+
const nextvalStatement = () => {
|
|
3512
|
+
const statement = this.#database.prepare("update sequences set value = value + 1 where name = ? returning value");
|
|
3513
|
+
statement.setReadBigInts(true);
|
|
3514
|
+
return statement;
|
|
3515
|
+
};
|
|
3516
|
+
let nextval;
|
|
3517
|
+
registerFunctions(this.#database, {
|
|
3518
|
+
now: () => this.#transactionStart ?? this.#statementStart ?? toStoredTimestamp(/* @__PURE__ */ new Date()),
|
|
3519
|
+
nextval: (name) => {
|
|
3520
|
+
nextval ??= nextvalStatement();
|
|
3521
|
+
const row = nextval.get(name);
|
|
3522
|
+
if (!row) throw new Error(`Unknown sequence ${name}`);
|
|
3523
|
+
return row.value;
|
|
3524
|
+
},
|
|
3525
|
+
assertWriteTransaction: () => {
|
|
3526
|
+
if (this.role !== "writer" || this.#transactionStart === void 0) throw new Error("A transaction-level lock needs a write transaction on SQLite");
|
|
3527
|
+
},
|
|
3528
|
+
notify: (channel, payload) => {
|
|
3529
|
+
if (this.#transactionStart === void 0) publishNotification(this.#options.databaseKey, channel, payload);
|
|
3530
|
+
else this.#pendingNotifications.push([channel, payload]);
|
|
3531
|
+
}
|
|
3532
|
+
});
|
|
3533
|
+
}
|
|
3534
|
+
get inTransaction() {
|
|
3535
|
+
return this.#transactionStart !== void 0;
|
|
3536
|
+
}
|
|
3537
|
+
/** Opens a transaction (`begin immediate` takes SQLite's write lock up front). */
|
|
3538
|
+
begin(mode) {
|
|
3539
|
+
this.#database.exec(mode === "immediate" ? "begin immediate" : "begin");
|
|
3540
|
+
this.#transactionStart = toStoredTimestamp(/* @__PURE__ */ new Date());
|
|
3541
|
+
this.#pendingNotifications = [];
|
|
3542
|
+
}
|
|
3543
|
+
/** Commits and delivers the notifications queued inside the transaction. */
|
|
3544
|
+
commit() {
|
|
3545
|
+
try {
|
|
3546
|
+
this.#database.exec("commit");
|
|
3547
|
+
} catch (error) {
|
|
3548
|
+
this.rollback();
|
|
3549
|
+
throw error;
|
|
3550
|
+
}
|
|
3551
|
+
const notifications = this.#pendingNotifications;
|
|
3552
|
+
this.#end();
|
|
3553
|
+
for (const [channel, payload] of notifications) publishNotification(this.#options.databaseKey, channel, payload);
|
|
3554
|
+
}
|
|
3555
|
+
/** Rolls back (a no-op when SQLite already ended the transaction) and drops queued notifications. */
|
|
3556
|
+
rollback() {
|
|
3557
|
+
try {
|
|
3558
|
+
if (this.#database.isTransaction) this.#database.exec("rollback");
|
|
3559
|
+
} finally {
|
|
3560
|
+
this.#end();
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
#end() {
|
|
3564
|
+
this.#transactionStart = void 0;
|
|
3565
|
+
this.#pendingNotifications = [];
|
|
3566
|
+
}
|
|
3567
|
+
#prepare(sql) {
|
|
3568
|
+
const cached = this.#statements.get(sql);
|
|
3569
|
+
if (cached) {
|
|
3570
|
+
this.#statements.delete(sql);
|
|
3571
|
+
this.#statements.set(sql, cached);
|
|
3572
|
+
return cached;
|
|
3573
|
+
}
|
|
3574
|
+
const statement = this.#database.prepare(sql);
|
|
3575
|
+
statement.setReadBigInts(true);
|
|
3576
|
+
const metadata = statement.columns();
|
|
3577
|
+
const prepared = {
|
|
3578
|
+
statement,
|
|
3579
|
+
columns: metadata.length === 0 ? void 0 : metadata,
|
|
3580
|
+
decoders: /* @__PURE__ */ new Map()
|
|
3581
|
+
};
|
|
3582
|
+
this.#statements.set(sql, prepared);
|
|
3583
|
+
if (this.#statements.size > STATEMENT_CACHE_SIZE) {
|
|
3584
|
+
const oldest = this.#statements.keys().next().value;
|
|
3585
|
+
if (oldest !== void 0) this.#statements.delete(oldest);
|
|
3586
|
+
}
|
|
3587
|
+
return prepared;
|
|
3588
|
+
}
|
|
3589
|
+
#decodersOf(prepared, context) {
|
|
3590
|
+
const { markers } = context;
|
|
3591
|
+
const strict = this.#options.strict && !context.handWritten;
|
|
3592
|
+
const cacheKey = markers === void 0 ? String(strict) : void 0;
|
|
3593
|
+
let decoders = cacheKey === void 0 ? void 0 : prepared.decoders.get(cacheKey);
|
|
3594
|
+
if (!decoders) {
|
|
3595
|
+
decoders = (prepared.columns ?? []).map((column) => {
|
|
3596
|
+
const marker = markers?.get(column.name);
|
|
3597
|
+
const decoder = decoderFor(column, marker);
|
|
3598
|
+
if (!strict || marker || column.type !== null) return decoder;
|
|
3599
|
+
return (value) => {
|
|
3600
|
+
assertNotUndecoded(column.name, value);
|
|
3601
|
+
return decoder(value);
|
|
3602
|
+
};
|
|
3603
|
+
});
|
|
3604
|
+
if (cacheKey !== void 0) prepared.decoders.set(cacheKey, decoders);
|
|
3605
|
+
}
|
|
3606
|
+
return decoders;
|
|
3607
|
+
}
|
|
3608
|
+
/** Runs one compiled statement and decodes its rows. */
|
|
3609
|
+
execute(query, context) {
|
|
3610
|
+
this.#statementStart = this.#transactionStart === void 0 ? toStoredTimestamp(/* @__PURE__ */ new Date()) : void 0;
|
|
3611
|
+
try {
|
|
3612
|
+
const prepared = this.#prepare(query.sql);
|
|
3613
|
+
const parameters = query.parameters.map(encodeParameter);
|
|
3614
|
+
if (!prepared.columns) {
|
|
3615
|
+
const { changes, lastInsertRowid } = prepared.statement.run(...parameters);
|
|
3616
|
+
return {
|
|
3617
|
+
numAffectedRows: BigInt(changes),
|
|
3618
|
+
insertId: BigInt(lastInsertRowid),
|
|
3619
|
+
rows: []
|
|
3620
|
+
};
|
|
3621
|
+
}
|
|
3622
|
+
const columns = prepared.columns;
|
|
3623
|
+
const decoders = this.#decodersOf(prepared, context);
|
|
3624
|
+
return { rows: prepared.statement.all(...parameters).map((source) => {
|
|
3625
|
+
const row = {};
|
|
3626
|
+
for (const [index, column] of columns.entries()) row[column.name] = decoders[index](source[column.name]);
|
|
3627
|
+
return row;
|
|
3628
|
+
}) };
|
|
3629
|
+
} catch (error) {
|
|
3630
|
+
throw translateSqliteError(error, this.#database, context.writtenTable, query.sql);
|
|
3631
|
+
} finally {
|
|
3632
|
+
this.#statementStart = void 0;
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
/** The planner statistics this database holds (`sqlite_stat1`), as text to compare; '' when it has none. */
|
|
3636
|
+
statistics() {
|
|
3637
|
+
try {
|
|
3638
|
+
return this.#database.prepare(`select group_concat(tbl || ' ' || coalesce(idx, '') || ' ' || stat, char(10)) as stats from sqlite_stat1`).get()?.stats ?? "";
|
|
3639
|
+
} catch {
|
|
3640
|
+
return "";
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
/** Runs SQL text with no parameters or results (pragmas, `vacuum into`). */
|
|
3644
|
+
exec(sql) {
|
|
3645
|
+
this.#database.exec(sql);
|
|
3646
|
+
}
|
|
3647
|
+
close() {
|
|
3648
|
+
this.#statements.clear();
|
|
3649
|
+
if (this.#database.isOpen) this.#database.close();
|
|
3650
|
+
}
|
|
3651
|
+
};
|
|
3652
|
+
//#endregion
|
|
3653
|
+
//#region ../../apps/api/src/db/sqlite/nullsNotDistinct.ts
|
|
3654
|
+
const NULLS_NOT_DISTINCT_INDEXES = [
|
|
3655
|
+
{
|
|
3656
|
+
name: "admin_role_permissions_grant_uq",
|
|
3657
|
+
table: "admin_role_permissions",
|
|
3658
|
+
columns: [
|
|
3659
|
+
"role_id",
|
|
3660
|
+
"action",
|
|
3661
|
+
"model_id"
|
|
3662
|
+
],
|
|
3663
|
+
nullable: ["model_id"]
|
|
3664
|
+
},
|
|
3665
|
+
{
|
|
3666
|
+
name: "app_role_permissions_grant_uq",
|
|
3667
|
+
table: "app_role_permissions",
|
|
3668
|
+
columns: [
|
|
3669
|
+
"role_id",
|
|
3670
|
+
"action",
|
|
3671
|
+
"model_id"
|
|
3672
|
+
],
|
|
3673
|
+
nullable: ["model_id"]
|
|
3674
|
+
},
|
|
3675
|
+
{
|
|
3676
|
+
name: "admin_user_roles_assignment_uq",
|
|
3677
|
+
table: "admin_user_roles",
|
|
3678
|
+
columns: [
|
|
3679
|
+
"admin_user_id",
|
|
3680
|
+
"role_id",
|
|
3681
|
+
"site_id"
|
|
3682
|
+
],
|
|
3683
|
+
nullable: ["site_id"]
|
|
3684
|
+
}
|
|
3685
|
+
];
|
|
3686
|
+
const quote = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
|
|
3687
|
+
/** The index's column list as SQL: `"a", "b", coalesce("c", '')`. */
|
|
3688
|
+
const indexExpressionSql = (index) => index.columns.map((column) => index.nullable.includes(column) ? `coalesce(${quote(column)}, '')` : quote(column)).join(", ");
|
|
3689
|
+
/** The index whose columns are exactly `columns` (in any order) on `table`. */
|
|
3690
|
+
const nullsNotDistinctIndexFor = (table, columns) => NULLS_NOT_DISTINCT_INDEXES.find((index) => index.table === table && index.columns.length === columns.length && index.columns.every((column) => columns.includes(column)));
|
|
3691
|
+
//#endregion
|
|
3692
|
+
//#region ../../apps/api/src/db/sqlite/plugin.ts
|
|
3693
|
+
/** Row-lock modifiers PostgreSQL needs and SQLite rejects; its write transactions are already serialised. */
|
|
3694
|
+
const ROW_LOCKS = /* @__PURE__ */ new Set([
|
|
3695
|
+
"ForUpdate",
|
|
3696
|
+
"ForNoKeyUpdate",
|
|
3697
|
+
"ForShare",
|
|
3698
|
+
"ForKeyShare",
|
|
3699
|
+
"NoWait",
|
|
3700
|
+
"SkipLocked"
|
|
3701
|
+
]);
|
|
3702
|
+
const MARKERS = /* @__PURE__ */ new WeakMap();
|
|
3703
|
+
/** The result-type markers of a query's output columns (read by the driver when it decodes rows). */
|
|
3704
|
+
const markersOf = (queryId) => MARKERS.get(queryId);
|
|
3705
|
+
/** `asc`/`desc` from an order item's direction node (Kysely emits it as a raw `asc`/`desc`). */
|
|
3706
|
+
const directionOf = (node) => {
|
|
3707
|
+
const direction = node.direction;
|
|
3708
|
+
if (direction && RawNode.is(direction)) return direction.sqlFragments.join("").trim().toLowerCase() === "desc" ? "desc" : "asc";
|
|
3709
|
+
return "asc";
|
|
3710
|
+
};
|
|
3711
|
+
/** Writes bound values as literals (SQLite proves a partial index's WHERE only from literal values). */
|
|
3712
|
+
var InlineValues = class extends OperationNodeTransformer {
|
|
3713
|
+
transformValue(node) {
|
|
3714
|
+
return node.immediate ? node : ValueNode.createImmediate(node.value);
|
|
3715
|
+
}
|
|
3716
|
+
};
|
|
3717
|
+
const inlineValues = new InlineValues();
|
|
3718
|
+
/** The conflict target's WHERE (matching a partial unique index) with its values written inline. */
|
|
3719
|
+
const withInlineIndexWhere = (node) => node.indexWhere ? Object.freeze({
|
|
3720
|
+
...node,
|
|
3721
|
+
indexWhere: inlineValues.transformNode(node.indexWhere)
|
|
3722
|
+
}) : node;
|
|
3723
|
+
var SqliteTransformer = class extends OperationNodeTransformer {
|
|
3724
|
+
#insertTable;
|
|
3725
|
+
transformSelectQuery(node, queryId) {
|
|
3726
|
+
const transformed = super.transformSelectQuery(node, queryId);
|
|
3727
|
+
const modifiers = transformed.endModifiers?.filter((modifier) => modifier.modifier === void 0 || !ROW_LOCKS.has(modifier.modifier));
|
|
3728
|
+
if (modifiers === transformed.endModifiers) return transformed;
|
|
3729
|
+
return Object.freeze({
|
|
3730
|
+
...transformed,
|
|
3731
|
+
endModifiers: modifiers?.length ? Object.freeze(modifiers) : void 0
|
|
3732
|
+
});
|
|
3733
|
+
}
|
|
3734
|
+
/** PostgreSQL places NULLs last ascending and first descending; SQLite does the reverse. Keep PostgreSQL's. */
|
|
3735
|
+
transformOrderByItem(node, queryId) {
|
|
3736
|
+
const transformed = super.transformOrderByItem(node, queryId);
|
|
3737
|
+
if (transformed.nulls) return transformed;
|
|
3738
|
+
const rawOrder = RawNode.is(transformed.orderBy) ? transformed.orderBy.sqlFragments.join(" ") : "";
|
|
3739
|
+
if (/\b(asc|desc|nulls)\s*$/i.test(rawOrder)) throw new Error(`Write the direction with orderBy(expr, 'asc' | 'desc'), not inside raw SQL: ${rawOrder}`);
|
|
3740
|
+
return Object.freeze({
|
|
3741
|
+
...transformed,
|
|
3742
|
+
nulls: directionOf(transformed) === "asc" ? "last" : "first"
|
|
3743
|
+
});
|
|
3744
|
+
}
|
|
3745
|
+
transformInsertQuery(node, queryId) {
|
|
3746
|
+
const previous = this.#insertTable;
|
|
3747
|
+
this.#insertTable = node.into?.table.identifier.name;
|
|
3748
|
+
try {
|
|
3749
|
+
return super.transformInsertQuery(node, queryId);
|
|
3750
|
+
} finally {
|
|
3751
|
+
this.#insertTable = previous;
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
/** A column target on a NULLS NOT DISTINCT constraint becomes the SQLite index's expressions. */
|
|
3755
|
+
transformOnConflict(node, queryId) {
|
|
3756
|
+
const transformed = withInlineIndexWhere(super.transformOnConflict(node, queryId));
|
|
3757
|
+
const columns = transformed.columns?.map((column) => column.column.name);
|
|
3758
|
+
const index = columns && this.#insertTable !== void 0 ? nullsNotDistinctIndexFor(this.#insertTable, columns) : void 0;
|
|
3759
|
+
if (!index) return transformed;
|
|
3760
|
+
return Object.freeze({
|
|
3761
|
+
...transformed,
|
|
3762
|
+
columns: void 0,
|
|
3763
|
+
indexExpression: RawNode.createWithSql(indexExpressionSql(index))
|
|
3764
|
+
});
|
|
3765
|
+
}
|
|
3766
|
+
/** SQLite has no DEFAULT keyword in VALUES; Kysely would write NULL there instead of the default. */
|
|
3767
|
+
transformDefaultInsertValue(_node) {
|
|
3768
|
+
throw new Error("A multi-row insert leaves a column out of some rows; SQLite cannot insert DEFAULT there. Give every row the same columns.");
|
|
3769
|
+
}
|
|
3770
|
+
};
|
|
3771
|
+
/**
|
|
3772
|
+
* Adapts Kysely queries written for PostgreSQL to SQLite (dialect boundary, ADR 0001): drops row locks,
|
|
3773
|
+
* spells out PostgreSQL's NULL ordering, retargets conflicts on NULLS NOT DISTINCT constraints and records
|
|
3774
|
+
* the result-type markers of computed columns for the driver.
|
|
3775
|
+
*/
|
|
3776
|
+
var SqlitePlugin = class {
|
|
3777
|
+
#transformer = new SqliteTransformer();
|
|
3778
|
+
transformQuery({ node, queryId }) {
|
|
3779
|
+
const markers = collectMarkers(node);
|
|
3780
|
+
if (markers) MARKERS.set(queryId, markers);
|
|
3781
|
+
return this.#transformer.transformNode(node, queryId);
|
|
3782
|
+
}
|
|
3783
|
+
async transformResult({ result }) {
|
|
3784
|
+
return result;
|
|
3785
|
+
}
|
|
3786
|
+
};
|
|
3787
|
+
//#endregion
|
|
3788
|
+
//#region ../../apps/api/src/db/sqlite/writeLock.ts
|
|
3789
|
+
const scopes = new AsyncLocalStorage();
|
|
3790
|
+
/** Runs `fn` (a Kysely transaction, including its begin) in a fresh transaction scope. */
|
|
3791
|
+
const runInTransactionScope = (fn) => scopes.run({
|
|
3792
|
+
holdsWriteLock: false,
|
|
3793
|
+
parent: scopes.getStore()
|
|
3794
|
+
}, fn);
|
|
3795
|
+
/** Whether `scope` or a transaction enclosing it holds the write lock. */
|
|
3796
|
+
const insideWriter = (scope) => {
|
|
3797
|
+
for (let current = scope; current; current = current.parent) if (current.holdsWriteLock) return true;
|
|
3798
|
+
return false;
|
|
3799
|
+
};
|
|
3800
|
+
const currentTransactionScope = () => scopes.getStore();
|
|
3801
|
+
var SqliteNestedWriteError = class extends Error {
|
|
3802
|
+
constructor(statement) {
|
|
3803
|
+
super(`A write outside the open SQLite write transaction would wait for that transaction forever. Run it on the transaction (trx) instead: ${statement.slice(0, 200)}`);
|
|
3804
|
+
this.name = "SqliteNestedWriteError";
|
|
3805
|
+
}
|
|
3806
|
+
};
|
|
3807
|
+
var WriteLock = class {
|
|
3808
|
+
#tail = Promise.resolve();
|
|
3809
|
+
#holder;
|
|
3810
|
+
/**
|
|
3811
|
+
* Waits for the write lock and returns its release function. `scope` is the transaction taking it (it
|
|
3812
|
+
* then holds the lock until commit or rollback), or undefined for one autocommit statement.
|
|
3813
|
+
*/
|
|
3814
|
+
async acquire(statement, scope) {
|
|
3815
|
+
if (this.#holder !== void 0 && insideWriter(scopes.getStore())) throw new SqliteNestedWriteError(statement);
|
|
3816
|
+
const previous = this.#tail;
|
|
3817
|
+
let release;
|
|
3818
|
+
this.#tail = new Promise((resolve) => {
|
|
3819
|
+
release = resolve;
|
|
3820
|
+
});
|
|
3821
|
+
await previous;
|
|
3822
|
+
this.#holder = scope;
|
|
3823
|
+
if (scope) scope.holdsWriteLock = true;
|
|
3824
|
+
let released = false;
|
|
3825
|
+
return () => {
|
|
3826
|
+
if (released) return;
|
|
3827
|
+
released = true;
|
|
3828
|
+
if (scope) scope.holdsWriteLock = false;
|
|
3829
|
+
this.#holder = void 0;
|
|
3830
|
+
release();
|
|
3831
|
+
};
|
|
3832
|
+
}
|
|
3833
|
+
};
|
|
3834
|
+
//#endregion
|
|
3835
|
+
//#region ../../apps/api/src/db/sqlite/driver.ts
|
|
3836
|
+
/** Statement text that may write (or call a function that writes). Anything unsure counts as a write. */
|
|
3837
|
+
const WRITE_WORDS = /\b(insert|update|delete|replace|create|drop|alter|vacuum|pragma|shapio_nextval|begin|commit|rollback|savepoint|release)\b/i;
|
|
3838
|
+
const isRead = (query) => {
|
|
3839
|
+
if (SelectQueryNode.is(query.query)) return !query.sql.includes("shapio_nextval(");
|
|
3840
|
+
if (RawNode.is(query.query)) return /^\s*(select|with)\b/i.test(query.sql) && !WRITE_WORDS.test(query.sql);
|
|
3841
|
+
return false;
|
|
3842
|
+
};
|
|
3843
|
+
const tableNameOf = (node) => {
|
|
3844
|
+
return (node?.table)?.identifier?.name;
|
|
3845
|
+
};
|
|
3846
|
+
const writtenTableOf = (query) => {
|
|
3847
|
+
const node = query.query;
|
|
3848
|
+
if (InsertQueryNode.is(node)) return tableNameOf(node.into);
|
|
3849
|
+
if (UpdateQueryNode.is(node) && node.table) return tableNameOf(node.table) ?? tableNameOf(node.table.node);
|
|
3850
|
+
};
|
|
3851
|
+
/**
|
|
3852
|
+
* The key a database's notification bus and session locks use: the absolute file path (every in-memory
|
|
3853
|
+
* database shares one key; notifications are only hints).
|
|
3854
|
+
*/
|
|
3855
|
+
const notificationKeyOf = (location) => location.kind === "memory" ? ":memory:" : resolve(location.path);
|
|
3856
|
+
const contextOf = (query) => ({
|
|
3857
|
+
markers: markersOf(query.queryId),
|
|
3858
|
+
writtenTable: writtenTableOf(query),
|
|
3859
|
+
handWritten: RawNode.is(query.query)
|
|
3860
|
+
});
|
|
3861
|
+
/** How often the writer refreshes the query planner's statistics. */
|
|
3862
|
+
const STATISTICS_INTERVAL_MS = 6e5;
|
|
3863
|
+
/**
|
|
3864
|
+
* Planner statistics (`sqlite_stat1`), as SQLite recommends for long-lived connections: `full` also
|
|
3865
|
+
* analyzes tables that were never analyzed (0x10000: every table, not only those this connection queried);
|
|
3866
|
+
* `refresh` re-analyzes the tables whose size changed a lot since. Both are usually no-ops. Without
|
|
3867
|
+
* statistics SQLite cannot tell that a list in `created_at` order is best read through the entries index.
|
|
3868
|
+
* A connection loads statistics when it opens (or its schema changes), so when they change the readers
|
|
3869
|
+
* are reopened: idle ones at once, busy ones when released.
|
|
3870
|
+
*/
|
|
3871
|
+
const OPTIMIZE = {
|
|
3872
|
+
full: "pragma optimize=0x10002",
|
|
3873
|
+
refresh: "pragma optimize"
|
|
3874
|
+
};
|
|
3875
|
+
/**
|
|
3876
|
+
* A Kysely driver over Node's built-in `node:sqlite` (no native dependency). One writer connection runs
|
|
3877
|
+
* every write, serialised by an in-process lock (`writeLock.ts`, `BEGIN IMMEDIATE`); reader connections
|
|
3878
|
+
* serve read-only transactions and autocommit reads concurrently (WAL). An in-memory database has the
|
|
3879
|
+
* writer only, and every statement takes the lock.
|
|
3880
|
+
*/
|
|
3881
|
+
var SqliteDriver = class {
|
|
3882
|
+
#options;
|
|
3883
|
+
#writeLock = new WriteLock();
|
|
3884
|
+
databaseKey;
|
|
3885
|
+
#writer;
|
|
3886
|
+
#autocommitReader;
|
|
3887
|
+
#idleReaders = [];
|
|
3888
|
+
#openReaders = 0;
|
|
3889
|
+
#readerWaiters = [];
|
|
3890
|
+
#allConnections = [];
|
|
3891
|
+
#statisticsTimer;
|
|
3892
|
+
/** Bumped when the statistics change; a reader opened under an older one is closed on release. */
|
|
3893
|
+
#statisticsGeneration = 0;
|
|
3894
|
+
#readerGenerations = /* @__PURE__ */ new WeakMap();
|
|
3895
|
+
constructor(options) {
|
|
3896
|
+
this.#options = options;
|
|
3897
|
+
this.databaseKey = notificationKeyOf(options.location);
|
|
3898
|
+
}
|
|
3899
|
+
get #hasReaders() {
|
|
3900
|
+
return this.#options.location.kind === "file";
|
|
3901
|
+
}
|
|
3902
|
+
#open(role) {
|
|
3903
|
+
const connection = new SqliteConnection({
|
|
3904
|
+
location: this.#options.location.kind === "memory" ? ":memory:" : this.databaseKey,
|
|
3905
|
+
databaseKey: this.databaseKey,
|
|
3906
|
+
role,
|
|
3907
|
+
busyTimeoutMs: this.#options.busyTimeoutMs,
|
|
3908
|
+
strict: this.#options.strict
|
|
3909
|
+
});
|
|
3910
|
+
this.#allConnections.push(connection);
|
|
3911
|
+
if (role === "reader") this.#readerGenerations.set(connection, this.#statisticsGeneration);
|
|
3912
|
+
return connection;
|
|
3913
|
+
}
|
|
3914
|
+
#close(connection) {
|
|
3915
|
+
connection.close();
|
|
3916
|
+
this.#allConnections = this.#allConnections.filter((candidate) => candidate !== connection);
|
|
3917
|
+
}
|
|
3918
|
+
/** Closes readers that hold old statistics: the autocommit one and idle ones now, busy ones on release. */
|
|
3919
|
+
#retireReaders() {
|
|
3920
|
+
this.#statisticsGeneration += 1;
|
|
3921
|
+
if (this.#autocommitReader) {
|
|
3922
|
+
this.#close(this.#autocommitReader);
|
|
3923
|
+
this.#autocommitReader = void 0;
|
|
3924
|
+
}
|
|
3925
|
+
for (const reader of this.#idleReaders) {
|
|
3926
|
+
this.#close(reader);
|
|
3927
|
+
this.#openReaders -= 1;
|
|
3928
|
+
}
|
|
3929
|
+
this.#idleReaders = [];
|
|
3930
|
+
}
|
|
3931
|
+
get writer() {
|
|
3932
|
+
if (!this.#writer) {
|
|
3933
|
+
this.#writer = this.#open("writer");
|
|
3934
|
+
this.#writer.exec(OPTIMIZE.full);
|
|
3935
|
+
this.#statisticsTimer = setInterval(() => {
|
|
3936
|
+
this.optimizeStatistics("refresh").catch((error) => {
|
|
3937
|
+
process.emitWarning(`SQLite statistics refresh (PRAGMA optimize) failed: ${String(error)}`);
|
|
3938
|
+
});
|
|
3939
|
+
}, STATISTICS_INTERVAL_MS);
|
|
3940
|
+
this.#statisticsTimer.unref();
|
|
3941
|
+
}
|
|
3942
|
+
return this.#writer;
|
|
3943
|
+
}
|
|
3944
|
+
/** Refreshes planner statistics on the writer, between write transactions (after migrations: `full`). */
|
|
3945
|
+
async optimizeStatistics(mode) {
|
|
3946
|
+
const release = await this.lockWriter(OPTIMIZE[mode], false);
|
|
3947
|
+
try {
|
|
3948
|
+
const before = this.writer.statistics();
|
|
3949
|
+
this.writer.exec(OPTIMIZE[mode]);
|
|
3950
|
+
if (this.writer.statistics() !== before) this.#retireReaders();
|
|
3951
|
+
} finally {
|
|
3952
|
+
release();
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
async init() {
|
|
3956
|
+
this.writer;
|
|
3957
|
+
}
|
|
3958
|
+
async acquireConnection() {
|
|
3959
|
+
return new SqliteSession(this);
|
|
3960
|
+
}
|
|
3961
|
+
async beginTransaction(connection, settings) {
|
|
3962
|
+
await connection.begin(settings.accessMode === "read only");
|
|
3963
|
+
}
|
|
3964
|
+
async commitTransaction(connection) {
|
|
3965
|
+
connection.commit();
|
|
3966
|
+
}
|
|
3967
|
+
async rollbackTransaction(connection) {
|
|
3968
|
+
connection.rollback();
|
|
3969
|
+
}
|
|
3970
|
+
async savepoint(connection, savepointName) {
|
|
3971
|
+
connection.savepointCommand("savepoint", savepointName);
|
|
3972
|
+
}
|
|
3973
|
+
async rollbackToSavepoint(connection, savepointName) {
|
|
3974
|
+
connection.savepointCommand("rollback to", savepointName);
|
|
3975
|
+
}
|
|
3976
|
+
async releaseSavepoint(connection, savepointName) {
|
|
3977
|
+
connection.savepointCommand("release", savepointName);
|
|
3978
|
+
}
|
|
3979
|
+
async releaseConnection(connection) {
|
|
3980
|
+
connection.release();
|
|
3981
|
+
}
|
|
3982
|
+
async destroy() {
|
|
3983
|
+
clearInterval(this.#statisticsTimer);
|
|
3984
|
+
this.#statisticsTimer = void 0;
|
|
3985
|
+
for (const connection of this.#allConnections) connection.close();
|
|
3986
|
+
this.#allConnections = [];
|
|
3987
|
+
this.#writer = void 0;
|
|
3988
|
+
this.#autocommitReader = void 0;
|
|
3989
|
+
this.#idleReaders = [];
|
|
3990
|
+
}
|
|
3991
|
+
/** Takes the write lock for one statement or a whole transaction (see `writeLock.ts`). */
|
|
3992
|
+
lockWriter(statement, transaction) {
|
|
3993
|
+
return this.#writeLock.acquire(statement, transaction ? currentTransactionScope() : void 0);
|
|
3994
|
+
}
|
|
3995
|
+
/** The connection autocommit reads use: a reader, or the writer for an in-memory database. */
|
|
3996
|
+
readerForStatement() {
|
|
3997
|
+
if (!this.#hasReaders) return;
|
|
3998
|
+
this.#autocommitReader ??= this.#open("reader");
|
|
3999
|
+
return this.#autocommitReader;
|
|
4000
|
+
}
|
|
4001
|
+
/** A reader for a read-only transaction; waits when `readers` are all in use. */
|
|
4002
|
+
async acquireReader() {
|
|
4003
|
+
if (!this.#hasReaders) return;
|
|
4004
|
+
const idle = this.#idleReaders.pop();
|
|
4005
|
+
if (idle) return idle;
|
|
4006
|
+
if (this.#openReaders < this.#options.readers) {
|
|
4007
|
+
this.#openReaders += 1;
|
|
4008
|
+
return this.#open("reader");
|
|
4009
|
+
}
|
|
4010
|
+
return new Promise((resolve) => this.#readerWaiters.push(resolve));
|
|
4011
|
+
}
|
|
4012
|
+
releaseReader(released) {
|
|
4013
|
+
let reader = released;
|
|
4014
|
+
if ((this.#readerGenerations.get(released) ?? 0) < this.#statisticsGeneration) {
|
|
4015
|
+
this.#close(released);
|
|
4016
|
+
reader = this.#open("reader");
|
|
4017
|
+
}
|
|
4018
|
+
const waiter = this.#readerWaiters.shift();
|
|
4019
|
+
if (waiter) waiter(reader);
|
|
4020
|
+
else this.#idleReaders.push(reader);
|
|
4021
|
+
}
|
|
4022
|
+
};
|
|
4023
|
+
/**
|
|
4024
|
+
* The connection Kysely holds for one query or transaction. It binds to a physical connection lazily:
|
|
4025
|
+
* a write transaction to the writer (holding the write lock until it ends), a read-only one to a reader, and
|
|
4026
|
+
* a single statement to whichever fits.
|
|
4027
|
+
*/
|
|
4028
|
+
var SqliteSession = class {
|
|
4029
|
+
#driver;
|
|
4030
|
+
#transaction;
|
|
4031
|
+
#releaseWriteLock;
|
|
4032
|
+
constructor(driver) {
|
|
4033
|
+
this.#driver = driver;
|
|
4034
|
+
}
|
|
4035
|
+
async begin(readOnly) {
|
|
4036
|
+
const reader = readOnly ? await this.#driver.acquireReader() : void 0;
|
|
4037
|
+
if (reader) {
|
|
4038
|
+
try {
|
|
4039
|
+
reader.begin("deferred");
|
|
4040
|
+
} catch (error) {
|
|
4041
|
+
this.#driver.releaseReader(reader);
|
|
4042
|
+
throw error;
|
|
4043
|
+
}
|
|
4044
|
+
this.#transaction = reader;
|
|
4045
|
+
return;
|
|
4046
|
+
}
|
|
4047
|
+
const release = await this.#driver.lockWriter("begin", true);
|
|
4048
|
+
try {
|
|
4049
|
+
this.#driver.writer.begin("immediate");
|
|
4050
|
+
} catch (error) {
|
|
4051
|
+
release();
|
|
4052
|
+
throw error;
|
|
4053
|
+
}
|
|
4054
|
+
this.#releaseWriteLock = release;
|
|
4055
|
+
this.#transaction = this.#driver.writer;
|
|
4056
|
+
}
|
|
4057
|
+
#finish(end) {
|
|
4058
|
+
const connection = this.#transaction;
|
|
4059
|
+
if (!connection) return;
|
|
4060
|
+
this.#transaction = void 0;
|
|
4061
|
+
try {
|
|
4062
|
+
end(connection);
|
|
4063
|
+
} finally {
|
|
4064
|
+
if (connection.role === "reader") this.#driver.releaseReader(connection);
|
|
4065
|
+
else {
|
|
4066
|
+
this.#releaseWriteLock?.();
|
|
4067
|
+
this.#releaseWriteLock = void 0;
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
commit() {
|
|
4072
|
+
this.#finish((connection) => connection.commit());
|
|
4073
|
+
}
|
|
4074
|
+
rollback() {
|
|
4075
|
+
this.#finish((connection) => connection.rollback());
|
|
4076
|
+
}
|
|
4077
|
+
/** Kysely releases the connection after a failed begin or commit too: never leave a lock behind. */
|
|
4078
|
+
release() {
|
|
4079
|
+
if (this.#transaction) this.rollback();
|
|
4080
|
+
}
|
|
4081
|
+
savepointCommand(command, name) {
|
|
4082
|
+
if (!this.#transaction) throw new Error("Savepoints need an open transaction");
|
|
4083
|
+
this.#transaction.exec(`${command} "${name.replace(/"/g, "\"\"")}"`);
|
|
4084
|
+
}
|
|
4085
|
+
async executeQuery(query) {
|
|
4086
|
+
const context = contextOf(query);
|
|
4087
|
+
if (this.#transaction) return this.#transaction.execute(query, context);
|
|
4088
|
+
const reader = isRead(query) ? this.#driver.readerForStatement() : void 0;
|
|
4089
|
+
if (reader) return reader.execute(query, context);
|
|
4090
|
+
const release = await this.#driver.lockWriter(query.sql, false);
|
|
4091
|
+
try {
|
|
4092
|
+
return this.#driver.writer.execute(query, context);
|
|
4093
|
+
} finally {
|
|
4094
|
+
release();
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
async *streamQuery(query) {
|
|
4098
|
+
const result = await this.executeQuery(query);
|
|
4099
|
+
for (const row of result.rows) yield { rows: [row] };
|
|
4100
|
+
}
|
|
4101
|
+
};
|
|
4102
|
+
//#endregion
|
|
4103
|
+
//#region ../../apps/api/src/db/sqlite/index.ts
|
|
4104
|
+
/**
|
|
4105
|
+
* SQLite runs DDL inside transactions, so each migration commits or rolls back whole. The driver handles
|
|
4106
|
+
* concurrency itself (one writer, several readers), so Kysely must not serialise connections on top.
|
|
4107
|
+
*/
|
|
4108
|
+
var ShapioSqliteAdapter = class extends SqliteAdapter {
|
|
4109
|
+
get supportsTransactionalDdl() {
|
|
4110
|
+
return true;
|
|
4111
|
+
}
|
|
4112
|
+
get supportsMultipleConnections() {
|
|
4113
|
+
return true;
|
|
4114
|
+
}
|
|
4115
|
+
};
|
|
4116
|
+
var ShapioSqliteDialect = class {
|
|
4117
|
+
driver;
|
|
4118
|
+
constructor(driver) {
|
|
4119
|
+
this.driver = driver;
|
|
4120
|
+
}
|
|
4121
|
+
createAdapter() {
|
|
4122
|
+
return new ShapioSqliteAdapter();
|
|
4123
|
+
}
|
|
4124
|
+
createDriver() {
|
|
4125
|
+
return this.driver;
|
|
4126
|
+
}
|
|
4127
|
+
createIntrospector(db) {
|
|
4128
|
+
return new SqliteIntrospector(db);
|
|
4129
|
+
}
|
|
4130
|
+
createQueryCompiler() {
|
|
4131
|
+
return new SqliteQueryCompiler();
|
|
4132
|
+
}
|
|
4133
|
+
};
|
|
4134
|
+
/** Runs every `execute` of a transaction builder (and of the builders it returns) in a transaction scope. */
|
|
4135
|
+
const scoped = (builder) => new Proxy(builder, { get(target, property, receiver) {
|
|
4136
|
+
if (property === "execute") return (callback) => runInTransactionScope(() => target.execute(callback));
|
|
4137
|
+
if (property === "setIsolationLevel" || property === "setAccessMode") return (value) => scoped(target[property](value));
|
|
4138
|
+
return Reflect.get(target, property, receiver);
|
|
4139
|
+
} });
|
|
4140
|
+
/**
|
|
4141
|
+
* Kysely on SQLite. Transactions run in a scope so a write that would wait for its own enclosing write
|
|
4142
|
+
* transaction fails at once instead of hanging (`writeLock.ts`).
|
|
4143
|
+
*/
|
|
4144
|
+
var SqliteKysely = class extends Kysely {
|
|
4145
|
+
sqliteDriver;
|
|
4146
|
+
constructor(config) {
|
|
4147
|
+
super(config);
|
|
4148
|
+
this.sqliteDriver = config.dialect.driver;
|
|
4149
|
+
}
|
|
4150
|
+
transaction() {
|
|
4151
|
+
return scoped(super.transaction());
|
|
4152
|
+
}
|
|
4153
|
+
};
|
|
4154
|
+
const DEFAULT_BUSY_TIMEOUT_MS = 5e3;
|
|
4155
|
+
const createSqliteDb = ({ location, readers, busyTimeoutMs = DEFAULT_BUSY_TIMEOUT_MS, strict = false }) => new SqliteKysely({
|
|
4156
|
+
dialect: new ShapioSqliteDialect(new SqliteDriver({
|
|
4157
|
+
location,
|
|
4158
|
+
readers,
|
|
4159
|
+
busyTimeoutMs,
|
|
4160
|
+
strict
|
|
4161
|
+
})),
|
|
4162
|
+
plugins: [new SqlitePlugin()]
|
|
4163
|
+
});
|
|
4164
|
+
/** The SQLite driver behind a handle, or undefined for another dialect. */
|
|
4165
|
+
const sqliteDriverOf = (db) => db instanceof SqliteKysely ? db.sqliteDriver : void 0;
|
|
4166
|
+
//#endregion
|
|
4167
|
+
//#region ../../apps/api/src/db/advisoryLocks.ts
|
|
4168
|
+
/**
|
|
4169
|
+
* Advisory-lock primitives. Kept in `db/` beside the Kysely instance so no other module writes lock SQL.
|
|
4170
|
+
* Keys are (namespace, key) int4 pairs; see constants/lockKeys.ts.
|
|
4171
|
+
*
|
|
4172
|
+
* PostgreSQL: advisory locks. SQLite (single process): session locks are an in-process mutex per database
|
|
4173
|
+
* and key; transaction locks are checks only, because every write transaction already holds SQLite's single
|
|
4174
|
+
* write lock, so no other transaction can run while one holds them.
|
|
4175
|
+
*/
|
|
4176
|
+
/** Stable 32-bit signed key for a string ID (e.g. a model UUID). Collisions only cost extra serialization. */
|
|
4177
|
+
const lockKeyFromId = (id) => createHash("sha256").update(id).digest().readInt32BE(0);
|
|
4178
|
+
//#endregion
|
|
4179
|
+
export { mysqlDriverOf as a, assertNotNested as c, toDatetimeText as d, AppError as f, createMysqlDb as i, withConnectionScope as l, createSqliteDb as n, DEFAULT_POOL_ACQUIRE_TIMEOUT_MS as o, sqliteDriverOf as r, pgAcquireError as s, lockKeyFromId as t, toStoredTimestamp as u };
|
|
4180
|
+
|
|
4181
|
+
//# sourceMappingURL=advisoryLocks-COlJQc1r.js.map
|