@atscript/db-mysql 0.1.38
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 +21 -0
- package/dist/index.cjs +1230 -0
- package/dist/index.d.ts +271 -0
- package/dist/index.mjs +1202 -0
- package/dist/plugin.cjs +111 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.mjs +87 -0
- package/package.json +80 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1202 @@
|
|
|
1
|
+
import { AtscriptDbView, BaseDbAdapter, DbError, DbSpace } from "@atscript/db";
|
|
2
|
+
import { buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildInsert, buildSelect, buildUpdate, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, refActionToSql, toSqlValue } from "@atscript/db-sql-tools";
|
|
3
|
+
import { AnnotationSpec } from "@atscript/core";
|
|
4
|
+
|
|
5
|
+
//#region packages/db-mysql/src/sql-builder.ts
|
|
6
|
+
function esc(name) {
|
|
7
|
+
return name.replace(/`/g, "``");
|
|
8
|
+
}
|
|
9
|
+
function qi(name) {
|
|
10
|
+
return `\`${esc(name)}\``;
|
|
11
|
+
}
|
|
12
|
+
function quoteTableName(name) {
|
|
13
|
+
const dot = name.indexOf(".");
|
|
14
|
+
if (dot >= 0) return `${qi(name.slice(0, dot))}.${qi(name.slice(dot + 1))}`;
|
|
15
|
+
return qi(name);
|
|
16
|
+
}
|
|
17
|
+
const mysqlDialect = {
|
|
18
|
+
quoteIdentifier(name) {
|
|
19
|
+
return qi(name);
|
|
20
|
+
},
|
|
21
|
+
quoteTable(name) {
|
|
22
|
+
return quoteTableName(name);
|
|
23
|
+
},
|
|
24
|
+
unlimitedLimit: "18446744073709551615",
|
|
25
|
+
toValue: toSqlValue,
|
|
26
|
+
toParam(value) {
|
|
27
|
+
if (value === undefined) return null;
|
|
28
|
+
return typeof value === "boolean" ? value ? 1 : 0 : value;
|
|
29
|
+
},
|
|
30
|
+
regex(quotedCol, value) {
|
|
31
|
+
const pattern = value instanceof RegExp ? value.source : String(value);
|
|
32
|
+
return {
|
|
33
|
+
sql: `${quotedCol} REGEXP ?`,
|
|
34
|
+
params: [pattern]
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
createViewPrefix: "CREATE OR REPLACE VIEW"
|
|
38
|
+
};
|
|
39
|
+
function buildInsert$1(table, data) {
|
|
40
|
+
return buildInsert(mysqlDialect, table, data);
|
|
41
|
+
}
|
|
42
|
+
function buildSelect$1(table, where, controls) {
|
|
43
|
+
return buildSelect(mysqlDialect, table, where, controls);
|
|
44
|
+
}
|
|
45
|
+
function buildUpdate$1(table, data, where, limit) {
|
|
46
|
+
return buildUpdate(mysqlDialect, table, data, where, limit);
|
|
47
|
+
}
|
|
48
|
+
function buildDelete$1(table, where, limit) {
|
|
49
|
+
return buildDelete(mysqlDialect, table, where, limit);
|
|
50
|
+
}
|
|
51
|
+
function buildCreateView$1(viewName, plan, columns, resolveFieldRef) {
|
|
52
|
+
return buildCreateView(mysqlDialect, viewName, plan, columns, resolveFieldRef);
|
|
53
|
+
}
|
|
54
|
+
function buildAggregateSelect$1(table, where, controls) {
|
|
55
|
+
return buildAggregateSelect(mysqlDialect, table, where, controls);
|
|
56
|
+
}
|
|
57
|
+
function buildAggregateCount$1(table, where, controls) {
|
|
58
|
+
return buildAggregateCount(mysqlDialect, table, where, controls);
|
|
59
|
+
}
|
|
60
|
+
function collationToMysql(collation) {
|
|
61
|
+
switch (collation) {
|
|
62
|
+
case "binary": return "utf8mb4_bin";
|
|
63
|
+
case "nocase": return "utf8mb4_general_ci";
|
|
64
|
+
case "unicode": return "utf8mb4_unicode_ci";
|
|
65
|
+
default: return "utf8mb4_unicode_ci";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Maps an Atscript field descriptor to a MySQL column type.
|
|
70
|
+
*
|
|
71
|
+
* Reads `designType`, primitive tags (via `type.type.tags`), and annotations
|
|
72
|
+
* from field metadata to produce the most specific MySQL type.
|
|
73
|
+
*
|
|
74
|
+
* For FK fields, delegates to the target PK's type via `field.fkTargetField`
|
|
75
|
+
* so the FK column type always matches the referenced column.
|
|
76
|
+
*/
|
|
77
|
+
/** Maps integer primitive tags to MySQL integer types. */ function intTypeFromTags(tags, unsigned) {
|
|
78
|
+
if (tags?.has("int8")) return unsigned ? "TINYINT UNSIGNED" : "TINYINT";
|
|
79
|
+
if (tags?.has("uint8") || tags?.has("byte")) return "TINYINT UNSIGNED";
|
|
80
|
+
if (tags?.has("int16")) return unsigned ? "SMALLINT UNSIGNED" : "SMALLINT";
|
|
81
|
+
if (tags?.has("uint16") || tags?.has("port")) return "SMALLINT UNSIGNED";
|
|
82
|
+
if (tags?.has("int32")) return unsigned ? "INT UNSIGNED" : "INT";
|
|
83
|
+
if (tags?.has("uint32")) return "INT UNSIGNED";
|
|
84
|
+
if (tags?.has("int64")) return unsigned ? "BIGINT UNSIGNED" : "BIGINT";
|
|
85
|
+
if (tags?.has("uint64")) return "BIGINT UNSIGNED";
|
|
86
|
+
return unsigned ? "INT UNSIGNED" : "INT";
|
|
87
|
+
}
|
|
88
|
+
function mysqlTypeFromField(field) {
|
|
89
|
+
if (field.fkTargetField) return mysqlTypeFromField(field.fkTargetField);
|
|
90
|
+
const tags = field.type?.type?.tags;
|
|
91
|
+
const metadata = field.type?.metadata;
|
|
92
|
+
const mysqlTypeOverride = metadata?.get("db.mysql.type");
|
|
93
|
+
if (mysqlTypeOverride) return mysqlTypeOverride;
|
|
94
|
+
const unsigned = metadata?.has("db.mysql.unsigned") ?? false;
|
|
95
|
+
const precision = metadata?.get("db.column.precision");
|
|
96
|
+
switch (field.designType) {
|
|
97
|
+
case "number": {
|
|
98
|
+
if (precision) return `DECIMAL(${precision.precision},${precision.scale})`;
|
|
99
|
+
if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "increment") return unsigned ? "BIGINT UNSIGNED" : "BIGINT";
|
|
100
|
+
if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "now") return "TIMESTAMP";
|
|
101
|
+
if (tags?.has("int")) return intTypeFromTags(tags, unsigned);
|
|
102
|
+
return "DOUBLE";
|
|
103
|
+
}
|
|
104
|
+
case "integer": return intTypeFromTags(tags, unsigned);
|
|
105
|
+
case "decimal": {
|
|
106
|
+
if (precision) return `DECIMAL(${precision.precision},${precision.scale})`;
|
|
107
|
+
return "DECIMAL(10,2)";
|
|
108
|
+
}
|
|
109
|
+
case "boolean": return "TINYINT(1)";
|
|
110
|
+
case "string": {
|
|
111
|
+
if (tags?.has("char")) return "CHAR(1)";
|
|
112
|
+
const maxLen = metadata?.get("expect.maxLength")?.length;
|
|
113
|
+
if (maxLen !== undefined && maxLen <= 65535) return `VARCHAR(${maxLen})`;
|
|
114
|
+
if (maxLen !== undefined && maxLen > 65535) return "LONGTEXT";
|
|
115
|
+
if (field.isPrimaryKey || field.defaultValue) return "VARCHAR(255)";
|
|
116
|
+
return "TEXT";
|
|
117
|
+
}
|
|
118
|
+
case "json":
|
|
119
|
+
case "object":
|
|
120
|
+
case "array": return "JSON";
|
|
121
|
+
default: {
|
|
122
|
+
if (field.isPrimaryKey || field.defaultValue) return "VARCHAR(255)";
|
|
123
|
+
return "TEXT";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function buildCreateTable(table, fields, foreignKeys, options) {
|
|
128
|
+
const colDefs = [];
|
|
129
|
+
const primaryKeys = fields.filter((f) => f.isPrimaryKey);
|
|
130
|
+
for (const field of fields) {
|
|
131
|
+
if (field.ignored) continue;
|
|
132
|
+
const sqlType = mysqlTypeFromField(field);
|
|
133
|
+
let def = `${qi(field.physicalName)} ${sqlType}`;
|
|
134
|
+
if (options?.incrementFields?.has(field.physicalName)) def += " AUTO_INCREMENT";
|
|
135
|
+
if (!field.optional && !field.isPrimaryKey && !options?.incrementFields?.has(field.physicalName)) def += " NOT NULL";
|
|
136
|
+
if (field.defaultValue?.kind === "value") def += ` DEFAULT ${defaultValueToSqlLiteral(field.designType, field.defaultValue.value)}`;
|
|
137
|
+
else if (field.defaultValue?.kind === "fn") {
|
|
138
|
+
if (field.defaultValue.fn === "uuid") def += " DEFAULT (UUID())";
|
|
139
|
+
else if (field.defaultValue.fn === "now") def += " DEFAULT CURRENT_TIMESTAMP";
|
|
140
|
+
}
|
|
141
|
+
const nativeCollate = field.type?.metadata?.get("db.mysql.collate");
|
|
142
|
+
if (nativeCollate) def += ` COLLATE ${nativeCollate}`;
|
|
143
|
+
else if (field.collate) def += ` COLLATE ${collationToMysql(field.collate)}`;
|
|
144
|
+
const onUpdate = options?.onUpdateFields?.get(field.physicalName);
|
|
145
|
+
if (onUpdate) def += ` ON UPDATE ${onUpdate}`;
|
|
146
|
+
colDefs.push(def);
|
|
147
|
+
}
|
|
148
|
+
if (primaryKeys.length === 1) {
|
|
149
|
+
const pkCol = qi(primaryKeys[0].physicalName);
|
|
150
|
+
for (let i = 0; i < colDefs.length; i++) if (colDefs[i].startsWith(pkCol)) {
|
|
151
|
+
colDefs[i] += " PRIMARY KEY";
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
} else if (primaryKeys.length > 1) {
|
|
155
|
+
const pkCols = primaryKeys.map((pk) => qi(pk.physicalName)).join(", ");
|
|
156
|
+
colDefs.push(`PRIMARY KEY (${pkCols})`);
|
|
157
|
+
}
|
|
158
|
+
if (foreignKeys) for (const fk of foreignKeys.values()) {
|
|
159
|
+
const localCols = fk.fields.map((f) => qi(f)).join(", ");
|
|
160
|
+
const targetCols = fk.targetFields.map((f) => qi(f)).join(", ");
|
|
161
|
+
let constraint = `FOREIGN KEY (${localCols}) REFERENCES ${qi(fk.targetTable)} (${targetCols})`;
|
|
162
|
+
if (fk.onDelete) constraint += ` ON DELETE ${refActionToSql(fk.onDelete)}`;
|
|
163
|
+
if (fk.onUpdate) constraint += ` ON UPDATE ${refActionToSql(fk.onUpdate)}`;
|
|
164
|
+
colDefs.push(constraint);
|
|
165
|
+
}
|
|
166
|
+
let sql = `CREATE TABLE IF NOT EXISTS ${quoteTableName(table)} (${colDefs.join(", ")})`;
|
|
167
|
+
const engine = options?.engine ?? "InnoDB";
|
|
168
|
+
const charset = options?.charset ?? "utf8mb4";
|
|
169
|
+
const collation = options?.collation ?? "utf8mb4_unicode_ci";
|
|
170
|
+
sql += ` ENGINE=${engine} DEFAULT CHARSET=${charset} COLLATE=${collation}`;
|
|
171
|
+
if (options?.autoIncrementStart !== undefined) sql += ` AUTO_INCREMENT=${options.autoIncrementStart}`;
|
|
172
|
+
return sql;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region packages/db-mysql/src/filter-builder.ts
|
|
177
|
+
function buildWhere(filter) {
|
|
178
|
+
return buildWhere$1(mysqlDialect, filter);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region packages/db-mysql/src/mysql-adapter.ts
|
|
183
|
+
function _define_property$1(obj, key, value) {
|
|
184
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
185
|
+
value,
|
|
186
|
+
enumerable: true,
|
|
187
|
+
configurable: true,
|
|
188
|
+
writable: true
|
|
189
|
+
});
|
|
190
|
+
else obj[key] = value;
|
|
191
|
+
return obj;
|
|
192
|
+
}
|
|
193
|
+
function utcDatetimeToEpochMs(value) {
|
|
194
|
+
if (typeof value === "number") return value;
|
|
195
|
+
if (value instanceof Date) return value.getTime();
|
|
196
|
+
if (typeof value === "string") {
|
|
197
|
+
const ms = Date.UTC(+value.slice(0, 4), +value.slice(5, 7) - 1, +value.slice(8, 10), +value.slice(11, 13), +value.slice(14, 16), +value.slice(17, 19));
|
|
198
|
+
return Number.isNaN(ms) ? value : ms;
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
/** Formats epoch ms as 'YYYY-MM-DD HH:MM:SS' in UTC for MySQL TIMESTAMP columns. */ function epochMsToUtcDatetime(ms) {
|
|
203
|
+
const d = new Date(ms);
|
|
204
|
+
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}:${String(d.getUTCSeconds()).padStart(2, "0")}`;
|
|
205
|
+
}
|
|
206
|
+
var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
207
|
+
/** Schema name for INFORMATION_SCHEMA queries (null falls through to DATABASE()). */ get _schema() {
|
|
208
|
+
return this._table.schema ?? null;
|
|
209
|
+
}
|
|
210
|
+
async _beginTransaction() {
|
|
211
|
+
const conn = await this.driver.getConnection();
|
|
212
|
+
await conn.exec("START TRANSACTION");
|
|
213
|
+
this._log("START TRANSACTION");
|
|
214
|
+
return conn;
|
|
215
|
+
}
|
|
216
|
+
async _commitTransaction(state) {
|
|
217
|
+
const conn = state;
|
|
218
|
+
try {
|
|
219
|
+
this._log("COMMIT");
|
|
220
|
+
await conn.exec("COMMIT");
|
|
221
|
+
} finally {
|
|
222
|
+
conn.release();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async _rollbackTransaction(state) {
|
|
226
|
+
const conn = state;
|
|
227
|
+
try {
|
|
228
|
+
this._log("ROLLBACK");
|
|
229
|
+
await conn.exec("ROLLBACK");
|
|
230
|
+
} finally {
|
|
231
|
+
conn.release();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Returns the active executor: dedicated connection if inside a transaction,
|
|
236
|
+
* otherwise the pool-based driver.
|
|
237
|
+
*/ _exec() {
|
|
238
|
+
const txState = this._getTransactionState();
|
|
239
|
+
return txState ?? this.driver;
|
|
240
|
+
}
|
|
241
|
+
/** MySQL InnoDB enforces FK constraints natively. */ supportsNativeForeignKeys() {
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
prepareId(id, _fieldType) {
|
|
245
|
+
return id;
|
|
246
|
+
}
|
|
247
|
+
supportsNativeValueDefaults() {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
nativeDefaultFns() {
|
|
251
|
+
return MysqlAdapter.NATIVE_DEFAULT_FNS;
|
|
252
|
+
}
|
|
253
|
+
onBeforeFlatten(type) {
|
|
254
|
+
const meta = type.metadata;
|
|
255
|
+
const engine = meta.get("db.mysql.engine");
|
|
256
|
+
if (engine) this._engine = engine;
|
|
257
|
+
const charset = meta.get("db.mysql.charset");
|
|
258
|
+
if (charset) this._charset = charset;
|
|
259
|
+
const collate = meta.get("db.mysql.collate");
|
|
260
|
+
if (collate) this._collation = collate;
|
|
261
|
+
}
|
|
262
|
+
onFieldScanned(field, _type, metadata) {
|
|
263
|
+
if (metadata.has("db.default.increment")) {
|
|
264
|
+
this._incrementFields.add(field);
|
|
265
|
+
const startVal = metadata.get("db.default.increment");
|
|
266
|
+
if (typeof startVal === "number") this._autoIncrementStart = startVal;
|
|
267
|
+
}
|
|
268
|
+
const onUpdate = metadata.get("db.mysql.onUpdate");
|
|
269
|
+
if (onUpdate) this._onUpdateFields.set(field, onUpdate);
|
|
270
|
+
const vectorMeta = metadata.get("db.search.vector");
|
|
271
|
+
if (vectorMeta) {
|
|
272
|
+
const indexName = vectorMeta.indexName || field;
|
|
273
|
+
this._vectorFields.set(field, {
|
|
274
|
+
dimensions: vectorMeta.dimensions,
|
|
275
|
+
similarity: vectorMeta.similarity || "cosine",
|
|
276
|
+
indexName
|
|
277
|
+
});
|
|
278
|
+
const threshold = metadata.get("db.search.vector.threshold");
|
|
279
|
+
if (threshold !== undefined) this._vectorThresholds.set(indexName, threshold);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
getDesiredTableOptions() {
|
|
283
|
+
return [
|
|
284
|
+
{
|
|
285
|
+
key: "engine",
|
|
286
|
+
value: this._engine
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
key: "charset",
|
|
290
|
+
value: this._charset
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
key: "collation",
|
|
294
|
+
value: this._collation
|
|
295
|
+
}
|
|
296
|
+
];
|
|
297
|
+
}
|
|
298
|
+
async getExistingTableOptions() {
|
|
299
|
+
const row = await this._exec().get(`SELECT ENGINE, TABLE_COLLATION
|
|
300
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
301
|
+
WHERE TABLE_NAME = ? AND TABLE_SCHEMA = COALESCE(?, DATABASE())`, [this._table.tableName, this._schema]);
|
|
302
|
+
if (!row) return [];
|
|
303
|
+
const charset = row.TABLE_COLLATION?.split("_")[0] ?? "utf8mb4";
|
|
304
|
+
return [
|
|
305
|
+
{
|
|
306
|
+
key: "engine",
|
|
307
|
+
value: row.ENGINE ?? "InnoDB"
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
key: "charset",
|
|
311
|
+
value: charset
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
key: "collation",
|
|
315
|
+
value: row.TABLE_COLLATION ?? "utf8mb4_unicode_ci"
|
|
316
|
+
}
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
async applyTableOptions(changes) {
|
|
320
|
+
const tableName = this.resolveTableName();
|
|
321
|
+
const clauses = [];
|
|
322
|
+
for (const change of changes) switch (change.key) {
|
|
323
|
+
case "engine": {
|
|
324
|
+
clauses.push(`ENGINE = ${change.newValue}`);
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
case "charset": {
|
|
328
|
+
clauses.push(`CHARACTER SET = ${change.newValue}`);
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
case "collation": {
|
|
332
|
+
clauses.push(`COLLATE = ${change.newValue}`);
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (clauses.length > 0) {
|
|
337
|
+
const ddl = `ALTER TABLE ${quoteTableName(tableName)} ${clauses.join(", ")}`;
|
|
338
|
+
this._log(ddl);
|
|
339
|
+
await this._exec().exec(ddl);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Returns a value formatter for TIMESTAMP-mapped fields.
|
|
344
|
+
* Number fields with @db.default.now map to MySQL TIMESTAMP — the formatter
|
|
345
|
+
* converts epoch ms to a UTC datetime string for the wire protocol.
|
|
346
|
+
*/ formatValue(field) {
|
|
347
|
+
if (field.designType === "number" && field.defaultValue?.kind === "fn" && field.defaultValue.fn === "now") return {
|
|
348
|
+
toStorage: (value) => typeof value === "number" ? epochMsToUtcDatetime(value) : value,
|
|
349
|
+
fromStorage: utcDatetimeToEpochMs
|
|
350
|
+
};
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Wraps an async write operation to catch MySQL constraint errors
|
|
355
|
+
* and rethrow as structured `DbError`.
|
|
356
|
+
*
|
|
357
|
+
* MySQL uses numeric error codes:
|
|
358
|
+
* - 1062 = ER_DUP_ENTRY (unique constraint violation)
|
|
359
|
+
* - 1451 = ER_ROW_IS_REFERENCED_2 (FK violation on delete)
|
|
360
|
+
* - 1452 = ER_NO_REFERENCED_ROW_2 (FK violation on insert/update)
|
|
361
|
+
*/ async _wrapConstraintError(fn) {
|
|
362
|
+
try {
|
|
363
|
+
return await fn();
|
|
364
|
+
} catch (error) {
|
|
365
|
+
if (error && typeof error === "object" && "errno" in error) {
|
|
366
|
+
const err = error;
|
|
367
|
+
if (err.errno === 1062) {
|
|
368
|
+
const match = err.message?.match(/for key '(?:\w+\.)?(\w+)'/);
|
|
369
|
+
const field = match?.[1] ?? "";
|
|
370
|
+
throw new DbError("CONFLICT", [{
|
|
371
|
+
path: field,
|
|
372
|
+
message: err.sqlMessage ?? err.message
|
|
373
|
+
}]);
|
|
374
|
+
}
|
|
375
|
+
if (err.errno === 1451 || err.errno === 1452) {
|
|
376
|
+
const errors = this._mapFkError(err.message);
|
|
377
|
+
throw new DbError("FK_VIOLATION", errors);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
_mapFkError(message) {
|
|
384
|
+
const fkMatch = message.match(/FOREIGN KEY \(`(\w+)`\)/);
|
|
385
|
+
if (fkMatch) {
|
|
386
|
+
const physicalCol = fkMatch[1];
|
|
387
|
+
const field = this._table.fieldDescriptors.find((f) => f.physicalName === physicalCol);
|
|
388
|
+
return [{
|
|
389
|
+
path: field?.path ?? physicalCol,
|
|
390
|
+
message
|
|
391
|
+
}];
|
|
392
|
+
}
|
|
393
|
+
return [{
|
|
394
|
+
path: "",
|
|
395
|
+
message
|
|
396
|
+
}];
|
|
397
|
+
}
|
|
398
|
+
async insertOne(data) {
|
|
399
|
+
const { sql, params } = buildInsert$1(this.resolveTableName(), data);
|
|
400
|
+
this._log(sql, params);
|
|
401
|
+
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
402
|
+
return { insertedId: this._resolveInsertedId(data, result.insertId) };
|
|
403
|
+
}
|
|
404
|
+
async insertMany(data) {
|
|
405
|
+
if (data.length === 0) return {
|
|
406
|
+
insertedCount: 0,
|
|
407
|
+
insertedIds: []
|
|
408
|
+
};
|
|
409
|
+
return this.withTransaction(async () => {
|
|
410
|
+
const tableName = this.resolveTableName();
|
|
411
|
+
const keys = Object.keys(data[0]);
|
|
412
|
+
const colsClause = keys.map((k) => qi(k)).join(", ");
|
|
413
|
+
const paramsPerRow = keys.length;
|
|
414
|
+
const maxRowsPerBatch = paramsPerRow > 0 ? Math.floor(6e4 / paramsPerRow) : data.length;
|
|
415
|
+
const allIds = [];
|
|
416
|
+
const rowPlaceholderClause = `(${keys.map(() => "?").join(", ")})`;
|
|
417
|
+
for (let offset = 0; offset < data.length; offset += maxRowsPerBatch) {
|
|
418
|
+
const batchEnd = Math.min(offset + maxRowsPerBatch, data.length);
|
|
419
|
+
const batchSize = batchEnd - offset;
|
|
420
|
+
const params = [];
|
|
421
|
+
for (let i = offset; i < batchEnd; i++) {
|
|
422
|
+
const row = data[i];
|
|
423
|
+
for (const k of keys) params.push(mysqlDialect.toValue(row[k]));
|
|
424
|
+
}
|
|
425
|
+
const valuesClause = Array(batchSize).fill(rowPlaceholderClause).join(", ");
|
|
426
|
+
const sql = `INSERT INTO ${quoteTableName(tableName)} (${colsClause}) VALUES ${valuesClause}`;
|
|
427
|
+
this._log(sql, params);
|
|
428
|
+
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
429
|
+
const firstId = Number(result.insertId);
|
|
430
|
+
for (let i = 0; i < batchSize; i++) allIds.push(this._resolveInsertedId(data[offset + i], firstId > 0 ? firstId + i : 0));
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
insertedCount: allIds.length,
|
|
434
|
+
insertedIds: allIds
|
|
435
|
+
};
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
async findOne(query) {
|
|
439
|
+
const where = buildWhere(query.filter);
|
|
440
|
+
const controls = {
|
|
441
|
+
...query.controls,
|
|
442
|
+
$limit: 1
|
|
443
|
+
};
|
|
444
|
+
const { sql, params } = buildSelect$1(this.resolveTableName(), where, controls);
|
|
445
|
+
this._log(sql, params);
|
|
446
|
+
return this._exec().get(sql, params);
|
|
447
|
+
}
|
|
448
|
+
async findMany(query) {
|
|
449
|
+
const where = buildWhere(query.filter);
|
|
450
|
+
const { sql, params } = buildSelect$1(this.resolveTableName(), where, query.controls);
|
|
451
|
+
this._log(sql, params);
|
|
452
|
+
return this._exec().all(sql, params);
|
|
453
|
+
}
|
|
454
|
+
async count(query) {
|
|
455
|
+
const where = buildWhere(query.filter);
|
|
456
|
+
const tableName = this.resolveTableName();
|
|
457
|
+
const sql = `SELECT COUNT(*) as cnt FROM ${quoteTableName(tableName)} WHERE ${where.sql}`;
|
|
458
|
+
this._log(sql, where.params);
|
|
459
|
+
const row = await this._exec().get(sql, where.params);
|
|
460
|
+
return row?.cnt ?? 0;
|
|
461
|
+
}
|
|
462
|
+
async aggregate(query) {
|
|
463
|
+
const where = buildWhere(query.filter);
|
|
464
|
+
const tableName = this.resolveTableName();
|
|
465
|
+
if (query.controls.$count) {
|
|
466
|
+
const { sql: sql$1, params: params$1 } = buildAggregateCount$1(tableName, where, query.controls);
|
|
467
|
+
this._log(sql$1, params$1);
|
|
468
|
+
const row = await this._exec().get(sql$1, params$1);
|
|
469
|
+
return [{ count: row?.count ?? 0 }];
|
|
470
|
+
}
|
|
471
|
+
const { sql, params } = buildAggregateSelect$1(tableName, where, query.controls);
|
|
472
|
+
this._log(sql, params);
|
|
473
|
+
return this._exec().all(sql, params);
|
|
474
|
+
}
|
|
475
|
+
async updateOne(filter, data) {
|
|
476
|
+
const where = buildWhere(filter);
|
|
477
|
+
const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, 1);
|
|
478
|
+
this._log(sql, params);
|
|
479
|
+
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
480
|
+
return {
|
|
481
|
+
matchedCount: result.affectedRows,
|
|
482
|
+
modifiedCount: result.changedRows
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
async updateMany(filter, data) {
|
|
486
|
+
const where = buildWhere(filter);
|
|
487
|
+
const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where);
|
|
488
|
+
this._log(sql, params);
|
|
489
|
+
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
490
|
+
return {
|
|
491
|
+
matchedCount: result.affectedRows,
|
|
492
|
+
modifiedCount: result.changedRows
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
async replaceOne(filter, data) {
|
|
496
|
+
const where = buildWhere(filter);
|
|
497
|
+
const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, 1);
|
|
498
|
+
this._log(sql, params);
|
|
499
|
+
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
500
|
+
return {
|
|
501
|
+
matchedCount: result.affectedRows,
|
|
502
|
+
modifiedCount: result.changedRows
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
async replaceMany(filter, data) {
|
|
506
|
+
const where = buildWhere(filter);
|
|
507
|
+
const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where);
|
|
508
|
+
this._log(sql, params);
|
|
509
|
+
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
510
|
+
return {
|
|
511
|
+
matchedCount: result.affectedRows,
|
|
512
|
+
modifiedCount: result.changedRows
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
async deleteOne(filter) {
|
|
516
|
+
const where = buildWhere(filter);
|
|
517
|
+
const { sql, params } = buildDelete$1(this.resolveTableName(), where, 1);
|
|
518
|
+
this._log(sql, params);
|
|
519
|
+
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
520
|
+
return { deletedCount: result.affectedRows };
|
|
521
|
+
}
|
|
522
|
+
async deleteMany(filter) {
|
|
523
|
+
const where = buildWhere(filter);
|
|
524
|
+
const { sql, params } = buildDelete$1(this.resolveTableName(), where);
|
|
525
|
+
this._log(sql, params);
|
|
526
|
+
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
527
|
+
return { deletedCount: result.affectedRows };
|
|
528
|
+
}
|
|
529
|
+
async ensureTable() {
|
|
530
|
+
if (this._supportsVector === undefined && this._vectorFields.size > 0) await this._detectVectorSupport();
|
|
531
|
+
if (this._table instanceof AtscriptDbView) return this._ensureView();
|
|
532
|
+
const sql = buildCreateTable(this.resolveTableName(), this._table.fieldDescriptors, this._table.foreignKeys, {
|
|
533
|
+
engine: this._engine,
|
|
534
|
+
charset: this._charset,
|
|
535
|
+
collation: this._collation,
|
|
536
|
+
autoIncrementStart: this._autoIncrementStart,
|
|
537
|
+
incrementFields: this._incrementFields,
|
|
538
|
+
onUpdateFields: this._onUpdateFields
|
|
539
|
+
});
|
|
540
|
+
this._log(sql);
|
|
541
|
+
await this._exec().exec(sql);
|
|
542
|
+
}
|
|
543
|
+
async _ensureView() {
|
|
544
|
+
const view = this._table;
|
|
545
|
+
const sql = buildCreateView$1(this.resolveTableName(), view.viewPlan, view.getViewColumnMappings(), (ref) => view.resolveFieldRef(ref, qi));
|
|
546
|
+
this._log(sql);
|
|
547
|
+
await this._exec().exec(sql);
|
|
548
|
+
}
|
|
549
|
+
async getExistingColumns() {
|
|
550
|
+
return this.getExistingColumnsForTable(this._table.tableName);
|
|
551
|
+
}
|
|
552
|
+
async getExistingColumnsForTable(tableName) {
|
|
553
|
+
const schema = this._schema;
|
|
554
|
+
const rows = await this._exec().all(`SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_DEFAULT
|
|
555
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
556
|
+
WHERE TABLE_NAME = ? AND TABLE_SCHEMA = COALESCE(?, DATABASE())
|
|
557
|
+
ORDER BY ORDINAL_POSITION`, [tableName, schema]);
|
|
558
|
+
return rows.map((r) => ({
|
|
559
|
+
name: r.COLUMN_NAME,
|
|
560
|
+
type: r.COLUMN_TYPE.toUpperCase(),
|
|
561
|
+
notnull: r.IS_NULLABLE === "NO",
|
|
562
|
+
pk: r.COLUMN_KEY === "PRI",
|
|
563
|
+
dflt_value: normalizeMysqlDefault(r.COLUMN_DEFAULT)
|
|
564
|
+
}));
|
|
565
|
+
}
|
|
566
|
+
async syncColumns(diff) {
|
|
567
|
+
const tableName = this.resolveTableName();
|
|
568
|
+
const added = [];
|
|
569
|
+
const renamed = [];
|
|
570
|
+
for (const { field, oldName } of diff.renamed ?? []) {
|
|
571
|
+
const ddl = `ALTER TABLE ${quoteTableName(tableName)} RENAME COLUMN ${qi(oldName)} TO ${qi(field.physicalName)}`;
|
|
572
|
+
this._log(ddl);
|
|
573
|
+
await this._exec().exec(ddl);
|
|
574
|
+
renamed.push(field.physicalName);
|
|
575
|
+
}
|
|
576
|
+
for (const field of diff.added) {
|
|
577
|
+
const sqlType = this.typeMapper(field);
|
|
578
|
+
let ddl = `ALTER TABLE ${quoteTableName(tableName)} ADD COLUMN ${qi(field.physicalName)} ${sqlType}`;
|
|
579
|
+
if (!field.optional && !field.isPrimaryKey) ddl += " NOT NULL";
|
|
580
|
+
if (field.defaultValue?.kind === "value") ddl += ` DEFAULT ${defaultValueToSqlLiteral(field.designType, field.defaultValue.value)}`;
|
|
581
|
+
else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValueForType(field.designType)}`;
|
|
582
|
+
if (field.collate) {
|
|
583
|
+
const nativeCollate = field.type?.metadata?.get("db.mysql.collate");
|
|
584
|
+
ddl += ` COLLATE ${nativeCollate ?? collationToMysql(field.collate)}`;
|
|
585
|
+
}
|
|
586
|
+
this._log(ddl);
|
|
587
|
+
await this._exec().exec(ddl);
|
|
588
|
+
added.push(field.physicalName);
|
|
589
|
+
}
|
|
590
|
+
for (const { field } of diff.typeChanged ?? []) {
|
|
591
|
+
const sqlType = this.typeMapper(field);
|
|
592
|
+
let ddl = `ALTER TABLE ${quoteTableName(tableName)} MODIFY COLUMN ${qi(field.physicalName)} ${sqlType}`;
|
|
593
|
+
if (!field.optional && !field.isPrimaryKey) ddl += " NOT NULL";
|
|
594
|
+
this._log(ddl);
|
|
595
|
+
await this._exec().exec(ddl);
|
|
596
|
+
}
|
|
597
|
+
for (const { field } of diff.nullableChanged ?? []) {
|
|
598
|
+
const sqlType = this.typeMapper(field);
|
|
599
|
+
const nullability = field.optional ? "NULL" : "NOT NULL";
|
|
600
|
+
const ddl = `ALTER TABLE ${quoteTableName(tableName)} MODIFY COLUMN ${qi(field.physicalName)} ${sqlType} ${nullability}`;
|
|
601
|
+
this._log(ddl);
|
|
602
|
+
await this._exec().exec(ddl);
|
|
603
|
+
}
|
|
604
|
+
for (const { field } of diff.defaultChanged ?? []) {
|
|
605
|
+
const sqlType = this.typeMapper(field);
|
|
606
|
+
let ddl = `ALTER TABLE ${quoteTableName(tableName)} MODIFY COLUMN ${qi(field.physicalName)} ${sqlType}`;
|
|
607
|
+
if (!field.optional && !field.isPrimaryKey) ddl += " NOT NULL";
|
|
608
|
+
if (field.defaultValue?.kind === "value") ddl += ` DEFAULT ${defaultValueToSqlLiteral(field.designType, field.defaultValue.value)}`;
|
|
609
|
+
else if (field.defaultValue?.kind === "fn") ddl += ` DEFAULT ${field.defaultValue.fn === "now" ? "CURRENT_TIMESTAMP" : field.defaultValue.fn === "uuid" ? "(UUID())" : `(${field.defaultValue.fn}())`}`;
|
|
610
|
+
else ddl += " DEFAULT NULL";
|
|
611
|
+
this._log(ddl);
|
|
612
|
+
await this._exec().exec(ddl);
|
|
613
|
+
}
|
|
614
|
+
return {
|
|
615
|
+
added,
|
|
616
|
+
renamed
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
async recreateTable() {
|
|
620
|
+
const tableName = this.resolveTableName();
|
|
621
|
+
const tempName = `${this._table.tableName}__tmp_${Date.now()}`;
|
|
622
|
+
await this._exec().exec("SET FOREIGN_KEY_CHECKS = 0");
|
|
623
|
+
try {
|
|
624
|
+
const createSql = buildCreateTable(tempName, this._table.fieldDescriptors, this._table.foreignKeys, {
|
|
625
|
+
engine: this._engine,
|
|
626
|
+
charset: this._charset,
|
|
627
|
+
collation: this._collation,
|
|
628
|
+
autoIncrementStart: this._autoIncrementStart,
|
|
629
|
+
incrementFields: this._incrementFields,
|
|
630
|
+
onUpdateFields: this._onUpdateFields
|
|
631
|
+
});
|
|
632
|
+
this._log(createSql);
|
|
633
|
+
await this._exec().exec(createSql);
|
|
634
|
+
const oldCols = (await this.getExistingColumns()).map((c) => c.name);
|
|
635
|
+
const newCols = this._table.fieldDescriptors.filter((f) => !f.ignored).map((f) => f.physicalName);
|
|
636
|
+
const oldColSet = new Set(oldCols);
|
|
637
|
+
const commonCols = newCols.filter((c) => oldColSet.has(c));
|
|
638
|
+
if (commonCols.length > 0) {
|
|
639
|
+
const fieldsByName = new Map(this._table.fieldDescriptors.map((f) => [f.physicalName, f]));
|
|
640
|
+
const colNames = commonCols.map((c) => qi(c)).join(", ");
|
|
641
|
+
const selectExprs = commonCols.map((c) => {
|
|
642
|
+
const field = fieldsByName.get(c);
|
|
643
|
+
if (field && !field.optional && !field.isPrimaryKey) {
|
|
644
|
+
const fallback = field.defaultValue?.kind === "value" ? defaultValueToSqlLiteral(field.designType, field.defaultValue.value) : defaultValueForType(field.designType);
|
|
645
|
+
return `COALESCE(${qi(c)}, ${fallback}) AS ${qi(c)}`;
|
|
646
|
+
}
|
|
647
|
+
return qi(c);
|
|
648
|
+
}).join(", ");
|
|
649
|
+
const copySql = `INSERT INTO ${qi(tempName)} (${colNames}) SELECT ${selectExprs} FROM ${quoteTableName(tableName)}`;
|
|
650
|
+
this._log(copySql);
|
|
651
|
+
await this._exec().exec(copySql);
|
|
652
|
+
}
|
|
653
|
+
await this._exec().exec(`DROP TABLE IF EXISTS ${quoteTableName(tableName)}`);
|
|
654
|
+
await this._exec().exec(`RENAME TABLE ${qi(tempName)} TO ${quoteTableName(tableName)}`);
|
|
655
|
+
} finally {
|
|
656
|
+
await this._exec().exec("SET FOREIGN_KEY_CHECKS = 1");
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
async dropTable() {
|
|
660
|
+
const ddl = `DROP TABLE IF EXISTS ${quoteTableName(this.resolveTableName())}`;
|
|
661
|
+
this._log(ddl);
|
|
662
|
+
const conn = await this.driver.getConnection();
|
|
663
|
+
await conn.exec("SET FOREIGN_KEY_CHECKS = 0");
|
|
664
|
+
try {
|
|
665
|
+
await conn.exec(ddl);
|
|
666
|
+
} finally {
|
|
667
|
+
await conn.exec("SET FOREIGN_KEY_CHECKS = 1");
|
|
668
|
+
conn.release();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async dropColumns(columns) {
|
|
672
|
+
const tableName = this.resolveTableName();
|
|
673
|
+
const drops = columns.map((col) => `DROP COLUMN ${qi(col)}`).join(", ");
|
|
674
|
+
const ddl = `ALTER TABLE ${quoteTableName(tableName)} ${drops}`;
|
|
675
|
+
this._log(ddl);
|
|
676
|
+
await this._exec().exec(ddl);
|
|
677
|
+
}
|
|
678
|
+
async dropTableByName(tableName) {
|
|
679
|
+
const ddl = `DROP TABLE IF EXISTS ${quoteTableName(tableName)}`;
|
|
680
|
+
this._log(ddl);
|
|
681
|
+
const conn = await this.driver.getConnection();
|
|
682
|
+
await conn.exec("SET FOREIGN_KEY_CHECKS = 0");
|
|
683
|
+
try {
|
|
684
|
+
await conn.exec(ddl);
|
|
685
|
+
} finally {
|
|
686
|
+
await conn.exec("SET FOREIGN_KEY_CHECKS = 1");
|
|
687
|
+
conn.release();
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
async dropViewByName(viewName) {
|
|
691
|
+
const ddl = `DROP VIEW IF EXISTS ${quoteTableName(viewName)}`;
|
|
692
|
+
this._log(ddl);
|
|
693
|
+
await this._exec().exec(ddl);
|
|
694
|
+
}
|
|
695
|
+
async renameTable(oldName) {
|
|
696
|
+
const newName = this.resolveTableName();
|
|
697
|
+
const ddl = `RENAME TABLE ${quoteTableName(oldName)} TO ${quoteTableName(newName)}`;
|
|
698
|
+
this._log(ddl);
|
|
699
|
+
await this._exec().exec(ddl);
|
|
700
|
+
}
|
|
701
|
+
typeMapper(field) {
|
|
702
|
+
if (this._vectorFields.has(field.path)) {
|
|
703
|
+
const vec = this._vectorFields.get(field.path);
|
|
704
|
+
return this._supportsVector ? `VECTOR(${vec.dimensions})` : "JSON";
|
|
705
|
+
}
|
|
706
|
+
return mysqlTypeFromField(field);
|
|
707
|
+
}
|
|
708
|
+
async syncIndexes() {
|
|
709
|
+
const tableName = this._table.tableName;
|
|
710
|
+
const schema = this._schema;
|
|
711
|
+
const stringFields = new Set(this._table.fieldDescriptors.filter((f) => f.designType === "string").map((f) => f.physicalName));
|
|
712
|
+
await this.syncIndexesWithDiff({
|
|
713
|
+
listExisting: async () => this._exec().all(`SELECT DISTINCT INDEX_NAME as name FROM INFORMATION_SCHEMA.STATISTICS
|
|
714
|
+
WHERE TABLE_NAME = ? AND TABLE_SCHEMA = COALESCE(?, DATABASE())`, [tableName, schema]),
|
|
715
|
+
createIndex: async (index) => {
|
|
716
|
+
const unique = index.type === "unique" ? "UNIQUE " : "";
|
|
717
|
+
const fulltext = index.type === "fulltext" ? "FULLTEXT " : "";
|
|
718
|
+
const isFulltext = index.type === "fulltext";
|
|
719
|
+
const cols = index.fields.map((f) => {
|
|
720
|
+
const col = qi(f.name);
|
|
721
|
+
const prefix = !isFulltext && stringFields.has(f.name) ? "(255)" : "";
|
|
722
|
+
const order = isFulltext ? "" : ` ${f.sort === "desc" ? "DESC" : "ASC"}`;
|
|
723
|
+
return `${col}${prefix}${order}`;
|
|
724
|
+
}).join(", ");
|
|
725
|
+
const sql = `CREATE ${fulltext}${unique}INDEX ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} (${cols})`;
|
|
726
|
+
this._log(sql);
|
|
727
|
+
await this._exec().exec(sql);
|
|
728
|
+
},
|
|
729
|
+
dropIndex: async (name) => {
|
|
730
|
+
const sql = `DROP INDEX ${qi(name)} ON ${quoteTableName(this.resolveTableName())}`;
|
|
731
|
+
this._log(sql);
|
|
732
|
+
await this._exec().exec(sql);
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
async syncForeignKeys() {
|
|
737
|
+
const existingByName = await this._getExistingFkConstraints();
|
|
738
|
+
const desiredFkKeys = new Set();
|
|
739
|
+
for (const fk of this._table.foreignKeys.values()) desiredFkKeys.add([...fk.fields].sort().join(","));
|
|
740
|
+
for (const [constraintName, columns] of existingByName) {
|
|
741
|
+
const key = columns.sort().join(",");
|
|
742
|
+
if (!desiredFkKeys.has(key)) {
|
|
743
|
+
const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} DROP FOREIGN KEY ${qi(constraintName)}`;
|
|
744
|
+
this._log(ddl);
|
|
745
|
+
await this._exec().exec(ddl);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
const existingKeys = new Set([...existingByName.values()].map((cols) => cols.sort().join(",")));
|
|
749
|
+
for (const fk of this._table.foreignKeys.values()) {
|
|
750
|
+
const key = [...fk.fields].sort().join(",");
|
|
751
|
+
if (!existingKeys.has(key)) {
|
|
752
|
+
const localCols = fk.fields.map((f) => qi(f)).join(", ");
|
|
753
|
+
const targetCols = fk.targetFields.map((f) => qi(f)).join(", ");
|
|
754
|
+
let ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD FOREIGN KEY (${localCols}) REFERENCES ${qi(fk.targetTable)} (${targetCols})`;
|
|
755
|
+
if (fk.onDelete) ddl += ` ON DELETE ${refActionToSql(fk.onDelete)}`;
|
|
756
|
+
if (fk.onUpdate) ddl += ` ON UPDATE ${refActionToSql(fk.onUpdate)}`;
|
|
757
|
+
this._log(ddl);
|
|
758
|
+
await this._exec().exec(ddl);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
async dropForeignKeys(fkFieldKeys) {
|
|
763
|
+
if (fkFieldKeys.length === 0) return;
|
|
764
|
+
const keySet = new Set(fkFieldKeys);
|
|
765
|
+
const existingByName = await this._getExistingFkConstraints();
|
|
766
|
+
for (const [constraintName, cols] of existingByName) {
|
|
767
|
+
const key = cols.sort().join(",");
|
|
768
|
+
if (keySet.has(key)) {
|
|
769
|
+
const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} DROP FOREIGN KEY ${qi(constraintName)}`;
|
|
770
|
+
this._log(ddl);
|
|
771
|
+
await this._exec().exec(ddl);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/** Queries INFORMATION_SCHEMA for existing FK constraints, grouped by constraint name → column names. */ async _getExistingFkConstraints() {
|
|
776
|
+
const rows = await this._exec().all(`SELECT kcu.CONSTRAINT_NAME, kcu.COLUMN_NAME
|
|
777
|
+
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
|
778
|
+
WHERE kcu.TABLE_NAME = ? AND kcu.TABLE_SCHEMA = COALESCE(?, DATABASE())
|
|
779
|
+
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL`, [this._table.tableName, this._schema]);
|
|
780
|
+
const byName = new Map();
|
|
781
|
+
for (const row of rows) {
|
|
782
|
+
let cols = byName.get(row.CONSTRAINT_NAME);
|
|
783
|
+
if (!cols) {
|
|
784
|
+
cols = [];
|
|
785
|
+
byName.set(row.CONSTRAINT_NAME, cols);
|
|
786
|
+
}
|
|
787
|
+
cols.push(row.COLUMN_NAME);
|
|
788
|
+
}
|
|
789
|
+
return byName;
|
|
790
|
+
}
|
|
791
|
+
getSearchIndexes() {
|
|
792
|
+
const indexes = [];
|
|
793
|
+
for (const index of this._table.indexes.values()) if (index.type === "fulltext") indexes.push({
|
|
794
|
+
name: index.key,
|
|
795
|
+
description: `FULLTEXT index on ${index.fields.map((f) => f.name).join(", ")}`,
|
|
796
|
+
type: "text"
|
|
797
|
+
});
|
|
798
|
+
for (const [field, vec] of this._vectorFields) indexes.push({
|
|
799
|
+
name: vec.indexName,
|
|
800
|
+
description: `VECTOR(${vec.dimensions}) on ${field}, ${vec.similarity}`,
|
|
801
|
+
type: "vector"
|
|
802
|
+
});
|
|
803
|
+
return indexes;
|
|
804
|
+
}
|
|
805
|
+
async search(text, query, indexName) {
|
|
806
|
+
const combinedWhere = this._buildSearchWhere(text, query, indexName);
|
|
807
|
+
const { sql, params } = buildSelect$1(this.resolveTableName(), combinedWhere, query.controls);
|
|
808
|
+
this._log(sql, params);
|
|
809
|
+
return this._exec().all(sql, params);
|
|
810
|
+
}
|
|
811
|
+
async searchWithCount(text, query, indexName) {
|
|
812
|
+
const combinedWhere = this._buildSearchWhere(text, query, indexName);
|
|
813
|
+
const tableName = this.resolveTableName();
|
|
814
|
+
const selectPromise = (async () => {
|
|
815
|
+
const { sql, params } = buildSelect$1(tableName, combinedWhere, query.controls);
|
|
816
|
+
this._log(sql, params);
|
|
817
|
+
return this._exec().all(sql, params);
|
|
818
|
+
})();
|
|
819
|
+
const countPromise = (async () => {
|
|
820
|
+
const sql = `SELECT COUNT(*) as cnt FROM ${quoteTableName(tableName)} WHERE ${combinedWhere.sql}`;
|
|
821
|
+
this._log(sql, combinedWhere.params);
|
|
822
|
+
const row = await this._exec().get(sql, combinedWhere.params);
|
|
823
|
+
return row?.cnt ?? 0;
|
|
824
|
+
})();
|
|
825
|
+
const [data, count] = await Promise.all([selectPromise, countPromise]);
|
|
826
|
+
return {
|
|
827
|
+
data,
|
|
828
|
+
count
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
_buildSearchWhere(text, query, indexName) {
|
|
832
|
+
const fulltextIndex = this._getFulltextIndex(indexName);
|
|
833
|
+
if (!fulltextIndex) throw new Error("No FULLTEXT index found for search");
|
|
834
|
+
const matchCols = fulltextIndex.fields.map((f) => qi(f.name)).join(", ");
|
|
835
|
+
const where = buildWhere(query.filter);
|
|
836
|
+
const matchClause = `MATCH(${matchCols}) AGAINST(? IN NATURAL LANGUAGE MODE)`;
|
|
837
|
+
return {
|
|
838
|
+
sql: where.sql === "1=1" ? matchClause : `${where.sql} AND ${matchClause}`,
|
|
839
|
+
params: [...where.params, text]
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
_getFulltextIndex(indexName) {
|
|
843
|
+
for (const index of this._table.indexes.values()) if (index.type === "fulltext") {
|
|
844
|
+
if (!indexName || index.key === indexName) return index;
|
|
845
|
+
}
|
|
846
|
+
return undefined;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Detects native VECTOR type support by inspecting the server version.
|
|
850
|
+
* MySQL 9.0+ supports the VECTOR column type natively.
|
|
851
|
+
* Caches the result for the lifetime of this adapter instance.
|
|
852
|
+
*/ async _detectVectorSupport() {
|
|
853
|
+
if (this._supportsVector !== undefined) return this._supportsVector;
|
|
854
|
+
try {
|
|
855
|
+
const row = await this.driver.get("SELECT VERSION() as v", []);
|
|
856
|
+
if (row?.v) {
|
|
857
|
+
const major = Number.parseInt(row.v, 10);
|
|
858
|
+
this._supportsVector = !Number.isNaN(major) && major >= 9;
|
|
859
|
+
} else this._supportsVector = false;
|
|
860
|
+
} catch {
|
|
861
|
+
this._supportsVector = false;
|
|
862
|
+
}
|
|
863
|
+
return this._supportsVector;
|
|
864
|
+
}
|
|
865
|
+
isVectorSearchable() {
|
|
866
|
+
return this._supportsVector === true && this._vectorFields.size > 0;
|
|
867
|
+
}
|
|
868
|
+
async vectorSearch(vector, query, indexName) {
|
|
869
|
+
await this._detectVectorSupport();
|
|
870
|
+
if (!this._supportsVector) throw new Error("Vector search requires MySQL 9.0+");
|
|
871
|
+
const { sql, params } = this._buildVectorSearchQuery(vector, query, indexName);
|
|
872
|
+
this._log(sql, params);
|
|
873
|
+
return this._exec().all(sql, params);
|
|
874
|
+
}
|
|
875
|
+
async vectorSearchWithCount(vector, query, indexName) {
|
|
876
|
+
await this._detectVectorSupport();
|
|
877
|
+
if (!this._supportsVector) throw new Error("Vector search requires MySQL 9.0+");
|
|
878
|
+
const { sql, params } = this._buildVectorSearchQuery(vector, query, indexName);
|
|
879
|
+
const { sql: countSql, params: countParams } = this._buildVectorSearchCountQuery(vector, query, indexName);
|
|
880
|
+
this._log(sql, params);
|
|
881
|
+
this._log(countSql, countParams);
|
|
882
|
+
const [data, countRow] = await Promise.all([this._exec().all(sql, params), this._exec().get(countSql, countParams)]);
|
|
883
|
+
return {
|
|
884
|
+
data,
|
|
885
|
+
count: countRow?.cnt ?? 0
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
/** Resolves vector field and computes shared context for vector search SQL builders. */ _prepareVectorSearch(vector, query, indexName) {
|
|
889
|
+
let field;
|
|
890
|
+
let vec;
|
|
891
|
+
if (indexName) {
|
|
892
|
+
let found = false;
|
|
893
|
+
for (const [f, v] of this._vectorFields) if (v.indexName === indexName) {
|
|
894
|
+
field = f;
|
|
895
|
+
vec = v;
|
|
896
|
+
found = true;
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
if (!found) throw new Error(`Vector index "${indexName}" not found`);
|
|
900
|
+
} else {
|
|
901
|
+
const first = this._vectorFields.entries().next();
|
|
902
|
+
if (first.done) throw new Error("No vector fields defined");
|
|
903
|
+
field = first.value[0];
|
|
904
|
+
vec = first.value[1];
|
|
905
|
+
}
|
|
906
|
+
const distanceFn = similarityToMysqlFn(vec.similarity);
|
|
907
|
+
const where = buildWhere(query.filter);
|
|
908
|
+
const controls = query.controls || {};
|
|
909
|
+
const threshold = this._resolveVectorThreshold(controls, vec.indexName);
|
|
910
|
+
return {
|
|
911
|
+
field,
|
|
912
|
+
vec,
|
|
913
|
+
distanceFn,
|
|
914
|
+
where,
|
|
915
|
+
controls,
|
|
916
|
+
threshold,
|
|
917
|
+
tableName: this.resolveTableName(),
|
|
918
|
+
vectorStr: vectorToString(vector)
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
_buildVectorSearchQuery(vector, query, indexName) {
|
|
922
|
+
const ctx = this._prepareVectorSearch(vector, query, indexName);
|
|
923
|
+
const inner = `SELECT *, ${ctx.distanceFn}(${qi(ctx.field)}, STRING_TO_VECTOR(?)) AS _distance FROM ${quoteTableName(ctx.tableName)} WHERE ${ctx.where.sql}`;
|
|
924
|
+
const params = [ctx.vectorStr, ...ctx.where.params];
|
|
925
|
+
let sql = `SELECT * FROM (${inner}) _v`;
|
|
926
|
+
if (ctx.threshold !== undefined) {
|
|
927
|
+
sql += ` WHERE _distance <= ?`;
|
|
928
|
+
params.push(2 * (1 - ctx.threshold));
|
|
929
|
+
}
|
|
930
|
+
sql += ` ORDER BY _distance ASC`;
|
|
931
|
+
if (ctx.controls.$skip) sql += ` LIMIT ${ctx.controls.$limit || 1e3} OFFSET ${ctx.controls.$skip}`;
|
|
932
|
+
else sql += ` LIMIT ${ctx.controls.$limit || 20}`;
|
|
933
|
+
return {
|
|
934
|
+
sql,
|
|
935
|
+
params
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
_buildVectorSearchCountQuery(vector, query, indexName) {
|
|
939
|
+
const ctx = this._prepareVectorSearch(vector, query, indexName);
|
|
940
|
+
const inner = `SELECT ${ctx.distanceFn}(${qi(ctx.field)}, STRING_TO_VECTOR(?)) AS _distance FROM ${quoteTableName(ctx.tableName)} WHERE ${ctx.where.sql}`;
|
|
941
|
+
const params = [ctx.vectorStr, ...ctx.where.params];
|
|
942
|
+
let sql = `SELECT COUNT(*) AS cnt FROM (${inner}) _v`;
|
|
943
|
+
if (ctx.threshold !== undefined) {
|
|
944
|
+
sql += ` WHERE _distance <= ?`;
|
|
945
|
+
params.push(2 * (1 - ctx.threshold));
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
sql,
|
|
949
|
+
params
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
/** Resolves threshold: query-time $threshold > schema-level @db.search.vector.threshold. */ _resolveVectorThreshold(controls, indexName) {
|
|
953
|
+
const queryThreshold = controls.$threshold;
|
|
954
|
+
if (queryThreshold !== undefined) return queryThreshold;
|
|
955
|
+
return this._vectorThresholds.get(indexName);
|
|
956
|
+
}
|
|
957
|
+
constructor(driver) {
|
|
958
|
+
super(), _define_property$1(this, "driver", void 0), _define_property$1(this, "supportsColumnModify", void 0), _define_property$1(this, "_engine", void 0), _define_property$1(this, "_charset", void 0), _define_property$1(this, "_collation", void 0), _define_property$1(this, "_autoIncrementStart", void 0), _define_property$1(this, "_incrementFields", void 0), _define_property$1(this, "_onUpdateFields", void 0), _define_property$1(this, "_supportsVector", void 0), _define_property$1(this, "_vectorFields", void 0), _define_property$1(this, "_vectorThresholds", void 0), this.driver = driver, this.supportsColumnModify = true, this._engine = "InnoDB", this._charset = "utf8mb4", this._collation = "utf8mb4_unicode_ci", this._incrementFields = new Set(), this._onUpdateFields = new Map(), this._vectorFields = new Map(), this._vectorThresholds = new Map();
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
_define_property$1(MysqlAdapter, "NATIVE_DEFAULT_FNS", new Set(["now", "increment"]));
|
|
962
|
+
/**
|
|
963
|
+
* Normalizes MySQL INFORMATION_SCHEMA.COLUMNS.COLUMN_DEFAULT values
|
|
964
|
+
* to match the format produced by `serializeDefaultValue()`.
|
|
965
|
+
*
|
|
966
|
+
* MySQL stores expression defaults as raw SQL (e.g., `CURRENT_TIMESTAMP`,
|
|
967
|
+
* `uuid()`), but the diff engine compares against serialized form (`fn:now`,
|
|
968
|
+
* `fn:uuid`). Without normalization, every table with function defaults
|
|
969
|
+
* produces phantom ALTER diffs on re-plan.
|
|
970
|
+
*/ function normalizeMysqlDefault(value) {
|
|
971
|
+
if (value === null) return undefined;
|
|
972
|
+
const lower = value.toLowerCase();
|
|
973
|
+
if (lower === "current_timestamp" || lower === "current_timestamp()") return "fn:now";
|
|
974
|
+
if (lower === "uuid()") return "fn:uuid";
|
|
975
|
+
if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1).replace(/''/g, "'");
|
|
976
|
+
return value;
|
|
977
|
+
}
|
|
978
|
+
/** Maps generic similarity metric to MySQL 9+ distance function name. */ function similarityToMysqlFn(similarity) {
|
|
979
|
+
switch (similarity) {
|
|
980
|
+
case "euclidean": return "VEC_DISTANCE_EUCLIDEAN";
|
|
981
|
+
case "dotProduct": return "VEC_DISTANCE_DOT";
|
|
982
|
+
default: return "VEC_DISTANCE_COSINE";
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
/** Formats a number[] vector as MySQL's STRING_TO_VECTOR input: '[1.0, 2.0, ...]'. */ function vectorToString(vector) {
|
|
986
|
+
return `[${vector.join(",")}]`;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
//#endregion
|
|
990
|
+
//#region packages/db-mysql/src/mysql2-driver.ts
|
|
991
|
+
function _define_property(obj, key, value) {
|
|
992
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
993
|
+
value,
|
|
994
|
+
enumerable: true,
|
|
995
|
+
configurable: true,
|
|
996
|
+
writable: true
|
|
997
|
+
});
|
|
998
|
+
else obj[key] = value;
|
|
999
|
+
return obj;
|
|
1000
|
+
}
|
|
1001
|
+
/** mysql2 rejects `undefined` in bind arrays — coerce to `null`. */ function sanitizeParams(params) {
|
|
1002
|
+
if (!params) return [];
|
|
1003
|
+
return params.map((v) => v === undefined ? null : v);
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Custom type-casting for mysql2 result columns to maintain cross-adapter consistency.
|
|
1007
|
+
*
|
|
1008
|
+
* - TIMESTAMP/DATETIME → epoch milliseconds (number) instead of Date objects
|
|
1009
|
+
* - DECIMAL/NEWDECIMAL → number instead of string
|
|
1010
|
+
*/ function atscriptTypeCast(field, next) {
|
|
1011
|
+
if (field.type === "TIMESTAMP" || field.type === "DATETIME") {
|
|
1012
|
+
const str = field.string();
|
|
1013
|
+
if (str === null) return null;
|
|
1014
|
+
return utcDatetimeToEpochMs(str);
|
|
1015
|
+
}
|
|
1016
|
+
if (field.type === "NEWDECIMAL" || field.type === "DECIMAL") {
|
|
1017
|
+
const str = field.string();
|
|
1018
|
+
return str === null ? null : Number(str);
|
|
1019
|
+
}
|
|
1020
|
+
return next();
|
|
1021
|
+
}
|
|
1022
|
+
var Mysql2Driver = class {
|
|
1023
|
+
getPool() {
|
|
1024
|
+
return this.pool || this.poolInit;
|
|
1025
|
+
}
|
|
1026
|
+
async run(sql, params) {
|
|
1027
|
+
const pool = await this.getPool();
|
|
1028
|
+
const [result] = await pool.query(sql, sanitizeParams(params));
|
|
1029
|
+
const header = result;
|
|
1030
|
+
return {
|
|
1031
|
+
affectedRows: header.affectedRows ?? 0,
|
|
1032
|
+
insertId: header.insertId ?? 0,
|
|
1033
|
+
changedRows: header.changedRows ?? 0
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
async all(sql, params) {
|
|
1037
|
+
const pool = await this.getPool();
|
|
1038
|
+
const [rows] = await pool.query(sql, sanitizeParams(params));
|
|
1039
|
+
return rows;
|
|
1040
|
+
}
|
|
1041
|
+
async get(sql, params) {
|
|
1042
|
+
const pool = await this.getPool();
|
|
1043
|
+
const [rows] = await pool.query(sql, sanitizeParams(params));
|
|
1044
|
+
return rows[0] ?? null;
|
|
1045
|
+
}
|
|
1046
|
+
async exec(sql) {
|
|
1047
|
+
const pool = await this.getPool();
|
|
1048
|
+
await pool.query(sql);
|
|
1049
|
+
}
|
|
1050
|
+
async getConnection() {
|
|
1051
|
+
const pool = await this.getPool();
|
|
1052
|
+
const conn = await pool.getConnection();
|
|
1053
|
+
return {
|
|
1054
|
+
async run(sql, params) {
|
|
1055
|
+
const [result] = await conn.query(sql, sanitizeParams(params));
|
|
1056
|
+
const header = result;
|
|
1057
|
+
return {
|
|
1058
|
+
affectedRows: header.affectedRows ?? 0,
|
|
1059
|
+
insertId: header.insertId ?? 0,
|
|
1060
|
+
changedRows: header.changedRows ?? 0
|
|
1061
|
+
};
|
|
1062
|
+
},
|
|
1063
|
+
async all(sql, params) {
|
|
1064
|
+
const [rows] = await conn.query(sql, sanitizeParams(params));
|
|
1065
|
+
return rows;
|
|
1066
|
+
},
|
|
1067
|
+
async get(sql, params) {
|
|
1068
|
+
const [rows] = await conn.query(sql, sanitizeParams(params));
|
|
1069
|
+
return rows[0] ?? null;
|
|
1070
|
+
},
|
|
1071
|
+
async exec(sql) {
|
|
1072
|
+
await conn.query(sql);
|
|
1073
|
+
},
|
|
1074
|
+
release() {
|
|
1075
|
+
conn.release();
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
async close() {
|
|
1080
|
+
const pool = await this.getPool();
|
|
1081
|
+
await pool.end();
|
|
1082
|
+
}
|
|
1083
|
+
constructor(poolOrConfig) {
|
|
1084
|
+
_define_property(this, "pool", void 0);
|
|
1085
|
+
_define_property(this, "poolInit", void 0);
|
|
1086
|
+
if (typeof poolOrConfig === "object" && "execute" in poolOrConfig) this.pool = poolOrConfig;
|
|
1087
|
+
else this.poolInit = import("mysql2/promise").then((mysql) => {
|
|
1088
|
+
if (typeof poolOrConfig === "string") this.pool = mysql.createPool({
|
|
1089
|
+
uri: poolOrConfig,
|
|
1090
|
+
timezone: "+00:00",
|
|
1091
|
+
supportBigNumbers: true,
|
|
1092
|
+
bigNumberStrings: false,
|
|
1093
|
+
typeCast: atscriptTypeCast
|
|
1094
|
+
});
|
|
1095
|
+
else this.pool = mysql.createPool({
|
|
1096
|
+
...poolOrConfig,
|
|
1097
|
+
timezone: "+00:00",
|
|
1098
|
+
supportBigNumbers: true,
|
|
1099
|
+
bigNumberStrings: false,
|
|
1100
|
+
typeCast: atscriptTypeCast
|
|
1101
|
+
});
|
|
1102
|
+
return this.pool;
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
|
|
1107
|
+
//#endregion
|
|
1108
|
+
//#region packages/db-mysql/src/plugin/annotations.ts
|
|
1109
|
+
const annotations = {
|
|
1110
|
+
engine: new AnnotationSpec({
|
|
1111
|
+
description: "Specifies the MySQL storage engine.\n\n**Default:** `\"InnoDB\"`\n\n```atscript\n@db.mysql.engine \"MyISAM\"\nexport interface Logs { ... }\n```",
|
|
1112
|
+
nodeType: ["interface"],
|
|
1113
|
+
multiple: false,
|
|
1114
|
+
argument: {
|
|
1115
|
+
name: "engine",
|
|
1116
|
+
type: "string",
|
|
1117
|
+
values: [
|
|
1118
|
+
"InnoDB",
|
|
1119
|
+
"MyISAM",
|
|
1120
|
+
"MEMORY",
|
|
1121
|
+
"CSV",
|
|
1122
|
+
"ARCHIVE"
|
|
1123
|
+
],
|
|
1124
|
+
description: "MySQL storage engine name."
|
|
1125
|
+
}
|
|
1126
|
+
}),
|
|
1127
|
+
charset: new AnnotationSpec({
|
|
1128
|
+
description: "Specifies the character set for the table or column.\n\n**Default:** `\"utf8mb4\"`\n\n```atscript\n@db.mysql.charset \"latin1\"\nexport interface Legacy { ... }\n```",
|
|
1129
|
+
nodeType: ["interface", "prop"],
|
|
1130
|
+
multiple: false,
|
|
1131
|
+
argument: {
|
|
1132
|
+
name: "charset",
|
|
1133
|
+
type: "string",
|
|
1134
|
+
values: [
|
|
1135
|
+
"utf8mb4",
|
|
1136
|
+
"utf8",
|
|
1137
|
+
"latin1",
|
|
1138
|
+
"ascii",
|
|
1139
|
+
"binary"
|
|
1140
|
+
],
|
|
1141
|
+
description: "MySQL character set name."
|
|
1142
|
+
}
|
|
1143
|
+
}),
|
|
1144
|
+
collate: new AnnotationSpec({
|
|
1145
|
+
description: "Specifies a native MySQL collation (overrides portable `@db.column.collate`).\n\n```atscript\n@db.mysql.collate \"utf8mb4_turkish_ci\"\nname: string\n```",
|
|
1146
|
+
nodeType: ["interface", "prop"],
|
|
1147
|
+
multiple: false,
|
|
1148
|
+
argument: {
|
|
1149
|
+
name: "collation",
|
|
1150
|
+
type: "string",
|
|
1151
|
+
description: "Native MySQL collation name (e.g., \"utf8mb4_turkish_ci\")."
|
|
1152
|
+
}
|
|
1153
|
+
}),
|
|
1154
|
+
unsigned: new AnnotationSpec({
|
|
1155
|
+
description: "Adds the UNSIGNED modifier to an integer column.\n\n```atscript\n@db.mysql.unsigned\nage: number.int\n```",
|
|
1156
|
+
nodeType: ["prop"],
|
|
1157
|
+
multiple: false
|
|
1158
|
+
}),
|
|
1159
|
+
type: new AnnotationSpec({
|
|
1160
|
+
description: "Overrides the native MySQL column type.\n\n```atscript\n@db.mysql.type \"MEDIUMTEXT\"\nbio: string\n```",
|
|
1161
|
+
nodeType: ["prop"],
|
|
1162
|
+
multiple: false,
|
|
1163
|
+
argument: {
|
|
1164
|
+
name: "type",
|
|
1165
|
+
type: "string",
|
|
1166
|
+
description: "Native MySQL column type (e.g., \"MEDIUMTEXT\", \"TINYTEXT\")."
|
|
1167
|
+
}
|
|
1168
|
+
}),
|
|
1169
|
+
onUpdate: new AnnotationSpec({
|
|
1170
|
+
description: "Sets the MySQL ON UPDATE clause for a column.\n\n```atscript\n@db.mysql.onUpdate \"CURRENT_TIMESTAMP\"\nupdatedAt: number.timestamp\n```",
|
|
1171
|
+
nodeType: ["prop"],
|
|
1172
|
+
multiple: false,
|
|
1173
|
+
argument: {
|
|
1174
|
+
name: "expression",
|
|
1175
|
+
type: "string",
|
|
1176
|
+
values: ["CURRENT_TIMESTAMP"],
|
|
1177
|
+
description: "Expression to evaluate on row update."
|
|
1178
|
+
}
|
|
1179
|
+
})
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
//#endregion
|
|
1183
|
+
//#region packages/db-mysql/src/plugin/index.ts
|
|
1184
|
+
const MysqlPlugin = () => ({
|
|
1185
|
+
name: "mysql",
|
|
1186
|
+
config() {
|
|
1187
|
+
return { annotations: { db: { mysql: annotations } } };
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
//#endregion
|
|
1192
|
+
//#region packages/db-mysql/src/index.ts
|
|
1193
|
+
function createAdapter(uri, options) {
|
|
1194
|
+
const driver = new Mysql2Driver({
|
|
1195
|
+
uri,
|
|
1196
|
+
...options
|
|
1197
|
+
});
|
|
1198
|
+
return new DbSpace(() => new MysqlAdapter(driver));
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
//#endregion
|
|
1202
|
+
export { Mysql2Driver, MysqlAdapter, MysqlPlugin, buildWhere, createAdapter };
|