@primate/mysql 0.4.0 → 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/lib/private/ColumnTypes.d.ts +1 -1
- package/lib/private/MySQL.d.ts +48 -0
- package/lib/private/MySQL.js +461 -0
- package/lib/private/typemap.d.ts +1 -1
- package/lib/private/typemap.js +11 -15
- package/lib/public/index.d.ts +2 -2
- package/lib/public/index.js +2 -2
- package/package.json +18 -13
- package/lib/private/Database.d.ts +0 -47
- package/lib/private/Database.js +0 -148
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { As, DataDict, DB, Sort, With } from "@primate/core/db";
|
|
2
|
+
import type { Dict } from "@rcompat/type";
|
|
3
|
+
import type { StoreSchema } from "pema";
|
|
4
|
+
declare const schema: import("pema").ObjectType<{
|
|
5
|
+
database: import("pema").StringType;
|
|
6
|
+
host: import("pema").DefaultType<import("pema").StringType, "localhost">;
|
|
7
|
+
password: import("pema").OptionalType<import("pema").StringType>;
|
|
8
|
+
port: import("pema").DefaultType<import("pema").UintType<"u32">, 3306>;
|
|
9
|
+
username: import("pema").OptionalType<import("pema").StringType>;
|
|
10
|
+
}>;
|
|
11
|
+
export default class MySQL implements DB {
|
|
12
|
+
#private;
|
|
13
|
+
static config: typeof schema.input;
|
|
14
|
+
constructor(config?: typeof schema.input, options?: {
|
|
15
|
+
debug?: boolean;
|
|
16
|
+
});
|
|
17
|
+
get explain(): Dict<{
|
|
18
|
+
query: string;
|
|
19
|
+
plans: string[];
|
|
20
|
+
}>;
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
get schema(): {
|
|
23
|
+
create: (as: As, store: StoreSchema) => Promise<void>;
|
|
24
|
+
delete: (table: string) => Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
create<O extends Dict>(as: As, record: Dict): Promise<O>;
|
|
27
|
+
read(as: As, args: {
|
|
28
|
+
count: true;
|
|
29
|
+
where: DataDict;
|
|
30
|
+
with?: never;
|
|
31
|
+
}): Promise<number>;
|
|
32
|
+
read(as: As, args: {
|
|
33
|
+
where: DataDict;
|
|
34
|
+
fields?: string[];
|
|
35
|
+
limit?: number;
|
|
36
|
+
sort?: Sort;
|
|
37
|
+
with?: With;
|
|
38
|
+
}): Promise<Dict[]>;
|
|
39
|
+
update(as: As, args: {
|
|
40
|
+
set: DataDict;
|
|
41
|
+
where: DataDict;
|
|
42
|
+
}): Promise<number>;
|
|
43
|
+
delete(as: As, args: {
|
|
44
|
+
where: DataDict;
|
|
45
|
+
}): Promise<number>;
|
|
46
|
+
}
|
|
47
|
+
export {};
|
|
48
|
+
//# sourceMappingURL=MySQL.d.ts.map
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import typemap from "#typemap";
|
|
2
|
+
import common from "@primate/core/db";
|
|
3
|
+
import E from "@primate/core/db/error";
|
|
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 ? `${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
|
+
const schema = p({
|
|
46
|
+
database: p.string,
|
|
47
|
+
host: p.string.default("localhost"),
|
|
48
|
+
password: p.string.optional(),
|
|
49
|
+
port: p.uint.port().default(3306),
|
|
50
|
+
username: p.string.optional(),
|
|
51
|
+
});
|
|
52
|
+
export default class MySQL {
|
|
53
|
+
static config;
|
|
54
|
+
#factory;
|
|
55
|
+
#client;
|
|
56
|
+
#debug = false;
|
|
57
|
+
#explain = {};
|
|
58
|
+
constructor(config, options) {
|
|
59
|
+
const parsed = schema.parse(config);
|
|
60
|
+
this.#factory = () => mysql.createPool({
|
|
61
|
+
host: parsed.host,
|
|
62
|
+
port: parsed.port,
|
|
63
|
+
database: parsed.database,
|
|
64
|
+
user: parsed.username,
|
|
65
|
+
password: parsed.password,
|
|
66
|
+
namedPlaceholders: true,
|
|
67
|
+
bigNumberStrings: true,
|
|
68
|
+
supportBigNumbers: true,
|
|
69
|
+
});
|
|
70
|
+
this.#debug = options?.debug ?? false;
|
|
71
|
+
}
|
|
72
|
+
get #db() {
|
|
73
|
+
return this.#client ??= this.#factory();
|
|
74
|
+
}
|
|
75
|
+
async #sql(query, params) {
|
|
76
|
+
const [rows] = await this.#db.query(query, params);
|
|
77
|
+
return rows;
|
|
78
|
+
}
|
|
79
|
+
async #execute(query, params) {
|
|
80
|
+
const [result] = await this.#db.execute(query, params);
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
async #capture(table, query, params) {
|
|
84
|
+
if (!this.#debug)
|
|
85
|
+
return;
|
|
86
|
+
const rows = await this.#sql(`EXPLAIN ${query}`, params);
|
|
87
|
+
this.#explain[table] = { query, plans: rows.map(r => JSON.stringify(r)) };
|
|
88
|
+
}
|
|
89
|
+
get explain() {
|
|
90
|
+
return this.#explain;
|
|
91
|
+
}
|
|
92
|
+
async close() {
|
|
93
|
+
await this.#db.end();
|
|
94
|
+
}
|
|
95
|
+
get schema() {
|
|
96
|
+
return {
|
|
97
|
+
create: async (as, store) => {
|
|
98
|
+
const columns = [];
|
|
99
|
+
for (const [key, value] of Object.entries(store)) {
|
|
100
|
+
const type = get_column(value.datatype);
|
|
101
|
+
if (key === as.pk) {
|
|
102
|
+
const is_int = common.INT_TYPES.includes(value.datatype);
|
|
103
|
+
const auto = as.generate_pk && is_int ? " AUTO_INCREMENT" : "";
|
|
104
|
+
// 36 is UUID length
|
|
105
|
+
const pk_type = value.datatype === "string" ? "VARCHAR(36)" : type;
|
|
106
|
+
columns.push(`${sql.quote(key)} ${pk_type} PRIMARY KEY${auto}`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
columns.push(`${sql.quote(key)} ${type}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
await this.#sql(Q `CREATE TABLE IF NOT EXISTS ${as.table} (${columns})`);
|
|
113
|
+
},
|
|
114
|
+
delete: async (table) => {
|
|
115
|
+
await this.#sql(Q `DROP TABLE IF EXISTS ${table}`);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async #where(as, where) {
|
|
120
|
+
const fields = Object.keys(where);
|
|
121
|
+
if (fields.length === 0)
|
|
122
|
+
return ["", {}];
|
|
123
|
+
const parts = [];
|
|
124
|
+
const binds = {};
|
|
125
|
+
for (const field of fields) {
|
|
126
|
+
const value = where[field];
|
|
127
|
+
const datatype = as.types[field];
|
|
128
|
+
const q = sql.quote(field);
|
|
129
|
+
const quoted = q;
|
|
130
|
+
if (value === null) {
|
|
131
|
+
parts.push(`${quoted} IS NULL`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (is.dict(value)) {
|
|
135
|
+
const ops = Object.entries(value);
|
|
136
|
+
if (ops.length === 0)
|
|
137
|
+
throw E.operator_empty(field);
|
|
138
|
+
for (const [op, op_value] of ops) {
|
|
139
|
+
const bind_key = sql.bindKey(field, op);
|
|
140
|
+
const ph = `${BIND_BY}${bind_key}`;
|
|
141
|
+
const numeric = is_bigint_key(datatype);
|
|
142
|
+
const lhs = numeric ? bigint_cast(quoted) : quoted;
|
|
143
|
+
const rhs = numeric ? bigint_cast(ph) : ph;
|
|
144
|
+
binds[bind_key] = await bind_value(datatype, op_value);
|
|
145
|
+
switch (op) {
|
|
146
|
+
case "$like":
|
|
147
|
+
parts.push(`${quoted} LIKE BINARY ${ph}`);
|
|
148
|
+
break;
|
|
149
|
+
case "$ilike":
|
|
150
|
+
parts.push(`LOWER(${quoted}) LIKE LOWER(${ph})`);
|
|
151
|
+
break;
|
|
152
|
+
case "$ne":
|
|
153
|
+
parts.push(`${lhs} != ${rhs}`);
|
|
154
|
+
break;
|
|
155
|
+
case "$gt":
|
|
156
|
+
case "$gte":
|
|
157
|
+
case "$lt":
|
|
158
|
+
case "$lte":
|
|
159
|
+
case "$after":
|
|
160
|
+
case "$before": {
|
|
161
|
+
const sql_op = sql.OPS[op];
|
|
162
|
+
parts.push(`${lhs} ${sql_op} ${rhs}`);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
default:
|
|
166
|
+
throw E.operator_unknown(field, op);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
parts.push(`${quoted} = ${BIND_BY}${field}`);
|
|
172
|
+
binds[field] = await bind_value(datatype, value);
|
|
173
|
+
}
|
|
174
|
+
return [`WHERE ${parts.join(" AND ")}`, binds];
|
|
175
|
+
}
|
|
176
|
+
#set(set) {
|
|
177
|
+
const columns = Object.keys(set);
|
|
178
|
+
if (columns.length === 0)
|
|
179
|
+
throw E.field_required("set");
|
|
180
|
+
return Q `SET ${columns.map(c => `${sql.quote(c)}=${BIND_BY}s_${c}`)}`;
|
|
181
|
+
}
|
|
182
|
+
async #bind_set(types, set) {
|
|
183
|
+
const columns = Object.keys(set);
|
|
184
|
+
if (columns.length === 0)
|
|
185
|
+
throw E.field_required("set");
|
|
186
|
+
const params = {};
|
|
187
|
+
for (const c of columns) {
|
|
188
|
+
params[`s_${c}`] = await bind_value(types[c], set[c]);
|
|
189
|
+
}
|
|
190
|
+
return params;
|
|
191
|
+
}
|
|
192
|
+
async #generate_pk(as) {
|
|
193
|
+
const pk = as.pk;
|
|
194
|
+
const type = as.types[pk];
|
|
195
|
+
const table = as.table;
|
|
196
|
+
if (type === "string")
|
|
197
|
+
return crypto.randomUUID();
|
|
198
|
+
if (common.BIGINT_STRING_TYPES.includes(type)) {
|
|
199
|
+
const cast = bigint_cast(sql.quote(pk));
|
|
200
|
+
const q = `SELECT MAX(${cast}) AS v FROM ${sql.quote(table)}`;
|
|
201
|
+
const rows = await this.#sql(q);
|
|
202
|
+
const v = rows[0]?.v;
|
|
203
|
+
return v ? BigInt(v) + 1n : 1n;
|
|
204
|
+
}
|
|
205
|
+
throw "unreachable";
|
|
206
|
+
}
|
|
207
|
+
#create(record) {
|
|
208
|
+
const fields = Object.keys(record);
|
|
209
|
+
return [
|
|
210
|
+
fields.map(sql.quote),
|
|
211
|
+
fields.map(field => `${BIND_BY}${field}`),
|
|
212
|
+
];
|
|
213
|
+
}
|
|
214
|
+
async #create_params(types, record) {
|
|
215
|
+
const params = {};
|
|
216
|
+
for (const k of Object.keys(record)) {
|
|
217
|
+
params[k] = await bind_value(types[k], record[k]);
|
|
218
|
+
}
|
|
219
|
+
return params;
|
|
220
|
+
}
|
|
221
|
+
async create(as, record) {
|
|
222
|
+
assert.dict(record);
|
|
223
|
+
const pk = as.pk;
|
|
224
|
+
const table = as.table;
|
|
225
|
+
// PK provided or none defined, simple insert
|
|
226
|
+
if (pk === null || pk in record) {
|
|
227
|
+
const [keys, values] = this.#create(record);
|
|
228
|
+
const query = keys.length > 0
|
|
229
|
+
? Q `INSERT INTO ${table} (${keys}) VALUES (${values})`
|
|
230
|
+
: Q `INSERT INTO ${table} () VALUES ()`;
|
|
231
|
+
await this.#sql(query, await this.#create_params(as.types, record));
|
|
232
|
+
return record;
|
|
233
|
+
}
|
|
234
|
+
if (as.generate_pk === false)
|
|
235
|
+
throw E.pk_required(pk);
|
|
236
|
+
const type = as.types[pk];
|
|
237
|
+
// integer types, use AUTO_INCREMENT
|
|
238
|
+
if (!common.BIGINT_STRING_TYPES.includes(type) && type !== "string") {
|
|
239
|
+
const [keys, values] = this.#create(record);
|
|
240
|
+
const query = keys.length > 0
|
|
241
|
+
? Q `INSERT INTO ${table} (${keys}) VALUES (${values})`
|
|
242
|
+
: Q `INSERT INTO ${table} () VALUES ()`;
|
|
243
|
+
const result = await this.#execute(query, await this.#create_params(as.types, record));
|
|
244
|
+
const pk_value = unbind_value(type, result.insertId);
|
|
245
|
+
return { ...record, [pk]: pk_value };
|
|
246
|
+
}
|
|
247
|
+
// string or bigint, generate manually
|
|
248
|
+
const pk_value = await this.#generate_pk(as);
|
|
249
|
+
const to_insert = { ...record, [pk]: pk_value };
|
|
250
|
+
const [keys, values] = this.#create(to_insert);
|
|
251
|
+
const query = Q `INSERT INTO ${table} (${keys}) VALUES (${values})`;
|
|
252
|
+
await this.#sql(query, await this.#create_params(as.types, to_insert));
|
|
253
|
+
return to_insert;
|
|
254
|
+
}
|
|
255
|
+
async read(as, args) {
|
|
256
|
+
assert.dict(args.where);
|
|
257
|
+
this.#explain = {};
|
|
258
|
+
if (args.count === true)
|
|
259
|
+
return this.#count(as, args.where);
|
|
260
|
+
if (common.withed(args)) {
|
|
261
|
+
return sql.joinable(as, args.with)
|
|
262
|
+
? this.#read_joined(as, args)
|
|
263
|
+
: this.#read_phased(as, args);
|
|
264
|
+
}
|
|
265
|
+
return this.#read(as, args);
|
|
266
|
+
}
|
|
267
|
+
async #count(as, where) {
|
|
268
|
+
const [WHERE, binds] = await this.#where(as, where);
|
|
269
|
+
const table = as.table;
|
|
270
|
+
const query = `SELECT COUNT(*) AS n FROM ${sql.quote(table)} ${WHERE}`;
|
|
271
|
+
const rows = await this.#sql(query, binds);
|
|
272
|
+
await this.#capture(table, query, binds);
|
|
273
|
+
return Number(rows[0]?.n ?? 0);
|
|
274
|
+
}
|
|
275
|
+
async #base_query(as, args) {
|
|
276
|
+
const SELECT = args.fields === undefined
|
|
277
|
+
? "*"
|
|
278
|
+
: args.fields.map(sql.quote).join(", ");
|
|
279
|
+
const [WHERE, binds] = await this.#where(as, args.where);
|
|
280
|
+
const ORDER_BY = order_by(as.types, args.sort);
|
|
281
|
+
const LIMIT = sql.limit(args.limit);
|
|
282
|
+
const table = sql.quote(as.table);
|
|
283
|
+
return [
|
|
284
|
+
`SELECT ${SELECT} FROM ${table} ${WHERE}${ORDER_BY}${LIMIT}`,
|
|
285
|
+
binds,
|
|
286
|
+
];
|
|
287
|
+
}
|
|
288
|
+
async #read(as, args) {
|
|
289
|
+
const [query, binds] = await this.#base_query(as, args);
|
|
290
|
+
const rows = await this.#sql(query, binds);
|
|
291
|
+
await this.#capture(as.table, query, binds);
|
|
292
|
+
return rows.map(r => unbind(as.types, r));
|
|
293
|
+
}
|
|
294
|
+
async #read_phased(as, args) {
|
|
295
|
+
const fields = common.expand(as, args.fields, args.with);
|
|
296
|
+
const rows = await this.#read(as, { ...args, fields });
|
|
297
|
+
const out = rows.map(row => common.project(row, args.fields));
|
|
298
|
+
for (const [table, relation] of Object.entries(args.with)) {
|
|
299
|
+
await this.#attach_relation(as, { rows, out, table, relation });
|
|
300
|
+
}
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
async #attach_relation(as, args) {
|
|
304
|
+
const relation = args.relation;
|
|
305
|
+
const by = relation.reverse ? relation.as.pk : relation.fk;
|
|
306
|
+
if (by === null)
|
|
307
|
+
throw E.relation_requires_pk("target");
|
|
308
|
+
const parent_by = relation.reverse ? relation.fk : as.pk;
|
|
309
|
+
if (parent_by === null)
|
|
310
|
+
throw E.relation_requires_pk("parent");
|
|
311
|
+
const join_values = [...new Set(args.rows.map(r => r[parent_by]).filter(v => v != null))];
|
|
312
|
+
const is_many = relation.kind === "many";
|
|
313
|
+
const many = is_many ? [] : null;
|
|
314
|
+
if (join_values.length === 0) {
|
|
315
|
+
for (const row of args.out)
|
|
316
|
+
row[args.table] = many;
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const related = await this.#load_related({ by, join_values, ...relation });
|
|
320
|
+
const grouped = new Map();
|
|
321
|
+
for (const row of related) {
|
|
322
|
+
const key = row[by];
|
|
323
|
+
grouped.set(key, grouped.get(key)?.concat(row) ?? [row]);
|
|
324
|
+
}
|
|
325
|
+
for (let i = 0; i < args.out.length; i++) {
|
|
326
|
+
const join_value = args.rows[i][parent_by];
|
|
327
|
+
if (join_value == null) {
|
|
328
|
+
args.out[i][args.table] = many;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const rows = grouped.get(join_value) ?? [];
|
|
332
|
+
args.out[i][args.table] = is_many
|
|
333
|
+
? rows.map(r => common.project(r, relation.fields))
|
|
334
|
+
: rows[0] ? common.project(rows[0], relation.fields) : null;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
async #load_related(args) {
|
|
338
|
+
const per_parent = args.kind === "one" ? 1 : args.limit;
|
|
339
|
+
const in_binds = {};
|
|
340
|
+
const placeholders = [];
|
|
341
|
+
for (let i = 0; i < args.join_values.length; i++) {
|
|
342
|
+
const key = `${args.by}__in${i}`;
|
|
343
|
+
in_binds[key] = await bind_value(args.as.types[args.by], args.join_values[i]);
|
|
344
|
+
placeholders.push(`${BIND_BY}${key}`);
|
|
345
|
+
}
|
|
346
|
+
const [where, where_binds] = await this.#where(args.as, args.where);
|
|
347
|
+
const where_part = where ? where.slice("WHERE ".length) : "";
|
|
348
|
+
const numeric = is_bigint_key(args.as.types[args.by]);
|
|
349
|
+
const in_placeholders = numeric
|
|
350
|
+
? placeholders.map(bigint_cast)
|
|
351
|
+
: placeholders;
|
|
352
|
+
const cast = bigint_cast(sql.quote(args.by));
|
|
353
|
+
const lhs = numeric ? cast : Q `${args.by}`;
|
|
354
|
+
const in_part = `${lhs} IN (${in_placeholders.join(", ")})`;
|
|
355
|
+
const where_parts = where_part
|
|
356
|
+
? [`(${where_part})`, `(${in_part})`]
|
|
357
|
+
: [in_part];
|
|
358
|
+
const WHERE = `WHERE ${where_parts.join(" AND ")}`;
|
|
359
|
+
const all_columns = Object.keys(args.as.types);
|
|
360
|
+
const fields = args.fields !== undefined && args.fields.length > 0
|
|
361
|
+
? args.fields
|
|
362
|
+
: all_columns;
|
|
363
|
+
const select_fields = [...new Set([...fields, args.by])];
|
|
364
|
+
const SELECT = select_fields.map(sql.quote).join(", ");
|
|
365
|
+
const base_order = `${numeric ? cast : Q `${args.by}`} ASC`;
|
|
366
|
+
const user_order = order_by(args.as.types, args.sort)
|
|
367
|
+
.replace(/^ ORDER BY /, "");
|
|
368
|
+
const ORDER_BY = user_order
|
|
369
|
+
? ` ORDER BY ${base_order}, ${user_order}`
|
|
370
|
+
: ` ORDER BY ${base_order}`;
|
|
371
|
+
const table = sql.quote(args.as.table);
|
|
372
|
+
let query;
|
|
373
|
+
if (per_parent !== undefined) {
|
|
374
|
+
const partition = numeric ? cast : Q `${args.by}`;
|
|
375
|
+
const rn_order = user_order ? ` ORDER BY ${user_order}` : "";
|
|
376
|
+
query = `
|
|
377
|
+
SELECT ${SELECT} FROM (
|
|
378
|
+
SELECT
|
|
379
|
+
${SELECT},
|
|
380
|
+
ROW_NUMBER() OVER (
|
|
381
|
+
PARTITION BY ${partition}
|
|
382
|
+
${rn_order}
|
|
383
|
+
) AS __rn
|
|
384
|
+
FROM ${table}
|
|
385
|
+
${WHERE}
|
|
386
|
+
) ranked
|
|
387
|
+
WHERE __rn <= ${per_parent}
|
|
388
|
+
${ORDER_BY}
|
|
389
|
+
`;
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
query = `SELECT ${SELECT} FROM ${table} ${WHERE}${ORDER_BY}`;
|
|
393
|
+
}
|
|
394
|
+
const binds = { ...where_binds, ...in_binds };
|
|
395
|
+
const rows = await this.#sql(query, binds);
|
|
396
|
+
await this.#capture(args.as.table, query, binds);
|
|
397
|
+
return rows.map(r => unbind(args.as.types, r));
|
|
398
|
+
}
|
|
399
|
+
#join(as, aliases, relation) {
|
|
400
|
+
const parent_alias = aliases[as.table];
|
|
401
|
+
const alias = aliases[relation.as.table];
|
|
402
|
+
const table = sql.quote(relation.as.table);
|
|
403
|
+
const by = relation.reverse ? relation.as.pk : relation.fk;
|
|
404
|
+
const parent_by = relation.reverse ? relation.fk : as.pk;
|
|
405
|
+
if (by === null)
|
|
406
|
+
throw E.relation_requires_pk("target");
|
|
407
|
+
if (parent_by === null)
|
|
408
|
+
throw E.relation_requires_pk("parent");
|
|
409
|
+
const left = `${alias}.${sql.quote(by)}`;
|
|
410
|
+
const right = `${parent_alias}.${sql.quote(parent_by)}`;
|
|
411
|
+
return `LEFT JOIN ${table} ${alias} ON ${left} = ${right}`;
|
|
412
|
+
}
|
|
413
|
+
async #read_joined(as, args) {
|
|
414
|
+
if (as.pk === null)
|
|
415
|
+
throw E.relation_requires_pk("parent");
|
|
416
|
+
const tables = [as.table, ...Object.values(args.with).map(r => r.as.table)];
|
|
417
|
+
const aliases = sql.aliases(tables);
|
|
418
|
+
const alias = aliases[as.table];
|
|
419
|
+
const fields = common.fields(args.fields, as.pk) ?? Object.keys(as.types);
|
|
420
|
+
const SELECT = [
|
|
421
|
+
...fields.map(f => `${alias}.${sql.quote(f)} AS ${alias}_${f}`),
|
|
422
|
+
...Object.values(args.with).flatMap(relation => {
|
|
423
|
+
const r_alias = aliases[relation.as.table];
|
|
424
|
+
const r_fields = relation.as.pk !== null
|
|
425
|
+
? common.fields(relation.fields, relation.as.pk)
|
|
426
|
+
: relation.fields;
|
|
427
|
+
return (r_fields ?? Object.keys(relation.as.types))
|
|
428
|
+
.map(f => `${r_alias}.${sql.quote(f)} AS ${r_alias}_${f}`);
|
|
429
|
+
}),
|
|
430
|
+
].join(", ");
|
|
431
|
+
const [SUBQUERY, binds] = await this.#base_query(as, { ...args, fields });
|
|
432
|
+
const FROM = `(${SUBQUERY}) ${alias}`;
|
|
433
|
+
const JOINS = Object.values(args.with)
|
|
434
|
+
.map(relation => this.#join(as, aliases, relation))
|
|
435
|
+
.join("\n");
|
|
436
|
+
const ORDER_BY = order_by(as.types, args.sort, alias);
|
|
437
|
+
const query = `SELECT ${SELECT} FROM ${FROM} ${JOINS} ${ORDER_BY}`;
|
|
438
|
+
const rows = await this.#sql(query, binds);
|
|
439
|
+
this.#capture(as.table, query, binds);
|
|
440
|
+
return sql.nest(as, { rows, aliases }, args, unbind_value);
|
|
441
|
+
}
|
|
442
|
+
async update(as, args) {
|
|
443
|
+
assert.nonempty(args.set);
|
|
444
|
+
assert.dict(args.where);
|
|
445
|
+
const [WHERE, where_binds] = await this.#where(as, args.where);
|
|
446
|
+
const SET = this.#set(args.set);
|
|
447
|
+
const set_binds = await this.#bind_set(as.types, args.set);
|
|
448
|
+
const query = `UPDATE ${sql.quote(as.table)} ${SET} ${WHERE}`;
|
|
449
|
+
const binds = { ...where_binds, ...set_binds };
|
|
450
|
+
const result = await this.#execute(query, binds);
|
|
451
|
+
return result.affectedRows;
|
|
452
|
+
}
|
|
453
|
+
async delete(as, args) {
|
|
454
|
+
assert.nonempty(args.where);
|
|
455
|
+
const [WHERE, binds] = await this.#where(as, args.where);
|
|
456
|
+
const query = `DELETE FROM ${sql.quote(as.table)} ${WHERE}`;
|
|
457
|
+
const result = await this.#execute(query, binds);
|
|
458
|
+
return result.affectedRows;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
//# sourceMappingURL=MySQL.js.map
|
package/lib/private/typemap.d.ts
CHANGED
package/lib/private/typemap.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import numeric from "@rcompat/is/numeric";
|
|
2
1
|
function identity(column) {
|
|
3
2
|
return {
|
|
4
3
|
bind: value => value,
|
|
@@ -16,8 +15,7 @@ function number(column) {
|
|
|
16
15
|
const typemap = {
|
|
17
16
|
blob: {
|
|
18
17
|
async bind(value) {
|
|
19
|
-
|
|
20
|
-
return new Uint8Array(arrayBuffer);
|
|
18
|
+
return new Uint8Array(await value.arrayBuffer());
|
|
21
19
|
},
|
|
22
20
|
column: "BLOB",
|
|
23
21
|
unbind(value) {
|
|
@@ -57,18 +55,16 @@ const typemap = {
|
|
|
57
55
|
},
|
|
58
56
|
},
|
|
59
57
|
i8: number("TINYINT"),
|
|
60
|
-
primary: {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
},
|
|
71
|
-
},
|
|
58
|
+
/*primary: {
|
|
59
|
+
bind(value) {
|
|
60
|
+
if (is.numeric(value)) return Number(value);
|
|
61
|
+
throw new Error(`\`${value}\` is not a valid primary key value`);
|
|
62
|
+
},
|
|
63
|
+
column: "INT NOT NULL AUTO_INCREMENT PRIMARY KEY",
|
|
64
|
+
unbind(value) {
|
|
65
|
+
return String(value);
|
|
66
|
+
},
|
|
67
|
+
},*/
|
|
72
68
|
string: identity("TEXT"),
|
|
73
69
|
time: identity("TEXT"),
|
|
74
70
|
u128: {
|
package/lib/public/index.d.ts
CHANGED
package/lib/public/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
export default (config) => new
|
|
1
|
+
import MySQL from "#MySQL";
|
|
2
|
+
export default (config) => new MySQL(config);
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primate/mysql",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.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
|
-
"
|
|
14
|
-
"type": "
|
|
15
|
-
"url": "https://github.com/primate-run/primate",
|
|
16
|
-
"directory": "packages/mysql"
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@rcompat/type": "^0.9.0"
|
|
17
21
|
},
|
|
18
22
|
"dependencies": {
|
|
19
|
-
"@rcompat/assert": "^0.
|
|
20
|
-
"@rcompat/is": "^0.
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"pema": ""
|
|
24
|
-
|
|
23
|
+
"@rcompat/assert": "^0.6.0",
|
|
24
|
+
"@rcompat/is": "^0.4.2",
|
|
25
|
+
"mysql2": "^3.17.1",
|
|
26
|
+
"@primate/core": "^0.5.0",
|
|
27
|
+
"pema": "0.5.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"primate": "^0.36.0"
|
|
25
31
|
},
|
|
26
|
-
"type": "module",
|
|
27
32
|
"imports": {
|
|
28
33
|
"#*": {
|
|
29
34
|
"apekit": "./src/private/*.ts",
|
|
@@ -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
|
package/lib/private/Database.js
DELETED
|
@@ -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
|