@primate/sqlite 0.7.0 → 0.8.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,8 +1,9 @@
1
- import type { As, DataDict, DB, Sort, With } from "@primate/core/db";
1
+ import type { As, DataDict, DB, Schema, Sort, With } from "@primate/core/db";
2
2
  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:">;
3
+ declare const schema: import("pema").ObjectType<import("pema").NormalizeSchemaObject<{
4
+ readonly database: import("pema").DefaultType<import("pema").StringType, ":memory:">;
5
+ }>, {
6
+ database: string;
6
7
  }>;
7
8
  export default class SQLite implements DB {
8
9
  #private;
@@ -14,10 +15,7 @@ export default class SQLite implements DB {
14
15
  query: string;
15
16
  plans: string[];
16
17
  }>;
17
- get schema(): {
18
- create: (as: As, store: StoreSchema) => void;
19
- delete: (table: string) => void;
20
- };
18
+ get schema(): Schema;
21
19
  create<O extends Dict>(as: As, record: Dict): Promise<O>;
22
20
  read(as: As, args: {
23
21
  count: true;
@@ -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;
@@ -137,25 +156,55 @@ export default class SQLite {
137
156
  }
138
157
  get schema() {
139
158
  return {
140
- create: (as, store) => {
159
+ create: (table, pk, types) => {
141
160
  const columns = [];
142
- for (const [key, value] of Object.entries(store)) {
143
- const column_type = get_column(value.datatype);
161
+ for (const [key, value] of Object.entries(types)) {
162
+ const column_type = get_column(value);
144
163
  const column = `${sql.quote(key)} ${column_type}`;
145
164
  const is_int = column_type === "INTEGER";
146
- if (key === as.pk) {
147
- const generate = as.generate_pk && is_int ? " AUTOINCREMENT" : "";
165
+ if (key === pk.name) {
166
+ const generate = pk.generate && is_int ? " AUTOINCREMENT" : "";
148
167
  columns.push(`${column} PRIMARY KEY${generate}`);
149
168
  }
150
169
  else {
151
170
  columns.push(column);
152
171
  }
153
172
  }
154
- this.#sql(Q `CREATE TABLE IF NOT EXISTS ${as.table} (${columns})`).run();
173
+ this.#sql(Q `CREATE TABLE IF NOT EXISTS ${table} (${columns})`).run();
155
174
  },
156
175
  delete: (table) => {
157
176
  this.#sql(Q `DROP TABLE IF EXISTS ${table}`).run();
158
177
  },
178
+ introspect: (table) => {
179
+ const rows = this.#sql(`PRAGMA table_info(${sql.quote(table)})`)
180
+ .all();
181
+ if (rows.length === 0)
182
+ return null;
183
+ const result = {};
184
+ for (const row of rows) {
185
+ const candidates = columns_to_types(row.type);
186
+ if (candidates.length > 0)
187
+ result[row.name] = candidates;
188
+ }
189
+ return result;
190
+ },
191
+ alter: (table, diff) => {
192
+ const existing = this.schema.introspect(table);
193
+ if (existing === null)
194
+ throw E.table_not_found(table);
195
+ for (const [from, to] of diff.rename) {
196
+ this.#sql(Q `ALTER TABLE ${table}
197
+ RENAME COLUMN ${[sql.quote(from)]} TO ${[sql.quote(to)]}`).run();
198
+ }
199
+ for (const field of diff.drop) {
200
+ this.#sql(Q `ALTER TABLE ${table}
201
+ DROP COLUMN ${[sql.quote(field)]}`).run();
202
+ }
203
+ for (const [field, type] of Object.entries(diff.add)) {
204
+ this.#sql(Q `ALTER TABLE ${table}
205
+ ADD COLUMN ${[`${sql.quote(field)} ${get_column(type)}`]}`).run();
206
+ }
207
+ },
159
208
  };
160
209
  }
161
210
  #where(types, where, alias) {
@@ -255,9 +304,9 @@ export default class SQLite {
255
304
  #generate_pk(as) {
256
305
  const pk = as.pk;
257
306
  const type = as.types[pk];
258
- if (type === "string")
259
- return crypto.randomUUID();
260
- if (common.BIGINT_STRING_TYPES.includes(type)) {
307
+ if (base.is_uuid_type(type))
308
+ return base.generate_uuid(type);
309
+ if (base.BIGINT_STRING_TYPES.includes(type)) {
261
310
  const query = Q `SELECT ${pk} AS v
262
311
  FROM ${as.table} ORDER BY LENGTH(${pk}) DESC, ${pk} DESC LIMIT 1`;
263
312
  const rows = this.#sql(query).all();
@@ -289,7 +338,7 @@ export default class SQLite {
289
338
  throw E.pk_required(pk);
290
339
  const type = as.types[pk];
291
340
  // integer types, use RETURNING
292
- if (!common.BIGINT_STRING_TYPES.includes(type) && type !== "string") {
341
+ if (!base.BIGINT_STRING_TYPES.includes(type) && !base.is_uuid_type(type)) {
293
342
  const [keys, values] = this.#create(record);
294
343
  const query = keys.length > 0
295
344
  ? Q `INSERT INTO ${table} (${keys}) VALUES (${values}) RETURNING ${pk}`
@@ -311,7 +360,7 @@ export default class SQLite {
311
360
  this.#explain = {};
312
361
  if (args.count === true)
313
362
  return this.#count(as, args.where);
314
- if (common.withed(args)) {
363
+ if (base.withed(args)) {
315
364
  return sql.joinable(as, args.with)
316
365
  ? this.#read_joined(as, args)
317
366
  : this.#read_phased(as, args);
@@ -325,9 +374,10 @@ export default class SQLite {
325
374
  const query = `SELECT COUNT(*) AS n FROM ${sql.quote(table)} ${WHERE}`;
326
375
  this.#capture(table, query, binds);
327
376
  const rows = this.#sql(query).all(binds);
328
- assert.true(rows.length === 1, `COUNT(*) must return 1 row (got ${rows.length})`);
377
+ const length = rows.length;
378
+ assert.true(length === 1, E.record_number_invalid(length, "count"));
329
379
  const n = rows[0].n;
330
- assert.bigint(n, `COUNT returned non-bigint: ${typeof n}`);
380
+ assert.bigint(n, E.return_invalid(n, "bigint", "count"));
331
381
  if (n > BigInt(Number.MAX_SAFE_INTEGER))
332
382
  throw E.count_overflow(table, n);
333
383
  return Number(n);
@@ -338,9 +388,9 @@ export default class SQLite {
338
388
  return this.#query_all(as, query, binds);
339
389
  }
340
390
  async #read_phased(as, args) {
341
- const fields = common.expand(as, args.fields, args.with);
391
+ const fields = base.expand(as, args.fields, args.with);
342
392
  const rows = await this.#read_base(as, { ...args, fields });
343
- const out = rows.map(row => common.project(row, args.fields));
393
+ const out = rows.map(row => base.project(row, args.fields));
344
394
  for (const [table, relation] of Object.entries(args.with)) {
345
395
  await this.#attach_relation(as, { rows, out, table, relation });
346
396
  }
@@ -348,10 +398,11 @@ export default class SQLite {
348
398
  }
349
399
  async #attach_relation(as, args) {
350
400
  const relation = args.relation;
351
- const by = relation.reverse ? relation.as.pk : relation.fk;
401
+ const reverse = assert.maybe.boolean(relation.reverse) ?? false;
402
+ const by = reverse ? relation.as.pk : relation.fk;
352
403
  if (by === null)
353
404
  throw E.relation_requires_pk("target");
354
- const parent_by = relation.reverse ? relation.fk : as.pk;
405
+ const parent_by = reverse ? relation.fk : as.pk;
355
406
  if (parent_by === null)
356
407
  throw E.relation_requires_pk("parent");
357
408
  const join_values = [...new Set(args.rows.map(r => r[parent_by]).filter(v => v != null))];
@@ -376,8 +427,8 @@ export default class SQLite {
376
427
  }
377
428
  const rows = grouped.get(join_value) ?? [];
378
429
  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;
430
+ ? rows.map(r => base.project(r, relation.fields))
431
+ : rows[0] ? base.project(rows[0], relation.fields) : null;
381
432
  }
382
433
  }
383
434
  async #load_related(args) {
@@ -400,7 +451,7 @@ export default class SQLite {
400
451
  const fields = args.fields !== undefined && args.fields.length > 0
401
452
  ? args.fields
402
453
  : all_columns;
403
- const SELECT = sql.columns(args.as.types, common.fields(fields, args.by));
454
+ const SELECT = sql.columns(args.as.types, base.fields(fields, args.by));
404
455
  const base_order = `${sql.quote(args.by)} ASC`;
405
456
  const user_order = sql.orderBy(args.as.types, args.sort)
406
457
  .replace(/^ ORDER BY /, "");
@@ -442,11 +493,12 @@ export default class SQLite {
442
493
  return this.#query_all(args.as, query, binds);
443
494
  }
444
495
  #join(as, aliases, relation) {
496
+ const reverse = assert.maybe.boolean(relation.reverse) ?? false;
445
497
  const parent_alias = aliases[as.table];
446
498
  const alias = aliases[relation.as.table];
447
499
  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;
500
+ const by = reverse ? relation.as.pk : relation.fk;
501
+ const parent_by = reverse ? relation.fk : as.pk;
450
502
  if (by === null)
451
503
  throw E.relation_requires_pk("target");
452
504
  if (parent_by === null)
@@ -469,13 +521,13 @@ export default class SQLite {
469
521
  const tables = [as.table, ...Object.values(args.with).map(r => r.as.table)];
470
522
  const aliases = sql.aliases(tables);
471
523
  const alias = aliases[as.table];
472
- const fields = common.fields(args.fields, as.pk);
524
+ const fields = base.fields(args.fields, as.pk);
473
525
  // subquery for limited parents
474
526
  const SELECT = [
475
527
  sql.aliased(aliases, as, fields),
476
528
  ...Object.values(args.with).map(relation => {
477
529
  return sql.aliased(aliases, relation.as, relation.as.pk !== null
478
- ? common.fields(relation.fields, relation.as.pk)
530
+ ? base.fields(relation.fields, relation.as.pk)
479
531
  : relation.fields);
480
532
  }),
481
533
  ].join(", ");
@@ -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.8.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.8.1",
21
+ "@rcompat/is": "^0.6.1",
22
+ "@rcompat/sqlite": "^0.11.1",
23
+ "@primate/core": "^0.6.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@rcompat/type": "^0.9.0",
27
- "pema": "^0.5.0"
26
+ "@rcompat/type": "^0.11.1",
27
+ "pema": "^0.6.0"
28
28
  },
29
29
  "peerDependencies": {
30
- "primate": "^0.36.0"
30
+ "primate": "^0.37.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
  }