@primate/sqlite 0.7.0 → 0.9.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,23 +1,24 @@
1
- import type { As, DataDict, DB, Sort, With } from "@primate/core/db";
1
+ import type { As, CheckPlaceholders, DataDict, ExtractPlaceholders, Schema, Sort, SQLDB, With } from "@primate/core/db";
2
+ import Client from "@rcompat/sqlite";
2
3
  import type { Dict } from "@rcompat/type";
3
- import type { StoreSchema } from "pema";
4
- declare const schema: import("pema").ObjectType<{
5
- database: import("pema").DefaultType<import("pema").StringType, ":memory:">;
4
+ import type { ObjectType, Parsed } from "pema";
5
+ declare const schema: ObjectType<import("pema").NormalizeSchemaObject<{
6
+ readonly database: import("pema").DefaultType<import("pema").StringType, ":memory:">;
7
+ }>, {
8
+ database: string;
6
9
  }>;
7
- export default class SQLite implements DB {
10
+ export default class SQLite implements SQLDB<Client> {
8
11
  #private;
9
12
  static config: typeof schema.input;
10
13
  constructor(config?: typeof schema.input, options?: {
11
14
  debug?: boolean;
12
15
  });
16
+ get client(): Client;
13
17
  get explain(): Dict<{
14
18
  query: string;
15
19
  plans: string[];
16
20
  }>;
17
- get schema(): {
18
- create: (as: As, store: StoreSchema) => void;
19
- delete: (table: string) => void;
20
- };
21
+ get schema(): Schema;
21
22
  create<O extends Dict>(as: As, record: Dict): Promise<O>;
22
23
  read(as: As, args: {
23
24
  count: true;
@@ -39,6 +40,13 @@ export default class SQLite implements DB {
39
40
  where: DataDict;
40
41
  }): Promise<number>;
41
42
  close(): void;
43
+ sql<Q extends string, I extends ObjectType<{
44
+ [K in ExtractPlaceholders<Q>]: Parsed<unknown>;
45
+ }> | undefined, O extends Parsed<unknown> | undefined>(options: {
46
+ input?: I extends ObjectType<any> ? CheckPlaceholders<I, Q> : I;
47
+ query: Q;
48
+ output?: O;
49
+ }): (args: I extends ObjectType<any> ? I["infer"] : void) => Promise<O extends Parsed<unknown> ? O["infer"] : void>;
42
50
  }
43
51
  export {};
44
52
  //# sourceMappingURL=SQLite.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import typemap from "#typemap";
2
- import common from "@primate/core/db";
3
- import E from "@primate/core/db/error";
2
+ import base from "@primate/core/db";
3
+ import E from "@primate/core/db/errors";
4
4
  import sql from "@primate/core/db/sql";
5
5
  import assert from "@rcompat/assert";
6
6
  import is from "@rcompat/is";
@@ -65,8 +65,8 @@ function cmp_expr(field, key, op, datatype) {
65
65
  const rhs = `${BIND_BY}${key}`;
66
66
  const sql_op = sql.OPS[op];
67
67
  assert.defined(sql_op, E.operator_unknown(field, op));
68
- if (common.BIGINT_STRING_TYPES.includes(datatype)) {
69
- const cmp = common.UNSIGNED_BIGINT_TYPES.includes(datatype)
68
+ if (base.BIGINT_STRING_TYPES.includes(datatype)) {
69
+ const cmp = base.UNSIGNED_BIGINT_TYPES.includes(datatype)
70
70
  ? u_cmp(field, `CAST(${field} AS TEXT)`, `CAST(${rhs} AS TEXT)`, sql_op)
71
71
  : i_cmp(field, rhs, sql_op);
72
72
  return `(${cmp})`;
@@ -91,16 +91,35 @@ function unbind_one(key, value) {
91
91
  async function bind(types, fields) {
92
92
  const out = {};
93
93
  for (const [key, value] of Object.entries(fields)) {
94
- if (is.dict(value))
95
- throw E.operator_scalar(key);
96
94
  const raw = key.startsWith("s_") ? key.slice(2) : key;
97
- out[`${BIND_BY}${key}`] = await bind_one(types[raw.split("__")[0]], value);
95
+ const type = types[raw.split("__")[0]];
96
+ if (is.dict(value) && type !== "json")
97
+ throw E.operator_scalar(key);
98
+ out[`${BIND_BY}${key}`] = await bind_one(type, value);
98
99
  }
99
100
  return out;
100
101
  }
101
102
  function unbind(types, row) {
102
103
  return sql.unbind(types, row, unbind_one);
103
104
  }
105
+ function columns_to_types(type) {
106
+ const t = type.toUpperCase();
107
+ if (t === "TEXT")
108
+ return [
109
+ "string", "url", "time", "datetime", "json",
110
+ "u64", "u128", "i64", "i128",
111
+ ...base.UUID_TYPES,
112
+ ];
113
+ if (t === "INTEGER")
114
+ return [
115
+ "boolean", "i8", "u8", "i16", "u16", "i32", "u32",
116
+ ];
117
+ if (t === "REAL")
118
+ return ["f32", "f64"];
119
+ if (t === "BLOB")
120
+ return ["blob"];
121
+ return [];
122
+ }
104
123
  export default class SQLite {
105
124
  #factory;
106
125
  #client;
@@ -115,6 +134,9 @@ export default class SQLite {
115
134
  get #db() {
116
135
  return this.#client ??= this.#factory();
117
136
  }
137
+ get client() {
138
+ return this.#db;
139
+ }
118
140
  #sql(query) {
119
141
  return this.#db.prepare(query);
120
142
  }
@@ -137,25 +159,55 @@ export default class SQLite {
137
159
  }
138
160
  get schema() {
139
161
  return {
140
- create: (as, store) => {
162
+ create: (table, pk, types) => {
141
163
  const columns = [];
142
- for (const [key, value] of Object.entries(store)) {
143
- const column_type = get_column(value.datatype);
164
+ for (const [key, value] of Object.entries(types)) {
165
+ const column_type = get_column(value);
144
166
  const column = `${sql.quote(key)} ${column_type}`;
145
167
  const is_int = column_type === "INTEGER";
146
- if (key === as.pk) {
147
- const generate = as.generate_pk && is_int ? " AUTOINCREMENT" : "";
168
+ if (key === pk.name) {
169
+ const generate = pk.generate && is_int ? " AUTOINCREMENT" : "";
148
170
  columns.push(`${column} PRIMARY KEY${generate}`);
149
171
  }
150
172
  else {
151
173
  columns.push(column);
152
174
  }
153
175
  }
154
- this.#sql(Q `CREATE TABLE IF NOT EXISTS ${as.table} (${columns})`).run();
176
+ this.#sql(Q `CREATE TABLE IF NOT EXISTS ${table} (${columns})`).run();
155
177
  },
156
178
  delete: (table) => {
157
179
  this.#sql(Q `DROP TABLE IF EXISTS ${table}`).run();
158
180
  },
181
+ introspect: (table) => {
182
+ const rows = this.#sql(`PRAGMA table_info(${sql.quote(table)})`)
183
+ .all();
184
+ if (rows.length === 0)
185
+ return null;
186
+ const result = {};
187
+ for (const row of rows) {
188
+ const candidates = columns_to_types(row.type);
189
+ if (candidates.length > 0)
190
+ result[row.name] = candidates;
191
+ }
192
+ return result;
193
+ },
194
+ alter: (table, diff) => {
195
+ const existing = this.schema.introspect(table);
196
+ if (existing === null)
197
+ throw E.table_not_found(table);
198
+ for (const [from, to] of diff.rename) {
199
+ this.#sql(Q `ALTER TABLE ${table}
200
+ RENAME COLUMN ${[sql.quote(from)]} TO ${[sql.quote(to)]}`).run();
201
+ }
202
+ for (const field of diff.drop) {
203
+ this.#sql(Q `ALTER TABLE ${table}
204
+ DROP COLUMN ${[sql.quote(field)]}`).run();
205
+ }
206
+ for (const [field, type] of Object.entries(diff.add)) {
207
+ this.#sql(Q `ALTER TABLE ${table}
208
+ ADD COLUMN ${[`${sql.quote(field)} ${get_column(type)}`]}`).run();
209
+ }
210
+ },
159
211
  };
160
212
  }
161
213
  #where(types, where, alias) {
@@ -167,7 +219,7 @@ export default class SQLite {
167
219
  const value = where[field];
168
220
  const datatype = types[field];
169
221
  const q = sql.quote(field);
170
- const quoted = alias ? `${alias}.${q}` : q;
222
+ const quoted = is.defined(alias) ? `${alias}.${q}` : q;
171
223
  if (value === null) {
172
224
  parts.push(`${quoted} IS NULL`);
173
225
  continue;
@@ -196,6 +248,12 @@ export default class SQLite {
196
248
  case "$before":
197
249
  parts.push(cmp_expr(quoted, bound_key, op, datatype));
198
250
  break;
251
+ case "$in": {
252
+ const arr = value.$in;
253
+ const placeholders = arr.map((_, i) => `${BIND_BY}${sql.bindKey(field, `in${i}`)}`);
254
+ parts.push(`${quoted} IN (${placeholders.join(", ")})`);
255
+ break;
256
+ }
199
257
  default:
200
258
  throw E.operator_unknown(field, op);
201
259
  }
@@ -229,6 +287,14 @@ export default class SQLite {
229
287
  case "$ne":
230
288
  filtered[bound_key] = op_value;
231
289
  break;
290
+ case "$in": {
291
+ const arr = op_value;
292
+ for (let i = 0; i < arr.length; i++) {
293
+ const bound_key = sql.bindKey(field, `in${i}`);
294
+ filtered[bound_key] = arr[i];
295
+ }
296
+ break;
297
+ }
232
298
  default:
233
299
  throw E.operator_unknown(field, op);
234
300
  }
@@ -255,9 +321,9 @@ export default class SQLite {
255
321
  #generate_pk(as) {
256
322
  const pk = as.pk;
257
323
  const type = as.types[pk];
258
- if (type === "string")
259
- return crypto.randomUUID();
260
- if (common.BIGINT_STRING_TYPES.includes(type)) {
324
+ if (base.is_uuid_type(type))
325
+ return base.generate_uuid(type);
326
+ if (base.BIGINT_STRING_TYPES.includes(type)) {
261
327
  const query = Q `SELECT ${pk} AS v
262
328
  FROM ${as.table} ORDER BY LENGTH(${pk}) DESC, ${pk} DESC LIMIT 1`;
263
329
  const rows = this.#sql(query).all();
@@ -289,7 +355,7 @@ export default class SQLite {
289
355
  throw E.pk_required(pk);
290
356
  const type = as.types[pk];
291
357
  // integer types, use RETURNING
292
- if (!common.BIGINT_STRING_TYPES.includes(type) && type !== "string") {
358
+ if (!base.BIGINT_STRING_TYPES.includes(type) && !base.is_uuid_type(type)) {
293
359
  const [keys, values] = this.#create(record);
294
360
  const query = keys.length > 0
295
361
  ? Q `INSERT INTO ${table} (${keys}) VALUES (${values}) RETURNING ${pk}`
@@ -311,7 +377,7 @@ export default class SQLite {
311
377
  this.#explain = {};
312
378
  if (args.count === true)
313
379
  return this.#count(as, args.where);
314
- if (common.withed(args)) {
380
+ if (base.withed(args)) {
315
381
  return sql.joinable(as, args.with)
316
382
  ? this.#read_joined(as, args)
317
383
  : this.#read_phased(as, args);
@@ -325,9 +391,10 @@ export default class SQLite {
325
391
  const query = `SELECT COUNT(*) AS n FROM ${sql.quote(table)} ${WHERE}`;
326
392
  this.#capture(table, query, binds);
327
393
  const rows = this.#sql(query).all(binds);
328
- assert.true(rows.length === 1, `COUNT(*) must return 1 row (got ${rows.length})`);
394
+ const length = rows.length;
395
+ assert.true(length === 1, E.record_number_invalid(length, "count"));
329
396
  const n = rows[0].n;
330
- assert.bigint(n, `COUNT returned non-bigint: ${typeof n}`);
397
+ assert.bigint(n, E.return_invalid(n, "bigint", "count"));
331
398
  if (n > BigInt(Number.MAX_SAFE_INTEGER))
332
399
  throw E.count_overflow(table, n);
333
400
  return Number(n);
@@ -338,9 +405,9 @@ export default class SQLite {
338
405
  return this.#query_all(as, query, binds);
339
406
  }
340
407
  async #read_phased(as, args) {
341
- const fields = common.expand(as, args.fields, args.with);
408
+ const fields = base.expand(as, args.fields, args.with);
342
409
  const rows = await this.#read_base(as, { ...args, fields });
343
- const out = rows.map(row => common.project(row, args.fields));
410
+ const out = rows.map(row => base.project(row, args.fields));
344
411
  for (const [table, relation] of Object.entries(args.with)) {
345
412
  await this.#attach_relation(as, { rows, out, table, relation });
346
413
  }
@@ -348,10 +415,11 @@ export default class SQLite {
348
415
  }
349
416
  async #attach_relation(as, args) {
350
417
  const relation = args.relation;
351
- const by = relation.reverse ? relation.as.pk : relation.fk;
418
+ const reverse = assert.maybe.boolean(relation.reverse) ?? false;
419
+ const by = reverse ? relation.as.pk : relation.fk;
352
420
  if (by === null)
353
421
  throw E.relation_requires_pk("target");
354
- const parent_by = relation.reverse ? relation.fk : as.pk;
422
+ const parent_by = reverse ? relation.fk : as.pk;
355
423
  if (parent_by === null)
356
424
  throw E.relation_requires_pk("parent");
357
425
  const join_values = [...new Set(args.rows.map(r => r[parent_by]).filter(v => v != null))];
@@ -376,8 +444,8 @@ export default class SQLite {
376
444
  }
377
445
  const rows = grouped.get(join_value) ?? [];
378
446
  args.out[i][args.table] = is_many
379
- ? rows.map(r => common.project(r, relation.fields))
380
- : rows[0] ? common.project(rows[0], relation.fields) : null;
447
+ ? rows.map(r => base.project(r, relation.fields))
448
+ : rows[0] ? base.project(rows[0], relation.fields) : null;
381
449
  }
382
450
  }
383
451
  async #load_related(args) {
@@ -400,7 +468,7 @@ export default class SQLite {
400
468
  const fields = args.fields !== undefined && args.fields.length > 0
401
469
  ? args.fields
402
470
  : all_columns;
403
- const SELECT = sql.columns(args.as.types, common.fields(fields, args.by));
471
+ const SELECT = sql.columns(args.as.types, base.fields(fields, args.by));
404
472
  const base_order = `${sql.quote(args.by)} ASC`;
405
473
  const user_order = sql.orderBy(args.as.types, args.sort)
406
474
  .replace(/^ ORDER BY /, "");
@@ -442,11 +510,12 @@ export default class SQLite {
442
510
  return this.#query_all(args.as, query, binds);
443
511
  }
444
512
  #join(as, aliases, relation) {
513
+ const reverse = assert.maybe.boolean(relation.reverse) ?? false;
445
514
  const parent_alias = aliases[as.table];
446
515
  const alias = aliases[relation.as.table];
447
516
  const table = sql.quote(relation.as.table);
448
- const by = relation.reverse ? relation.as.pk : relation.fk;
449
- const parent_by = relation.reverse ? relation.fk : as.pk;
517
+ const by = reverse ? relation.as.pk : relation.fk;
518
+ const parent_by = reverse ? relation.fk : as.pk;
450
519
  if (by === null)
451
520
  throw E.relation_requires_pk("target");
452
521
  if (parent_by === null)
@@ -460,8 +529,9 @@ export default class SQLite {
460
529
  const WHERE = this.#where(as.types, args.where);
461
530
  const ORDER_BY = sql.orderBy(as.types, args.sort);
462
531
  const LIMIT = sql.limit(args.limit);
532
+ const OFFSET = args.offset !== undefined ? ` OFFSET ${args.offset}` : "";
463
533
  const table = sql.quote(as.table);
464
- return `SELECT ${SELECT} FROM ${table} ${WHERE}${ORDER_BY}${LIMIT}`;
534
+ return `SELECT ${SELECT} FROM ${table} ${WHERE}${ORDER_BY}${LIMIT}${OFFSET}`;
465
535
  }
466
536
  async #read_joined(as, args) {
467
537
  if (as.pk === null)
@@ -469,13 +539,13 @@ export default class SQLite {
469
539
  const tables = [as.table, ...Object.values(args.with).map(r => r.as.table)];
470
540
  const aliases = sql.aliases(tables);
471
541
  const alias = aliases[as.table];
472
- const fields = common.fields(args.fields, as.pk);
542
+ const fields = base.fields(args.fields, as.pk);
473
543
  // subquery for limited parents
474
544
  const SELECT = [
475
545
  sql.aliased(aliases, as, fields),
476
546
  ...Object.values(args.with).map(relation => {
477
547
  return sql.aliased(aliases, relation.as, relation.as.pk !== null
478
- ? common.fields(relation.fields, relation.as.pk)
548
+ ? base.fields(relation.fields, relation.as.pk)
479
549
  : relation.fields);
480
550
  }),
481
551
  ].join(", ");
@@ -511,5 +581,33 @@ export default class SQLite {
511
581
  close() {
512
582
  this.#db.close();
513
583
  }
584
+ sql(options) {
585
+ if (is.defined(options.input)) {
586
+ const placeholders = new Set([...options.query.matchAll(/:(\w+)/g)].map(m => m[1]));
587
+ const input_keys = new Set(Object.keys(options.input.properties));
588
+ for (const key of input_keys) {
589
+ if (!placeholders.has(key))
590
+ throw E.sql_placeholder_missing(key);
591
+ }
592
+ for (const key of placeholders) {
593
+ if (!input_keys.has(key))
594
+ throw E.sql_input_missing(key);
595
+ }
596
+ }
597
+ return async (args) => {
598
+ const input = is.defined(options.input)
599
+ ? options.input.parse(args)
600
+ : undefined;
601
+ const query = options.query.replace(/:(\w+)/g, (_, name) => `$${name}`);
602
+ const binds = is.defined(input)
603
+ ? await bind(Object.fromEntries(Object.entries(options.input.properties)
604
+ .map(([k, v]) => [k, v.datatype])), input)
605
+ : {};
606
+ const rows = this.#sql(query).all(binds);
607
+ return (is.defined(options.output)
608
+ ? options.output.parse(rows)
609
+ : undefined);
610
+ };
611
+ }
514
612
  }
515
613
  //# sourceMappingURL=SQLite.js.map
@@ -63,6 +63,15 @@ const typemap = {
63
63
  },
64
64
  },
65
65
  i8: number("INTEGER"),
66
+ json: {
67
+ bind(value) {
68
+ return JSON.stringify(value);
69
+ },
70
+ column: "TEXT",
71
+ unbind(value) {
72
+ return JSON.parse(value);
73
+ },
74
+ },
66
75
  string: identity("TEXT"),
67
76
  time: identity("TEXT"),
68
77
  u128: {
@@ -95,6 +104,9 @@ const typemap = {
95
104
  return new URL(value);
96
105
  },
97
106
  },
107
+ uuid: identity("TEXT"),
108
+ uuid_v4: identity("TEXT"),
109
+ uuid_v7: identity("TEXT"),
98
110
  };
99
111
  export default typemap;
100
112
  //# sourceMappingURL=typemap.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primate/sqlite",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "SQLite databases for Primate",
5
5
  "homepage": "https://primate.run/docs/database/sqlite",
6
6
  "bugs": "https://github.com/primate-run/primate/issues",
@@ -17,27 +17,27 @@
17
17
  "!/**/*.spec.*"
18
18
  ],
19
19
  "dependencies": {
20
- "@rcompat/assert": "^0.6.0",
21
- "@rcompat/is": "^0.4.2",
22
- "@rcompat/sqlite": "^0.8.0",
23
- "@primate/core": "^0.5.0"
20
+ "@rcompat/assert": "^0.13.0",
21
+ "@rcompat/is": "^0.11.0",
22
+ "@rcompat/sqlite": "^0.16.0",
23
+ "@primate/core": "^0.7.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@rcompat/type": "^0.9.0",
27
- "pema": "^0.5.0"
26
+ "@rcompat/type": "^0.17.0",
27
+ "pema": "^0.7.0"
28
28
  },
29
29
  "peerDependencies": {
30
- "primate": "^0.36.0"
30
+ "primate": "^0.38.0"
31
31
  },
32
32
  "imports": {
33
33
  "#*": {
34
- "apekit": "./src/private/*.ts",
34
+ "@primate/source": "./src/private/*.ts",
35
35
  "default": "./lib/private/*.js"
36
36
  }
37
37
  },
38
38
  "exports": {
39
39
  ".": {
40
- "apekit": "./src/public/index.ts",
40
+ "@primate/source": "./src/public/index.ts",
41
41
  "default": "./lib/public/index.js"
42
42
  }
43
43
  },
@@ -45,8 +45,8 @@
45
45
  "build": "npm run clean && tsc",
46
46
  "clean": "rm -rf ./lib",
47
47
  "lint": "eslint .",
48
- "test": "npm run build && npx proby",
49
- "test:bun": "npm run build && bun x --bun proby",
50
- "test:deno": "npm run build && deno run -A npm:proby"
48
+ "test": "npx proby",
49
+ "test:bun": "bunx --bun proby",
50
+ "test:deno": "deno run -A npm:proby"
51
51
  }
52
52
  }