@primate/mysql 0.4.0 → 0.6.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.
@@ -1,4 +1,4 @@
1
- import type TypedArray from "@rcompat/type/TypedArray";
1
+ import type { TypedArray } from "@rcompat/type";
2
2
  type Param = bigint | boolean | Date | null | number | string | TypedArray;
3
3
  type Validate<T extends {
4
4
  [K in keyof T]: Param;
@@ -20,6 +20,8 @@ type ColumnTypes = Validate<{
20
20
  "TIME": string;
21
21
  TINYINT: number;
22
22
  "TINYINT UNSIGNED": number;
23
+ JSON: string;
24
+ "BINARY(16)": Uint8Array<ArrayBuffer>;
23
25
  }>;
24
26
  export type { ColumnTypes as default };
25
27
  //# sourceMappingURL=ColumnTypes.d.ts.map
@@ -0,0 +1,50 @@
1
+ import type { As, DataDict, DB, Schema, Sort, With } from "@primate/core/db";
2
+ import type { Dict } from "@rcompat/type";
3
+ declare const schema: import("pema").ObjectType<import("pema").NormalizeSchemaObject<{
4
+ readonly host: import("pema").DefaultType<import("pema").StringType, "localhost">;
5
+ readonly port: import("pema").DefaultType<import("pema").UintType<"u32">, 3306>;
6
+ readonly database: import("pema").StringType;
7
+ readonly password: import("pema").OptionalType<import("pema").StringType>;
8
+ readonly username: import("pema").OptionalType<import("pema").StringType>;
9
+ }>, {
10
+ host: string;
11
+ port: number;
12
+ database: string;
13
+ password: string | undefined;
14
+ username: string | undefined;
15
+ }>;
16
+ export default class MySQL implements DB {
17
+ #private;
18
+ static config: typeof schema.input;
19
+ constructor(config?: typeof schema.input, options?: {
20
+ debug?: boolean;
21
+ });
22
+ get explain(): Dict<{
23
+ query: string;
24
+ plans: string[];
25
+ }>;
26
+ close(): Promise<void>;
27
+ get schema(): Schema;
28
+ create<O extends Dict>(as: As, record: Dict): Promise<O>;
29
+ read(as: As, args: {
30
+ count: true;
31
+ where: DataDict;
32
+ with?: never;
33
+ }): Promise<number>;
34
+ read(as: As, args: {
35
+ where: DataDict;
36
+ fields?: string[];
37
+ limit?: number;
38
+ sort?: Sort;
39
+ with?: With;
40
+ }): Promise<Dict[]>;
41
+ update(as: As, args: {
42
+ set: DataDict;
43
+ where: DataDict;
44
+ }): Promise<number>;
45
+ delete(as: As, args: {
46
+ where: DataDict;
47
+ }): Promise<number>;
48
+ }
49
+ export {};
50
+ //# sourceMappingURL=MySQL.d.ts.map
@@ -0,0 +1,528 @@
1
+ import typemap from "#typemap";
2
+ import base from "@primate/core/db";
3
+ import E from "@primate/core/db/errors";
4
+ import sql from "@primate/core/db/sql";
5
+ import assert from "@rcompat/assert";
6
+ import is from "@rcompat/is";
7
+ import mysql from "mysql2/promise";
8
+ import p from "pema";
9
+ const BIND_BY = ":";
10
+ const Q = sql.Q;
11
+ function bigint_cast(arg) {
12
+ return `CAST(${arg} AS DECIMAL(65,0))`;
13
+ }
14
+ function is_bigint_key(k) {
15
+ return k === "u64" || k === "u128" || k === "i64" || k === "i128";
16
+ }
17
+ function order_by(types, sort, alias) {
18
+ if (sort === undefined)
19
+ return "";
20
+ const entries = Object.entries(sort);
21
+ if (entries.length === 0)
22
+ return "";
23
+ const parts = entries.map(([k, dir]) => {
24
+ const quoted = sql.quote(k);
25
+ const base = alias !== undefined ? `${alias}.${quoted}` : quoted;
26
+ const expression = is_bigint_key(types[k]) ? bigint_cast(base) : base;
27
+ return `${expression} ${dir.toLowerCase() === "desc" ? "DESC" : "ASC"}`;
28
+ });
29
+ return ` ORDER BY ${parts.join(", ")}`;
30
+ }
31
+ function get_column(key) {
32
+ return typemap[key].column;
33
+ }
34
+ async function bind_value(key, value) {
35
+ if (value === null)
36
+ return null;
37
+ return await typemap[key].bind(value);
38
+ }
39
+ function unbind_value(key, value) {
40
+ return typemap[key].unbind(value);
41
+ }
42
+ function unbind(types, row) {
43
+ return sql.unbind(types, row, unbind_value);
44
+ }
45
+ function columns_to_types(data_t, column_t) {
46
+ const unsigned = column_t.includes("unsigned");
47
+ if (data_t === "varchar")
48
+ return ["string", "url"];
49
+ if (data_t === "text")
50
+ return ["string", "url", "time"];
51
+ if (data_t === "binary" && column_t.includes("(16)"))
52
+ return base.UUID_TYPES;
53
+ if (data_t === "smallint")
54
+ return unsigned ? ["u16"] : ["i16"];
55
+ if (data_t === "int")
56
+ return unsigned ? ["u32"] : ["i32"];
57
+ if (data_t === "bigint")
58
+ return unsigned ? ["u64"] : ["i64"];
59
+ if (data_t === "decimal")
60
+ return ["u128", "i128"];
61
+ if (data_t === "double")
62
+ return ["f32", "f64"];
63
+ if (data_t === "datetime")
64
+ return ["datetime"];
65
+ if (data_t === "tinyint" && column_t === "tinyint(1)")
66
+ return ["boolean"];
67
+ if (data_t === "tinyint")
68
+ return unsigned ? ["u8"] : ["i8", "boolean"];
69
+ if (data_t === "blob")
70
+ return ["blob"];
71
+ if (data_t === "json")
72
+ return ["json"];
73
+ return [];
74
+ }
75
+ const schema = p({
76
+ host: p.string.default("localhost"),
77
+ port: p.uint.port().default(3306),
78
+ database: p.string,
79
+ password: p.string.optional(),
80
+ username: p.string.optional(),
81
+ });
82
+ export default class MySQL {
83
+ static config;
84
+ #factory;
85
+ #client;
86
+ #debug = false;
87
+ #explain = {};
88
+ constructor(config, options) {
89
+ const parsed = schema.parse(config);
90
+ this.#factory = () => mysql.createPool({
91
+ host: parsed.host,
92
+ port: parsed.port,
93
+ database: parsed.database,
94
+ user: parsed.username,
95
+ password: parsed.password,
96
+ namedPlaceholders: true,
97
+ bigNumberStrings: true,
98
+ supportBigNumbers: true,
99
+ });
100
+ this.#debug = options?.debug ?? false;
101
+ }
102
+ get #db() {
103
+ return this.#client ??= this.#factory();
104
+ }
105
+ async #sql(query, params) {
106
+ const [rows] = await this.#db.query(query, params);
107
+ return rows;
108
+ }
109
+ async #execute(query, params) {
110
+ const [result] = await this.#db.execute(query, params);
111
+ return result;
112
+ }
113
+ async #capture(table, query, params) {
114
+ if (!this.#debug)
115
+ return;
116
+ const rows = await this.#sql(`EXPLAIN ${query}`, params);
117
+ this.#explain[table] = { query, plans: rows.map(r => JSON.stringify(r)) };
118
+ }
119
+ get explain() {
120
+ return this.#explain;
121
+ }
122
+ async close() {
123
+ await this.#db.end();
124
+ }
125
+ get schema() {
126
+ return {
127
+ create: async (table, pk, types) => {
128
+ const columns = [];
129
+ for (const [key, value] of Object.entries(types)) {
130
+ const type = get_column(value);
131
+ if (key === pk.name) {
132
+ const is_int = base.INT_TYPES.includes(value);
133
+ const auto = pk.generate && is_int ? " AUTO_INCREMENT" : "";
134
+ // 36 is UUID length
135
+ const pk_type = value === "string" ? "VARCHAR(36)" : type;
136
+ columns.push(`${sql.quote(key)} ${pk_type} PRIMARY KEY${auto}`);
137
+ }
138
+ else {
139
+ columns.push(`${sql.quote(key)} ${type}`);
140
+ }
141
+ }
142
+ await this.#sql(Q `CREATE TABLE IF NOT EXISTS ${table} (${columns})`);
143
+ },
144
+ delete: async (table) => {
145
+ await this.#sql(Q `DROP TABLE IF EXISTS ${table}`);
146
+ },
147
+ introspect: async (table) => {
148
+ const rows = await this.#sql(`
149
+ SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE
150
+ FROM information_schema.COLUMNS
151
+ WHERE TABLE_SCHEMA = DATABASE()
152
+ AND TABLE_NAME = :name
153
+ `, { name: table });
154
+ if (rows.length === 0)
155
+ return null;
156
+ const result = {};
157
+ for (const row of rows) {
158
+ const candidates = columns_to_types(row.DATA_TYPE, row.COLUMN_TYPE);
159
+ if (candidates.length > 0)
160
+ result[row.COLUMN_NAME] = candidates;
161
+ }
162
+ return result;
163
+ },
164
+ alter: async (table, diff) => {
165
+ const existing = await this.schema.introspect(table);
166
+ if (existing === null)
167
+ throw E.table_not_found(table);
168
+ for (const [from, to] of diff.rename) {
169
+ await this.#sql(Q `
170
+ ALTER TABLE ${table}
171
+ RENAME COLUMN ${[sql.quote(from)]} TO ${[sql.quote(to)]}`);
172
+ }
173
+ const parts = [];
174
+ for (const field of diff.drop) {
175
+ parts.push(`DROP COLUMN ${sql.quote(field)}`);
176
+ }
177
+ for (const [field, type] of Object.entries(diff.add)) {
178
+ parts.push(`ADD COLUMN ${sql.quote(field)} ${get_column(type)}`);
179
+ }
180
+ if (parts.length > 0) {
181
+ await this.#sql(Q `ALTER TABLE ${table} ${[parts.join(", ")]}`);
182
+ }
183
+ },
184
+ };
185
+ }
186
+ async #where(as, where) {
187
+ const fields = Object.keys(where);
188
+ if (fields.length === 0)
189
+ return ["", {}];
190
+ const parts = [];
191
+ const binds = {};
192
+ for (const field of fields) {
193
+ const value = where[field];
194
+ const datatype = as.types[field];
195
+ const q = sql.quote(field);
196
+ const quoted = q;
197
+ if (value === null) {
198
+ parts.push(`${quoted} IS NULL`);
199
+ continue;
200
+ }
201
+ if (is.dict(value)) {
202
+ const ops = Object.entries(value);
203
+ if (ops.length === 0)
204
+ throw E.operator_empty(field);
205
+ for (const [op, op_value] of ops) {
206
+ const bind_key = sql.bindKey(field, op);
207
+ const ph = `${BIND_BY}${bind_key}`;
208
+ const numeric = is_bigint_key(datatype);
209
+ const lhs = numeric ? bigint_cast(quoted) : quoted;
210
+ const rhs = numeric ? bigint_cast(ph) : ph;
211
+ binds[bind_key] = await bind_value(datatype, op_value);
212
+ switch (op) {
213
+ case "$like":
214
+ parts.push(`${quoted} LIKE BINARY ${ph}`);
215
+ break;
216
+ case "$ilike":
217
+ parts.push(`LOWER(${quoted}) LIKE LOWER(${ph})`);
218
+ break;
219
+ case "$ne":
220
+ parts.push(`${lhs} != ${rhs}`);
221
+ break;
222
+ case "$gt":
223
+ case "$gte":
224
+ case "$lt":
225
+ case "$lte":
226
+ case "$after":
227
+ case "$before": {
228
+ const sql_op = sql.OPS[op];
229
+ parts.push(`${lhs} ${sql_op} ${rhs}`);
230
+ break;
231
+ }
232
+ default:
233
+ throw E.operator_unknown(field, op);
234
+ }
235
+ }
236
+ continue;
237
+ }
238
+ parts.push(`${quoted} = ${BIND_BY}${field}`);
239
+ binds[field] = await bind_value(datatype, value);
240
+ }
241
+ return [`WHERE ${parts.join(" AND ")}`, binds];
242
+ }
243
+ #set(set) {
244
+ const columns = Object.keys(set);
245
+ if (columns.length === 0)
246
+ throw E.field_required("set");
247
+ return Q `SET ${columns.map(c => `${sql.quote(c)}=${BIND_BY}s_${c}`)}`;
248
+ }
249
+ async #bind_set(types, set) {
250
+ const columns = Object.keys(set);
251
+ if (columns.length === 0)
252
+ throw E.field_required("set");
253
+ const params = {};
254
+ for (const c of columns) {
255
+ params[`s_${c}`] = await bind_value(types[c], set[c]);
256
+ }
257
+ return params;
258
+ }
259
+ async #generate_pk(as) {
260
+ const pk = as.pk;
261
+ const type = as.types[pk];
262
+ const table = as.table;
263
+ if (base.is_uuid_type(type))
264
+ return base.generate_uuid(type);
265
+ if (base.BIGINT_STRING_TYPES.includes(type)) {
266
+ const cast = bigint_cast(sql.quote(pk));
267
+ const q = `SELECT MAX(${cast}) AS v FROM ${sql.quote(table)}`;
268
+ const rows = await this.#sql(q);
269
+ const v = rows[0]?.v;
270
+ return v ? BigInt(v) + 1n : 1n;
271
+ }
272
+ throw "unreachable";
273
+ }
274
+ #create(record) {
275
+ const fields = Object.keys(record);
276
+ return [
277
+ fields.map(sql.quote),
278
+ fields.map(field => `${BIND_BY}${field}`),
279
+ ];
280
+ }
281
+ async #create_params(types, record) {
282
+ const params = {};
283
+ for (const k of Object.keys(record)) {
284
+ params[k] = await bind_value(types[k], record[k]);
285
+ }
286
+ return params;
287
+ }
288
+ async create(as, record) {
289
+ assert.dict(record);
290
+ const pk = as.pk;
291
+ const table = as.table;
292
+ // PK provided or none defined, simple insert
293
+ if (pk === null || pk in record) {
294
+ const [keys, values] = this.#create(record);
295
+ const query = keys.length > 0
296
+ ? Q `INSERT INTO ${table} (${keys}) VALUES (${values})`
297
+ : Q `INSERT INTO ${table} () VALUES ()`;
298
+ await this.#sql(query, await this.#create_params(as.types, record));
299
+ return record;
300
+ }
301
+ if (as.generate_pk === false)
302
+ throw E.pk_required(pk);
303
+ const type = as.types[pk];
304
+ // integer types, use AUTO_INCREMENT
305
+ if (!base.BIGINT_STRING_TYPES.includes(type) && !base.is_uuid_type(type)) {
306
+ const [keys, values] = this.#create(record);
307
+ const query = keys.length > 0
308
+ ? Q `INSERT INTO ${table} (${keys}) VALUES (${values})`
309
+ : Q `INSERT INTO ${table} () VALUES ()`;
310
+ const result = await this.#execute(query, await this.#create_params(as.types, record));
311
+ const pk_value = unbind_value(type, result.insertId);
312
+ return { ...record, [pk]: pk_value };
313
+ }
314
+ // string or bigint, generate manually
315
+ const pk_value = await this.#generate_pk(as);
316
+ const to_insert = { ...record, [pk]: pk_value };
317
+ const [keys, values] = this.#create(to_insert);
318
+ const query = Q `INSERT INTO ${table} (${keys}) VALUES (${values})`;
319
+ await this.#sql(query, await this.#create_params(as.types, to_insert));
320
+ return to_insert;
321
+ }
322
+ async read(as, args) {
323
+ assert.dict(args.where);
324
+ this.#explain = {};
325
+ if (args.count === true)
326
+ return this.#count(as, args.where);
327
+ if (base.withed(args)) {
328
+ return sql.joinable(as, args.with)
329
+ ? this.#read_joined(as, args)
330
+ : this.#read_phased(as, args);
331
+ }
332
+ return this.#read(as, args);
333
+ }
334
+ async #count(as, where) {
335
+ const [WHERE, binds] = await this.#where(as, where);
336
+ const table = as.table;
337
+ const query = `SELECT COUNT(*) AS n FROM ${sql.quote(table)} ${WHERE}`;
338
+ const rows = await this.#sql(query, binds);
339
+ await this.#capture(table, query, binds);
340
+ return Number(rows[0]?.n ?? 0);
341
+ }
342
+ async #base_query(as, args) {
343
+ const SELECT = args.fields === undefined
344
+ ? "*"
345
+ : args.fields.map(sql.quote).join(", ");
346
+ const [WHERE, binds] = await this.#where(as, args.where);
347
+ const ORDER_BY = order_by(as.types, args.sort);
348
+ const LIMIT = sql.limit(args.limit);
349
+ const table = sql.quote(as.table);
350
+ return [
351
+ `SELECT ${SELECT} FROM ${table} ${WHERE}${ORDER_BY}${LIMIT}`,
352
+ binds,
353
+ ];
354
+ }
355
+ async #read(as, args) {
356
+ const [query, binds] = await this.#base_query(as, args);
357
+ const rows = await this.#sql(query, binds);
358
+ await this.#capture(as.table, query, binds);
359
+ return rows.map(r => unbind(as.types, r));
360
+ }
361
+ async #read_phased(as, args) {
362
+ const fields = base.expand(as, args.fields, args.with);
363
+ const rows = await this.#read(as, { ...args, fields });
364
+ const out = rows.map(row => base.project(row, args.fields));
365
+ for (const [table, relation] of Object.entries(args.with)) {
366
+ await this.#attach_relation(as, { rows, out, table, relation });
367
+ }
368
+ return out;
369
+ }
370
+ async #attach_relation(as, args) {
371
+ const relation = args.relation;
372
+ const by = relation.reverse ? relation.as.pk : relation.fk;
373
+ if (by === null)
374
+ throw E.relation_requires_pk("target");
375
+ const parent_by = relation.reverse ? relation.fk : as.pk;
376
+ if (parent_by === null)
377
+ throw E.relation_requires_pk("parent");
378
+ const join_values = [...new Set(args.rows.map(r => r[parent_by]).filter(v => v != null))];
379
+ const is_many = relation.kind === "many";
380
+ const many = is_many ? [] : null;
381
+ if (join_values.length === 0) {
382
+ for (const row of args.out)
383
+ row[args.table] = many;
384
+ return;
385
+ }
386
+ const related = await this.#load_related({ by, join_values, ...relation });
387
+ const grouped = new Map();
388
+ for (const row of related) {
389
+ const key = row[by];
390
+ grouped.set(key, grouped.get(key)?.concat(row) ?? [row]);
391
+ }
392
+ for (let i = 0; i < args.out.length; i++) {
393
+ const join_value = args.rows[i][parent_by];
394
+ if (join_value == null) {
395
+ args.out[i][args.table] = many;
396
+ continue;
397
+ }
398
+ const rows = grouped.get(join_value) ?? [];
399
+ args.out[i][args.table] = is_many
400
+ ? rows.map(r => base.project(r, relation.fields))
401
+ : rows[0] ? base.project(rows[0], relation.fields) : null;
402
+ }
403
+ }
404
+ async #load_related(args) {
405
+ const per_parent = args.kind === "one" ? 1 : args.limit;
406
+ const in_binds = {};
407
+ const placeholders = [];
408
+ for (let i = 0; i < args.join_values.length; i++) {
409
+ const key = `${args.by}__in${i}`;
410
+ in_binds[key] = await bind_value(args.as.types[args.by], args.join_values[i]);
411
+ placeholders.push(`${BIND_BY}${key}`);
412
+ }
413
+ const [where, where_binds] = await this.#where(args.as, args.where);
414
+ const where_part = where ? where.slice("WHERE ".length) : "";
415
+ const numeric = is_bigint_key(args.as.types[args.by]);
416
+ const in_placeholders = numeric
417
+ ? placeholders.map(bigint_cast)
418
+ : placeholders;
419
+ const cast = bigint_cast(sql.quote(args.by));
420
+ const lhs = numeric ? cast : Q `${args.by}`;
421
+ const in_part = `${lhs} IN (${in_placeholders.join(", ")})`;
422
+ const where_parts = where_part
423
+ ? [`(${where_part})`, `(${in_part})`]
424
+ : [in_part];
425
+ const WHERE = `WHERE ${where_parts.join(" AND ")}`;
426
+ const all_columns = Object.keys(args.as.types);
427
+ const fields = args.fields !== undefined && args.fields.length > 0
428
+ ? args.fields
429
+ : all_columns;
430
+ const select_fields = [...new Set([...fields, args.by])];
431
+ const SELECT = select_fields.map(sql.quote).join(", ");
432
+ const base_order = `${numeric ? cast : Q `${args.by}`} ASC`;
433
+ const user_order = order_by(args.as.types, args.sort)
434
+ .replace(/^ ORDER BY /, "");
435
+ const ORDER_BY = user_order
436
+ ? ` ORDER BY ${base_order}, ${user_order}`
437
+ : ` ORDER BY ${base_order}`;
438
+ const table = sql.quote(args.as.table);
439
+ let query;
440
+ if (per_parent !== undefined) {
441
+ const partition = numeric ? cast : Q `${args.by}`;
442
+ const rn_order = user_order ? ` ORDER BY ${user_order}` : "";
443
+ query = `
444
+ SELECT ${SELECT} FROM (
445
+ SELECT
446
+ ${SELECT},
447
+ ROW_NUMBER() OVER (
448
+ PARTITION BY ${partition}
449
+ ${rn_order}
450
+ ) AS __rn
451
+ FROM ${table}
452
+ ${WHERE}
453
+ ) ranked
454
+ WHERE __rn <= ${per_parent}
455
+ ${ORDER_BY}
456
+ `;
457
+ }
458
+ else {
459
+ query = `SELECT ${SELECT} FROM ${table} ${WHERE}${ORDER_BY}`;
460
+ }
461
+ const binds = { ...where_binds, ...in_binds };
462
+ const rows = await this.#sql(query, binds);
463
+ await this.#capture(args.as.table, query, binds);
464
+ return rows.map(r => unbind(args.as.types, r));
465
+ }
466
+ #join(as, aliases, relation) {
467
+ const parent_alias = aliases[as.table];
468
+ const alias = aliases[relation.as.table];
469
+ const table = sql.quote(relation.as.table);
470
+ const by = relation.reverse ? relation.as.pk : relation.fk;
471
+ const parent_by = relation.reverse ? relation.fk : as.pk;
472
+ if (by === null)
473
+ throw E.relation_requires_pk("target");
474
+ if (parent_by === null)
475
+ throw E.relation_requires_pk("parent");
476
+ const left = `${alias}.${sql.quote(by)}`;
477
+ const right = `${parent_alias}.${sql.quote(parent_by)}`;
478
+ return `LEFT JOIN ${table} ${alias} ON ${left} = ${right}`;
479
+ }
480
+ async #read_joined(as, args) {
481
+ if (as.pk === null)
482
+ throw E.relation_requires_pk("parent");
483
+ const tables = [as.table, ...Object.values(args.with).map(r => r.as.table)];
484
+ const aliases = sql.aliases(tables);
485
+ const alias = aliases[as.table];
486
+ const fields = base.fields(args.fields, as.pk) ?? Object.keys(as.types);
487
+ const SELECT = [
488
+ ...fields.map(f => `${alias}.${sql.quote(f)} AS ${alias}_${f}`),
489
+ ...Object.values(args.with).flatMap(relation => {
490
+ const r_alias = aliases[relation.as.table];
491
+ const r_fields = relation.as.pk !== null
492
+ ? base.fields(relation.fields, relation.as.pk)
493
+ : relation.fields;
494
+ return (r_fields ?? Object.keys(relation.as.types))
495
+ .map(f => `${r_alias}.${sql.quote(f)} AS ${r_alias}_${f}`);
496
+ }),
497
+ ].join(", ");
498
+ const [SUBQUERY, binds] = await this.#base_query(as, { ...args, fields });
499
+ const FROM = `(${SUBQUERY}) ${alias}`;
500
+ const JOINS = Object.values(args.with)
501
+ .map(relation => this.#join(as, aliases, relation))
502
+ .join("\n");
503
+ const ORDER_BY = order_by(as.types, args.sort, alias);
504
+ const query = `SELECT ${SELECT} FROM ${FROM} ${JOINS} ${ORDER_BY}`;
505
+ const rows = await this.#sql(query, binds);
506
+ this.#capture(as.table, query, binds);
507
+ return sql.nest(as, { rows, aliases }, args, unbind_value);
508
+ }
509
+ async update(as, args) {
510
+ assert.nonempty(args.set);
511
+ assert.dict(args.where);
512
+ const [WHERE, where_binds] = await this.#where(as, args.where);
513
+ const SET = this.#set(args.set);
514
+ const set_binds = await this.#bind_set(as.types, args.set);
515
+ const query = `UPDATE ${sql.quote(as.table)} ${SET} ${WHERE}`;
516
+ const binds = { ...where_binds, ...set_binds };
517
+ const result = await this.#execute(query, binds);
518
+ return result.affectedRows;
519
+ }
520
+ async delete(as, args) {
521
+ assert.nonempty(args.where);
522
+ const [WHERE, binds] = await this.#where(as, args.where);
523
+ const query = `DELETE FROM ${sql.quote(as.table)} ${WHERE}`;
524
+ const result = await this.#execute(query, binds);
525
+ return result.affectedRows;
526
+ }
527
+ }
528
+ //# sourceMappingURL=MySQL.js.map
@@ -1,5 +1,5 @@
1
1
  import type ColumnTypes from "#ColumnTypes";
2
- import type TypeMap from "@primate/core/database/TypeMap";
2
+ import type { TypeMap } from "@primate/core/db";
3
3
  declare const typemap: TypeMap<ColumnTypes>;
4
4
  export default typemap;
5
5
  //# sourceMappingURL=typemap.d.ts.map
@@ -1,4 +1,3 @@
1
- import numeric from "@rcompat/is/numeric";
2
1
  function identity(column) {
3
2
  return {
4
3
  bind: value => value,
@@ -13,11 +12,22 @@ function number(column) {
13
12
  unbind: (value) => Number(value),
14
13
  };
15
14
  }
15
+ function uuid_to_bin(uuid) {
16
+ return Uint8Array.fromHex(uuid.replace(/-/g, ""));
17
+ }
18
+ function bin_to_uuid(buf) {
19
+ const hex = buf.toHex();
20
+ const a = hex.slice(0, 8);
21
+ const b = hex.slice(8, 12);
22
+ const c = hex.slice(12, 16);
23
+ const d = hex.slice(16, 20);
24
+ const e = hex.slice(20);
25
+ return `${a}-${b}-${c}-${d}-${e}`;
26
+ }
16
27
  const typemap = {
17
28
  blob: {
18
29
  async bind(value) {
19
- const arrayBuffer = await value.arrayBuffer();
20
- return new Uint8Array(arrayBuffer);
30
+ return new Uint8Array(await value.arrayBuffer());
21
31
  },
22
32
  column: "BLOB",
23
33
  unbind(value) {
@@ -57,16 +67,13 @@ const typemap = {
57
67
  },
58
68
  },
59
69
  i8: number("TINYINT"),
60
- primary: {
70
+ json: {
61
71
  bind(value) {
62
- if (numeric(value)) {
63
- return Number(value);
64
- }
65
- throw new Error(`\`${value}\` is not a valid primary key value`);
72
+ return JSON.stringify(value);
66
73
  },
67
- column: "INT NOT NULL AUTO_INCREMENT PRIMARY KEY",
74
+ column: "JSON",
68
75
  unbind(value) {
69
- return String(value);
76
+ return JSON.parse(value);
70
77
  },
71
78
  },
