From 567d8da242cf8640244ac648e4fa5b737fe7fe2c Mon Sep 17 00:00:00 2001 From: Ben Merckx Date: Wed, 21 Aug 2024 11:42:27 +0200 Subject: [PATCH 1/9] Add deno drivers --- package.json | 2 +- src/core/Driver.ts | 1 + src/core/expr/Include.ts | 30 ++++++++++---- src/driver/@db/sqlite.ts | 76 ++++++++++++++++++++++++++++++++++ test/driver/@db-sqlite.test.ts | 11 +++++ 5 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 src/driver/@db/sqlite.ts create mode 100644 test/driver/@db-sqlite.test.ts diff --git a/package.json b/package.json index 10f02f3..4bc2265 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "cycles": "madge --circular src/index.ts src/sqlite.ts src/mysql.ts src/postgres.ts", "test:bun": "bun test", "test:node": "node --test-force-exit --test-concurrency=1 --import tsx --test \"**/*.test.ts\"", - "test:deno": "deno test --no-check --allow-read" + "test:deno": "deno test --no-check -A --unstable-ffi" }, "sideEffects": false, "exports": { diff --git a/src/core/Driver.ts b/src/core/Driver.ts index 1a3a050..911f404 100644 --- a/src/core/Driver.ts +++ b/src/core/Driver.ts @@ -9,6 +9,7 @@ export interface BatchQuery { } export interface DriverSpecs { parsesJson: boolean + parsesNestedJson?: boolean } export interface PrepareOptions { isSelection: boolean diff --git a/src/core/expr/Include.ts b/src/core/expr/Include.ts index 48b221b..c83a906 100644 --- a/src/core/expr/Include.ts +++ b/src/core/expr/Include.ts @@ -1,4 +1,4 @@ -import type {DriverSpecs} from '../Driver.ts' +import type { DriverSpecs } from '../Driver.ts' import { type HasData, type HasSql, @@ -6,16 +6,18 @@ import { internalData, internalSql } from '../Internal.ts' -import type {QueryMeta} from '../MetaData.ts' -import type {MapRowContext, RowOfRecord} from '../Selection.ts' -import {type Sql, sql} from '../Sql.ts' -import type {Select, SelectBase, SelectData} from '../query/Select.ts' +import type { QueryMeta } from '../MetaData.ts' +import type { MapRowContext, RowOfRecord } from '../Selection.ts' +import { type Sql, sql } from '../Sql.ts' +import type { Select, SelectBase, SelectData } from '../query/Select.ts' export interface IncludeData extends SelectData { first: boolean } +let isNested = false + export class Include implements HasData>, HasSql { @@ -28,9 +30,19 @@ export class Include #mapFromDriverValue = (value: any, specs: DriverSpecs): any => { const {select, first} = getData(this) - const parsed = specs.parsesJson ? value : JSON.parse(value) - if (first) - return parsed ? select!.mapRow({values: parsed, index: 0, specs}) : null + const {parsesJson, parsesNestedJson = parsesJson} = specs + const isPreParsed = isNested + ? parsesNestedJson + : parsesJson + const parsed = isPreParsed ? value : JSON.parse(value) + if (first) { + isNested = true + const result = parsed + ? select!.mapRow({values: parsed, index: 0, specs}) + : null + isNested = false + return result + } if (!parsed) return [] const rows: Array> = parsed const ctx: MapRowContext = { @@ -38,11 +50,13 @@ export class Include index: 0, specs } + isNested = true for (let i = 0; i < rows.length; i++) { ctx.values = rows[i] ctx.index = 0 rows[i] = select!.mapRow(ctx) as Array } + isNested = false return rows ?? [] } diff --git a/src/driver/@db/sqlite.ts b/src/driver/@db/sqlite.ts new file mode 100644 index 0000000..1caf9c2 --- /dev/null +++ b/src/driver/@db/sqlite.ts @@ -0,0 +1,76 @@ +// @ts-ignore +import type {Database as Client, Statement} from 'jsr:@db/sqlite' +import {SyncDatabase, type TransactionOptions} from '../../core/Database.ts' +import type {BatchQuery, SyncDriver, SyncStatement} from '../../core/Driver.ts' +import {sqliteDialect} from '../../sqlite.ts' +import {sqliteDiff} from '../../sqlite/diff.ts' +import {execTransaction} from '../../sqlite/transactions.ts' + +class PreparedStatement implements SyncStatement { + constructor(private stmt: Statement) {} + + all(params: Array) { + return >this.stmt.all(...params) + } + + run(params: Array) { + return this.stmt.run(...params) + } + + get(params: Array) { + return this.stmt.get(...params) + } + + values(params: Array) { + return this.stmt.values(...params) + } + + free() { + this.stmt.finalize() + } +} + +class DbSqliteDriver implements SyncDriver { + parsesJson = true + parsesNestedJson = false + + constructor( + private client: Client, + private depth = 0 + ) {} + + exec(query: string): void { + this.client.exec(query) + } + + close() { + this.client.close() + } + + prepare(sql: string) { + return new PreparedStatement(this.client.prepare(sql)) + } + + batch(queries: Array): Array> { + return this.transaction(tx => { + return queries.map(({sql, params}) => tx.prepare(sql).values(params)) + }, {}) + } + + transaction( + run: (inner: SyncDriver) => T, + options: TransactionOptions['sqlite'] + ): T { + return execTransaction( + this, + this.depth, + depth => new DbSqliteDriver(this.client, depth), + run, + options + ) + } +} + +export function connect(db: Client): SyncDatabase<'sqlite'> { + return new SyncDatabase(new DbSqliteDriver(db), sqliteDialect, sqliteDiff) +} diff --git a/test/driver/@db-sqlite.test.ts b/test/driver/@db-sqlite.test.ts new file mode 100644 index 0000000..6b46ab8 --- /dev/null +++ b/test/driver/@db-sqlite.test.ts @@ -0,0 +1,11 @@ +import {testDriver} from '../TestDriver.ts' +import {isDeno} from '../TestRuntime.ts' + +async function createDb() { + // @ts-ignore + const {Database} = await import('jsr:@db/sqlite') + const {connect} = await import('../../src/driver/@db/sqlite.ts') + return connect(new Database(':memory:')) +} + +if (isDeno) await testDriver(import.meta, createDb) From 596512dd596dd0edfad19d72aae23b30bf64e45a Mon Sep 17 00:00:00 2001 From: Ben Merckx Date: Tue, 10 Sep 2024 12:30:47 +0200 Subject: [PATCH 2/9] Restructure drivers --- .npmrc | 1 + README.md | 18 +-- bun.lockb | Bin 146714 -> 150407 bytes deno.json | 3 +- jsr.json | 3 +- package.json | 3 +- src/driver.ts | 3 + src/driver/{@db/sqlite.ts => @db_sqlite.ts} | 27 +++-- src/driver/@electric-sql_pglite.ts | 104 +++++++++++++++++ src/driver/jsr.ts | 1 + src/driver/npm.ts | 5 + src/driver/pglite.ts | 105 +----------------- test/TestRuntime.ts | 2 + test/driver/@db-sqlite.test.ts | 11 -- test/driver/@db_sqlite.test.ts | 10 ++ ...e.test.ts => @electric-sql_pglite.test.ts} | 4 +- test/driver/better-sqlite3.test.ts | 4 +- test/driver/bun-sqlite.test.ts | 4 +- test/driver/mysql2.test.ts | 8 +- test/driver/pg.test.ts | 8 +- test/driver/sql.js.test.ts | 4 +- 21 files changed, 173 insertions(+), 155 deletions(-) create mode 100644 .npmrc create mode 100644 src/driver.ts rename src/driver/{@db/sqlite.ts => @db_sqlite.ts} (68%) create mode 100644 src/driver/@electric-sql_pglite.ts create mode 100644 src/driver/jsr.ts create mode 100644 src/driver/npm.ts delete mode 100644 test/driver/@db-sqlite.test.ts create mode 100644 test/driver/@db_sqlite.test.ts rename test/driver/{pglite.test.ts => @electric-sql_pglite.test.ts} (58%) diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..41583e3 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/README.md b/README.md index d32fcfa..81e81fa 100644 --- a/README.md +++ b/README.md @@ -149,20 +149,20 @@ const PostTags = table({ Currently supported drivers: -| Driver | import | -| ---------------- | ------------------------------ | -| `better-sqlite3` | `'rado/driver/better-sqlite3'` | -| `bun-sqlite` | `'rado/driver/bun-sqlite'` | -| `mysql2` | `'rado/driver/mysql2'` | -| `pg` | `'rado/driver/pg'` | -| `pglite` | `'rado/driver/pglite'` | -| `sql.js` | `'rado/driver/sql.js'` | +| Driver | import | +| ---------------- | -------------------------------- | +| `better-sqlite3` | `{'better-sqlite3' as connect}'` | +| `bun-sqlite` | `{'bun-sqlite' as connect}` | +| `mysql2` | `{'mysql2' as connect}` | +| `pg` | `{'pg' as connect}` | +| `pglite` | `{'pglite' as connect}` | +| `sql.js` | `{'sql.js' as connect}` | Pass an instance of the database to the `connect` function to get started: ```ts import Database from 'better-sqlite3' -import {connect} from 'rado/driver/better-sqlite3' +import {'better-sqlite3' as connect} from 'rado/driver' const db = connect(new Database('foobar.db')) ``` diff --git a/bun.lockb b/bun.lockb index 1c53b9af1b9aa0c7e85d7de3d9af943a5e213d07..ec0ae903f5e8ba908c9c103a14fba086d22b0e16 100755 GIT binary patch delta 28687 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 {} + constructor(private stmt: Statement) {} - all(params: Array) { + all(params: RestBindParameters) { return >this.stmt.all(...params) } - run(params: Array) { + run(params: RestBindParameters) { return this.stmt.run(...params) } - get(params: Array) { + get(params: RestBindParameters) { return this.stmt.get(...params) } - values(params: Array) { + values(params: RestBindParameters) { return this.stmt.values(...params) } diff --git a/src/driver/@electric-sql_pglite.ts b/src/driver/@electric-sql_pglite.ts new file mode 100644 index 0000000..aeb8654 --- /dev/null +++ b/src/driver/@electric-sql_pglite.ts @@ -0,0 +1,104 @@ +import type {PGlite, Transaction} from '@electric-sql/pglite' +import {AsyncDatabase, type TransactionOptions} from '../core/Database.ts' +import type {AsyncDriver, AsyncStatement, BatchQuery} from '../core/Driver.ts' +import {postgresDialect} from '../postgres/dialect.ts' +import {postgresDiff} from '../postgres/diff.ts' +import {setTransaction} from '../postgres/transactions.ts' + +type Queryable = PGlite | Transaction + +class PreparedStatement implements AsyncStatement { + constructor( + private client: Queryable, + private sql: string + ) {} + + all(params: Array): Promise> { + return this.client + .query(this.sql, params, { + rowMode: 'object' + }) + .then(res => res.rows) + } + + async run(params: Array) { + await this.client.query(this.sql, params, { + rowMode: 'array' + }) + } + + get(params: Array): Promise { + return this.all(params).then(rows => rows[0] ?? null) + } + + values(params: Array): Promise>> { + return this.client + .query>(this.sql, params, { + rowMode: 'array' + }) + .then(res => res.rows) + } + + free() {} +} + +export class PGliteDriver implements AsyncDriver { + parsesJson = true + + constructor( + private client: Queryable, + private depth = 0 + ) {} + + async exec(query: string) { + await this.client.exec(query) + } + + close(): Promise { + if ('close' in this.client) { + return Promise.resolve() + // This fails currently + // return this.client.close() + } + throw new Error('Cannot close a transaction') + } + + prepare(sql: string): PreparedStatement { + return new PreparedStatement(this.client, sql) + } + + async batch(queries: Array): Promise>> { + const transact = async (tx: AsyncDriver) => { + const results = [] + for (const {sql, params} of queries) + results.push(await tx.prepare(sql).values(params)) + return results + } + if (this.depth > 0) return transact(this) + return this.transaction(transact, {}) + } + + async transaction( + run: (inner: AsyncDriver) => Promise, + options: TransactionOptions['postgres'] + ): Promise { + if (this.depth === 0 && 'transaction' in this.client) + return this.client.transaction(async (tx: Transaction) => { + await tx.query(setTransaction(options)) + return run(new PGliteDriver(tx, this.depth + 1)) + }) as Promise + await this.exec(`savepoint d${this.depth}`) + try { + const result = await run(new PGliteDriver(this.client, this.depth + 1)) + await this.exec(`release d${this.depth}`) + return result + } catch (error) { + await this.exec(`rollback to d${this.depth}`) + throw error + } + } +} + +export function connect(db: PGlite): AsyncDatabase<'postgres'> { + return new AsyncDatabase(new PGliteDriver(db), postgresDialect, postgresDiff) +} diff --git a/src/driver/jsr.ts b/src/driver/jsr.ts new file mode 100644 index 0000000..4b8ae72 --- /dev/null +++ b/src/driver/jsr.ts @@ -0,0 +1 @@ +export {connect as '@db/sqlite'} from './@db_sqlite.ts' diff --git a/src/driver/npm.ts b/src/driver/npm.ts new file mode 100644 index 0000000..9d8a30f --- /dev/null +++ b/src/driver/npm.ts @@ -0,0 +1,5 @@ +export {connect as '@electric-sql/pglite'} from './@electric-sql_pglite.ts' +export {connect as 'better-sqlite3'} from './better-sqlite3.ts' +export {connect as 'mysql2'} from './mysql2.ts' +export {connect as 'pg'} from './pg.ts' +export {connect as 'sql.js'} from './sql.js.ts' diff --git a/src/driver/pglite.ts b/src/driver/pglite.ts index 55ae06e..d901b4d 100644 --- a/src/driver/pglite.ts +++ b/src/driver/pglite.ts @@ -1,104 +1 @@ -import type {PGlite, Transaction} from '@electric-sql/pglite' -import type {AsyncDriver, AsyncStatement, BatchQuery} from '../core/Driver.ts' -import {AsyncDatabase, type TransactionOptions} from '../index.ts' -import {postgresDialect} from '../postgres/dialect.ts' -import {postgresDiff} from '../postgres/diff.ts' -import {setTransaction} from '../postgres/transactions.ts' - -type Queryable = PGlite | Transaction - -class PreparedStatement implements AsyncStatement { - constructor( - private client: Queryable, - private sql: string - ) {} - - all(params: Array): Promise> { - return this.client - .query(this.sql, params, { - rowMode: 'object' - }) - .then(res => res.rows) - } - - async run(params: Array) { - await this.client.query(this.sql, params, { - rowMode: 'array' - }) - } - - get(params: Array): Promise { - return this.all(params).then(rows => rows[0] ?? null) - } - - values(params: Array): Promise>> { - return this.client - .query>(this.sql, params, { - rowMode: 'array' - }) - .then(res => res.rows) - } - - free() {} -} - -export class PGliteDriver implements AsyncDriver { - parsesJson = true - - constructor( - private client: Queryable, - private depth = 0 - ) {} - - async exec(query: string) { - await this.client.exec(query) - } - - close(): Promise { - if ('close' in this.client) { - return Promise.resolve() - // This fails currently - // return this.client.close() - } - throw new Error('Cannot close a transaction') - } - - prepare(sql: string): PreparedStatement { - return new PreparedStatement(this.client, sql) - } - - async batch(queries: Array): Promise>> { - const transact = async (tx: AsyncDriver) => { - const results = [] - for (const {sql, params} of queries) - results.push(await tx.prepare(sql).values(params)) - return results - } - if (this.depth > 0) return transact(this) - return this.transaction(transact, {}) - } - - async transaction( - run: (inner: AsyncDriver) => Promise, - options: TransactionOptions['postgres'] - ): Promise { - if (this.depth === 0 && 'transaction' in this.client) - return this.client.transaction(async (tx: Transaction) => { - await tx.query(setTransaction(options)) - return run(new PGliteDriver(tx, this.depth + 1)) - }) as Promise - await this.exec(`savepoint d${this.depth}`) - try { - const result = await run(new PGliteDriver(this.client, this.depth + 1)) - await this.exec(`release d${this.depth}`) - return result - } catch (error) { - await this.exec(`rollback to d${this.depth}`) - throw error - } - } -} - -export function connect(db: PGlite): AsyncDatabase<'postgres'> { - return new AsyncDatabase(new PGliteDriver(db), postgresDialect, postgresDiff) -} +export * from './@electric-sql_pglite.ts' diff --git a/test/TestRuntime.ts b/test/TestRuntime.ts index 5607df6..c1ced0e 100644 --- a/test/TestRuntime.ts +++ b/test/TestRuntime.ts @@ -1,3 +1,5 @@ export const isBun = 'Bun' in globalThis export const isDeno = 'Deno' in globalThis export const isNode = !isBun && 'process' in globalThis +// @ts-ignore +export const isCi = !isDeno && 'CI' in process.env diff --git a/test/driver/@db-sqlite.test.ts b/test/driver/@db-sqlite.test.ts deleted file mode 100644 index 6b46ab8..0000000 --- a/test/driver/@db-sqlite.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {testDriver} from '../TestDriver.ts' -import {isDeno} from '../TestRuntime.ts' - -async function createDb() { - // @ts-ignore - const {Database} = await import('jsr:@db/sqlite') - const {connect} = await import('../../src/driver/@db/sqlite.ts') - return connect(new Database(':memory:')) -} - -if (isDeno) await testDriver(import.meta, createDb) diff --git a/test/driver/@db_sqlite.test.ts b/test/driver/@db_sqlite.test.ts new file mode 100644 index 0000000..724af8a --- /dev/null +++ b/test/driver/@db_sqlite.test.ts @@ -0,0 +1,10 @@ +import {testDriver} from '../TestDriver.ts' +import {isDeno} from '../TestRuntime.ts' + +async function createDb() { + const {Database} = await import('@db/sqlite') + const driver = await import('../../src/driver.ts') + return driver['@db/sqlite'](new Database(':memory:')) +} + +if (isDeno) await testDriver(import.meta, createDb) diff --git a/test/driver/pglite.test.ts b/test/driver/@electric-sql_pglite.test.ts similarity index 58% rename from test/driver/pglite.test.ts rename to test/driver/@electric-sql_pglite.test.ts index 716ff66..6074a73 100644 --- a/test/driver/pglite.test.ts +++ b/test/driver/@electric-sql_pglite.test.ts @@ -1,7 +1,7 @@ -import {connect} from '../../src/driver/pglite.ts' import {testDriver} from '../TestDriver.ts' await testDriver(import.meta, async () => { + const driver = await import('../../src/driver.ts') const {PGlite} = await import('@electric-sql/pglite') - return connect(new PGlite()) + return driver['@electric-sql/pglite'](new PGlite()) }) diff --git a/test/driver/better-sqlite3.test.ts b/test/driver/better-sqlite3.test.ts index 9722e9c..54b7f31 100644 --- a/test/driver/better-sqlite3.test.ts +++ b/test/driver/better-sqlite3.test.ts @@ -4,6 +4,6 @@ import {isNode} from '../TestRuntime.ts' if (isNode) await testDriver(import.meta, async () => { const {default: Database} = await import('better-sqlite3') - const {connect} = await import('../../src/driver/better-sqlite3.ts') - return connect(new Database(':memory:')) + const driver = await import('../../src/driver.ts') + return driver['better-sqlite3'](new Database(':memory:')) }) diff --git a/test/driver/bun-sqlite.test.ts b/test/driver/bun-sqlite.test.ts index 9939960..bb7e737 100644 --- a/test/driver/bun-sqlite.test.ts +++ b/test/driver/bun-sqlite.test.ts @@ -3,8 +3,8 @@ import {isBun} from '../TestRuntime.ts' async function createDb() { const {Database} = await import('bun:sqlite') - const {connect} = await import('../../src/driver/bun-sqlite.ts') - return connect(new Database(':memory:')) + const driver = await import('../../src/driver.ts') + return driver['bun:sqlite'](new Database(':memory:')) } if (isBun) await testDriver(import.meta, createDb) diff --git a/test/driver/mysql2.test.ts b/test/driver/mysql2.test.ts index 9d03444..e4f469b 100644 --- a/test/driver/mysql2.test.ts +++ b/test/driver/mysql2.test.ts @@ -1,16 +1,16 @@ -import {connect} from '../../src/driver/mysql2.ts' import {testDriver} from '../TestDriver.ts' -import {isDeno} from '../TestRuntime.ts' +import {isCi} from '../TestRuntime.ts' -if (!isDeno && process.env.CI) +if (isCi) await testDriver( import.meta, async () => { + const driver = await import('../../src/driver.ts') const {default: mysql2} = await import('mysql2') const client = mysql2.createConnection( 'mysql://root:mysql@0.0.0.0:3306/mysql' ) - return connect(client) + return driver.mysql2(client) }, false ) diff --git a/test/driver/pg.test.ts b/test/driver/pg.test.ts index 8136540..f0418bc 100644 --- a/test/driver/pg.test.ts +++ b/test/driver/pg.test.ts @@ -1,13 +1,13 @@ -import {connect} from '../../src/driver/pg.ts' import {testDriver} from '../TestDriver.ts' -import {isDeno} from '../TestRuntime.ts' +import {isCi} from '../TestRuntime.ts' -if (!isDeno && process.env.CI) +if (isCi) await testDriver(import.meta, async () => { + const driver = await import('../../src/driver.ts') const {default: pg} = await import('pg') const client = new pg.Client({ connectionString: 'postgres://postgres:postgres@0.0.0.0:5432/postgres' }) await client.connect() - return connect(client) + return driver.pg(client) }) diff --git a/test/driver/sql.js.test.ts b/test/driver/sql.js.test.ts index f342e5f..87e415a 100644 --- a/test/driver/sql.js.test.ts +++ b/test/driver/sql.js.test.ts @@ -1,8 +1,8 @@ -import {connect} from '../../src/driver/sql.js.ts' import {testDriver} from '../TestDriver.ts' await testDriver(import.meta, async () => { + const driver = await import('../../src/driver.ts') const {default: init} = await import('sql.js') const {Database} = await init() - return connect(new Database()) + return driver['sql.js'](new Database()) }) From 9553085d1b200033c91983ed4ca3ba66a04b65c9 Mon Sep 17 00:00:00 2001 From: Ben Merckx Date: Tue, 10 Sep 2024 12:53:32 +0200 Subject: [PATCH 3/9] Rename drivers to be a bit more descriptive. We won't always have a package name anyway. --- jsr.json | 2 +- src/driver-jsr.ts | 1 + src/driver.ts | 8 +- src/driver/@electric-sql_pglite.ts | 104 ----------------- .../{@db_sqlite.ts => denodrivers-sqlite.ts} | 0 src/driver/jsr.ts | 1 - src/driver/npm.ts | 5 - src/driver/pglite.ts | 105 +++++++++++++++++- 8 files changed, 112 insertions(+), 114 deletions(-) create mode 100644 src/driver-jsr.ts delete mode 100644 src/driver/@electric-sql_pglite.ts rename src/driver/{@db_sqlite.ts => denodrivers-sqlite.ts} (100%) delete mode 100644 src/driver/jsr.ts delete mode 100644 src/driver/npm.ts diff --git a/jsr.json b/jsr.json index 99ff436..41b0ed5 100644 --- a/jsr.json +++ b/jsr.json @@ -9,6 +9,6 @@ "./sqlite": "./src/sqlite.ts", "./mysql": "./src/mysql.ts", "./postgres": "./src/postgres.ts", - "./driver": "./src/driver/jsr.ts" + "./driver": "./src/driver-jsr.ts" } } diff --git a/src/driver-jsr.ts b/src/driver-jsr.ts new file mode 100644 index 0000000..afa884d --- /dev/null +++ b/src/driver-jsr.ts @@ -0,0 +1 @@ +export {connect as '@db/sqlite'} from './driver/denodrivers-sqlite.ts' diff --git a/src/driver.ts b/src/driver.ts index e8013fe..75b2579 100644 --- a/src/driver.ts +++ b/src/driver.ts @@ -1,3 +1,7 @@ +export * from './driver-jsr.ts' +export {connect as 'better-sqlite3'} from './driver/better-sqlite3.ts' export {connect as 'bun:sqlite'} from './driver/bun-sqlite.ts' -export * from './driver/jsr.ts' -export * from './driver/npm.ts' +export {connect as 'mysql2'} from './driver/mysql2.ts' +export {connect as 'pg'} from './driver/pg.ts' +export {connect as '@electric-sql/pglite'} from './driver/pglite.ts' +export {connect as 'sql.js'} from './driver/sql.js.ts' diff --git a/src/driver/@electric-sql_pglite.ts b/src/driver/@electric-sql_pglite.ts deleted file mode 100644 index aeb8654..0000000 --- a/src/driver/@electric-sql_pglite.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type {PGlite, Transaction} from '@electric-sql/pglite' -import {AsyncDatabase, type TransactionOptions} from '../core/Database.ts' -import type {AsyncDriver, AsyncStatement, BatchQuery} from '../core/Driver.ts' -import {postgresDialect} from '../postgres/dialect.ts' -import {postgresDiff} from '../postgres/diff.ts' -import {setTransaction} from '../postgres/transactions.ts' - -type Queryable = PGlite | Transaction - -class PreparedStatement implements AsyncStatement { - constructor( - private client: Queryable, - private sql: string - ) {} - - all(params: Array): Promise> { - return this.client - .query(this.sql, params, { - rowMode: 'object' - }) - .then(res => res.rows) - } - - async run(params: Array) { - await this.client.query(this.sql, params, { - rowMode: 'array' - }) - } - - get(params: Array): Promise { - return this.all(params).then(rows => rows[0] ?? null) - } - - values(params: Array): Promise>> { - return this.client - .query>(this.sql, params, { - rowMode: 'array' - }) - .then(res => res.rows) - } - - free() {} -} - -export class PGliteDriver implements AsyncDriver { - parsesJson = true - - constructor( - private client: Queryable, - private depth = 0 - ) {} - - async exec(query: string) { - await this.client.exec(query) - } - - close(): Promise { - if ('close' in this.client) { - return Promise.resolve() - // This fails currently - // return this.client.close() - } - throw new Error('Cannot close a transaction') - } - - prepare(sql: string): PreparedStatement { - return new PreparedStatement(this.client, sql) - } - - async batch(queries: Array): Promise>> { - const transact = async (tx: AsyncDriver) => { - const results = [] - for (const {sql, params} of queries) - results.push(await tx.prepare(sql).values(params)) - return results - } - if (this.depth > 0) return transact(this) - return this.transaction(transact, {}) - } - - async transaction( - run: (inner: AsyncDriver) => Promise, - options: TransactionOptions['postgres'] - ): Promise { - if (this.depth === 0 && 'transaction' in this.client) - return this.client.transaction(async (tx: Transaction) => { - await tx.query(setTransaction(options)) - return run(new PGliteDriver(tx, this.depth + 1)) - }) as Promise - await this.exec(`savepoint d${this.depth}`) - try { - const result = await run(new PGliteDriver(this.client, this.depth + 1)) - await this.exec(`release d${this.depth}`) - return result - } catch (error) { - await this.exec(`rollback to d${this.depth}`) - throw error - } - } -} - -export function connect(db: PGlite): AsyncDatabase<'postgres'> { - return new AsyncDatabase(new PGliteDriver(db), postgresDialect, postgresDiff) -} diff --git a/src/driver/@db_sqlite.ts b/src/driver/denodrivers-sqlite.ts similarity index 100% rename from src/driver/@db_sqlite.ts rename to src/driver/denodrivers-sqlite.ts diff --git a/src/driver/jsr.ts b/src/driver/jsr.ts deleted file mode 100644 index 4b8ae72..0000000 --- a/src/driver/jsr.ts +++ /dev/null @@ -1 +0,0 @@ -export {connect as '@db/sqlite'} from './@db_sqlite.ts' diff --git a/src/driver/npm.ts b/src/driver/npm.ts deleted file mode 100644 index 9d8a30f..0000000 --- a/src/driver/npm.ts +++ /dev/null @@ -1,5 +0,0 @@ -export {connect as '@electric-sql/pglite'} from './@electric-sql_pglite.ts' -export {connect as 'better-sqlite3'} from './better-sqlite3.ts' -export {connect as 'mysql2'} from './mysql2.ts' -export {connect as 'pg'} from './pg.ts' -export {connect as 'sql.js'} from './sql.js.ts' diff --git a/src/driver/pglite.ts b/src/driver/pglite.ts index d901b4d..aeb8654 100644 --- a/src/driver/pglite.ts +++ b/src/driver/pglite.ts @@ -1 +1,104 @@ -export * from './@electric-sql_pglite.ts' +import type {PGlite, Transaction} from '@electric-sql/pglite' +import {AsyncDatabase, type TransactionOptions} from '../core/Database.ts' +import type {AsyncDriver, AsyncStatement, BatchQuery} from '../core/Driver.ts' +import {postgresDialect} from '../postgres/dialect.ts' +import {postgresDiff} from '../postgres/diff.ts' +import {setTransaction} from '../postgres/transactions.ts' + +type Queryable = PGlite | Transaction + +class PreparedStatement implements AsyncStatement { + constructor( + private client: Queryable, + private sql: string + ) {} + + all(params: Array): Promise> { + return this.client + .query(this.sql, params, { + rowMode: 'object' + }) + .then(res => res.rows) + } + + async run(params: Array) { + await this.client.query(this.sql, params, { + rowMode: 'array' + }) + } + + get(params: Array): Promise { + return this.all(params).then(rows => rows[0] ?? null) + } + + values(params: Array): Promise>> { + return this.client + .query>(this.sql, params, { + rowMode: 'array' + }) + .then(res => res.rows) + } + + free() {} +} + +export class PGliteDriver implements AsyncDriver { + parsesJson = true + + constructor( + private client: Queryable, + private depth = 0 + ) {} + + async exec(query: string) { + await this.client.exec(query) + } + + close(): Promise { + if ('close' in this.client) { + return Promise.resolve() + // This fails currently + // return this.client.close() + } + throw new Error('Cannot close a transaction') + } + + prepare(sql: string): PreparedStatement { + return new PreparedStatement(this.client, sql) + } + + async batch(queries: Array): Promise>> { + const transact = async (tx: AsyncDriver) => { + const results = [] + for (const {sql, params} of queries) + results.push(await tx.prepare(sql).values(params)) + return results + } + if (this.depth > 0) return transact(this) + return this.transaction(transact, {}) + } + + async transaction( + run: (inner: AsyncDriver) => Promise, + options: TransactionOptions['postgres'] + ): Promise { + if (this.depth === 0 && 'transaction' in this.client) + return this.client.transaction(async (tx: Transaction) => { + await tx.query(setTransaction(options)) + return run(new PGliteDriver(tx, this.depth + 1)) + }) as Promise + await this.exec(`savepoint d${this.depth}`) + try { + const result = await run(new PGliteDriver(this.client, this.depth + 1)) + await this.exec(`release d${this.depth}`) + return result + } catch (error) { + await this.exec(`rollback to d${this.depth}`) + throw error + } + } +} + +export function connect(db: PGlite): AsyncDatabase<'postgres'> { + return new AsyncDatabase(new PGliteDriver(db), postgresDialect, postgresDiff) +} From 0823dd6c3ab76425b44baa9e95209354a454b812 Mon Sep 17 00:00:00 2001 From: Ben Merckx Date: Tue, 10 Sep 2024 13:01:10 +0200 Subject: [PATCH 4/9] Oops, seems this only works from tsc 5.6 onwards --- bun.lockb | Bin 146836 -> 147208 bytes package.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/bun.lockb b/bun.lockb index 5598bfc29e0a0cd486822aed02f0a492fede03ae..912089bd0c6b2a3b2f4f116f4ebd103e1e9cf4ed 100755 GIT binary patch delta 8214 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 RvLIpaEW6d_c*wSxe*z5|O(y^V delta 8048 zcmaJ`30M@zw(c5epn;La1rP~>D;h>w2OVHQje?U=aYN#Yf`9=*!3B+LHupqJQ<_8* z<1>j_Zh}U^HNh;32|mek)oV~(5qC6hQSkkzyC=STzn9nF_jBt1>r{1}Q_IXyOPbx* zK5~206qcU6M!M7Px5~XCjqkMYnsVcj`&Z%Xryc8lXHLp4mZV83n!XLT1@uR-RR0eAJYbjeX>NY0dsDTpUmB0?Epo9-fn5IB~5_Yt8XiYF(lSIvJLF-^^>={1!+UFookeNVB)>nmr9m;WOye z`g?FnU-s0Jypp`^;-A4OP4$Ddv<%SGvT2AUwTA9UyCdDcXjyhMySOlaX13Iru1!%rEG>YOu#|_~%;LgXQzfZ;hSvT|w4;n12X76V z_=1+#-LM#4w}lvb;lwGD^cI8;a5xG_8ut5WNE1+!S(G_3KU-SO^W1}b%^WL9ZJUtF{%}pFEMN*0EP$!Q1qMY!O(xU9*W7&L;hbiobY%L+X;6}|) z=4kKw44e`*K38kLh1Ym=Ni`yJ8ZTvl()R%@#dQgmMm7kx)&EnU|Dray7kIL#U$317 zk`x5*w_#~_ErJbz&4Zur!$+VJVv}cC7Y1gzG85^ z(i+>m4;&a@9b%T#!Q%PKjuy6=rw*|yzky>@Ep+8Ip=M^~RYR=ob*`jY74Kw8dLA_f zwPp-onr3AOc@=8fqPu~paiJQe1dMu6nY<6IAFu9VQGBqEu(TH%)%LG(Wtdg&fp0Wa_DoCl*H}bE%HQ^dh_aVi}Ef?*sB=A>W*gR99S%vGq34k zRzgt`uhykKYgWdA#ejKmpAKf_Bd|y?Cv|`)!J@$!uW4^qj5rjC$=s*EnI-eokyhnP zXw(;a4mZnp!FqFJe~Z!sagnbR_X#m81z_ams*a`{3^AY|8k=(}+NgDul0-1P#ue!7y^LG`t|lhE6ok zaI-Q7EDp>`efdT(dO3z4YBn6VarX|1a%;?83a<{e$YW8$l;CxHQKE4<^R(_} zwv|_nwJHXjcR02v-LYn6I2Z!a)|awFuXEu(o#WiFI?SkY<33}|%1AKEE}9{OYrwFQ zboQ&x)Bxo+=slX-ds>u%D3O=85Lf&gy8?!x>d{TY0g6#~uqb6H(R(DGmTXqq4tK06 z%%VJmn7VjhLy5dFgUHlDFbkL`tuDC1jqJI*(44bb{%)|oSHG{rVjn-pygz5y1A z9t`SaR)Repw-b(CniUsyGLykjtxYETfTw0*D~^(+$I%LFhO}sXD}!)a)5dkuVg#c_q`pIb3>Hntgwk`gBu!Pl zD8@}-9*=*HS(VOxq)LKr9ENUewb} zsnY9QX@e?kf;PP#>IB5=%w1)(z-ZK%RP4f|V3b@e9;;dL#zzj-VF4qe13E*WL*v|@ zR$rlJY&cg6t?X@{T4*(#%Hqok6Ipv+1ql^JR%KPT7QQq0nI7i`La)GC5{;u1Yl#+x znkP9Q4BHr6;~yxImzKZVU_G@q$WdsHqmE*o3>L4})l9R<`X)x84bbtip#{de@v7-o zmdll5EBlP67F!Kz!=c7&= z`F^l7Tsf1@-OdR?n-z z4ugfCv#|zVdM=&MK4;{$U{`tA`E=g(ypgXwZ)4Z_Nw5=O2^VbaCSQ6XoiDjy-c|1_Hc+0B_isk)H%R0hVyn z#=Q8_n+WhG0tEBny>21ETL|!$jkV+rV3)z{jW*VrZ)ik-jR+9TkEh;7fVUCgZ5s>V z55XRQjlE-IfqdH?1b7Dl{$XQ5JmU`p_y+<63+BpQOyFI_ch|;3_-?QYu%LT3)`92U zLwxrTA6O?Ia3AsAM|}5ftP8IKI}8^7z{bLO=>x>~0P%r!=V1>K-$TUr(8j{~Nw5=O z34hvH55DwI#P=uS1B>9j9wEL*i0_e&Mezo(%V74$HWtG-JVtzv5g(YDr#2zJCdAid zW4-u8um@mcn{CX(w>2ZaX2kcz#u9kO6U6rf@qt;_DJ*@R8#AseU^bS#ZZ}v3Gm0Pv z+E?T;mM(G`glZC=69EPY{ssv13=js0DiRKp5bgvaRg^kGnC%3imIRv!bB55>8Ny0u z2zGIjgcBqr$PfmLr80yiGKA|Sq={ZG5aL`QY;=JzOf-;inFPBlgb`wcD}?o~5SmCx z7pZRelY&6R=WY-(#6uDukT6z(Fj{Owq~c=*g3%qqSdrllVU#oZ zq6qVX(A5jVN-qcn;v@+tNJ#L8P$-sqLs;Ss;W`P^L@yr*aXt_>`amca4J2G9!R`xT zhS=Z>VZASeCK6_e)RquZT0;1|C4^G(5CWSc(ptgH72C+XBv@;hc_M?%d{IVbfl%7O zEEJh!7KzQbpcEk`@1q%nOpJWNSrk>KT+3}Im_Mf`PW)3>cK{@ z32-h?31im`@@M!$>Qz3iCtJWEihbd%D|6#D9Xp6R6h&PJ7EgMSiS_IBD!wDRT(M2fZ=}k~|0Bh1^vK>Uapf!dZBl*aUvezZD!5 z`i}uRmwd_#da@iRrMfn{^`17r?I}^_d5Ow3Yxq)759bs9eNaCQy5-Y7O9fh59UnKryxf5;RF#to!+a z<2#M|yexbcql;JcssMRMZr>rwWCeNET!3sY*){azQD)WuT0DhO2-det&m z8pSifUR_(E`vpVWr)w)k8FjHruL?mGzHid6U+E293q`pEpyy3!POu$-xg_A>g1w9` z*6Qw^Ak9-NBx#+lbq1fWYj5dV7x2ZpCPW%_u^v@4=rCXqKq-7%ckhPsZIu0C-_f=1 zDBnVva`LX&OkHf$tFW-7BPdf&HtCuP^# zQOn9#y~5hKh>w@C&)k>LB;xVr0&3?NT1BP6Y+x!-0K5oH0djzBU^I{pj09ZJ)>^)E zIlJj3CxR!4ZEvto7j#6iGjI~sCjeUTqy+)(01u!w;10L}u0TEfE&&&S)4;y~$Wkrv zJ8%j(1JnWMfpfrF;37c&o^Ymq(dUAH>y$dV{J$KDW7kWK08Q^r;5u*(xB@f)SM@Ua z-T)}ew*VIi&cI!i{{Zd)w*e<;O|XwuZGj}+L*W5%A9x7-2|NPmWfWF3%oBhbGQa@H zfC5l7Zvc)maf;jrbu9tP3}wR$CB)?YUB$`4XFV+wgLiyHUM>W3}}mT z0N@8uy8U6v-;r9f!O)%oXfO`F19&J90<;Gl16~jhLi61lp!tk~4F{eDx&z&SEK zFj$JVD=f7k^aOeUCLjWc1W2dt)eANnpqIt~aex_!1?HgMLi3*jVgm*O1At^83Fr&- z0r~^|fDklz4mK5_HZ<3E*yn+vKmzy>*hJXDu!Df3D3eaw2ypTvpETVLXPEzCAnW1q z67T}bqX0S>#sXsiIuM9&gUy1S1Y`mefbqaMU?MOXptiX{9zbm!$VYiPPzV$O(|}pP z44S{0Krv84f^PA_&slJbIiN`#fAD;Dq}+Ecdov`^@gL~or76u{ZSDAyfsKnW^)%sx zaufS^vVg##B<(+!UVZzY@=CZK2M4n$GTIa^%@vJsV3opS7Yk_jJN$6eExa^jj+1@F zhH{6WS!zxaNxPUo%MxW@Gb{zg$iOdR&Vy>7AW4P5e6B`5rYfTJj#Hh)sJ~B-<-$_Aom}7rd7R#5gV`UUv=K zWZ$y=Blt&{B22v`$#G3#2~HU|XU#yHS`$e_TPq6Clx-IFZ&;MrvzN7Xl->5Rw(b6I zdd$-Ic9FP``5S(=i&6V96m32Yr|n|Qe#p1%;u};mCX)AKn*X}tD4R8Myl>U#1{P(C zHbt3nOsNk4;qT~%1&JB^kt_XW`b+dq4fTV?gZ+5*Cei%>vj;kEH#+YsJv3^z({Bhe z8tK9$`U*}Cf3q0XS*FG4SVHOw325iI5vjP*=`e3}%R_&WNGIHn4;Ayj#*6hOw^3aD z8r?f?PA**A)ApP8O*PJJeos>@t#vKCY_(W~B(wL$PBLGL1~Qc*sFDT5IxbEMKA-1o zy16nHFT-l3#*V9%#mQdv)3Y|5&}%S^TO&k%C01h}@k=GMw{zU7Y?(51MrrftAZ@%+ z7;t^Mh(Cx)X-pR*4`STi#nOXl;J7=fIN-lEzssGE)do=}Ov>>LaT|_-j_a52>fWFJ z-uTYBI6uyW#y!zAfrg@wNHtM` zj!U2kpFX}*R}-)f9pOl(XQKET?(H16MrqF;UEX|s+$R_ck{V$)^}IAiym^GRlBxWH zFdo5?r%C=SUnGOFiDER&-*gd)JnH^||MyGCL+gUI5Vhzj&~d5s*}vMvRNfneT@zu7 zGtpPm-ooccyfIC5hY56CO4Y>f==1f_+vA@4EfVA5km$I@+Vgg{|A4hqzJ2Q8xaS)2 zM$+Pc3_RE2X^r$FQxlW)li2kmI~Wl+O#4|^TK&c2#k1>U`hfc&^K%RS*{?1q(&Imm z1pE)+Cl(xX9d6!mJG^ZB<8Ozgeb}UTbG~G8o_AL2DOV9z%?7&{{iMC^aeVpWYBqs6 Z#h9YYFCJxE6T~yYhBoEDdm5gN{2vai3>*Le diff --git a/package.json b/package.json index ce17666..667e517 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "sql.js": "^1.11.0", "sqlite3": "^5.1.7", "tsx": "^4.19.0", - "typescript": "^5.5.4", + "typescript": "^5.6.2", "@alinea/suite": "^0.4.0", "@db/sqlite": "npm:@jsr/db__sqlite" } From 7b330e8df6da7da930137e8e661a245b42bd8fd1 Mon Sep 17 00:00:00 2001 From: Ben Merckx Date: Wed, 11 Sep 2024 12:18:47 +0200 Subject: [PATCH 5/9] Remove @db/sqlite driver for now, see denodrivers/sqlite3#138 --- jsr.json | 3 +- src/core/Driver.ts | 1 - src/core/expr/Include.ts | 22 +++------ src/driver-jsr.ts | 1 - src/driver.ts | 1 - src/driver/denodrivers-sqlite.ts | 79 -------------------------------- 6 files changed, 7 insertions(+), 100 deletions(-) delete mode 100644 src/driver-jsr.ts delete mode 100644 src/driver/denodrivers-sqlite.ts diff --git a/jsr.json b/jsr.json index 41b0ed5..79cbb1b 100644 --- a/jsr.json +++ b/jsr.json @@ -8,7 +8,6 @@ ".": "./src/index.ts", "./sqlite": "./src/sqlite.ts", "./mysql": "./src/mysql.ts", - "./postgres": "./src/postgres.ts", - "./driver": "./src/driver-jsr.ts" + "./postgres": "./src/postgres.ts" } } diff --git a/src/core/Driver.ts b/src/core/Driver.ts index 911f404..1a3a050 100644 --- a/src/core/Driver.ts +++ b/src/core/Driver.ts @@ -9,7 +9,6 @@ export interface BatchQuery { } export interface DriverSpecs { parsesJson: boolean - parsesNestedJson?: boolean } export interface PrepareOptions { isSelection: boolean diff --git a/src/core/expr/Include.ts b/src/core/expr/Include.ts index c83a906..5a888d0 100644 --- a/src/core/expr/Include.ts +++ b/src/core/expr/Include.ts @@ -1,4 +1,4 @@ -import type { DriverSpecs } from '../Driver.ts' +import type {DriverSpecs} from '../Driver.ts' import { type HasData, type HasSql, @@ -6,18 +6,16 @@ import { internalData, internalSql } from '../Internal.ts' -import type { QueryMeta } from '../MetaData.ts' -import type { MapRowContext, RowOfRecord } from '../Selection.ts' -import { type Sql, sql } from '../Sql.ts' -import type { Select, SelectBase, SelectData } from '../query/Select.ts' +import type {QueryMeta} from '../MetaData.ts' +import type {MapRowContext, RowOfRecord} from '../Selection.ts' +import {type Sql, sql} from '../Sql.ts' +import type {Select, SelectBase, SelectData} from '../query/Select.ts' export interface IncludeData extends SelectData { first: boolean } -let isNested = false - export class Include implements HasData>, HasSql { @@ -30,17 +28,11 @@ export class Include #mapFromDriverValue = (value: any, specs: DriverSpecs): any => { const {select, first} = getData(this) - const {parsesJson, parsesNestedJson = parsesJson} = specs - const isPreParsed = isNested - ? parsesNestedJson - : parsesJson - const parsed = isPreParsed ? value : JSON.parse(value) + const parsed = specs.parsesJson ? value : JSON.parse(value) if (first) { - isNested = true const result = parsed ? select!.mapRow({values: parsed, index: 0, specs}) : null - isNested = false return result } if (!parsed) return [] @@ -50,13 +42,11 @@ export class Include index: 0, specs } - isNested = true for (let i = 0; i < rows.length; i++) { ctx.values = rows[i] ctx.index = 0 rows[i] = select!.mapRow(ctx) as Array } - isNested = false return rows ?? [] } diff --git a/src/driver-jsr.ts b/src/driver-jsr.ts deleted file mode 100644 index afa884d..0000000 --- a/src/driver-jsr.ts +++ /dev/null @@ -1 +0,0 @@ -export {connect as '@db/sqlite'} from './driver/denodrivers-sqlite.ts' diff --git a/src/driver.ts b/src/driver.ts index 75b2579..f709603 100644 --- a/src/driver.ts +++ b/src/driver.ts @@ -1,4 +1,3 @@ -export * from './driver-jsr.ts' export {connect as 'better-sqlite3'} from './driver/better-sqlite3.ts' export {connect as 'bun:sqlite'} from './driver/bun-sqlite.ts' export {connect as 'mysql2'} from './driver/mysql2.ts' diff --git a/src/driver/denodrivers-sqlite.ts b/src/driver/denodrivers-sqlite.ts deleted file mode 100644 index 676a9ee..0000000 --- a/src/driver/denodrivers-sqlite.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { - Database as Client, - RestBindParameters, - Statement -} from '@db/sqlite' -import {SyncDatabase, type TransactionOptions} from '../core/Database.ts' -import type {BatchQuery, SyncDriver, SyncStatement} from '../core/Driver.ts' -import {sqliteDialect} from '../sqlite.ts' -import {sqliteDiff} from '../sqlite/diff.ts' -import {execTransaction} from '../sqlite/transactions.ts' - -class PreparedStatement implements SyncStatement { - constructor(private stmt: Statement) {} - - all(params: RestBindParameters) { - return >this.stmt.all(...params) - } - - run(params: RestBindParameters) { - return this.stmt.run(...params) - } - - get(params: RestBindParameters) { - return this.stmt.get(...params) - } - - values(params: RestBindParameters) { - return this.stmt.values(...params) - } - - free() { - this.stmt.finalize() - } -} - -class DbSqliteDriver implements SyncDriver { - parsesJson = true - parsesNestedJson = false - - constructor( - private client: Client, - private depth = 0 - ) {} - - exec(query: string): void { - this.client.exec(query) - } - - close() { - this.client.close() - } - - prepare(sql: string) { - return new PreparedStatement(this.client.prepare(sql)) - } - - batch(queries: Array): Array> { - return this.transaction(tx => { - return queries.map(({sql, params}) => tx.prepare(sql).values(params)) - }, {}) - } - - transaction( - run: (inner: SyncDriver) => T, - options: TransactionOptions['sqlite'] - ): T { - return execTransaction( - this, - this.depth, - depth => new DbSqliteDriver(this.client, depth), - run, - options - ) - } -} - -export function connect(db: Client): SyncDatabase<'sqlite'> { - return new SyncDatabase(new DbSqliteDriver(db), sqliteDialect, sqliteDiff) -} From 8ff8a0a918b1797360821feccae37259d772c7a4 Mon Sep 17 00:00:00 2001 From: Ben Merckx Date: Wed, 11 Sep 2024 12:56:49 +0200 Subject: [PATCH 6/9] Revert readme changes, we'll document this later --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1c8e469..9d5188a 100644 --- a/README.md +++ b/README.md @@ -150,20 +150,20 @@ const PostTags = table({ Currently supported drivers: -| Driver | import | -| ---------------- | -------------------------------- | -| `better-sqlite3` | `{'better-sqlite3' as connect}'` | -| `bun-sqlite` | `{'bun-sqlite' as connect}` | -| `mysql2` | `{'mysql2' as connect}` | -| `pg` | `{'pg' as connect}` | -| `pglite` | `{'pglite' as connect}` | -| `sql.js` | `{'sql.js' as connect}` | +| Driver | import | +| ---------------------- | ------------------------------ | +| `better-sqlite3` | `'rado/driver/better-sqlite3'` | +| `bun:sqlite` | `'rado/driver/bun-sqlite'` | +| `mysql2` | `'rado/driver/mysql2'` | +| `pg` | `'rado/driver/pg'` | +| `@electric-sql/pglite` | `'rado/driver/pglite'` | +| `sql.js` | `'rado/driver/sql.js'` | Pass an instance of the database to the `connect` function to get started: ```ts import Database from 'better-sqlite3' -import {'better-sqlite3' as connect} from 'rado/driver' +import {connect} from 'rado/driver/better-sqlite3' const db = connect(new Database('foobar.db')) ``` From dafe900aeb0824e3ec6a32900ebe6cb5db66d387 Mon Sep 17 00:00:00 2001 From: Ben Merckx Date: Wed, 11 Sep 2024 12:57:02 +0200 Subject: [PATCH 7/9] Bundle driver tests --- bun.lockb | Bin 147208 -> 147208 bytes package.json | 2 +- test/TestDriver.ts | 35 -------- test/driver.test.ts | 108 +++++++++++++++++++++++ test/driver/@db_sqlite.test.ts | 10 --- test/driver/@electric-sql_pglite.test.ts | 7 -- test/driver/better-sqlite3.test.ts | 9 -- test/driver/bun-sqlite.test.ts | 10 --- test/driver/mysql2.test.ts | 16 ---- test/driver/pg.test.ts | 13 --- test/driver/sql.js.test.ts | 8 -- 11 files changed, 109 insertions(+), 109 deletions(-) delete mode 100644 test/TestDriver.ts create mode 100644 test/driver.test.ts delete mode 100644 test/driver/@db_sqlite.test.ts delete mode 100644 test/driver/@electric-sql_pglite.test.ts delete mode 100644 test/driver/better-sqlite3.test.ts delete mode 100644 test/driver/bun-sqlite.test.ts delete mode 100644 test/driver/mysql2.test.ts delete mode 100644 test/driver/pg.test.ts delete mode 100644 test/driver/sql.js.test.ts diff --git a/bun.lockb b/bun.lockb index 912089bd0c6b2a3b2f4f116f4ebd103e1e9cf4ed..e234d1e410997ce7957df418462d3d68671ddbb7 100755 GIT binary patch delta 30 mcmeD9$IQyCeV&Gd}67foZFwgLdSM+++e delta 30 mcmeD9$I Promise, - supportsDiff = true -) { - const db = await createDb() - suite(meta, test => { - testBasic(db, test) - testColumns(db, test) - testSubquery(db, test) - testPreparedQuery(db, test) - testJoins(db, test) - testJson(db, test) - testTransactions(db, test) - testConstraints(db, test) - testCTE(db, test) - testInclude(db, test) - - if (supportsDiff) testMigration(db, test) - }) -} diff --git a/test/driver.test.ts b/test/driver.test.ts new file mode 100644 index 0000000..db85378 --- /dev/null +++ b/test/driver.test.ts @@ -0,0 +1,108 @@ +import * as driver from '@/driver.ts' +import {type DefineTest, type Describe, suite} from '@alinea/suite' +import {isBun, isCi, isNode} from './TestRuntime.ts' +import {testBasic} from './integration/TestBasic.ts' +import {testCTE} from './integration/TestCTE.ts' +import {testColumns} from './integration/TestColumns.ts' +import {testConstraints} from './integration/TestConstraints.ts' +import {testInclude} from './integration/TestInclude.ts' +import {testJoins} from './integration/TestJoins.ts' +import {testJson} from './integration/TestJson.ts' +import {testMigration} from './integration/TestMigration.ts' +import {testPreparedQuery} from './integration/TestPreparedQuery.ts' +import {testSubquery} from './integration/TestSubquery.ts' +import {testTransactions} from './integration/TestTransactions.ts' + +const init = { + 'better-sqlite3': { + condition: isNode, + supportsDiff: true, + async client() { + const {default: Database} = await import('better-sqlite3') + return new Database(':memory:') + } + }, + 'bun:sqlite': { + condition: isBun, + supportsDiff: true, + async client() { + const {Database} = await import('bun:sqlite') + return new Database(':memory:') + } + }, + mysql2: { + condition: isCi, + supportsDiff: false, + async client() { + const {default: mysql2} = await import('mysql2') + const client = mysql2.createConnection( + 'mysql://root:mysql@0.0.0.0:3306/mysql' + ) + return client + } + }, + '@electric-sql/pglite': { + condition: true, + supportsDiff: true, + async client() { + const {PGlite} = await import('@electric-sql/pglite') + return new PGlite() + } + }, + pg: { + condition: isCi, + supportsDiff: true, + async client() { + const {default: pg} = await import('pg') + const client = new pg.Client({ + connectionString: 'postgres://postgres:postgres@0.0.0.0:5432/postgres' + }) + await client.connect() + return client + } + }, + 'sql.js': { + condition: true, + supportsDiff: true, + async client() { + const {default: init} = await import('sql.js') + const {Database} = await init() + return new Database() + } + } +} + +async function createTests() { + const clients = await Promise.all( + Object.entries(init) + .filter(([name, meta]) => meta.condition) + .map( + async ([name, meta]) => + [name, await meta.client()] as [keyof typeof init, any] + ) + ) + return (test: DefineTest) => { + for (const [name, client] of clients) { + const {supportsDiff} = init[name] + const db = driver[name](client) + const prefixed: Describe = (description, fn) => + test(`${name}: ${description}`, fn) + const withName = Object.assign(prefixed, test) + + testBasic(db, withName) + testColumns(db, withName) + testSubquery(db, withName) + testPreparedQuery(db, withName) + testJoins(db, withName) + testJson(db, withName) + testTransactions(db, withName) + testConstraints(db, withName) + testCTE(db, withName) + testInclude(db, withName) + + if (supportsDiff) testMigration(db, withName) + } + } +} + +suite(import.meta, await createTests()) diff --git a/test/driver/@db_sqlite.test.ts b/test/driver/@db_sqlite.test.ts deleted file mode 100644 index 724af8a..0000000 --- a/test/driver/@db_sqlite.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {testDriver} from '../TestDriver.ts' -import {isDeno} from '../TestRuntime.ts' - -async function createDb() { - const {Database} = await import('@db/sqlite') - const driver = await import('../../src/driver.ts') - return driver['@db/sqlite'](new Database(':memory:')) -} - -if (isDeno) await testDriver(import.meta, createDb) diff --git a/test/driver/@electric-sql_pglite.test.ts b/test/driver/@electric-sql_pglite.test.ts deleted file mode 100644 index 6074a73..0000000 --- a/test/driver/@electric-sql_pglite.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {testDriver} from '../TestDriver.ts' - -await testDriver(import.meta, async () => { - const driver = await import('../../src/driver.ts') - const {PGlite} = await import('@electric-sql/pglite') - return driver['@electric-sql/pglite'](new PGlite()) -}) diff --git a/test/driver/better-sqlite3.test.ts b/test/driver/better-sqlite3.test.ts deleted file mode 100644 index 54b7f31..0000000 --- a/test/driver/better-sqlite3.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {testDriver} from '../TestDriver.ts' -import {isNode} from '../TestRuntime.ts' - -if (isNode) - await testDriver(import.meta, async () => { - const {default: Database} = await import('better-sqlite3') - const driver = await import('../../src/driver.ts') - return driver['better-sqlite3'](new Database(':memory:')) - }) diff --git a/test/driver/bun-sqlite.test.ts b/test/driver/bun-sqlite.test.ts deleted file mode 100644 index bb7e737..0000000 --- a/test/driver/bun-sqlite.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {testDriver} from '../TestDriver.ts' -import {isBun} from '../TestRuntime.ts' - -async function createDb() { - const {Database} = await import('bun:sqlite') - const driver = await import('../../src/driver.ts') - return driver['bun:sqlite'](new Database(':memory:')) -} - -if (isBun) await testDriver(import.meta, createDb) diff --git a/test/driver/mysql2.test.ts b/test/driver/mysql2.test.ts deleted file mode 100644 index e4f469b..0000000 --- a/test/driver/mysql2.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {testDriver} from '../TestDriver.ts' -import {isCi} from '../TestRuntime.ts' - -if (isCi) - await testDriver( - import.meta, - async () => { - const driver = await import('../../src/driver.ts') - const {default: mysql2} = await import('mysql2') - const client = mysql2.createConnection( - 'mysql://root:mysql@0.0.0.0:3306/mysql' - ) - return driver.mysql2(client) - }, - false - ) diff --git a/test/driver/pg.test.ts b/test/driver/pg.test.ts deleted file mode 100644 index f0418bc..0000000 --- a/test/driver/pg.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {testDriver} from '../TestDriver.ts' -import {isCi} from '../TestRuntime.ts' - -if (isCi) - await testDriver(import.meta, async () => { - const driver = await import('../../src/driver.ts') - const {default: pg} = await import('pg') - const client = new pg.Client({ - connectionString: 'postgres://postgres:postgres@0.0.0.0:5432/postgres' - }) - await client.connect() - return driver.pg(client) - }) diff --git a/test/driver/sql.js.test.ts b/test/driver/sql.js.test.ts deleted file mode 100644 index 87e415a..0000000 --- a/test/driver/sql.js.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {testDriver} from '../TestDriver.ts' - -await testDriver(import.meta, async () => { - const driver = await import('../../src/driver.ts') - const {default: init} = await import('sql.js') - const {Database} = await init() - return driver['sql.js'](new Database()) -}) From 00fbad223ebfe2118e489344afb25126b1d1e19d Mon Sep 17 00:00:00 2001 From: Ben Merckx Date: Wed, 11 Sep 2024 13:42:43 +0200 Subject: [PATCH 8/9] Cleanup --- deno.json | 3 +-- package.json | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/deno.json b/deno.json index 806c824..516b961 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,5 @@ { "imports": { - "@/": "./src/", - "@db/sqlite": "jsr:@db/sqlite@^0.12.0" + "@/": "./src/" } } diff --git a/package.json b/package.json index b754d7f..d88780f 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "sqlite3": "^5.1.7", "tsx": "^4.19.0", "typescript": "^5.6.2", - "@alinea/suite": "^0.4.0", - "@db/sqlite": "npm:@jsr/db__sqlite" + "@alinea/suite": "^0.4.0" } } From a01e7e10778175d156bb8248aa519a7741d04ab0 Mon Sep 17 00:00:00 2001 From: Ben Merckx Date: Wed, 11 Sep 2024 13:43:07 +0200 Subject: [PATCH 9/9] Check in lockfile --- bun.lockb | Bin 147208 -> 143523 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/bun.lockb b/bun.lockb index e234d1e410997ce7957df418462d3d68671ddbb7..705f5087d1259b2bf12476cab2c685c45a1c07e6 100755 GIT binary patch delta 20322 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 N=DV}Fjb*;me*wUitDpb? delta 22954 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

W{$D2#ZlQ`jR?w8nIl^k&EES{VfKKuv%R?O(=(H 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