@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.
- package/dist/SqliteCreateTableStatementRenderer.d.ts +6 -0
- package/dist/SqliteDriver.d.ts +12 -0
- package/dist/SqliteStatementRenderer.d.ts +6 -0
- package/dist/columnTypes.d.ts +1 -0
- package/dist/command.d.ts +13 -0
- package/dist/connection-options.d.ts +6 -0
- package/dist/connection.d.ts +20 -0
- package/dist/converters/BinaryConverter.d.ts +5 -0
- package/dist/converters/BoolConverter.d.ts +5 -0
- package/dist/converters/DateTimeConverter.d.ts +5 -0
- package/dist/converters/DecimalConverter.d.ts +5 -0
- package/dist/converters/SqliteRowTypeConverters.d.ts +3 -0
- package/dist/converters/UuidConverter.d.ts +5 -0
- package/dist/i-sqlite-driver.d.ts +17 -0
- package/dist/index.d.ts +2 -0
- package/dist/sqlitePath.d.ts +5 -0
- package/dist/transaction.d.ts +9 -0
- package/package.json +24 -4
- package/src/SqliteCreateTableStatementRenderer.ts +24 -0
- package/src/SqliteDriver.ts +53 -0
- package/src/SqliteStatementRenderer.test.ts +60 -0
- package/src/SqliteStatementRenderer.ts +28 -0
- package/src/columnTypes.ts +10 -0
- package/src/command.ts +231 -0
- package/src/connection-options.ts +7 -0
- package/src/connection.ts +83 -0
- package/src/converters/BinaryConverter.test.ts +11 -0
- package/src/converters/BinaryConverter.ts +6 -0
- package/src/converters/BoolConverter.test.ts +14 -0
- package/src/converters/BoolConverter.ts +6 -0
- package/src/converters/DateTimeConverter.test.ts +13 -0
- package/src/converters/DateTimeConverter.ts +8 -0
- package/src/converters/DecimalConverter.test.ts +11 -0
- package/src/converters/DecimalConverter.ts +7 -0
- package/src/converters/SqliteRowTypeConverters.test.ts +18 -0
- package/src/converters/SqliteRowTypeConverters.ts +24 -0
- package/src/converters/UuidConverter.test.ts +11 -0
- package/src/converters/UuidConverter.ts +6 -0
- package/src/createFromDriver.test.ts +15 -0
- package/src/expected-repository-console.test.ts +95 -0
- package/src/i-sqlite-driver.ts +21 -0
- package/src/index.ts +2 -0
- package/src/sqlite.test.ts +296 -0
- package/src/sqlitePath.ts +8 -0
- package/src/transaction.ts +21 -0
- package/README.md +0 -3
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
CRUDRepository,
|
|
4
|
+
ReadonlyRepository,
|
|
5
|
+
untyped,
|
|
6
|
+
Datasource,
|
|
7
|
+
DatasourceError,
|
|
8
|
+
DatasourceFactory,
|
|
9
|
+
IsolationLevel,
|
|
10
|
+
type Cell,
|
|
11
|
+
type Cells,
|
|
12
|
+
columnType,
|
|
13
|
+
type IDriver,
|
|
14
|
+
RowTypeConverter,
|
|
15
|
+
CreateTableStatement,
|
|
16
|
+
Native,
|
|
17
|
+
ReturnStatement,
|
|
18
|
+
SelectStatement,
|
|
19
|
+
type Scalar,
|
|
20
|
+
} from "@datasources/core";
|
|
21
|
+
import { SqliteCommand } from "./command.ts";
|
|
22
|
+
import { SqliteConnection } from "./connection.ts";
|
|
23
|
+
import { SqliteRowTypeConverters } from "./converters/SqliteRowTypeConverters.ts";
|
|
24
|
+
import type { SqliteDatabase, SqliteStatement } from "./i-sqlite-driver.ts";
|
|
25
|
+
import { filenameOf, sqliteFilename } from "./sqlitePath.ts";
|
|
26
|
+
import { SqliteDriver } from "./SqliteDriver.ts";
|
|
27
|
+
import { SqliteTransaction } from "./transaction.ts";
|
|
28
|
+
|
|
29
|
+
class FakeDatabase implements SqliteDatabase {
|
|
30
|
+
static latest: FakeDatabase | undefined;
|
|
31
|
+
runs: Array<{ sql: string; params?: Record<string, Scalar> }> = [];
|
|
32
|
+
alls: Array<{ sql: string; params?: Record<string, Scalar> }> = [];
|
|
33
|
+
execs: string[] = [];
|
|
34
|
+
closed = false;
|
|
35
|
+
rows: Cells[] = [{ id: 1 }];
|
|
36
|
+
bySql?: Record<string, Cells[]>;
|
|
37
|
+
omitRowid = false;
|
|
38
|
+
columns?: Array<{ name: string; column: string | null; table: string | null }>;
|
|
39
|
+
rawRows?: Array<Array<Cell | null>>;
|
|
40
|
+
|
|
41
|
+
constructor(public filename: string) {
|
|
42
|
+
FakeDatabase.latest = this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
prepare(sql: string): SqliteStatement {
|
|
46
|
+
return {
|
|
47
|
+
run: (params) => {
|
|
48
|
+
this.runs.push({ sql, params });
|
|
49
|
+
return this.omitRowid ? { changes: 1 } : { changes: 2, lastInsertRowid: 5 };
|
|
50
|
+
},
|
|
51
|
+
all: (params) => {
|
|
52
|
+
this.alls.push({ sql, params });
|
|
53
|
+
return this.bySql?.[sql] ?? this.rows;
|
|
54
|
+
},
|
|
55
|
+
iterate: (params) => {
|
|
56
|
+
this.alls.push({ sql, params });
|
|
57
|
+
return (this.bySql?.[sql] ?? this.rows)[Symbol.iterator]();
|
|
58
|
+
},
|
|
59
|
+
columns: this.columns ? () => this.columns! : undefined,
|
|
60
|
+
raw: this.rawRows
|
|
61
|
+
? () => ({
|
|
62
|
+
iterate: () => this.rawRows![Symbol.iterator](),
|
|
63
|
+
})
|
|
64
|
+
: undefined,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
exec(sql: string): void {
|
|
69
|
+
this.execs.push(sql);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
close(): void {
|
|
73
|
+
this.closed = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const opened = async (filename = ":memory:"): Promise<SqliteConnection> => {
|
|
78
|
+
const connection = new SqliteConnection({ filename, Database: FakeDatabase });
|
|
79
|
+
await connection.open();
|
|
80
|
+
return connection;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
DatasourceFactory.reset();
|
|
85
|
+
DatasourceFactory.register("sqlite", new SqliteDriver());
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("sqlitePath", () => {
|
|
89
|
+
it("parses sqlite URLs and filenames", () => {
|
|
90
|
+
expect(sqliteFilename("mysql://x")).toBeUndefined();
|
|
91
|
+
expect(sqliteFilename("sqlite:")).toBe(":memory:");
|
|
92
|
+
expect(sqliteFilename("sqlite:///:memory")).toBe(":memory:");
|
|
93
|
+
expect(sqliteFilename("sqlite:///:memory:")).toBe(":memory:");
|
|
94
|
+
expect(sqliteFilename("sqlite:///tmp/db.sqlite")).toBe("tmp/db.sqlite");
|
|
95
|
+
expect(filenameOf({})).toBe(":memory:");
|
|
96
|
+
expect(filenameOf({ filename: "x.db" })).toBe("x.db");
|
|
97
|
+
expect(filenameOf({ url: "sqlite:///tmp/db.sqlite" })).toBe("tmp/db.sqlite");
|
|
98
|
+
expect(filenameOf({ url: "mysql://x" })).toBe(":memory:");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("SqliteConnection", () => {
|
|
103
|
+
it("opens, reopens, transacts, and closes", async () => {
|
|
104
|
+
const connection = await opened("x.db");
|
|
105
|
+
const db = FakeDatabase.latest!;
|
|
106
|
+
await expect(connection.open()).rejects.toThrow("Connection is already open");
|
|
107
|
+
expect(connection.isOpen).toBe(true);
|
|
108
|
+
expect(db.filename).toBe("x.db");
|
|
109
|
+
const txn = await connection.startTransaction();
|
|
110
|
+
await txn.commit();
|
|
111
|
+
const txn2 = await connection.startTransaction(IsolationLevel.ReadUncommitted);
|
|
112
|
+
expect(txn2.isolation).toBe(IsolationLevel.ReadUncommitted);
|
|
113
|
+
await txn2.abort();
|
|
114
|
+
expect(db.execs).toEqual([
|
|
115
|
+
"PRAGMA read_uncommitted = 0",
|
|
116
|
+
"BEGIN",
|
|
117
|
+
"COMMIT",
|
|
118
|
+
"PRAGMA read_uncommitted = 1",
|
|
119
|
+
"BEGIN",
|
|
120
|
+
"ROLLBACK",
|
|
121
|
+
]);
|
|
122
|
+
expect(txn.isolation).toBe("readCommitted");
|
|
123
|
+
await connection.close();
|
|
124
|
+
expect(db.closed).toBe(true);
|
|
125
|
+
expect(() => connection.ensureOpen()).toThrow(DatasourceError);
|
|
126
|
+
expect(() => connection.require()).toThrow("not open");
|
|
127
|
+
await expect(connection.startTransaction()).rejects.toThrow(DatasourceError);
|
|
128
|
+
await connection.close();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("defaults the filename and requires a database module to open", async () => {
|
|
132
|
+
const connection = new SqliteConnection();
|
|
133
|
+
expect(connection.filename).toBe(":memory:");
|
|
134
|
+
await expect(connection.open()).rejects.toThrow("SQLite driver is required");
|
|
135
|
+
await connection.close();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("SqliteCommand", () => {
|
|
140
|
+
it("executes SQL on the open database", async () => {
|
|
141
|
+
const connection = await opened();
|
|
142
|
+
const db = FakeDatabase.latest!;
|
|
143
|
+
const command = new SqliteCommand(connection);
|
|
144
|
+
db.rows = [{ id: 1, name: "a" }];
|
|
145
|
+
expect(await command.execute("DELETE FROM items", [{ name: "id", value: 1 }])).toEqual({
|
|
146
|
+
result: null,
|
|
147
|
+
rowsAffected: 2,
|
|
148
|
+
parameters: [{ name: "id", value: 1 }],
|
|
149
|
+
});
|
|
150
|
+
expect(await command.executeNative(new Native(""))).toEqual({ result: undefined, rowsAffected: 0 });
|
|
151
|
+
db.rows = [{ abs: 3 }];
|
|
152
|
+
const x = { name: "x", value: -3 };
|
|
153
|
+
expect(await command.executeStatement(new ReturnStatement("abs", [x]), [x])).toEqual({
|
|
154
|
+
result: 3,
|
|
155
|
+
rowsAffected: 1,
|
|
156
|
+
parameters: [x],
|
|
157
|
+
});
|
|
158
|
+
db.rows = [];
|
|
159
|
+
expect(await command.executeNative(new Native("SELECT 1"))).toEqual({ result: null, rowsAffected: 2 });
|
|
160
|
+
db.omitRowid = true;
|
|
161
|
+
expect(await command.executeNative(new Native("INSERT INTO items DEFAULT VALUES"))).toEqual({
|
|
162
|
+
result: null,
|
|
163
|
+
rowsAffected: 1,
|
|
164
|
+
});
|
|
165
|
+
db.rows = [{ id: 1, name: "a" }, { id: 2, name: "b" }];
|
|
166
|
+
const reader = await command.executeReader("SELECT * FROM items");
|
|
167
|
+
expect(await reader.next()).toBe(true);
|
|
168
|
+
expect(reader.current).toEqual({ id: 1, name: "a" });
|
|
169
|
+
expect(await reader.next()).toBe(true);
|
|
170
|
+
expect(reader.current).toEqual({ id: 2, name: "b" });
|
|
171
|
+
await reader.dispose();
|
|
172
|
+
const empty = await command.executeReaderNative(new Native(""));
|
|
173
|
+
expect(await empty.next()).toBe(false);
|
|
174
|
+
expect(await (await command.executeReaderNative(new Native(";"))).next()).toBe(false);
|
|
175
|
+
db.bySql = { "SELECT 1 AS id": [{ id: 1 }], "SELECT 2 AS id": [{ id: 2 }] };
|
|
176
|
+
const sets = await command.executeReaderNative(new Native("SELECT 1 AS id; SELECT 2 AS id"));
|
|
177
|
+
const first: Cells[] = [];
|
|
178
|
+
while (await sets.next()) first.push(sets.current!);
|
|
179
|
+
const moved = await sets.nextResult();
|
|
180
|
+
const second: Cells[] = [];
|
|
181
|
+
while (await sets.next()) second.push(sets.current!);
|
|
182
|
+
expect(moved).toBe(true);
|
|
183
|
+
expect(first).toEqual([{ id: 1 }]);
|
|
184
|
+
expect(second).toEqual([{ id: 2 }]);
|
|
185
|
+
expect(await sets.nextResult()).toBe(false);
|
|
186
|
+
await sets.dispose();
|
|
187
|
+
db.bySql = undefined;
|
|
188
|
+
db.rows = [];
|
|
189
|
+
expect(await command.executeStatement(new SelectStatement({ table: "dual", fields: ["1"] }))).toEqual({
|
|
190
|
+
result: null,
|
|
191
|
+
rowsAffected: 0,
|
|
192
|
+
parameters: undefined,
|
|
193
|
+
});
|
|
194
|
+
db.rows = [{}];
|
|
195
|
+
expect(await command.executeStatement(new SelectStatement({ table: "dual", fields: ["1"] }))).toMatchObject({ result: null });
|
|
196
|
+
db.omitRowid = false;
|
|
197
|
+
expect(await command.executeStatement(new CreateTableStatement("items", [{ name: "id", type: "INTEGER" }]))).toMatchObject({
|
|
198
|
+
result: null,
|
|
199
|
+
rowsAffected: 2,
|
|
200
|
+
});
|
|
201
|
+
db.columns = [
|
|
202
|
+
{ name: "id", column: "id", table: "a" },
|
|
203
|
+
{ name: "id", column: null, table: null },
|
|
204
|
+
];
|
|
205
|
+
db.rawRows = [[1, null]];
|
|
206
|
+
const qualified = await command.executeReaderNative(new Native("SELECT a.id, id"));
|
|
207
|
+
expect(await qualified.next()).toBe(true);
|
|
208
|
+
expect(qualified.current).toEqual({ "a.id": 1, id: null });
|
|
209
|
+
expect(await qualified.next()).toBe(false);
|
|
210
|
+
await qualified.dispose();
|
|
211
|
+
db.columns = [{ name: "id", column: "id", table: "t" }];
|
|
212
|
+
db.rawRows = undefined;
|
|
213
|
+
db.rows = [{ id: 4 }];
|
|
214
|
+
const plain = await command.executeReaderNative(new Native("SELECT id"));
|
|
215
|
+
expect(await plain.next()).toBe(true);
|
|
216
|
+
expect(plain.current).toEqual({ id: 4 });
|
|
217
|
+
await plain.dispose();
|
|
218
|
+
await connection.close();
|
|
219
|
+
await expect(command.execute("DELETE FROM items")).rejects.toThrow("not open");
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("Sqlite datasource", () => {
|
|
224
|
+
it("registers and constructs children", async () => {
|
|
225
|
+
expect(await new Datasource("sqlite", new SqliteDriver()).createConnection()).toBeInstanceOf(
|
|
226
|
+
SqliteConnection,
|
|
227
|
+
);
|
|
228
|
+
const of = (): IDriver => ({
|
|
229
|
+
connection: () => new SqliteConnection({ Database: FakeDatabase }),
|
|
230
|
+
command: (c) => c.createCommand(),
|
|
231
|
+
transaction: (c, isolation = "readCommitted") =>
|
|
232
|
+
new SqliteTransaction(c as SqliteConnection, isolation),
|
|
233
|
+
rowTypeConverters: SqliteRowTypeConverters,
|
|
234
|
+
columnType,
|
|
235
|
+
});
|
|
236
|
+
const ds = new Datasource("sqlite", of());
|
|
237
|
+
expect(ds.provider).toBe("sqlite");
|
|
238
|
+
const connection = await ds.createConnection();
|
|
239
|
+
await (connection as SqliteConnection).open();
|
|
240
|
+
expect(connection.createCommand()).toBeInstanceOf(SqliteCommand);
|
|
241
|
+
const command = await ds.createCommand(connection);
|
|
242
|
+
expect(command).toBeInstanceOf(SqliteCommand);
|
|
243
|
+
expect(command.connection).toBe(connection);
|
|
244
|
+
await command.execute("DELETE FROM items");
|
|
245
|
+
await command.executeReaderNative(new Native("SELECT 1"));
|
|
246
|
+
await expect(ds.createCommand()).rejects.toThrow("Connection is already open");
|
|
247
|
+
const repo = ds.createRepository(untyped(), connection);
|
|
248
|
+
expect(ds.createReadonlyRepository(untyped(), connection).constructor).toBe(ReadonlyRepository);
|
|
249
|
+
expect(repo).toBeInstanceOf(CRUDRepository);
|
|
250
|
+
expect(new Datasource("sqlite", of()).createRepository(untyped())).toBeInstanceOf(CRUDRepository);
|
|
251
|
+
expect(Datasource.create({ PROVIDER: "sqlite" }).provider).toBe("sqlite");
|
|
252
|
+
expect(SqliteConnection.from({ DATASOURCE_URL: "sqlite:///:memory:" }).filename).toBe(":memory:");
|
|
253
|
+
expect(SqliteConnection.from().filename).toBe(":memory:");
|
|
254
|
+
const provider = new SqliteDriver();
|
|
255
|
+
expect(provider.columnType("int")).toBe("INTEGER");
|
|
256
|
+
expect(provider.columnType("uuid")).toBe("TEXT");
|
|
257
|
+
expect(provider.columnType("string")).toBe("TEXT");
|
|
258
|
+
expect(provider.columnType("bool")).toBe("BOOLEAN");
|
|
259
|
+
expect(provider.columnType("decimal")).toBe("NUMERIC");
|
|
260
|
+
expect(provider.columnType("float")).toBe("REAL");
|
|
261
|
+
expect(provider.columnType("datetime")).toBe("TEXT");
|
|
262
|
+
expect(provider.columnType("binary")).toBe("BLOB");
|
|
263
|
+
expect(() => provider.columnType("xml")).toThrow(DatasourceError);
|
|
264
|
+
expect(provider.command(connection)).toBeInstanceOf(SqliteCommand);
|
|
265
|
+
expect(provider.transaction(connection as SqliteConnection).isolation).toBe("readCommitted");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("maps property targets", () => {
|
|
269
|
+
const provider = new SqliteDriver();
|
|
270
|
+
const created = "2020-01-02T03:04:05.000Z";
|
|
271
|
+
const converter = RowTypeConverter.from(provider.rowTypeConverters, {
|
|
272
|
+
Key: "uuid",
|
|
273
|
+
Active: "bool",
|
|
274
|
+
Login: "string",
|
|
275
|
+
Created: "datetime",
|
|
276
|
+
});
|
|
277
|
+
const row = converter.convert({
|
|
278
|
+
Key: "11111111-1111-1111-1111-111111111111",
|
|
279
|
+
Active: 1,
|
|
280
|
+
Login: "ada",
|
|
281
|
+
Created: created,
|
|
282
|
+
});
|
|
283
|
+
expect(row).toEqual({
|
|
284
|
+
Key: "11111111-1111-1111-1111-111111111111",
|
|
285
|
+
Active: true,
|
|
286
|
+
Login: "ada",
|
|
287
|
+
Created: new Date(created),
|
|
288
|
+
});
|
|
289
|
+
expect(converter.reverseConvert(row)).toEqual({
|
|
290
|
+
Key: "11111111-1111-1111-1111-111111111111",
|
|
291
|
+
Active: 1,
|
|
292
|
+
Login: "ada",
|
|
293
|
+
Created: created,
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const sqliteFilename = (url: string): string | undefined => {
|
|
2
|
+
if (!/^sqlite:/i.test(url)) return undefined;
|
|
3
|
+
const path = url.replace(/^sqlite:(?:\/\/\/|\/\/)?/i, "");
|
|
4
|
+
return path === "" || path === ":memory" || path === ":memory:" ? ":memory:" : path;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const filenameOf = (options: { filename?: string; url?: string }): string =>
|
|
8
|
+
options.filename ?? (options.url ? sqliteFilename(options.url) ?? ":memory:" : ":memory:");
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IsolationLevel,
|
|
3
|
+
type ITransaction,
|
|
4
|
+
type IsolationLevelName,
|
|
5
|
+
} from "@datasources/core";
|
|
6
|
+
import type { SqliteConnection } from "./connection.ts";
|
|
7
|
+
|
|
8
|
+
export class SqliteTransaction implements ITransaction {
|
|
9
|
+
constructor(
|
|
10
|
+
readonly connection: SqliteConnection,
|
|
11
|
+
readonly isolation: IsolationLevelName = IsolationLevel.ReadCommitted,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
async commit(): Promise<void> {
|
|
15
|
+
this.connection.exec("COMMIT");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async abort(): Promise<void> {
|
|
19
|
+
this.connection.exec("ROLLBACK");
|
|
20
|
+
}
|
|
21
|
+
}
|
package/README.md
DELETED