72
79
  string: identity("TEXT"),
@@ -101,6 +108,21 @@ const typemap = {
101
108
  return new URL(value);
102
109
  },
103
110
  },
111
+ uuid: {
112
+ bind: uuid_to_bin,
113
+ column: "BINARY(16)",
114
+ unbind: bin_to_uuid,
115
+ },
116
+ uuid_v4: {
117
+ bind: uuid_to_bin,
118
+ column: "BINARY(16)",
119
+ unbind: bin_to_uuid,
120
+ },
121
+ uuid_v7: {
122
+ bind: uuid_to_bin,
123
+ column: "BINARY(16)",
124
+ unbind: bin_to_uuid,
125
+ },
104
126
  };
105
127
  export default typemap;
106
128
  //# sourceMappingURL=typemap.js.map
@@ -1,4 +1,4 @@
1
- import Database from "#Database";
2
- declare const _default: (config: typeof Database.config) => Database;
1
+ import MySQL from "#MySQL";
2
+ declare const _default: (config: typeof MySQL.config) => MySQL;
3
3
  export default _default;
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,3 @@
1
- import Database from "#Database";
2
- export default (config) => new Database(config);
1
+ import MySQL from "#MySQL";
2
+ export default (config) => new MySQL(config);
3
3
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,38 +1,43 @@
1
1
  {
2
2
  "name": "@primate/mysql",
3
- "version": "0.4.0",
4
- "description": "Primate MySQL database",
3
+ "version": "0.6.0",
4
+ "description": "MySQL databases for Primate",
5
5
  "homepage": "https://primate.run/docs/database/mysql",
6
6
  "bugs": "https://github.com/primate-run/primate/issues",
7
+ "type": "module",
7
8
  "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/primate-run/primate",
12
+ "directory": "packages/mysql"
13
+ },
8
14
  "files": [
9
15
  "/lib/**/*.js",
10
16
  "/lib/**/*.d.ts",
11
17
  "!/**/*.spec.*"
12
18
  ],
