@deterministic-code/datasources-sqlite 0.0.0-stage → 0.1.1

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.
Files changed (46) hide show
  1. package/dist/SqliteCreateTableStatementRenderer.d.ts +6 -0
  2. package/dist/SqliteDriver.d.ts +12 -0
  3. package/dist/SqliteStatementRenderer.d.ts +6 -0
  4. package/dist/columnTypes.d.ts +1 -0
  5. package/dist/command.d.ts +13 -0
  6. package/dist/connection-options.d.ts +6 -0
  7. package/dist/connection.d.ts +20 -0
  8. package/dist/converters/BinaryConverter.d.ts +5 -0
  9. package/dist/converters/BoolConverter.d.ts +5 -0
  10. package/dist/converters/DateTimeConverter.d.ts +5 -0
  11. package/dist/converters/DecimalConverter.d.ts +5 -0
  12. package/dist/converters/SqliteRowTypeConverters.d.ts +3 -0
  13. package/dist/converters/UuidConverter.d.ts +5 -0
  14. package/dist/i-sqlite-driver.d.ts +17 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/sqlitePath.d.ts +5 -0
  17. package/dist/transaction.d.ts +9 -0
  18. package/package.json +24 -4
  19. package/src/SqliteCreateTableStatementRenderer.ts +24 -0
  20. package/src/SqliteDriver.ts +53 -0
  21. package/src/SqliteStatementRenderer.test.ts +60 -0
  22. package/src/SqliteStatementRenderer.ts +28 -0
  23. package/src/columnTypes.ts +10 -0
  24. package/src/command.ts +231 -0
  25. package/src/connection-options.ts +7 -0
  26. package/src/connection.ts +83 -0
  27. package/src/converters/BinaryConverter.test.ts +11 -0
  28. package/src/converters/BinaryConverter.ts +6 -0
  29. package/src/converters/BoolConverter.test.ts +14 -0
  30. package/src/converters/BoolConverter.ts +6 -0
  31. package/src/converters/DateTimeConverter.test.ts +13 -0
  32. package/src/converters/DateTimeConverter.ts +8 -0
  33. package/src/converters/DecimalConverter.test.ts +11 -0
  34. package/src/converters/DecimalConverter.ts +7 -0
  35. package/src/converters/SqliteRowTypeConverters.test.ts +18 -0
  36. package/src/converters/SqliteRowTypeConverters.ts +24 -0
  37. package/src/converters/UuidConverter.test.ts +11 -0
  38. package/src/converters/UuidConverter.ts +6 -0
  39. package/src/createFromDriver.test.ts +15 -0
  40. package/src/expected-repository-console.test.ts +95 -0
  41. package/src/i-sqlite-driver.ts +21 -0
  42. package/src/index.ts +2 -0
  43. package/src/sqlite.test.ts +296 -0
  44. package/src/sqlitePath.ts +8 -0
  45. package/src/transaction.ts +21 -0
  46. package/README.md +0 -3
