@uniflowed/sql 0.14.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.
@@ -0,0 +1,80 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/sql/better-sqlite3`: the `better-sqlite3` package.
4
+ //
5
+ // import Database from "better-sqlite3";
6
+ // import { fromBetterSqlite3 } from "@uniflowed/sql/better-sqlite3";
7
+ //
8
+ // const db = fromBetterSqlite3(new Database("app.db"));
9
+ //
10
+ // Experimental: typed against better-sqlite3's documented `Statement` API
11
+ // (`raw`, `safeIntegers`, `reader`); this repository's CI does not install it.
12
+
13
+ import type { Queryable, SqlParam } from "../index.js";
14
+ import { SqlError, singleConnection } from "../index.js";
15
+ import { DEFAULT_STATEMENT_CACHE, count, rowId, statementCache } from "../internal/statements.js";
16
+
17
+ /** The part of better-sqlite3's `Statement` this uses. */
18
+ export interface BetterSqlite3Statement {
19
+ readonly reader: boolean;
20
+ all(...params: $ReadOnlyArray<SqlParam>): $ReadOnlyArray<mixed>;
21
+ run(...params: $ReadOnlyArray<SqlParam>): {
22
+ readonly changes: number,
23
+ readonly lastInsertRowid: number | bigint,
24
+ ...
25
+ };
26
+ raw(enabled: boolean): mixed;
27
+ safeIntegers(enabled: boolean): mixed;
28
+ }
29
+
30
+ /** The part of better-sqlite3's `Database` this uses. */
31
+ export interface BetterSqlite3Database {
32
+ prepare(text: string): BetterSqlite3Statement;
33
+ }
34
+
35
+ export type BetterSqlite3Options = {|
36
+ readonly begin?: string,
37
+ readonly statementCache?: number,
38
+ readonly maxParams?: number,
39
+ |};
40
+
41
+ /** A [`Queryable`] over one better-sqlite3 `Database`. */
42
+ export function fromBetterSqlite3(
43
+ database: BetterSqlite3Database,
44
+ options: BetterSqlite3Options = {},
45
+ ): Queryable {
46
+ const statement = statementCache((text) => {
47
+ const prepared = database.prepare(text);
48
+ prepared.safeIntegers(true);
49
+ // `raw` throws on a statement that returns no data, so only a reader
50
+ // gets it — and only a reader is ever asked for rows.
51
+ if (prepared.reader) {
52
+ prepared.raw(true);
53
+ }
54
+ return prepared;
55
+ }, options.statementCache ?? DEFAULT_STATEMENT_CACHE);
56
+ return singleConnection({
57
+ engine: "sqlite",
58
+ maxParams: options.maxParams ?? 32766,
59
+ begin: options.begin,
60
+ run: async (text, params, mode) => {
61
+ const prepared = statement(text);
62
+ if (mode === "rows" && prepared.reader) {
63
+ const rows: Array<$ReadOnlyArray<mixed>> = [];
64
+ for (const row of prepared.all(...params)) {
65
+ if (!Array.isArray(row)) {
66
+ throw new SqlError({ kind: "unsupported", feature: "better-sqlite3 rows as objects" });
67
+ }
68
+ rows.push(row);
69
+ }
70
+ return { rows, rowsAffected: 0, lastInsertId: null };
71
+ }
72
+ const result = prepared.run(...params);
73
+ return {
74
+ rows: [],
75
+ rowsAffected: count(result.changes),
76
+ lastInsertId: rowId(result.lastInsertRowid),
77
+ };
78
+ },
79
+ });
80
+ }
@@ -0,0 +1,73 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/sql/bun-sqlite`: Bun's built-in `bun:sqlite`.
4
+ //
5
+ // import { Database } from "bun:sqlite";
6
+ // import { fromBunSqlite } from "@uniflowed/sql/bun-sqlite";
7
+ //
8
+ // const db = fromBunSqlite(new Database("app.db"));
9
+ //
10
+ // Safe integers are turned on per statement, so the `Database` does not have
11
+ // to be opened with `safeIntegers: true` — though it does no harm.
12
+
13
+ import type { Queryable, SqlParam } from "../index.js";
14
+ import { SqlError, singleConnection } from "../index.js";
15
+ import { DEFAULT_STATEMENT_CACHE, count, rowId, statementCache } from "../internal/statements.js";
16
+
17
+ /** The part of `bun:sqlite`'s `Statement` this uses. */
18
+ export interface BunSqliteStatement {
19
+ values(...params: $ReadOnlyArray<SqlParam>): $ReadOnlyArray<mixed>;
20
+ run(...params: $ReadOnlyArray<SqlParam>): {
21
+ readonly changes: number | bigint,
22
+ readonly lastInsertRowid: number | bigint,
23
+ ...
24
+ };
25
+ safeIntegers(enabled: boolean): mixed;
26
+ }
27
+
28
+ /** The part of `bun:sqlite`'s `Database` this uses. */
29
+ export interface BunSqliteDatabase {
30
+ prepare(text: string): BunSqliteStatement;
31
+ }
32
+
33
+ export type BunSqliteOptions = {|
34
+ readonly begin?: string,
35
+ readonly statementCache?: number,
36
+ readonly maxParams?: number,
37
+ |};
38
+
39
+ /** A [`Queryable`] over one `bun:sqlite` `Database`. */
40
+ export function fromBunSqlite(
41
+ database: BunSqliteDatabase,
42
+ options: BunSqliteOptions = {},
43
+ ): Queryable {
44
+ const statement = statementCache((text) => {
45
+ const prepared = database.prepare(text);
46
+ prepared.safeIntegers(true);
47
+ return prepared;
48
+ }, options.statementCache ?? DEFAULT_STATEMENT_CACHE);
49
+ return singleConnection({
50
+ engine: "sqlite",
51
+ maxParams: options.maxParams ?? 32766,
52
+ begin: options.begin,
53
+ run: async (text, params, mode) => {
54
+ const prepared = statement(text);
55
+ if (mode === "rows") {
56
+ const rows: Array<$ReadOnlyArray<mixed>> = [];
57
+ for (const row of prepared.values(...params)) {
58
+ if (!Array.isArray(row)) {
59
+ throw new SqlError({ kind: "unsupported", feature: "bun:sqlite rows as objects" });
60
+ }
61
+ rows.push(row);
62
+ }
63
+ return { rows, rowsAffected: 0, lastInsertId: null };
64
+ }
65
+ const result = prepared.run(...params);
66
+ return {
67
+ rows: [],
68
+ rowsAffected: count(result.changes),
69
+ lastInsertId: rowId(result.lastInsertRowid),
70
+ };
71
+ },
72
+ });
73
+ }
package/adapters/d1.js ADDED
@@ -0,0 +1,71 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/sql/d1`: a Cloudflare D1 binding.
4
+ //
5
+ // import { fromD1 } from "@uniflowed/sql/d1";
6
+ //
7
+ // export default { fetch: (request, env) => handle(request, fromD1(env.DB)) };
8
+ //
9
+ // Experimental, and narrower than the other adapters in three ways D1 decides:
10
+ //
11
+ // * **No interactive transactions.** The returned `Queryable` has no
12
+ // `transaction`, so `:batch*` and `:copyfrom` run their statements in order
13
+ // without one. Use D1's own `batch()` where atomicity matters.
14
+ // * **100 bound parameters per statement**, which `:copyfrom` chunks under.
15
+ // * **No `bigint`.** D1 returns every INTEGER as a `number`, so the checked
16
+ // `INTEGER` codec throws on a value past 2^53 instead of rounding it, and a
17
+ // `bigint` parameter is sent as its digits, which INTEGER affinity stores
18
+ // exactly.
19
+
20
+ import type { Queryable, SqlParam } from "../index.js";
21
+ import { SqlError } from "../index.js";
22
+ import { count, rowId } from "../internal/statements.js";
23
+
24
+ /** The part of a D1 prepared statement this uses. */
25
+ export interface D1Statement {
26
+ bind(...params: $ReadOnlyArray<mixed>): D1Statement;
27
+ raw(): Promise<$ReadOnlyArray<mixed>>;
28
+ run(): Promise<{
29
+ readonly meta: { readonly changes?: number, readonly last_row_id?: number, ... },
30
+ ...
31
+ }>;
32
+ }
33
+
34
+ /** The part of a D1 database binding this uses. */
35
+ export interface D1Database {
36
+ prepare(text: string): D1Statement;
37
+ }
38
+
39
+ function toD1(param: SqlParam): mixed {
40
+ if (typeof param === "bigint") {
41
+ return String(param);
42
+ }
43
+ return param;
44
+ }
45
+
46
+ /** A [`Queryable`] over a D1 binding. */
47
+ export function fromD1(database: D1Database): Queryable {
48
+ return {
49
+ engine: "sqlite",
50
+ maxParams: 100,
51
+ query: async (text, params, mode) => {
52
+ const statement = database.prepare(text).bind(...params.map(toD1));
53
+ if (mode === "rows") {
54
+ const rows: Array<$ReadOnlyArray<mixed>> = [];
55
+ for (const row of await statement.raw()) {
56
+ if (!Array.isArray(row)) {
57
+ throw new SqlError({ kind: "unsupported", feature: "D1 rows as objects" });
58
+ }
59
+ rows.push(row);
60
+ }
61
+ return { rows, rowsAffected: 0, lastInsertId: null };
62
+ }
63
+ const { meta } = await statement.run();
64
+ return {
65
+ rows: [],
66
+ rowsAffected: count(meta.changes ?? 0),
67
+ lastInsertId: rowId(meta.last_row_id),
68
+ };
69
+ },
70
+ };
71
+ }
@@ -0,0 +1,140 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/sql/mysql2`: the `mysql2` package's promise API.
4
+ //
5
+ // import mysql from "mysql2/promise";
6
+ // import { fromMysql2Pool } from "@uniflowed/sql/mysql2";
7
+ //
8
+ // const db = fromMysql2Pool(mysql.createPool(url));
9
+ //
10
+ // `tests/sqlc/mysql.test.js` runs it against the MySQL 8.4 server CI's
11
+ // `sqlc Flow target` job provides; locally it needs `UF_SQLC_MYSQL_URL`.
12
+ //
13
+ // Statements are server-side prepared (`execute`) and rows are arrays. Each
14
+ // statement asks for big numbers and dates as strings, and its `typeCast`
15
+ // reads `JSON` and `DECIMAL` columns as their text: the text MySQL sent, which
16
+ // `@uniflowed/sql/mysql` decodes, whatever the pool was configured with.
17
+ // mysql2 reads `jsonStrings` and `decimalNumbers` from the connection's
18
+ // config only, so asking for them per statement would be silently ignored,
19
+ // and a pool opened without them would hand the codecs a parsed object.
20
+ //
21
+ // A parameterless statement run for its effect goes over the text protocol
22
+ // (`query`) instead: MySQL refuses to prepare `SAVEPOINT`, `RELEASE
23
+ // SAVEPOINT` and `ROLLBACK TO SAVEPOINT`, and such a statement returns no rows
24
+ // whose representation could differ.
25
+
26
+ import type { Connection, QueryResult, Queryable, Run } from "../index.js";
27
+ import { transactionOn } from "../index.js";
28
+
29
+ /** What a `typeCast` function is handed for one column. */
30
+ export type Mysql2Field = {
31
+ readonly type: string,
32
+ string(encoding?: string): string | null,
33
+ ...
34
+ };
35
+
36
+ type Mysql2Options = {|
37
+ readonly sql: string,
38
+ readonly values: $ReadOnlyArray<mixed>,
39
+ readonly rowsAsArray: true,
40
+ readonly supportBigNumbers: true,
41
+ readonly bigNumberStrings: true,
42
+ readonly dateStrings: true,
43
+ readonly typeCast: (field: Mysql2Field, next: () => mixed) => mixed,
44
+ |};
45
+
46
+ type Mysql2Result =
47
+ | $ReadOnlyArray<mixed>
48
+ | { readonly affectedRows: number, readonly insertId: number | string, ... };
49
+
50
+ /** A `mysql2/promise` connection. */
51
+ export interface Mysql2Connection {
52
+ execute(options: Mysql2Options): Promise<[Mysql2Result, mixed]>;
53
+ query(options: Mysql2Options): Promise<[Mysql2Result, mixed]>;
54
+ }
55
+
56
+ /** A connection checked out of a `mysql2/promise` pool. */
57
+ export interface Mysql2PoolConnection extends Mysql2Connection {
58
+ release(): void;
59
+ }
60
+
61
+ /** A `mysql2/promise` pool. */
62
+ export interface Mysql2Pool extends Mysql2Connection {
63
+ getConnection(): Promise<Mysql2PoolConnection>;
64
+ }
65
+
66
+ /**
67
+ * `JSON` and `DECIMAL` columns as the text MySQL sent; everything else as the
68
+ * statement's options already ask. `JSON` arrives with the binary charset, so
69
+ * it is read as UTF-8, which is what the JSON type stores.
70
+ */
71
+ export function typeCast(field: Mysql2Field, next: () => mixed): mixed {
72
+ switch (field.type) {
73
+ case "JSON":
74
+ return field.string("utf8");
75
+ case "NEWDECIMAL":
76
+ case "DECIMAL":
77
+ return field.string();
78
+ default:
79
+ return next();
80
+ }
81
+ }
82
+
83
+ function lastId(value: number | string): bigint | null {
84
+ if (typeof value === "string" && /^\d+$/.test(value)) {
85
+ return BigInt(value);
86
+ }
87
+ if (typeof value === "number" && Number.isSafeInteger(value)) {
88
+ return BigInt(value);
89
+ }
90
+ return null;
91
+ }
92
+
93
+ function runOn(connection: Mysql2Connection): Run {
94
+ return async (text, params, mode): Promise<QueryResult> => {
95
+ const options: Mysql2Options = {
96
+ sql: text,
97
+ values: params,
98
+ rowsAsArray: true,
99
+ supportBigNumbers: true,
100
+ bigNumberStrings: true,
101
+ dateStrings: true,
102
+ typeCast,
103
+ };
104
+ const [result] =
105
+ mode === "exec" && params.length === 0
106
+ ? await connection.query(options)
107
+ : await connection.execute(options);
108
+ if (Array.isArray(result)) {
109
+ const rows: Array<$ReadOnlyArray<mixed>> = [];
110
+ for (const row of result) {
111
+ if (Array.isArray(row)) {
112
+ rows.push(row);
113
+ }
114
+ }
115
+ return { rows, rowsAffected: 0, lastInsertId: null };
116
+ }
117
+ return { rows: [], rowsAffected: result.affectedRows, lastInsertId: lastId(result.insertId) };
118
+ };
119
+ }
120
+
121
+ function connection(client: Mysql2Connection): Connection {
122
+ return { engine: "mysql", maxParams: 65535, run: runOn(client) };
123
+ }
124
+
125
+ /** A [`Queryable`] over a `mysql2/promise` pool. */
126
+ export function fromMysql2Pool(pool: Mysql2Pool): Queryable {
127
+ return {
128
+ engine: "mysql",
129
+ maxParams: 65535,
130
+ query: runOn(pool),
131
+ transaction: async <T>(body: (tx: Queryable) => Promise<T>): Promise<T> => {
132
+ const client = await pool.getConnection();
133
+ try {
134
+ return await transactionOn(connection(client), body);
135
+ } finally {
136
+ client.release();
137
+ }
138
+ },
139
+ };
140
+ }
@@ -0,0 +1,93 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/sql/node-sqlite`: Node's built-in `node:sqlite`.
4
+ //
5
+ // import { DatabaseSync } from "node:sqlite";
6
+ // import { fromNodeSqlite } from "@uniflowed/sql/node-sqlite";
7
+ //
8
+ // const db = fromNodeSqlite(new DatabaseSync("app.db"));
9
+ //
10
+ // Needs `StatementSync#setReturnArrays`, which Node added in 24.0 and 22.16.
11
+ // An older Node gets an error that says so, rather than rows keyed by column
12
+ // name — which would silently merge two columns that share one.
13
+
14
+ import type { Queryable, SqlParam } from "../index.js";
15
+ import { SqlError, singleConnection } from "../index.js";
16
+ import { DEFAULT_STATEMENT_CACHE, count, rowId, statementCache } from "../internal/statements.js";
17
+
18
+ /** The part of `node:sqlite`'s `StatementSync` this uses. */
19
+ export interface NodeSqliteStatement {
20
+ all(...params: $ReadOnlyArray<SqlParam>): $ReadOnlyArray<mixed>;
21
+ run(...params: $ReadOnlyArray<SqlParam>): {
22
+ readonly changes: number | bigint,
23
+ readonly lastInsertRowid: number | bigint,
24
+ ...
25
+ };
26
+ setReadBigInts(enabled: boolean): void;
27
+ setReturnArrays(enabled: boolean): void;
28
+ }
29
+
30
+ /** The part of `node:sqlite`'s `DatabaseSync` this uses. */
31
+ export interface NodeSqliteDatabase {
32
+ prepare(text: string): NodeSqliteStatement;
33
+ }
34
+
35
+ export type NodeSqliteOptions = {|
36
+ /** How a transaction starts. `BEGIN IMMEDIATE` takes the write lock up front. */
37
+ readonly begin?: string,
38
+ /** Prepared statements kept; 0 turns the cache off. */
39
+ readonly statementCache?: number,
40
+ /** `SQLITE_MAX_VARIABLE_NUMBER` if the build changed it. */
41
+ readonly maxParams?: number,
42
+ |};
43
+
44
+ function prepared(database: NodeSqliteDatabase, text: string): NodeSqliteStatement {
45
+ const statement = database.prepare(text);
46
+ statement.setReadBigInts(true);
47
+ // Declared, so that a Node without it is refused with a reason rather than
48
+ // failing on the call below.
49
+ if (!("setReturnArrays" in statement)) {
50
+ throw new SqlError({
51
+ kind: "unsupported",
52
+ feature: "node:sqlite without StatementSync#setReturnArrays (Node 24.0 or 22.16 and newer)",
53
+ });
54
+ }
55
+ statement.setReturnArrays(true);
56
+ return statement;
57
+ }
58
+
59
+ /** A [`Queryable`] over one `DatabaseSync`. */
60
+ export function fromNodeSqlite(
61
+ database: NodeSqliteDatabase,
62
+ options: NodeSqliteOptions = {},
63
+ ): Queryable {
64
+ const statement = statementCache(
65
+ (text) => prepared(database, text),
66
+ options.statementCache ?? DEFAULT_STATEMENT_CACHE,
67
+ );
68
+ return singleConnection({
69
+ engine: "sqlite",
70
+ maxParams: options.maxParams ?? 32766,
71
+ begin: options.begin,
72
+ run: async (text, params, mode) => {
73
+ const prepared = statement(text);
74
+ if (mode === "rows") {
75
+ const rows = prepared.all(...params);
76
+ const arrays: Array<$ReadOnlyArray<mixed>> = [];
77
+ for (const row of rows) {
78
+ if (!Array.isArray(row)) {
79
+ throw new SqlError({ kind: "unsupported", feature: "node:sqlite rows as objects" });
80
+ }
81
+ arrays.push(row);
82
+ }
83
+ return { rows: arrays, rowsAffected: 0, lastInsertId: null };
84
+ }
85
+ const result = prepared.run(...params);
86
+ return {
87
+ rows: [],
88
+ rowsAffected: count(result.changes),
89
+ lastInsertId: rowId(result.lastInsertRowid),
90
+ };
91
+ },
92
+ });
93
+ }
package/adapters/pg.js ADDED
@@ -0,0 +1,88 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/sql/pg`: node-postgres.
4
+ //
5
+ // import pg from "pg";
6
+ // import { fromPgPool } from "@uniflowed/sql/pg";
7
+ //
8
+ // const db = fromPgPool(new pg.Pool({ connectionString }));
9
+ //
10
+ // Every query asks for array rows and replaces node-postgres's type parsers
11
+ // with the identity for that query alone, so the pool's own configuration is
12
+ // left as the application set it, and generated code still sees the server's
13
+ // text. A transaction checks a client out of the pool for its duration.
14
+
15
+ import type { Connection, QueryResult, Queryable, Run } from "../index.js";
16
+ import { singleConnection, transactionOn } from "../index.js";
17
+
18
+ type PgResult = { readonly rows: $ReadOnlyArray<mixed>, readonly rowCount: number | null, ... };
19
+
20
+ type PgQuery = {|
21
+ readonly text: string,
22
+ readonly values: $ReadOnlyArray<mixed>,
23
+ readonly rowMode: "array",
24
+ readonly types: {| readonly getTypeParser: () => (value: string) => string |},
25
+ |};
26
+
27
+ /** A connected `pg.Client` or a client checked out of a pool. */
28
+ export interface PgClient {
29
+ query(config: PgQuery): Promise<PgResult>;
30
+ }
31
+
32
+ /** A client checked out of a `pg.Pool`. */
33
+ export interface PgPoolClient extends PgClient {
34
+ release(error?: mixed): void;
35
+ }
36
+
37
+ /** A `pg.Pool`. */
38
+ export interface PgPool {
39
+ query(config: PgQuery): Promise<PgResult>;
40
+ connect(): Promise<PgPoolClient>;
41
+ }
42
+
43
+ const RAW = { getTypeParser: () => (value: string) => value };
44
+
45
+ function runOn(client: PgClient): Run {
46
+ return async (text, params): Promise<QueryResult> => {
47
+ const result = await client.query({ text, values: params, rowMode: "array", types: RAW });
48
+ const rows: Array<$ReadOnlyArray<mixed>> = [];
49
+ for (const row of result.rows) {
50
+ if (Array.isArray(row)) {
51
+ rows.push(row);
52
+ }
53
+ }
54
+ return { rows, rowsAffected: result.rowCount ?? 0, lastInsertId: null };
55
+ };
56
+ }
57
+
58
+ function connection(client: PgClient): Connection {
59
+ return { engine: "postgresql", maxParams: 65535, run: runOn(client) };
60
+ }
61
+
62
+ /** A [`Queryable`] over one connected `pg.Client`, which it serialises. */
63
+ export function fromPgClient(client: PgClient): Queryable {
64
+ return singleConnection(connection(client));
65
+ }
66
+
67
+ /** A [`Queryable`] over a `pg.Pool`. */
68
+ export function fromPgPool(pool: PgPool): Queryable {
69
+ return {
70
+ engine: "postgresql",
71
+ maxParams: 65535,
72
+ query: runOn(pool),
73
+ transaction: async <T>(body: (tx: Queryable) => Promise<T>): Promise<T> => {
74
+ const client = await pool.connect();
75
+ let broken: mixed = undefined;
76
+ try {
77
+ return await transactionOn(connection(client), body);
78
+ } catch (error) {
79
+ broken = error;
80
+ throw error;
81
+ } finally {
82
+ // Handing the error back makes the pool discard a connection that may
83
+ // still be inside a failed transaction rather than lend it out again.
84
+ client.release(broken);
85
+ }
86
+ },
87
+ };
88
+ }
@@ -0,0 +1,72 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/sql/pglite`: PGlite, PostgreSQL compiled to WebAssembly.
4
+ //
5
+ // import { PGlite } from "@electric-sql/pglite";
6
+ // import { fromPGlite } from "@uniflowed/sql/pglite";
7
+ //
8
+ // const db = fromPGlite(await PGlite.create());
9
+ //
10
+ // PGlite parses and serialises values itself, by type. This adapter turns both
11
+ // off — an identity parser and serialiser for every type it knows — so rows
12
+ // arrive as the server's text and parameters leave as the codecs wrote them,
13
+ // exactly as with every other PostgreSQL adapter.
14
+
15
+ import type { Queryable, SqlParam } from "../index.js";
16
+ import { singleConnection } from "../index.js";
17
+
18
+ /** The part of a `PGlite` instance this uses. */
19
+ export interface PGliteDatabase {
20
+ query(
21
+ text: string,
22
+ params: $ReadOnlyArray<SqlParam>,
23
+ options: {|
24
+ readonly rowMode: "array",
25
+ readonly parsers: { readonly [string]: (value: string) => mixed },
26
+ readonly serializers: { readonly [string]: (value: mixed) => mixed },
27
+ |},
28
+ ): Promise<{ readonly rows: $ReadOnlyArray<mixed>, readonly affectedRows?: number, ... }>;
29
+ readonly parsers: { readonly [string]: mixed };
30
+ readonly serializers: { readonly [string]: mixed };
31
+ }
32
+
33
+ const identity = (value: mixed) => value;
34
+
35
+ function identities(table: { readonly [string]: mixed }): {
36
+ readonly [string]: (value: mixed) => mixed,
37
+ } {
38
+ const out: { [string]: (value: mixed) => mixed } = {};
39
+ for (const key of Object.keys(table)) {
40
+ out[key] = identity;
41
+ }
42
+ return out;
43
+ }
44
+
45
+ /** A [`Queryable`] over one PGlite database. */
46
+ export function fromPGlite(database: PGliteDatabase): Queryable {
47
+ // Rebuilt when PGlite learns a type (an extension, an array type it
48
+ // refreshes), which is rare; every other query reuses the same two tables.
49
+ let known = -1;
50
+ let parsers: { readonly [string]: (value: mixed) => mixed } = {};
51
+ let serializers: { readonly [string]: (value: mixed) => mixed } = {};
52
+ return singleConnection({
53
+ engine: "postgresql",
54
+ maxParams: 65535,
55
+ run: async (text, params) => {
56
+ const size = Object.keys(database.parsers).length + Object.keys(database.serializers).length;
57
+ if (size !== known) {
58
+ known = size;
59
+ parsers = identities(database.parsers);
60
+ serializers = identities(database.serializers);
61
+ }
62
+ const result = await database.query(text, params, { rowMode: "array", parsers, serializers });
63
+ const rows: Array<$ReadOnlyArray<mixed>> = [];
64
+ for (const row of result.rows) {
65
+ if (Array.isArray(row)) {
66
+ rows.push(row);
67
+ }
68
+ }
69
+ return { rows, rowsAffected: result.affectedRows ?? 0, lastInsertId: null };
70
+ },
71
+ });
72
+ }