13
- "repository": {
14
- "type": "git",
15
- "url": "https://github.com/primate-run/primate",
16
- "directory": "packages/mysql"
19
+ "devDependencies": {
20
+ "@rcompat/type": "^0.11.1"
17
21
  },
18
22
  "dependencies": {
19
- "@rcompat/assert": "^0.3.1",
20
- "@rcompat/is": "^0.1.4",
21
- "@rcompat/record": "^0.9.1",
22
- "mysql2": "^3.15.1",
23
- "pema": "",
24
- "@primate/core": "^0.4.0"
23
+ "@rcompat/assert": "^0.8.1",
24
+ "@rcompat/is": "^0.6.1",
25
+ "mysql2": "^3.20.0",
26
+ "@primate/core": "^0.6.0",
27
+ "pema": "0.6.0"
28
+ },
29
+ "peerDependencies": {
30
+ "primate": "^0.37.0"
25
31
  },
26
- "type": "module",
27
32
  "imports": {
28
33
  "#*": {
29
- "apekit": "./src/private/*.ts",
34
+ "@primate/source": "./src/private/*.ts",
30
35
  "default": "./lib/private/*.js"
31
36
  }
32
37
  },
33
38
  "exports": {
34
39
  ".": {
35
- "apekit": "./src/public/index.ts",
40
+ "@primate/source": "./src/public/index.ts",
36
41
  "default": "./lib/public/index.js"
37
42
  }
38
43
  },
@@ -40,6 +45,6 @@
40
45
  "build": "npm run clean && tsc",
41
46
  "clean": "rm -rf ./lib",
42
47
  "lint": "eslint .",
43
- "test": "npm run build && npx proby"
48
+ "test": "npx proby"
44
49
  }