@@ -0,0 +1,83 @@
1
+ import {
2
+ DatasourceError,
3
+ IsolationLevel,
4
+ StatementTextRenderer,
5
+ type DatasourceOptions,
6
+ type ICommand,
7
+ type IConnection,
8
+ type IsolationLevelName,
9
+ type ITransaction,
10
+ } from "@datasources/core";
11
+ import { createRequire } from "node:module";
12
+ import { SqliteCommand } from "./command.ts";
13
+ import type { SqliteConnectionOptions } from "./connection-options.ts";
14
+ import type { SqliteDatabase, SqliteDatabaseCtor } from "./i-sqlite-driver.ts";
15
+ import { filenameOf } from "./sqlitePath.ts";
16
+ import { SqliteStatementRenderer } from "./SqliteStatementRenderer.ts";
17
+ import { SqliteTransaction } from "./transaction.ts";
18
+
19
+ const require = createRequire(import.meta.url);
20
+
21
+ const pragma = (isolation: IsolationLevelName): string =>
22
+ isolation === IsolationLevel.ReadUncommitted
23
+ ? "PRAGMA read_uncommitted = 1"
24
+ : "PRAGMA read_uncommitted = 0";
25
+
26
+ export class SqliteConnection implements IConnection {
27
+ readonly provider = "sqlite";
28
+ filename: string;
29
+ isOpen = false;
30
+ private db?: SqliteDatabase;
31
+ private readonly Database?: SqliteDatabaseCtor;
32
+
33
+ static from = (options?: DatasourceOptions): SqliteConnection =>
34
+ new SqliteConnection({
35
+ url: options?.DATASOURCE_URL,
36
+ Database: require("better-sqlite3") as SqliteDatabaseCtor,
37
+ });
38
+
39
+ constructor(options: SqliteConnectionOptions = {}) {
40
+ this.filename = filenameOf(options);
41
+ this.Database = options.Database;
42
+ }
43
+
44
+ createCommand = (): ICommand =>
45
+ new SqliteCommand(this, new StatementTextRenderer(t => new SqliteStatementRenderer(t)));
46
+
47
+ async open(): Promise<void> {
48
+ if (this.isOpen) throw new DatasourceError("Connection is already open");
49
+ if (!this.Database) throw new DatasourceError("SQLite driver is required");
50
+ this.db = new this.Database(this.filename, { timeout: 0 });
51
+ this.isOpen = true;
52
+ }
53
+
54
+ async close(): Promise<void> {
55
+ this.db?.close();
56
+ this.db = undefined;
57
+ this.isOpen = false;
58
+ }
59
+
60
+ [Symbol.asyncDispose] = (): Promise<void> => this.close();
61
+
62
+ async startTransaction(
63
+ isolation: IsolationLevelName = IsolationLevel.ReadCommitted,
64
+ ): Promise<ITransaction> {
65
+ this.ensureOpen();
66
+ this.exec(pragma(isolation));
67
+ this.exec("BEGIN");
68
+ return new SqliteTransaction(this, isolation);
69
+ }
70
+
71
+ ensureOpen(): void {
72
+ if (!this.isOpen) throw new DatasourceError("Connection is not open");
73
+ }
74
+
75
+ require(): SqliteDatabase {
76
+ if (!this.db) throw new DatasourceError("Connection is not open");
77
+ return this.db;
78
+ }
79
+
80
+ exec(sql: string): void {
81
+ this.require().exec(sql);
82
+ }
83
+ }
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { BinaryConverter } from "./BinaryConverter.ts";
3
+
4
+ describe("BinaryConverter", () => {
5
+ it("maps binary", () => {
6
+ const converter = new BinaryConverter();
7
+ const bytes = new Uint8Array([1, 2]);
8
+ expect(converter.convert(bytes)).toBe(bytes);
9
+ expect(converter.reverseConvert(bytes)).toBe(bytes);
10
+ });
11
+ });
@@ -0,0 +1,6 @@
1
+ import { TypeConverter, type Cell } from "@datasources/core";
2
+
3
+ export class BinaryConverter extends TypeConverter<Cell, Cell> {
4
+ convert = (from: Cell): Cell => from;
5
+ reverseConvert = (to: Cell): Cell => to;
6
+ }
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { BoolConverter } from "./BoolConverter.ts";
3
+
4
+ describe("BoolConverter", () => {
5
+ it("maps bool", () => {
6
+ const converter = new BoolConverter();
7
+ expect(converter.convert(1)).toBe(true);
8
+ expect(converter.convert(0)).toBe(false);
9
+ expect(converter.convert("x")).toBe("x");
10
+ expect(converter.reverseConvert(true)).toBe(1);
11
+ expect(converter.reverseConvert(false)).toBe(0);
12
+ expect(converter.reverseConvert("x")).toBe("x");
13
+ });
14
+ });
@@ -0,0 +1,6 @@
1
+ import { TypeConverter, type Cell } from "@datasources/core";
2
+
3
+ export class BoolConverter extends TypeConverter<Cell, Cell> {
4
+ convert = (from: Cell): Cell => (typeof from === "number" ? from !== 0 : from);
5
+ reverseConvert = (to: Cell): Cell => (typeof to === "boolean" ? (to ? 1 : 0) : to);
6
+ }
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { DateTimeConverter } from "./DateTimeConverter.ts";
3
+
4
+ describe("DateTimeConverter", () => {
5
+ it("maps datetime", () => {
6
+ const converter = new DateTimeConverter();
7
+ const date = new Date("2020-01-02T03:04:05.000Z");
8
+ expect(converter.convert("2020-01-02T03:04:05.000Z")).toEqual(date);
9
+ expect(converter.convert(date)).toBe(date);
10
+ expect(converter.reverseConvert(date)).toBe("2020-01-02T03:04:05.000Z");
11
+ expect(converter.reverseConvert("x")).toBe("x");
12
+ });
13
+ });
@@ -0,0 +1,8 @@
1
+ import { TypeConverter, type Cell } from "@datasources/core";
2
+
3
+ export class DateTimeConverter extends TypeConverter<Cell, Cell> {
4
+ convert = (from: Cell): Cell =>
5
+ typeof from === "string" ? new Date(from) : from;
6
+ reverseConvert = (to: Cell): Cell =>
7
+ to instanceof Date ? to.toISOString() : to;
8
+ }
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { DecimalConverter } from "./DecimalConverter.ts";
3
+
4
+ describe("DecimalConverter", () => {
5
+ it("maps decimal", () => {
6
+ const converter = new DecimalConverter();
7
+ expect(converter.convert("10.5")).toBe("10.5");
8
+ expect(converter.reverseConvert(10.5)).toBe("10.5");
9
+ expect(converter.reverseConvert("10.5")).toBe("10.5");
10
+ });
11
+ });
@@ -0,0 +1,7 @@
1
+ import { TypeConverter, type Cell } from "@datasources/core";
2
+
3
+ export class DecimalConverter extends TypeConverter<Cell, Cell> {
4
+ convert = (from: Cell): Cell => from;
5
+ reverseConvert = (to: Cell): Cell =>
6
+ typeof to === "number" ? String(to) : to;
7
+ }
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { SqliteDriver } from "../SqliteDriver.ts";
3
+ import { reverse, SqliteRowTypeConverters } from "./SqliteRowTypeConverters.ts";
4
+
5
+ describe("SqliteRowTypeConverters", () => {
6
+ it("registers default types", () => {
7
+ expect(Object.keys(new SqliteDriver().rowTypeConverters)).toEqual(Object.keys(SqliteRowTypeConverters));
8
+ });
9
+
10
+ it("reverses booleans, dates, and bytes", () => {
11
+ const when = new Date("2024-01-02T00:00:00.000Z");
12
+ expect(reverse(true)).toBe(1);
13
+ expect(reverse(false)).toBe(0);
14
+ expect(reverse(when)).toBe(when.toISOString());
15
+ expect(reverse(new Uint8Array([1, 2]))).toEqual(new Uint8Array([1, 2]));
16
+ expect(reverse("a")).toBe("a");
17
+ });
18
+ });
@@ -0,0 +1,24 @@
1
+ import { type Cell, type TypeConverter } from "@datasources/core";
2
+ import { BinaryConverter } from "./BinaryConverter.ts";
3
+ import { BoolConverter } from "./BoolConverter.ts";
4
+ import { DateTimeConverter } from "./DateTimeConverter.ts";
5
+ import { DecimalConverter } from "./DecimalConverter.ts";
6
+ import { UuidConverter } from "./UuidConverter.ts";
7
+
8
+ export const SqliteRowTypeConverters: Record<string, TypeConverter<Cell, Cell>> = {
9
+ bool: new BoolConverter(),
10
+ datetime: new DateTimeConverter(),
11
+ uuid: new UuidConverter(),
12
+ decimal: new DecimalConverter(),
13
+ binary: new BinaryConverter(),
14
+ };
15
+
16
+ export const reverse = (value: Cell): Cell =>
17
+ typeof value === "boolean"
18
+ ? SqliteRowTypeConverters.bool.reverseConvert(value)
19
+ : value instanceof Date
20
+ ? SqliteRowTypeConverters.datetime.reverseConvert(value)
21
+ : value instanceof Uint8Array
22
+ ? SqliteRowTypeConverters.binary.reverseConvert(value)
23
+ : value;
24
+
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { UuidConverter } from "./UuidConverter.ts";
3
+
4
+ describe("UuidConverter", () => {
5
+ it("maps uuid", () => {
6
+ const converter = new UuidConverter();
7
+ const id = "550e8400-e29b-41d4-a716-446655440000";
8
+ expect(converter.convert(id)).toBe(id);
9
+ expect(converter.reverseConvert(id)).toBe(id);
10
+ });
11
+ });
@@ -0,0 +1,6 @@
1
+ import { TypeConverter, type Cell } from "@datasources/core";
2
+
3
+ export class UuidConverter extends TypeConverter<Cell, Cell> {
4
+ convert = (from: Cell): Cell => from;
5
+ reverseConvert = (to: Cell): Cell => to;
6
+ }
@@ -0,0 +1,15 @@
1
+ import { CRUDRepository, Datasource } from "@datasources/core";
2
+ import { expect, it } from "vitest";
3
+ import { SqliteDriver } from "./SqliteDriver.ts";
4
+
5
+ class Item {
6
+ id = 0;
7
+ name = "";
8
+ }
9
+
10
+ it("create from a driver", () => {
11
+ const driver = new SqliteDriver();
12
+ const ds = new Datasource("sqlite", driver, { DATASOURCE_URL: "sqlite:///:memory:" });
13
+ const repo = ds.createRepository(Item);
14
+ expect(repo instanceof CRUDRepository).toBe(true);
15
+ });
@@ -0,0 +1,95 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parse } from "yaml";
4
+ import { describe, expect, it } from "vitest";
5
+ import {
6
+ expectedRepositoryConsoleSignatures,
7
+ repositoryQueries,
8
+ type RepositorySpec,
9
+ } from "../../../specs/expected-repository-console.ts";
10
+ import { repoRoot } from "../../../sql-spec-runner.ts";
11
+
12
+ describe("expected repository console", () => {
13
+ const spec = parse(
14
+ readFileSync(join(repoRoot(), "specs", "repositories", "user-crud.yaml"), "utf8"),
15
+ ) as RepositorySpec;
16
+
17
+ it("expands user-crud into statement SQL", () => {
18
+ expect(repositoryQueries(spec)).toEqual([
19
+ "INSERT INTO user (balance, created, is_enabled, location, username, uuid, version) VALUES ($balance, $created, $is_enabled, $location, $username, $uuid, $version)",
20
+ "INSERT INTO user (balance, created, is_enabled, location, username, uuid, version) VALUES ($balance, $created, $is_enabled, $location, $username, $uuid, $version)",
21
+ "SELECT balance, created, id, is_enabled, location, username, uuid, version FROM user WHERE id = $id",
22
+ "SELECT balance, created, id, is_enabled, location, username, uuid, version FROM user WHERE username = $username",
23
+ "SELECT balance, created, id, is_enabled, location, username, uuid, version FROM user WHERE uuid = $uuid",
24
+ "SELECT balance, created, id, is_enabled, location, username, uuid, version FROM user WHERE is_enabled = $is_enabled",
25
+ "SELECT balance, created, id, is_enabled, location, username, uuid, version FROM user WHERE created = $created",
26
+ "SELECT balance, created, id, is_enabled, location, username, uuid, version FROM user",
27
+ "UPDATE user SET balance = $balance, created = $created, is_enabled = $is_enabled, location = $location, username = $username, uuid = $uuid, version = $version WHERE uuid = $uuid",
28
+ "SELECT balance, created, id, is_enabled, location, username, uuid, version FROM user WHERE uuid = $uuid",
29
+ "SELECT balance, created, id, is_enabled, location, username, uuid, version FROM user WHERE created = $created",
30
+ "SELECT balance, created, id, is_enabled, location, username, uuid, version FROM user WHERE is_enabled = $is_enabled",
31
+ "DELETE FROM user WHERE created = $created",
32
+ "SELECT balance, created, id, is_enabled, location, username, uuid, version FROM user WHERE id = $id",
33
+ "SELECT balance, created, id, is_enabled, location, username, uuid, version FROM user",
34
+ ]);
35
+ });
36
+
37
+ it("expands complex-crud-mappings into flattened statement SQL", () => {
38
+ const mapped = parse(
39
+ readFileSync(join(repoRoot(), "specs", "repositories", "complex-crud-mappings.yaml"), "utf8"),
40
+ ) as RepositorySpec;
41
+ expect(repositoryQueries(mapped)).toEqual([
42
+ "INSERT INTO users (create_string, decimal_text, username, usertypename, uuid, version) VALUES ($create_string, $decimal_text, $username, $usertypename, $uuid, $version)",
43
+ "INSERT INTO users (create_string, decimal_text, username, usertypename, uuid, version) VALUES ($create_string, $decimal_text, $username, $usertypename, $uuid, $version)",
44
+ "SELECT create_string, decimal_text, id, username, usertypename, uuid, version FROM users WHERE id = $id",
45
+ "SELECT create_string, decimal_text, id, username, usertypename, uuid, version FROM users WHERE username = $username",
46
+ "SELECT create_string, decimal_text, id, username, usertypename, uuid, version FROM users WHERE uuid = $uuid",
47
+ "SELECT create_string, decimal_text, id, username, usertypename, uuid, version FROM users WHERE usertypename = $usertypename",
48
+ "SELECT create_string, decimal_text, id, username, usertypename, uuid, version FROM users WHERE create_string = $create_string",
49
+ "SELECT create_string, decimal_text, id, username, usertypename, uuid, version FROM users",
50
+ "UPDATE users SET create_string = $create_string, decimal_text = $decimal_text, username = $username, usertypename = $usertypename, uuid = $uuid, version = $version WHERE uuid = $uuid",
51
+ "SELECT create_string, decimal_text, id, username, usertypename, uuid, version FROM users WHERE uuid = $uuid",
52
+ "SELECT create_string, decimal_text, id, username, usertypename, uuid, version FROM users WHERE create_string = $create_string",
53
+ "SELECT create_string, decimal_text, id, username, usertypename, uuid, version FROM users WHERE usertypename = $usertypename",
54
+ "DELETE FROM users WHERE create_string = $create_string",
55
+ "SELECT create_string, decimal_text, id, username, usertypename, uuid, version FROM users WHERE id = $id",
56
+ "SELECT create_string, decimal_text, id, username, usertypename, uuid, version FROM users",
57
+ ]);
58
+ });
59
+
60
+ it("expands crud-mapper into remapped statement SQL", () => {
61
+ const mapped = parse(
62
+ readFileSync(join(repoRoot(), "specs", "repositories", "crud-mapper.yaml"), "utf8"),
63
+ ) as RepositorySpec;
64
+ const select =
65
+ "SELECT balance, created, id, is_enabled, location, username, uuid, version FROM people";
66
+ const insert =
67
+ "INSERT INTO people (balance, created, is_enabled, location, username, uuid, version) VALUES ($balance, $created, $is_enabled, $location, $username, $uuid, $version) RETURNING id";
68
+ expect(repositoryQueries(mapped)).toEqual([
69
+ insert,
70
+ insert,
71
+ `${select} WHERE id = $id LIMIT 1`,
72
+ `${select} WHERE username = $username LIMIT 1`,
73
+ `${select} WHERE username = $username LIMIT 1`,
74
+ "UPDATE people SET balance = $balance, created = $created, is_enabled = $is_enabled, location = $location, username = $username, uuid = $uuid, version = $version WHERE id = $id RETURNING id",
75
+ `${select} WHERE id = $id LIMIT 1`,
76
+ "UPDATE people SET balance = $balance, created = $created, is_enabled = $is_enabled, location = $location, username = $username, uuid = $uuid, version = $version WHERE uuid = $uuid RETURNING id",
77
+ `${select} WHERE uuid = $uuid LIMIT 1`,
78
+ "DELETE FROM people WHERE id = $id RETURNING id",
79
+ `${select} WHERE id = $id LIMIT 1`,
80
+ "DELETE FROM people WHERE username = $username RETURNING id",
81
+ `${select} WHERE id = $id LIMIT 1`,
82
+ ]);
83
+ });
84
+
85
+ it("pairs Start and Finish signatures", () => {
86
+ const signatures = expectedRepositoryConsoleSignatures(spec);
87
+ expect(signatures).toHaveLength(30);
88
+ expect(signatures[0]).toBe(
89
+ "[datasource][Start]-[INSERT INTO user (balance, created, is_enabled, location, username, uuid, version) VALUES ($balance, $created, $is_enabled, $location, $username, $uuid, $version)]",
90
+ );
91
+ expect(signatures[1]).toBe(
92
+ "[datasource][Finish]-[INSERT INTO user (balance, created, is_enabled, location, username, uuid, version) VALUES ($balance, $created, $is_enabled, $location, $username, $uuid, $version)]",
93
+ );
94
+ });
95
+ });
@@ -0,0 +1,21 @@
1
+ import type { Cell, Cells, Scalar } from "@datasources/core";
2
+
3
+ export interface SqliteStatement {
4
+ run(params?: Record<string, Scalar>): {
5
+ changes: number;
6
+ lastInsertRowid?: Cell;
7
+ };
8
+ all(params?: Record<string, Scalar>): Cells[];
9
+ iterate(params?: Record<string, Scalar>): IterableIterator<Cells>;
10
+ }
11
+
12
+ export interface SqliteDatabase {
13
+ prepare(sql: string): SqliteStatement;
14
+ exec(sql: string): void;
15
+ close(): void;
16
+ }
17
+
18
+ export type SqliteDatabaseCtor = new (
19
+ filename: string,
20
+ options?: { timeout?: number },
21
+ ) => SqliteDatabase;
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { SqliteDriver } from "./SqliteDriver.ts";
2
+ export { SqliteStatementRenderer } from "./SqliteStatementRenderer.ts";