45
50
  }
@@ -1,47 +0,0 @@
1
- import Database from "@primate/core/Database";
2
- import type As from "@primate/core/database/As";
3
- import type DataDict from "@primate/core/database/DataDict";
4
- import type TypeMap from "@primate/core/database/TypeMap";
5
- import type Dict from "@rcompat/type/Dict";
6
- import type StoreSchema from "pema/StoreSchema";
7
- declare const schema: import("pema").ObjectType<{
8
- database: import("pema/string").StringType;
9
- host: import("pema").DefaultType<import("pema/string").StringType, "localhost">;
10
- password: import("pema").OptionalType<import("pema/string").StringType>;
11
- port: import("pema").DefaultType<import("pema/uint").UintType<"u32">, 3306>;
12
- username: import("pema").OptionalType<import("pema/string").StringType>;
13
- }>;
14
- export default class MySQLDatabase extends Database {
15
- #private;
16
- static config: typeof schema.input;
17
- constructor(config?: typeof schema.input);
18
- get typemap(): TypeMap<Dict>;
19
- formatBinds(binds: Dict): Dict;
20
- close(): Promise<void>;
21
- get schema(): {
22
- create: (name: string, store: StoreSchema) => Promise<void>;
23
- delete: (name: string) => Promise<void>;
24
- };
25
- create<O extends Dict>(as: As, args: {
26
- record: DataDict;
27
- }): Promise<O>;
28
- read(as: As, args: {
29
- count: true;
30
- criteria: DataDict;
31
- }): Promise<number>;
32
- read(as: As, args: {
33
- criteria: DataDict;
34
- fields?: string[];
35
- limit?: number;
36
- sort?: Dict<"asc" | "desc">;
37
- }): Promise<Dict[]>;
38
- update(as: As, args: {
39
- changes: DataDict;
40
- criteria: DataDict;
41
- }): Promise<number>;
42
- delete(as: As, args: {
43
- criteria: DataDict;
44
- }): Promise<number>;
45
- }
46
- export {};
47
- //# sourceMappingURL=Database.d.ts.map
@@ -1,148 +0,0 @@
1
- import typemap from "#typemap";
2
- import Database from "@primate/core/Database";
3
- import assert from "@rcompat/assert";
4
- import mysql from "mysql2/promise";
5
- import pema from "pema";
6
- import string from "pema/string";
7
- import uint from "pema/uint";
8
- const schema = pema({
9
- database: string,
10
- host: string.default("localhost"),
11
- password: string.optional(),
12
- port: uint.port().default(3306),
13
- username: string.optional(),
14
- });
15
- export default class MySQLDatabase extends Database {
16
- #factory;
17
- #client;
18
- static config;
19
- constructor(config) {
20
- super(":");
21
- const parsed = schema.parse(config);
22
- this.#factory = () => mysql.createPool({
23
- host: parsed.host,
24
- port: parsed.port,
25
- database: parsed.database,
26
- user: parsed.username,
27
- password: parsed.password,
28
- connectionLimit: 10,
29
- queueLimit: 0,
30
- keepAliveInitialDelay: 0,
31
- enableKeepAlive: true,
32
- waitForConnections: true,
33
- namedPlaceholders: true,
34
- bigNumberStrings: true,
35
- supportBigNumbers: true,
36
- });
37
- }
38
- get typemap() {
39
- return typemap;
40
- }
41
- #get() {
42
- if (this.#client === undefined) {
43
- this.#client = this.#factory();
44
- }
45
- return this.#client;
46
- }
47
- formatBinds(binds) {
48
- return Object.fromEntries(Object.entries(binds).map(([k, v]) => [k.replace(/^[:$]/, ""), v]));
49
- }
50
- async close() {
51
- await this.#get().end();
52
- }
53
- async #with(executor) {
54
- const connection = await this.#get().getConnection();
55
- try {
56
- return await executor(connection);
57
- }
58
- finally {
59
- connection.release();
60
- }
61
- }
62
- async #new(name, store) {
63
- const body = Object.entries(store)
64
- .map(([key, value]) => `${this.ident(key)} ${this.column(value.datatype)}`)
65
- .join(",");
66
- const query = `CREATE TABLE IF NOT EXISTS ${this.ident(name)} (${body})`;
67
- await this.#with(async (connection) => {
68
- await connection.query(query);
69
- });
70
- }
71
- async #drop(name) {
72
- const query = `DROP TABLE IF EXISTS ${this.ident(name)}`;
73
- await this.#with(async (connection) => {
74
- await connection.query(query);
75
- });
76
- }
77
- get schema() {
78
- return {
79
- create: this.#new.bind(this),
80
- delete: this.#drop.bind(this),
81
- };
82
- }
83
- async create(as, args) {
84
- const keys = Object.keys(args.record);
85
- const columns = keys.map(k => this.ident(k));
86
- const values = keys.map(key => `:${key}`).join(",");
87
- const payload = `(${columns.join(",")}) VALUES (${values})`;
88
- const query = `INSERT INTO ${this.table(as)} ${payload};`;
89
- const binds = await this.bind(as.types, args.record);
90
- return this.#with(async (connection) => {
91
- const [{ insertId }] = await connection.query(query, binds);
92
- return this.unbind(as.types, { ...args.record, id: insertId });
93
- });
94
- }
95
- async read(as, args) {
96
- const where = this.toWhere(as.types, args.criteria);
97
- const binds = await this.bindCriteria(as.types, args.criteria);
98
- if (args.count === true) {
99
- return this.#with(async (connection) => {
100
- const query = `SELECT COUNT(*) AS n FROM ${this.table(as)} ${where}`;
101
- const [[{ n }]] = await connection.query(query, binds);
102
- return Number(n);
103
- });
104
- }
105
- const select = this.toSelect(as.types, args.fields);
106
- const sort = this.toSort(as.types, args.sort);
107
- const limit = this.toLimit(args.limit);
108
- const query = `SELECT ${select}
109
- FROM ${this.table(as)} ${where}${sort}${limit};`;
110
- return this.#with(async (connection) => {
111
- const [records] = await connection.query(query, binds);
112
- return records.map(record => this.unbind(as.types, record));
113
- });
114
- }
115
- async update(as, args) {
116
- assert(Object.keys(args.criteria).length > 0, "update: no criteria");
117
- const where = this.toWhere(as.types, args.criteria);
118
- const criteria = await this.bindCriteria(as.types, args.criteria);
119
- const { set, binds: set_binds } = await this.toSet(as.types, args.changes);
120
- const binds = { ...criteria, ...set_binds };
121
- const query = `
122
- UPDATE ${this.table(as)}
123
- ${set}
124
- WHERE id IN (
125
- SELECT id FROM (
126
- SELECT id FROM ${this.table(as)}
127
- ${where}
128
- ) AS to_update
129
- );
130
- `;
131
- return this.#with(async (connection) => {
132
- const [{ affectedRows }] = await connection.query(query, binds);
133
- return affectedRows;
134
- });
135
- }
136
- async delete(as, args) {
137
- assert(Object.keys(args.criteria).length > 0, "delete: no criteria");
138
- const where = this.toWhere(as.types, args.criteria);
139
- const binds = await this.bindCriteria(as.types, args.criteria);
140
- const query = `DELETE FROM ${this.table(as)} ${where}`;
141
- return this.#with(async (connection) => {
142
- const [{ affectedRows }] = await connection.query(query, binds);
143
- return affectedRows;
144
- });
145
- }
146
- ;
147
- }
148
- //# sourceMappingURL=Database.js.map