From e5458d88def08928f53529d8da4096bc271b5a9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Thu, 30 May 2024 21:20:06 +0200 Subject: [PATCH 01/29] feat(collections): add deferred stack --- README.md | 2 + collections/README.md | 17 ++ collections/deferred_stack.test.ts | 226 ++++++++++++++++++++++ collections/deferred_stack.ts | 289 +++++++++++++++++++++++++++++ collections/deno.json | 9 + collections/mod.ts | 1 + deno.json | 1 + 7 files changed, 545 insertions(+) create mode 100644 collections/README.md create mode 100644 collections/deferred_stack.test.ts create mode 100644 collections/deferred_stack.ts create mode 100644 collections/deno.json create mode 100644 collections/mod.ts diff --git a/README.md b/README.md index 457c3cb..2aac915 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ console.log(dump(buffer)); ## Packages +- [collections](https://jsr.io/@stdext/collections): The collections package + contains commonly used utilities and structures - [crypto](https://jsr.io/@stdext/crypto): The crypto package contains utility for crypto and hashing - [encoding](https://jsr.io/@stdext/encoding): The encoding package contains diff --git a/collections/README.md b/collections/README.md new file mode 100644 index 0000000..449fbec --- /dev/null +++ b/collections/README.md @@ -0,0 +1,17 @@ +# @stdext/collections + +The collections package contains commonly used utilities and structures. + +## Entrypoints + +### Deferred Stack + +Contains the DeferredStack utility class. + +```ts +const deferred = new DeferredStack({ maxSize: 1 }); +deferred.add(1); +const e1 = await deferred.pop(); +setTimeout(() => e1.release(), 5000); +const e2 = await deferred.pop(); // will be queued until e1 is released +``` diff --git a/collections/deferred_stack.test.ts b/collections/deferred_stack.test.ts new file mode 100644 index 0000000..95809d0 --- /dev/null +++ b/collections/deferred_stack.test.ts @@ -0,0 +1,226 @@ +import { + assert, + assertEquals, + assertFalse, + assertMatch, + assertThrows, +} from "@std/assert"; +import { DeferredStack } from "./deferred_stack.ts"; + +Deno.test("deferred", async (t) => { + await t.step("fill and empty x2", async () => { + const deferred = new DeferredStack({ maxSize: 2 }); + assertEquals(deferred.maxSize, 2); + assertEquals(deferred.elements.length, 0); + assertEquals(deferred.stack.length, 0); + assertEquals(deferred.queue.length, 0); + assertEquals(deferred.totalCount, 0); + assertEquals(deferred.inUseCount, 0); + assertEquals(deferred.availableCount, 0); + assertEquals(deferred.queuedCount, 0); + + deferred.add(1); + assertEquals(deferred.maxSize, 2); + assertEquals(deferred.elements.length, 1); + assertEquals(deferred.stack.length, 1); + assertEquals(deferred.queue.length, 0); + assertEquals(deferred.totalCount, 1); + assertEquals(deferred.inUseCount, 0); + assertEquals(deferred.availableCount, 1); + assertEquals(deferred.queuedCount, 0); + + deferred.add(2); + assertEquals(deferred.maxSize, 2); + assertEquals(deferred.elements.length, 2); + assertEquals(deferred.stack.length, 2); + assertEquals(deferred.queue.length, 0); + assertEquals(deferred.totalCount, 2); + assertEquals(deferred.inUseCount, 0); + assertEquals(deferred.availableCount, 2); + assertEquals(deferred.queuedCount, 0); + + assertThrows(() => deferred.add(3), Error, "Max size reached"); + assertEquals(deferred.maxSize, 2); + assertEquals(deferred.elements.length, 2); + assertEquals(deferred.stack.length, 2); + assertEquals(deferred.queue.length, 0); + assertEquals(deferred.totalCount, 2); + assertEquals(deferred.inUseCount, 0); + assertEquals(deferred.availableCount, 2); + assertEquals(deferred.queuedCount, 0); + + const e1 = await deferred.pop(); + assertEquals(deferred.maxSize, 2); + assertEquals(deferred.elements.length, 2); + assertEquals(deferred.stack.length, 1); + assertEquals(deferred.queue.length, 0); + assertEquals(deferred.totalCount, 2); + assertEquals(deferred.inUseCount, 1); + assertEquals(deferred.availableCount, 1); + assertEquals(deferred.queuedCount, 0); + assertEquals(e1.active, true); + assertEquals(e1.value, 2); + e1.release(); + assertEquals(deferred.maxSize, 2); + assertEquals(deferred.elements.length, 2); + assertEquals(deferred.stack.length, 2); + assertEquals(deferred.queue.length, 0); + assertEquals(deferred.totalCount, 2); + assertEquals(deferred.inUseCount, 0); + assertEquals(deferred.availableCount, 2); + assertEquals(deferred.queuedCount, 0); + assertEquals(e1.active, false); + assertThrows(() => e1.value, Error, "Element is not active"); + + const e2 = await deferred.pop(); + assertEquals(deferred.maxSize, 2); + assertEquals(deferred.elements.length, 2); + assertEquals(deferred.stack.length, 1); + assertEquals(deferred.queue.length, 0); + assertEquals(deferred.totalCount, 2); + assertEquals(deferred.inUseCount, 1); + assertEquals(deferred.availableCount, 1); + assertEquals(deferred.queuedCount, 0); + assertEquals(e1.active, false); + assertEquals(e2.active, true); + assertEquals(e2.value, 2); + + const e3 = await deferred.pop(); + assertEquals(deferred.maxSize, 2); + assertEquals(deferred.elements.length, 2); + assertEquals(deferred.stack.length, 0); + assertEquals(deferred.queue.length, 0); + assertEquals(deferred.totalCount, 2); + assertEquals(deferred.inUseCount, 2); + assertEquals(deferred.availableCount, 0); + assertEquals(deferred.queuedCount, 0); + assertEquals(e1.active, false); + assertEquals(e3.active, true); + assertEquals(e3.value, 1); + + let e4Resolved = false; + let e5Resolved = false; + + const e4 = deferred.pop().then((r) => { + e4Resolved = true; + return r; + }); + assertFalse(e4Resolved); + assertEquals(deferred.maxSize, 2); + assertEquals(deferred.elements.length, 2); + assertEquals(deferred.stack.length, 0); + assertEquals(deferred.queue.length, 1); + assertEquals(deferred.totalCount, 2); + assertEquals(deferred.inUseCount, 2); + assertEquals(deferred.availableCount, 0); + assertEquals(deferred.queuedCount, 1); + + const e5 = deferred.pop().then((r) => { + e5Resolved = true; + return r; + }); + assertFalse(e5Resolved); + assertEquals(deferred.maxSize, 2); + assertEquals(deferred.elements.length, 2); + assertEquals(deferred.stack.length, 0); + assertEquals(deferred.queue.length, 2); + assertEquals(deferred.totalCount, 2); + assertEquals(deferred.inUseCount, 2); + assertEquals(deferred.availableCount, 0); + assertEquals(deferred.queuedCount, 2); + + e2.release(); + await e4; + assert(e4Resolved); + assertFalse(e5Resolved); + assertEquals(deferred.maxSize, 2); + assertEquals(deferred.elements.length, 2); + assertEquals(deferred.stack.length, 0); + assertEquals(deferred.queue.length, 1); + assertEquals(deferred.totalCount, 2); + assertEquals(deferred.inUseCount, 2); + assertEquals(deferred.availableCount, 0); + assertEquals(deferred.queuedCount, 1); + assertEquals(e1.active, false); + assertEquals(e2.active, false); + + e3.release(); + await e5; + assert(e5Resolved); + assertEquals(deferred.maxSize, 2); + assertEquals(deferred.elements.length, 2); + assertEquals(deferred.stack.length, 0); + assertEquals(deferred.queue.length, 0); + assertEquals(deferred.totalCount, 2); + assertEquals(deferred.inUseCount, 2); + assertEquals(deferred.availableCount, 0); + assertEquals(deferred.queuedCount, 0); + assertEquals(e1.active, false); + assertEquals(e2.active, false); + assertEquals(e3.active, false); + + (await e4).release(); + assertEquals(deferred.maxSize, 2); + assertEquals(deferred.elements.length, 2); + assertEquals(deferred.stack.length, 1); + assertEquals(deferred.queue.length, 0); + assertEquals(deferred.totalCount, 2); + assertEquals(deferred.inUseCount, 1); + assertEquals(deferred.availableCount, 1); + assertEquals(deferred.queuedCount, 0); + assertEquals(e1.active, false); + assertEquals(e2.active, false); + assertEquals(e3.active, false); + assertEquals((await e4).active, false); + + (await e5).release(); + assertEquals(deferred.maxSize, 2); + assertEquals(deferred.elements.length, 2); + assertEquals(deferred.stack.length, 2); + assertEquals(deferred.queue.length, 0); + assertEquals(deferred.totalCount, 2); + assertEquals(deferred.inUseCount, 0); + assertEquals(deferred.availableCount, 2); + assertEquals(deferred.queuedCount, 0); + assertEquals(e1.active, false); + assertEquals(e2.active, false); + assertEquals(e3.active, false); + assertEquals((await e4).active, false); + assertEquals((await e5).active, false); + + const e6 = await deferred.pop(); + e6.remove(); + assertEquals(deferred.maxSize, 2); + assertEquals(deferred.elements.length, 1); + assertEquals(deferred.stack.length, 1); + assertEquals(deferred.queue.length, 0); + assertEquals(deferred.totalCount, 1); + assertEquals(deferred.inUseCount, 0); + assertEquals(deferred.availableCount, 1); + assertEquals(deferred.queuedCount, 0); + assertEquals(e1.active, false); + assertEquals(e2.active, false); + assertEquals(e3.active, false); + assertEquals((await e4).active, false); + assertEquals((await e5).active, false); + assertEquals(e6.active, false); + + const e7 = await deferred.pop(); + e7.remove(); + assertEquals(deferred.maxSize, 2); + assertEquals(deferred.elements.length, 0); + assertEquals(deferred.stack.length, 0); + assertEquals(deferred.queue.length, 0); + assertEquals(deferred.totalCount, 0); + assertEquals(deferred.inUseCount, 0); + assertEquals(deferred.availableCount, 0); + assertEquals(deferred.queuedCount, 0); + assertEquals(e1.active, false); + assertEquals(e2.active, false); + assertEquals(e3.active, false); + assertEquals((await e4).active, false); + assertEquals((await e5).active, false); + assertEquals(e6.active, false); + assertEquals(e7.active, false); + }); +}); diff --git a/collections/deferred_stack.ts b/collections/deferred_stack.ts new file mode 100644 index 0000000..f8eda86 --- /dev/null +++ b/collections/deferred_stack.ts @@ -0,0 +1,289 @@ +/** + * DeferredStackOptions + * + * Options for DeferredStack + */ +export type DeferredStackOptions = { + /** + * The maximum stack size to be allowed. + */ + maxSize?: number; +}; + +/** + * DeferredStack + * + * When you have a stack that you want to defer the acquire of an element until it is available. + * + * ```ts + * const deferred = new DeferredStack({ maxSize: 1 }); + * deferred.add(1); + * const e1 = await deferred.pop(); + * setTimeout(() => e1.release(), 5000); + * const e2 = await deferred.pop(); // will be queued until e1 is released + * ``` + */ +export class DeferredStack { + /** + * The maximum stack size to be allowed, if the stack is full, the acquire will be queued. + * + * @default 10 + */ + readonly maxSize: number = 10; + + /** + * The list of all elements + * + * Cannot be larger than maxSize + */ + #elements: Array> = []; + + /** + * The stack of available elements + */ + #stack: Array> = []; + + /** + * The queue of requested connections + */ + readonly queue: Array>> = []; + + /** + * The list of all elements + */ + get elements(): Array> { + return this.#elements; + } + + /** + * The stack of available elements + */ + get stack(): Array> { + return this.#stack; + } + + /** + * The number of elements in the stack + */ + get totalCount(): number { + return this.#elements.length; + } + + /** + * The number of elements in the stack + */ + get inUseCount(): number { + return this.#elements.length - this.availableCount; + } + + /** + * The number of available elements in the stack + */ + get availableCount(): number { + return this.#stack.length; + } + + /** + * The number of queued acquires + */ + get queuedCount(): number { + return this.queue.length; + } + + constructor(options: DeferredStackOptions) { + this.maxSize = options.maxSize ?? 10; + } + + /** + * Add an element to the stack + * + * If there are any queued acquires, the first one will be resolved with the pushed element. + * If the stack is full, an error will be thrown. + * + * @throws Error("Max size reached") + */ + add(element: T): void { + if (this.#elements.length >= this.maxSize) { + throw new Error("Max size reached"); + } + + const newElement = new DeferredStackElement({ + value: element, + releaseFn: (element) => this.#release(element), + removeFn: (element) => this.#remove(element), + }); + + this.#elements.push(newElement); + + this.#push(newElement); + } + + /** + * Pop an element from the stack + * + * If there are no elements in the stack, the acquire will be queued and resolved when an element is pushed. + */ + pop(): Promise> { + const element = this.#stack.pop(); + + if (element) { + element._activate(); + return Promise.resolve(element); + } + + const p = Promise.withResolvers>(); + + this.queue.push(p); + + return p.promise; + } + + /** + * Push an element to the stack or resolve the first queued acquire + */ + #push(element: DeferredStackElement): void { + if (this.queue.length) { + const p = this.queue.shift()!; + element._activate(); + p.resolve(element); + } else { + this.#stack.push(element); + } + } + + /** + * Release element back to the deferred stack + * + * To avoid that previous users of the element can still access it, + * the element is removed from the stack, and added again. + */ + #release(element: DeferredStackElement): void { + const value = element.value; + element.remove(); + this.add(value); + } + + /** + * Removes element from the deferred stack + */ + #remove(element: DeferredStackElement): void { + this.#elements = this.#elements.filter((el) => el._id !== element._id); + this.#stack = this.#stack.filter((el) => el._id !== element._id); + } +} + +/** + * DeferredStackElementOptions + */ +export type DeferredStackElementOptions = { + /** + * The value of the element + */ + value: T; + /** + * The release function to be called when the element is released + */ + releaseFn: (element: DeferredStackElement) => void; + /** + * The remove function to be called when the element is removed + */ + removeFn: (element: DeferredStackElement) => void; +}; + +/** + * DeferredStackElement + * + * Represents an element in the DeferredStack with helpful methods to manage it. + * + * To access the value of the element, use the `value` property. + */ +export class DeferredStackElement { + /** + * The unique identifier of the element + */ + _id = crypto.randomUUID(); + + /** + * Whether the element is in use + */ + #active = false; + + /** + * Whether the element is disposed and should not be available anymore + */ + #disposed = false; + + /** + * The value of the element + */ + #value: T; + + /** + * The release function to be called when the element is released + */ + #releaseFn: DeferredStackElementOptions["releaseFn"]; + + /** + * The remove function to be called when the element is removed + */ + #removeFn: DeferredStackElementOptions["removeFn"]; + + /** + * Whether the element is in use + */ + get active(): boolean { + return this.#active; + } + + /** + * Whether the element is disposed and should not be available anymore + */ + get disposed(): boolean { + return this.#disposed; + } + + /** + * The value of the element + * + * @throws Error("Element is not active") + * @throws Error("Element is disposed") + */ + get value(): T { + if (!this.active) throw new Error("Element is not active"); + if (this.#disposed) throw new Error("Element is disposed"); + return this.#value; + } + + constructor( + options: DeferredStackElementOptions, + ) { + this.#value = options.value; + this.#releaseFn = options.releaseFn; + this.#removeFn = options.removeFn; + } + + /** + * Activates the element + * + * Only the DeferredStack should call this method. + */ + _activate(): void { + this.#active = true; + } + + /** + * Releases the element back to the DeferredStack + */ + release(): void { + return this.#releaseFn(this); + } + + /** + * Removes the element from the DeferredStack + */ + remove(): void { + this.#active = false; + this.#disposed = true; + return this.#removeFn(this); + } +} diff --git a/collections/deno.json b/collections/deno.json new file mode 100644 index 0000000..26b4211 --- /dev/null +++ b/collections/deno.json @@ -0,0 +1,9 @@ +{ + "version": "0.0.5", + "name": "@stdext/collections", + "lock": false, + "exports": { + ".": "./mod.ts", + "./deferred-stack": "./deferred_stack.ts" + } +} diff --git a/collections/mod.ts b/collections/mod.ts new file mode 100644 index 0000000..92eecbe --- /dev/null +++ b/collections/mod.ts @@ -0,0 +1 @@ +export * from "./deferred_stack.ts"; diff --git a/deno.json b/deno.json index 1df3aad..775cd65 100644 --- a/deno.json +++ b/deno.json @@ -17,6 +17,7 @@ "format:check": "deno fmt --check && deno task --cwd crypto format:check" }, "workspaces": [ + "./collections", "./crypto", "./encoding", "./http" From 933e358ef3e9c7241ee29718a6502e19d2d7812f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Thu, 30 May 2024 21:51:32 +0200 Subject: [PATCH 02/29] feat(sql): add sql standard interfaces --- README.md | 2 + deno.json | 3 +- sql/README.md | 230 ++++++++++++ sql/client.ts | 76 ++++ sql/connection.ts | 58 +++ sql/core.ts | 513 ++++++++++++++++++++++++++ sql/deno.json | 9 + sql/errors.ts | 31 ++ sql/events.ts | 196 ++++++++++ sql/meta.ts | 6 + sql/mod.ts | 7 + sql/pool.ts | 171 +++++++++ sql/testing.ts | 904 ++++++++++++++++++++++++++++++++++++++++++++++ 13 files changed, 2205 insertions(+), 1 deletion(-) create mode 100644 sql/README.md create mode 100644 sql/client.ts create mode 100644 sql/connection.ts create mode 100644 sql/core.ts create mode 100644 sql/deno.json create mode 100644 sql/errors.ts create mode 100644 sql/events.ts create mode 100644 sql/meta.ts create mode 100644 sql/mod.ts create mode 100644 sql/pool.ts create mode 100644 sql/testing.ts diff --git a/README.md b/README.md index 2aac915..d05c707 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ console.log(dump(buffer)); utility for text encoding. - [http](https://jsr.io/@stdext/http): The http package contains utility for fetching and http servers +- [sql](https://jsr.io/@stdext/sql): The SQL package contains a standard + interface for SQL based databases ## Versioning diff --git a/deno.json b/deno.json index 775cd65..fca372d 100644 --- a/deno.json +++ b/deno.json @@ -20,7 +20,8 @@ "./collections", "./crypto", "./encoding", - "./http" + "./http", + "./sql" ], "exclude": [ "./crypto/hash/_wasm/target" diff --git a/sql/README.md b/sql/README.md new file mode 100644 index 0000000..53655f2 --- /dev/null +++ b/sql/README.md @@ -0,0 +1,230 @@ +# @stdext/sql + +The SQL package contains a standard interface for SQL based databases + +Inspired by [rust sqlx](https://docs.rs/sqlx/latest/sqlx/index.html) and +[go sql](https://pkg.go.dev/database/sql). + +The goal for this package is to have a standard interface for SQL like database +clients that can be used in Deno, Node and other JS runtimes. + +## Usage + +Minimal usage example: + +```ts +await using db = new Client(connectionUrl, connectionOptions); +await db.connect(); +await db.execute("SOME INSERT QUERY"); +const res = await db.query("SELECT * FROM table"); +``` + +`@stdext/std` provides a standard for interacting with a database. + +### Client + +Full compliance with `@stdext/std` provides a database client with the following +methods (see [SqlClient](./lib/client.ts)): + +- `connect` (See [SqlConnection](./lib/connection.ts)): Creates a connection to + the database +- `close` (See [SqlConnection](./lib/connection.ts)): Closes the connection to + the database +- `execute` (See [SqlQueriable](./lib/core.ts)): Executes a SQL statement +- `query` (See [SqlQueriable](./lib/core.ts)): Queries the database and returns + an array of object +- `queryOne` (See [SqlQueriable](./lib/core.ts)): Queries the database and + returns at most one entry as an object +- `queryMany` (See [SqlQueriable](./lib/core.ts)): Queries the database with an + async generator and yields each entry as an object +- `queryArray` (See [SqlQueriable](./lib/core.ts)): Queries the database and + returns an array of arrays +- `queryOneArray` (See [SqlQueriable](./lib/core.ts)): Queries the database and + returns at most one entry as an array +- `queryManyArray` (See [SqlQueriable](./lib/core.ts)): Queries the database + with an async generator and yields each entry as an array +- `sql` (See [SqlQueriable](./lib/core.ts)): Allows you to create a query using + template literals, and returns the entries as an array of objects. This is a + wrapper around `query` +- `sqlArray` (See [SqlQueriable](./lib/core.ts)): Allows you to create a query + using template literals, and returns the entries as an array of arrays. This + is a wrapper around `queryArray` +- `prepare` (See [SqlPreparable](./lib/core.ts)): Returns a prepared statement + class that contains a subset of the Queriable functions (see + [SqlPreparedQueriable](./lib/core.ts)) +- `beginTransaction` (See [SqlTransactionable](./lib/core.ts)): Returns a + transaction class that contains implements the queriable functions, as well as + transaction related functions (see [SqlTransactionQueriable](./lib/core.ts)) +- `transaction` (See [SqlTransactionable](./lib/core.ts)): A wrapper function + for transactions, handles the logic of beginning, committing and rollback a + transaction. + +### ClientPool + +Full compliance with `@stdext/std` provides a database client pool (a pool of +clients) with the following methods (see [SqlClientPool](./lib/pool.ts)): + +- `connect` (See [SqlConnection](./lib/core.ts)): Creates the connection classes + and adds them to a connection pool, and optionally connects them to the + database +- `close` (See [SqlConnection](./lib/core.ts)): Closes all connections in the + pool +- `acquire` (See [SqlPoolable](./lib/core.ts)): Retrieves a + [SqlPoolClient](./lib/pool.ts) (a subset of [Client](#client)), and connects + it if not already connected +- `release` (See [SqlPoolable](./lib/core.ts)): Releases a connection back to + the pool + +### Examples + +Using const (requires manual close at the end) + +```ts +const db = new Client(connectionUrl, connectionOptions); +await db.connect(); +await db.execute("SOME INSERT QUERY"); +const res = await db.query("SELECT * FROM table"); +await db.close(); +``` + +Query object + +```ts +await using db = new Client(connectionUrl, connectionOptions); +await db.connect(); +await db.execute("SOME INSERT QUERY"); +const res = await db.query("SELECT * FROM table"); +console.log(res); +// [{ col1: "some value" }] +``` + +Query array + +```ts +await using db = new Client(connectionUrl, connectionOptions); +await db.connect(); +await db.execute("SOME INSERT QUERY"); +const res = await db.queryArray("SELECT * FROM table"); +console.log(res); +// [[ "some value" ]] +``` + +Query with template literals + +```ts +await using db = new Client(connectionUrl, connectionOptions); +await db.connect(); +await db.execute("SOME INSERT QUERY"); +const res = await db.sqlArray`SELECT * FROM table where id = ${id}`; +console.log(res); +// [[ "some value" ]] +``` + +Transaction + +```ts +await using db = new Client(connectionUrl, connectionOptions); +await db.connect(); +await db.execute("SOME INSERT QUERY"); +const transaction = await db.beginTransaction(); +await transaction.execute("SOME INSERT QUERY"); +await transaction.commitTransaction(); +// transaction can no longer be used +``` + +Transaction wrapper + +```ts +await using db = new Client(connectionUrl, connectionOptions); +await db.connect(); +await db.execute("SOME INSERT QUERY"); +const res = await db.transaction(async (t) => { + await t.execute("SOME INSERT QUERY"); + return t.query("SOME SELECT QUERY"); +}); +console.log(res); +// [{ col1: "some value" }] +``` + +Prepared statement + +```ts +await using db = new Client(connectionUrl, connectionOptions); +await db.connect(); +await db.execute("SOME INSERT QUERY"); +const prepared = db.prepare("SOME PREPARED STATEMENT"); +await prepared.query([...params]); +console.log(res); +// [{ col1: "some value" }] +``` + +## Implementation + +To be fully compliant with `@stdext/std`, you will need to implement the +following classes for your database driver: + +- `Connection` ([SqlConnection](./lib/connection.ts)): This represents the + connection to the database. This should preferably only contain the + functionality of containing a connection, and provide a minimum query method + to be used to query the database. The query method does not need to follow any + specific spec, but must be consumable by all query functions as well as + execute functions (see `SqlQueriable`) +- `Prepared` ([SqlPreparedQueriable](./lib/core.ts)): This represents a prepared + statement. All queriable methods must be implemented +- `Transaction` ([SqlTransactionQueriable](./lib/core.ts)): This represents a + transaction. All queriable methods must be implemented +- `Client` ([SqlClient](./lib/client.ts)): This represents a database client +- `ClientPool` ([SqlClientPool](./lib/pool.ts)): This represents a pool of + clients +- `PoolClient` ([SqlPoolClient](./lib/pool.ts)): This represents a client to be + provided by a pool + +It is also however advisable to create the following sub classes to use in other +classes: + +- [SqlQueriable](./lib/core.ts): This represents a queriable base class that + contains the standard `@stdext/std` query methods. In most cases, this serves + as a base class for all other queriable classes +- [SqlEventTarget](./lib/events.ts): A typed event target class +- [SqlError](./lib/errors.ts): A typed error class + +### Inheritance graph + +In most cases, these are the classes and the inheritance graph that should be +implemented. Notice how almost everything extends `SqlBase` at the base. + +- SqlConnection + - SqlBase +- SqlPreparedQueriable + - SqlBase +- SqlTransactionQueriable + - SqlPreparable + - SqlQueriable + - SqlBase +- SqlClient + - SqlTransactionable + - SqlPreparable + - SqlQueriable + - SqlBase +- SqlPoolClient + - SqlTransactionable + - SqlPreparable + - SqlQueriable + - SqlBase +- SqlClientPool + - SqlBase + +### Other + +There is also a [SqlDeferredStack](./lib//deferred.ts) that is used by the pool +as a helper for queuing and stacking the pool clients. + +### Base class + +`@stdext/std` uses implementable interfaces as well as base classes that needs +to be inherited for desired functionality. + +At the base layer, you will have to extend the `SqlBase` class for all +`@stdext/std` derived classes. Alternatively, if this is not possible, you can +implement SqlBase, and ensure that the dynamic and static properties are +accessible. diff --git a/sql/client.ts b/sql/client.ts new file mode 100644 index 0000000..55b2321 --- /dev/null +++ b/sql/client.ts @@ -0,0 +1,76 @@ +import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; +import type { + SqlPreparable, + SqlPreparedQueriable, + SqlQueriable, + SqlQueryOptions, + SqlTransactionable, + SqlTransactionOptions, + SqlTransactionQueriable, +} from "./core.ts"; +import type { SqlEventable, SqlEventTarget } from "./events.ts"; + +/** + * SqlClient + * + * This represents a database client. When you need a single connection + * to the database, you will in most cases use this interface. + */ +export interface SqlClient< + EventTarget extends SqlEventTarget = SqlEventTarget, + ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, + Connection extends SqlConnection = SqlConnection< + ConnectionOptions + >, + ParameterType extends unknown = unknown, + QueryOptions extends SqlQueryOptions = SqlQueryOptions, + Prepared extends SqlPreparedQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions + > = SqlPreparedQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions + >, + TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, + Transaction extends SqlTransactionQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions, + TransactionOptions + > = SqlTransactionQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions, + TransactionOptions + >, +> extends + SqlConnection, + SqlQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions + >, + SqlPreparable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions, + Prepared + >, + SqlTransactionable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions, + TransactionOptions, + Transaction + >, + SqlEventable { +} diff --git a/sql/connection.ts b/sql/connection.ts new file mode 100644 index 0000000..aace091 --- /dev/null +++ b/sql/connection.ts @@ -0,0 +1,58 @@ +import type { SqlBase } from "./core.ts"; + +/** + * SqlConnectionOptions + * + * The options that will be used when connecting to the database. + */ +export interface SqlConnectionOptions { + /** + * Transforms the value that will be sent to the database + */ + transformInput?: (value: unknown) => unknown; + /** + * Transforms the value received from the database + */ + transformOutput?: (value: unknown) => unknown; +} + +/** + * SqlConnection + * + * This represents a connection to a database. + * When a user wants a single connection to the database, + * they should use a class implementing or using this interface. + * + * The class implementing this interface should be able to connect to the database, + * and have the following constructor arguments (if more options are needed, extend the SqlConnectionOptions): + * - connectionUrl: string|URL + * - connectionOptions?: SqlConnectionOptions; + */ +export interface SqlConnection< + ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, +> extends SqlBase, AsyncDisposable { + /** + * Connection URL + */ + readonly connectionUrl: string; + + /** + * Connection options + */ + readonly connectionOptions: ConnectionOptions; + + /** + * Whether the connection is connected to the database + */ + get connected(): boolean; + + /** + * Create a connection to the database + */ + connect(): Promise; + + /** + * Close the connection to the database + */ + close(): Promise; +} diff --git a/sql/core.ts b/sql/core.ts new file mode 100644 index 0000000..1fe9e03 --- /dev/null +++ b/sql/core.ts @@ -0,0 +1,513 @@ +// deno-lint-ignore-file no-explicit-any +import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; +import type { DeferredStack as SqlDeferredStack } from "../collections/deferred_stack.ts"; +import { VERSION } from "./meta.ts"; + +/** + * Row + * + * Row type for SQL queries, represented as an object entry. + */ +export type Row = Record; + +/** + * ArrayRow + * + * Row type for SQL queries, represented as an array entry. + */ +export type ArrayRow = T[]; + +/** + * SqlTransactionOptions + * + * Core transaction options + * Used to type the options for the transaction methods + */ +export type SqlTransactionOptions = { + beginTransactionOptions?: Record; + commitTransactionOptions?: Record; + rollbackTransactionOptions?: Record; +}; + +/** + * SqlQueryOptions + * + * Options to pass to the query methods. + */ +export interface SqlQueryOptions extends SqlConnectionOptions { + /** + * A signal to abort the query. + */ + signal?: AbortSignal; +} + +/** + * SqlConnectableBase + * + * The base interface for everything that interracts with the connection like querying. + */ +export interface SqlConnectableBase< + Connection extends SqlConnection = SqlConnection, +> extends SqlBase { + /** + * The connection to the database + */ + connection: Connection; + + /** + * Whether the connection is connected or not + */ + get connected(): boolean; +} + +/** + * SqlConnectablePoolBase + * + * The base interface for pool clients that interracts with the connection like querying. + */ +export interface SqlConnectablePoolBase< + Connection extends SqlConnection = SqlConnection, +> extends SqlConnectableBase, AsyncDisposable { +} + +/** + * SqlPreparedQueriable + * + * Represents a prepared statement to be executed separately from creation. + */ +export interface SqlPreparedQueriable< + ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, + Connection extends SqlConnection = SqlConnection< + ConnectionOptions + >, + ParameterType extends unknown = unknown, + QueryOptions extends SqlQueryOptions = SqlQueryOptions, +> extends SqlConnectableBase { + /** + * The SQL statement + */ + readonly sql: string; + /** + * The (global) options to pass to the query method. + */ + readonly queryOptions: QueryOptions; + + /** + * Executes the prepared statement + * + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the number of affected rows if any + */ + execute( + params?: ParameterType[], + options?: QueryOptions, + ): Promise; + /** + * Query the database with the prepared statement + * + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the rows returned by the query as object entries + */ + query = Row>( + params?: ParameterType[], + options?: QueryOptions, + ): Promise; + /** + * Query the database with the prepared statement, and return at most one row + * + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the row returned by the query as an object entry, or undefined if no row is returned + */ + queryOne = Row>( + params?: ParameterType[], + options?: QueryOptions, + ): Promise; + /** + * Query the database with the prepared statement, and return an iterator. + * Usefull when querying large datasets, as this should take advantage of data streams. + * + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the rows returned by the query as object entries + */ + queryMany = Row>( + params?: ParameterType[], + options?: QueryOptions, + ): AsyncGenerator; + /** + * Query the database with the prepared statement + * + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the rows returned by the query as array entries + */ + queryArray = ArrayRow>( + params?: ParameterType[], + options?: QueryOptions, + ): Promise; + /** + * Query the database with the prepared statement, and return at most one row + * + * @param sql the SQL statement + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the row returned by the query as an array entry, or undefined if no row is returned + */ + queryOneArray = ArrayRow>( + params?: ParameterType[], + options?: QueryOptions, + ): Promise; + + /** + * Query the database with the prepared statement, and return an iterator. + * Usefull when querying large datasets, as this should take advantage of data streams. + * + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the rows returned by the query as array entries + */ + queryManyArray = ArrayRow>( + params?: ParameterType[], + options?: QueryOptions, + ): AsyncGenerator; +} + +/** + * SqlQueriable + * + * Represents an object that can execute SQL queries. + */ +export interface SqlQueriable< + ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, + Connection extends SqlConnection = SqlConnection< + ConnectionOptions + >, + ParameterType extends unknown = unknown, + QueryOptions extends SqlQueryOptions = SqlQueryOptions, +> extends SqlConnectableBase { + /** + * The (global) options to pass to the query method. + */ + readonly queryOptions: QueryOptions; + + /** + * Execute a SQL statement + * + * @param sql the SQL statement + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the number of affected rows if any + */ + execute( + sql: string, + params?: ParameterType[], + options?: QueryOptions, + ): Promise; + /** + * Query the database + * + * @param sql the SQL statement + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the rows returned by the query as object entries + */ + query = Row>( + sql: string, + params?: ParameterType[], + options?: QueryOptions, + ): Promise; + /** + * Query the database and return at most one row + * + * @param sql the SQL statement + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the row returned by the query as an object entry, or undefined if no row is returned + */ + queryOne = Row>( + sql: string, + params?: ParameterType[], + options?: QueryOptions, + ): Promise; + /** + * Query the database and return an iterator. + * Usefull when querying large datasets, as this should take advantage of data streams. + * + * @param sql the SQL statement + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the rows returned by the query as object entries + */ + queryMany = Row>( + sql: string, + params?: ParameterType[], + options?: QueryOptions, + ): AsyncGenerator; + /** + * Query the database + * + * @param sql the SQL statement + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the rows returned by the query as array entries + */ + queryArray = ArrayRow>( + sql: string, + params?: ParameterType[], + options?: QueryOptions, + ): Promise; + /** + * Query the database and return at most one row + * + * @param sql the SQL statement + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the row returned by the query as an array entry, or undefined if no row is returned + */ + queryOneArray = ArrayRow>( + sql: string, + params?: ParameterType[], + options?: QueryOptions, + ): Promise; + + /** + * Query the database and return an iterator. + * Usefull when querying large datasets, as this should take advantage of data streams. + * + * @param sql the SQL statement + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the rows returned by the query as array entries + */ + queryManyArray = ArrayRow>( + sql: string, + params?: ParameterType[], + options?: QueryOptions, + ): AsyncGenerator; + + /** + * Query the database using tagged template + * + * @returns the rows returned by the query as object entries + */ + sql = Row>( + strings: TemplateStringsArray, + ...parameters: ParameterType[] + ): Promise; + + /** + * Query the database using tagged template + * + * @returns the rows returned by the query as array entries + */ + sqlArray = ArrayRow>( + strings: TemplateStringsArray, + ...parameters: ParameterType[] + ): Promise; +} + +/** + * SqlPreparable + * + * This interface is to be implemented by any class that supports creating a prepared statement. + * A prepared statement should in most cases be unique to a connection, + * and should not live after the related connection is closed. + */ +export interface SqlPreparable< + ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, + Connection extends SqlConnection = SqlConnection< + ConnectionOptions + >, + ParameterType extends unknown = unknown, + QueryOptions extends SqlQueryOptions = SqlQueryOptions, + Prepared extends SqlPreparedQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions + > = SqlPreparedQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions + >, +> extends SqlConnectableBase { + /** + * Create a prepared statement that can be executed multiple times. + * This is useful when you want to execute the same SQL statement multiple times with different parameters. + * + * @param sql the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns a prepared statement + * + * @example + * ```ts + * const stmt = db.prepare("SELECT * FROM table WHERE id = ?"); + * + * for (let i = 0; i < 10; i++) { + * const row of stmt.query([i]) + * console.log(row); + * } + * ``` + */ + prepare( + sql: string, + options?: QueryOptions, + ): Prepared; +} + +/** + * SqlTransactionQueriable + * + * Represents a transaction. + */ +export interface SqlTransactionQueriable< + ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, + Connection extends SqlConnection = SqlConnection< + ConnectionOptions + >, + ParameterType extends unknown = unknown, + QueryOptions extends SqlQueryOptions = SqlQueryOptions, + TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, +> extends + SqlQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions + > { + /** + * Whether the connection is in an active transaction or not. + */ + get inTransaction(): boolean; + + /** + * Commit the transaction + */ + commitTransaction( + options?: TransactionOptions["commitTransactionOptions"], + ): Promise; + /** + * Rollback the transaction + */ + rollbackTransaction( + options?: TransactionOptions["rollbackTransactionOptions"], + ): Promise; + /** + * Create a save point + * + * @param name the name of the save point + */ + createSavepoint(name?: string): Promise; + /** + * Release a save point + * + * @param name the name of the save point + */ + releaseSavepoint(name?: string): Promise; +} + +/** + * SqlTransactionable + * + * Represents an object that can create a transaction. + */ +export interface SqlTransactionable< + ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, + Connection extends SqlConnection = SqlConnection< + ConnectionOptions + >, + ParameterType extends unknown = unknown, + QueryOptions extends SqlQueryOptions = SqlQueryOptions, + TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, + Transaction extends SqlTransactionQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions, + TransactionOptions + > = SqlTransactionQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions, + TransactionOptions + >, +> extends SqlConnectableBase { + /** + * Starts a transaction + */ + beginTransaction( + options?: TransactionOptions["beginTransactionOptions"], + ): Promise; + + /** + * Transaction wrapper + * + * Automatically begins a transaction, executes the callback function, and commits the transaction. + * + * If the callback function throws an error, the transaction will be rolled back and the error will be rethrown. + * If the callback function returns successfully, the transaction will be committed. + * + * @param fn callback function to be executed within a transaction + * @returns the result of the callback function + */ + transaction( + fn: (t: Transaction) => Promise, + ): Promise; +} + +/** + * SqlPoolable + * + * Represents an object that can acquire a connection from a pool. + */ +export interface SqlPoolable< + Connectable extends SqlConnectablePoolBase = SqlConnectablePoolBase, + DeferredStack extends SqlDeferredStack = SqlDeferredStack< + Connectable + >, +> extends SqlBase { + /** + * The deferred stack of connections + */ + deferredStack: DeferredStack; + + /** + * Acquire a connection from the pool + */ + acquire(): Promise; + + /** + * Releases the connection to the pool + */ + release(connectable: Connectable): Promise; +} + +/** + * Base class for all SQLx classes + * + * All SQLx implemented classes should have this class as its base class for inheritance, + * or at the least imlement it as an interface and manually include the static and dynamic properties + */ +export class SqlBase { + /** + * The version of SQLx + * @example + * ```ts + * import { VERSION } from "@halvardm/sqlx"; + * ``` + */ + readonly sqlxVersion: string = VERSION; + /** + * The version of SQLx + * @example + * ```ts + * import { VERSION } from "@halvardm/sqlx"; + * ``` + */ + static readonly sqlxVersion: string = VERSION; +} diff --git a/sql/deno.json b/sql/deno.json new file mode 100644 index 0000000..33f5fe4 --- /dev/null +++ b/sql/deno.json @@ -0,0 +1,9 @@ +{ + "version": "0.0.5", + "name": "@stdext/sql", + "lock": false, + "exports": { + ".": "./mod.ts", + "./testing": "./lib/testing.ts" + } +} diff --git a/sql/errors.ts b/sql/errors.ts new file mode 100644 index 0000000..b641875 --- /dev/null +++ b/sql/errors.ts @@ -0,0 +1,31 @@ +import type { SqlBase } from "./core.ts"; +import { VERSION } from "./meta.ts"; + +/** + * SqlError + * + * Base SQLx Error + */ +export class SqlError extends Error implements SqlBase { + readonly sqlxVersion = VERSION; + static readonly sqlxVersion = VERSION; + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +/** + * SqlDeferredError + * + * Error that is thrown by DeferredStack + */ +export class SqlDeferredError extends SqlError { +} + +/** + * Check if an error is a SqlError + */ +export function isSqlError(err: unknown): err is SqlError { + return err instanceof SqlError; +} diff --git a/sql/events.ts b/sql/events.ts new file mode 100644 index 0000000..85d7836 --- /dev/null +++ b/sql/events.ts @@ -0,0 +1,196 @@ +/** + * Events + */ +import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; +import type { SqlBase, SqlConnectableBase } from "./core.ts"; +import { VERSION } from "./meta.ts"; + +/** + * Event types + */ + +/** + * SQLx Client event types + */ +export type SqlClientEventType = "connect" | "close" | "error"; + +/** + * SQLx Pool Connection event types + */ +export type SqlPoolConnectionEventType = + | SqlClientEventType + | "acquire" + | "release"; + +/** + * EventInits + */ + +/** + * SqlErrorEventInit + */ +export interface SqlErrorEventInit< + Connectable extends SqlConnectableBase = SqlConnectableBase, +> extends ErrorEventInit { + connectable?: Connectable; +} + +/** + * SqlPoolEventInit + * + * SQLx Pool event init + */ +export interface SqlConnectableEventInit< + Connectable extends SqlConnectableBase = SqlConnectableBase, +> extends EventInit { + connectable: Connectable; +} + +/** + * Event classes + */ + +/** + * Base SQLx error event class + */ +export class SqlErrorEvent< + EventInit extends SqlErrorEventInit = SqlErrorEventInit, +> extends ErrorEvent implements SqlBase { + readonly sqlxVersion = VERSION; + static readonly sqlxVersion = VERSION; + constructor(type: "error", eventInitDict?: EventInit) { + super(type, eventInitDict); + } +} + +/** + * Base SQLx event class + */ +export class SqlEvent< + EventType extends SqlPoolConnectionEventType = SqlPoolConnectionEventType, + EventInit extends SqlConnectableEventInit = SqlConnectableEventInit, +> extends Event implements SqlBase { + readonly sqlxVersion = VERSION; + static readonly sqlxVersion = VERSION; + constructor(type: EventType, eventInitDict?: EventInit) { + super(type, eventInitDict); + } +} + +/** + * SqlClientConnectEvent class + */ +export class SqlConnectableConnectEvent< + EventInit extends SqlConnectableEventInit = SqlConnectableEventInit, +> extends SqlEvent<"connect", EventInit> { + constructor(eventInitDict: EventInit) { + super("connect", eventInitDict); + } +} + +/** + * SqlConnectionCloseEvent class + */ +export class SqlConnectableCloseEvent< + EventInit extends SqlConnectableEventInit = SqlConnectableEventInit, +> extends SqlEvent<"close", EventInit> { + constructor(eventInitDict: EventInit) { + super("close", eventInitDict); + } +} + +/** + * SqlPoolConnectionAcquireEvent class + */ +export class SqlPoolConnectableAcquireEvent< + EventInit extends SqlConnectableEventInit = SqlConnectableEventInit, +> extends SqlEvent<"acquire", EventInit> { + constructor(eventInitDict: EventInit) { + super("acquire", eventInitDict); + } +} + +/** + * SqlPoolConnectionReleaseEvent class + */ +export class SqlPoolConnectableReleaseEvent< + EventInit extends SqlConnectableEventInit = SqlConnectableEventInit, +> extends SqlEvent<"release", EventInit> { + constructor(eventInitDict: EventInit) { + super("release", eventInitDict); + } +} + +/** + * Event targets + */ + +/** + * SqlEventTarget + * + * The EventTarget to be used by SQLx + */ +export class SqlEventTarget< + ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, + Connection extends SqlConnection = SqlConnection< + ConnectionOptions + >, + EventType extends SqlPoolConnectionEventType = SqlPoolConnectionEventType, + EventInit extends SqlConnectableEventInit> = + SqlConnectableEventInit>, + Event extends SqlEvent = SqlEvent< + EventType, + EventInit + >, + Listener extends EventListenerOrEventListenerObject = + EventListenerOrEventListenerObject, + ListenerOptions extends AddEventListenerOptions = AddEventListenerOptions, + RemoveListenerOptions extends EventListenerOptions = EventListenerOptions, +> extends EventTarget { + /** + * With typed events. + * + * @inheritdoc + */ + addEventListener( + type: EventType, + listener: Listener | null, + options?: boolean | ListenerOptions, + ): void { + return super.addEventListener(type, listener, options); + } + + /** + * With typed events. + * + * @inheritdoc + */ + dispatchEvent(event: Event): boolean { + return super.dispatchEvent(event); + } + + /** + * With typed events. + * + * @inheritdoc + */ + removeEventListener( + type: EventType, + callback: Listener | null, + options?: boolean | RemoveListenerOptions, + ): void { + return super.removeEventListener(type, callback, options); + } +} + +/** + * SqlEventable + */ +export interface SqlEventable< + EventTarget extends SqlEventTarget = SqlEventTarget, +> extends SqlBase { + /** + * The EventTarget to reduce inheritance + */ + eventTarget: EventTarget; +} diff --git a/sql/meta.ts b/sql/meta.ts new file mode 100644 index 0000000..0a88f0c --- /dev/null +++ b/sql/meta.ts @@ -0,0 +1,6 @@ +/** + * The version of SQLx + * + * This will stay 0 until the first stable release. + */ +export const VERSION: string = "0"; diff --git a/sql/mod.ts b/sql/mod.ts new file mode 100644 index 0000000..f3b9a6f --- /dev/null +++ b/sql/mod.ts @@ -0,0 +1,7 @@ +export * from "./client.ts"; +export * from "./connection.ts"; +export * from "./core.ts"; +export * from "./errors.ts"; +export * from "./events.ts"; +export * from "./meta.ts"; +export * from "./pool.ts"; diff --git a/sql/pool.ts b/sql/pool.ts new file mode 100644 index 0000000..24d4aa1 --- /dev/null +++ b/sql/pool.ts @@ -0,0 +1,171 @@ +import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; +import type { + SqlConnectablePoolBase, + SqlPoolable, + SqlPreparable, + SqlPreparedQueriable, + SqlQueriable, + SqlQueryOptions, + SqlTransactionable, + SqlTransactionOptions, + SqlTransactionQueriable, +} from "./core.ts"; +import type { + DeferredStack as SqlDeferredStack, + DeferredStackOptions, +} from "../collections/deferred_stack.ts"; +import type { SqlEventable, SqlEventTarget } from "./events.ts"; + +/** + * SqlClientPoolOptions + * + * This represents the options for a connection pool. + */ +export interface SqlClientPoolOptions + extends SqlConnectionOptions, DeferredStackOptions { + /** + * Whether to lazily initialize connections. + * + * This means that connections will only be created + * if there are no idle connections available when + * acquiring a connection, and max pool size has not been reached. + */ + lazyInitialization?: boolean; +} + +/** + * SqlPoolClient + * + * This represents a connection to a database from a pool. + * When a user wants to use a connection from a pool, + * they should use a class implementing this interface. + */ +export interface SqlPoolClient< + ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, + Connection extends SqlConnection = SqlConnection< + ConnectionOptions + >, + ParameterType extends unknown = unknown, + QueryOptions extends SqlQueryOptions = SqlQueryOptions, + Prepared extends SqlPreparedQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions + > = SqlPreparedQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions + >, + TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, + Transaction extends SqlTransactionQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions, + TransactionOptions + > = SqlTransactionQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions, + TransactionOptions + >, +> extends + SqlConnectablePoolBase, + SqlQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions + >, + SqlPreparable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions, + Prepared + >, + SqlTransactionable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions, + TransactionOptions, + Transaction + > { + /** + * Release the connection to the pool + */ + release(): Promise; +} + +/** + * SqlClientPool + * + * This represents a pool of connections to a database. + * When a user wants to use a pool of connections to the database, + * they should use a class implementing this interface. + */ +export interface SqlClientPool< + ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, + Connection extends SqlConnection = SqlConnection< + ConnectionOptions + >, + ParameterType extends unknown = unknown, + QueryOptions extends SqlQueryOptions = SqlQueryOptions, + Prepared extends SqlPreparedQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions + > = SqlPreparedQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions + >, + TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, + Transaction extends SqlTransactionQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions, + TransactionOptions + > = SqlTransactionQueriable< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions, + TransactionOptions + >, + PoolClient extends SqlPoolClient< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions, + Prepared, + TransactionOptions, + Transaction + > = SqlPoolClient< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions, + Prepared, + TransactionOptions, + Transaction + >, + DeferredStack extends SqlDeferredStack = SqlDeferredStack< + PoolClient + >, + EventTarget extends SqlEventTarget = SqlEventTarget, +> extends + SqlConnection, + SqlPoolable< + PoolClient, + DeferredStack + >, + SqlEventable { +} diff --git a/sql/testing.ts b/sql/testing.ts new file mode 100644 index 0000000..55308c5 --- /dev/null +++ b/sql/testing.ts @@ -0,0 +1,904 @@ +// deno-lint-ignore-file no-explicit-any +import { + assert, + assertEquals, + assertFalse, + assertInstanceOf, +} from "@std/assert"; +import { + type ArrayRow, + type Row, + SqlBase, + type SqlPreparable, + type SqlPreparedQueriable, + type SqlQueriable, + type SqlTransactionable, + type SqlTransactionQueriable, +} from "./core.ts"; +import type { SqlClient } from "./client.ts"; +import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; +import type { + SqlClientPool, + SqlClientPoolOptions, + SqlPoolClient, +} from "./pool.ts"; +import { VERSION } from "./meta.ts"; +import type { SqlQueryOptions } from "./core.ts"; +import { DeferredStack } from "../collections/deferred_stack.ts"; + +interface ConnectionConstructor extends SqlBase { + new ( + ...args: any[] + ): SqlConnection; +} +interface ClientConstructor extends SqlBase { + new ( + ...args: any[] + ): SqlClient; +} +interface ClientPoolConstructor extends SqlBase { + new ( + ...args: any[] + ): SqlClientPool; +} + +export type BaseQueriableTestOptions = { + t: Deno.TestContext; + queries: { + /** + * Should create a table "sqlxtesttable" if not exist with a single column "testcol" of string type (min 10 characters) + */ + createTable: string; + /** + * Should drop the table "sqlxtesttable" if exists + */ + dropTable: string; + /** + * Should insert a single row into the table + */ + insertOneToTable: string; + /** + * Should insert multiple rows into the table + */ + insertManyToTable: string; + /** + * Should select a single row from the table + */ + selectOneFromTable: string; + /** + * Should select a single row by matching the testcol value + */ + selectByMatchFromTable: string; + /** + * Should select multiple rows from the table + */ + selectManyFromTable: string; + /** + * Should return "1" as "result" + */ + select1AsString: string; + /** + * Should return 1+1 as "result" + */ + select1Plus1AsNumber: string; + /** + * Should delete a single row by matching the testcol value + */ + deleteByMatchFromTable: string; + /** + * Should delete all rows from the table + */ + deleteAllFromTable: string; + }; +}; + +export type TestConnectAndClosePoolClient = PoolTestOptions; + +export type TestPreparableOptions = BaseQueriableTestOptions & { + db: SqlPreparable; +}; + +export type TestPreparedQueriableOptions = + & Omit + & { + db: SqlPreparedQueriable; + cases: { + execute: { + params?: any[]; + expected: number | undefined; + }; + query: { + params?: any[]; + expected: Row[]; + }; + queryOne: { + params?: any[]; + expected: Row | undefined; + }; + queryMany: { + params?: any[]; + expected: Row[]; + }; + queryArray: { + params?: any[]; + expected: ArrayRow[]; + }; + queryOneArray: { + params?: any[]; + expected: ArrayRow; + }; + queryManyArray: { + params?: any[]; + expected: ArrayRow[]; + }; + }; + }; + +export type TestQueriableOptions = BaseQueriableTestOptions & { + db: SqlQueriable; +}; + +export type TestTransactionableOptions = BaseQueriableTestOptions & { + db: SqlTransactionable & SqlQueriable; +}; +export type TestTransactionOptions = BaseQueriableTestOptions & { + db: SqlTransactionQueriable; +}; + +export type SqlBaseStaticTestOptions = { + t: Deno.TestContext; + Base: typeof SqlBase; +}; + +export type ConnectionTestOptions = { + t: Deno.TestContext; + connection: SqlConnection; +}; + +export type ConnectionConstructorTestOptions = { + t: Deno.TestContext; + Connection: ConnectionConstructor; + connectionUrl: string | URL; + connectionOptions: SqlConnectionOptions; +}; + +export type ClientTestOptions = BaseQueriableTestOptions & { + Client: ClientConstructor; + connectionUrl: string | URL; + connectionOptions: SqlConnectionOptions & SqlQueryOptions; +}; + +export type PoolTestOptions = BaseQueriableTestOptions & { + Client: ClientPoolConstructor; + connectionUrl: string | URL; + connectionOptions: + & SqlConnectionOptions + & SqlQueryOptions + & SqlClientPoolOptions; +}; + +async function testConnectAndCloseClient( + { t, Client, connectionUrl, connectionOptions }: ClientTestOptions, +): Promise { + await t.step("testConnectAndClose", async (t) => { + await t.step("should connect and close with using", async () => { + await using db = new Client(connectionUrl, connectionOptions); + + await db.connect(); + }); + + await t.step("should connect and close", async () => { + const db = new Client(connectionUrl, connectionOptions); + + await db.connect(); + + await db.close(); + }); + + await t.step("should connect and close with events", async () => { + const db = new Client(connectionUrl, connectionOptions); + + let connectListenerCalled = false; + let closeListenerCalled = false; + let error: Error | undefined = undefined; + + try { + db.eventTarget.addEventListener("connect", () => { + connectListenerCalled = true; + }); + + db.eventTarget.addEventListener("close", () => { + closeListenerCalled = true; + }); + + await db.connect(); + await db.close(); + } catch (e) { + error = e; + } + + assertEquals( + connectListenerCalled, + true, + "Connect listener not called: " + error?.message, + ); + assertEquals( + closeListenerCalled, + true, + "Close listener not called: " + error?.message, + ); + }); + }); +} + +async function testConnectAndClosePoolClient( + { t, Client, connectionUrl, connectionOptions }: + TestConnectAndClosePoolClient, +): Promise { + await t.step("testConnectAndClose", async (t) => { + await t.step("should connect and close", async () => { + const db = new Client(connectionUrl, connectionOptions); + + assertEquals(db.connected, false); + + await db.connect(); + + await db.close(); + }); + await t.step("should connect and close with using", async () => { + await using db = new Client(connectionUrl, { + ...connectionOptions, + lazyInitialization: true, + }); + let connectListenerCalled = false; + + db.eventTarget.addEventListener("connect", () => { + connectListenerCalled = true; + }); + + await db.connect(); + + assertFalse( + connectListenerCalled, + "Connect listener called, but should not have been due to lazyInitialization", + ); + }); + await t.step("should connect and close with events", async () => { + const db = new Client(connectionUrl, { + ...connectionOptions, + lazyInitialization: false, + }); + + let connectListenerCalled = false; + let closeListenerCalled = false; + let error: Error | undefined = undefined; + + try { + db.eventTarget.addEventListener("connect", () => { + connectListenerCalled = true; + }); + + db.eventTarget.addEventListener("close", () => { + closeListenerCalled = true; + }); + + await db.connect(); + await db.close(); + } catch (e) { + error = e; + } + + assertEquals( + connectListenerCalled, + true, + "Connect listener not called: " + error?.message, + ); + assertEquals( + closeListenerCalled, + true, + "Close listener not called: " + error?.message, + ); + }); + }); +} + +async function testPreparedQueriable( + { t, db, cases }: TestPreparedQueriableOptions, +): Promise { + await t.step("testPreparedQueriable", async (t) => { + await t.step("has properties", () => { + assertEquals(typeof db["execute"], "function"); + assertEquals(typeof db["query"], "function"); + assertEquals(typeof db["queryOne"], "function"); + assertEquals(typeof db["queryMany"], "function"); + assertEquals(typeof db["queryArray"], "function"); + assertEquals(typeof db["queryOneArray"], "function"); + assertEquals(typeof db["queryManyArray"], "function"); + }); + + await t.step("should execute", async () => { + if (cases.execute.expected) { + const res = await db.execute(cases.execute.params); + assertEquals(res, cases.execute.expected); + } + }); + + await t.step("should query", async () => { + const res = await db.query(cases.query.params); + assertEquals(res, cases.query.expected); + }); + + await t.step("should queryOne", async () => { + const res = await db.queryOne(cases.queryOne.params); + assertEquals(res, cases.queryOne.expected); + }); + + await t.step("should queryMany", async () => { + const actual = []; + for await (const res of db.queryMany(cases.queryMany.params)) { + actual.push(res); + } + assertEquals(actual, cases.queryMany.expected); + }); + + await t.step("should queryArray", async () => { + const res = await db.queryArray(cases.queryArray.params); + assertEquals(res, cases.queryArray.expected); + }); + + await t.step("should queryOneArray", async () => { + const res = await db.queryOneArray(cases.queryOneArray.params); + assertEquals(res, cases.queryOneArray.expected); + }); + + await t.step("should queryManyArray", async () => { + const actual = []; + for await (const res of db.queryManyArray(cases.queryManyArray.params)) { + actual.push(res); + } + assertEquals(actual, cases.queryManyArray.expected); + }); + }); +} + +async function testPreparable( + { t, db, queries }: TestPreparableOptions, +): Promise { + await t.step("testPreparable", async (t) => { + assertEquals(typeof db["prepare"], "function"); + + await t.step("prepare select1AsString", async (t) => { + const statement = db.prepare(queries.select1AsString); + await testPreparedQueriable({ + t, + db: statement, + cases: { + execute: { expected: undefined }, + query: { expected: [{ result: "1" }] }, + queryOne: { expected: { result: "1" } }, + queryMany: { expected: [{ result: "1" }] }, + queryArray: { expected: [["1"]] }, + queryOneArray: { expected: ["1"] }, + queryManyArray: { expected: [["1"]] }, + }, + }); + }); + await t.step("prepare select1Plus1AsNumber", async (t) => { + const statement = db.prepare(queries.select1Plus1AsNumber); + await testPreparedQueriable({ + t, + db: statement, + cases: { + execute: { expected: undefined }, + query: { expected: [{ result: 2 }] }, + queryOne: { expected: { result: 2 } }, + queryMany: { expected: [{ result: 2 }] }, + queryArray: { expected: [[2]] }, + queryOneArray: { expected: [2] }, + queryManyArray: { expected: [[2]] }, + }, + }); + }); + await t.step("prepare selectByMatchFromTable", async (t) => { + const statement = db.prepare(queries.selectOneFromTable); + await testPreparedQueriable({ + t, + db: statement, + cases: { + execute: { params: ["test"], expected: undefined }, + query: { params: ["test"], expected: [{ testcol: "test" }] }, + queryOne: { params: ["test"], expected: { testcol: "test" } }, + queryMany: { params: ["test"], expected: [{ testcol: "test" }] }, + queryArray: { params: ["test"], expected: [["test"]] }, + queryOneArray: { params: ["test"], expected: ["test"] }, + queryManyArray: { params: ["test"], expected: [["test"]] }, + }, + }); + }); + await t.step("prepare selectByMatchFromTable", async (t) => { + const statement = db.prepare(queries.selectByMatchFromTable); + await testPreparedQueriable({ + t, + db: statement, + cases: { + execute: { params: ["test"], expected: undefined }, + query: { params: ["test"], expected: [{ testcol: "test" }] }, + queryOne: { params: ["test"], expected: { testcol: "test" } }, + queryMany: { params: ["test"], expected: [{ testcol: "test" }] }, + queryArray: { params: ["test"], expected: [["test"]] }, + queryOneArray: { params: ["test"], expected: ["test"] }, + queryManyArray: { params: ["test"], expected: [["test"]] }, + }, + }); + }); + await t.step("prepare selectManyFromTable", async (t) => { + const statement = db.prepare(queries.selectManyFromTable); + await testPreparedQueriable({ + t, + db: statement, + cases: { + execute: { expected: undefined }, + query: { + expected: [{ testcol: "test" }, { testcol: "test1" }, { + testcol: "test2", + }, { testcol: "test3" }], + }, + queryOne: { expected: { testcol: "test" } }, + queryMany: { + expected: [{ testcol: "test" }, { testcol: "test1" }, { + testcol: "test2", + }, { testcol: "test3" }], + }, + queryArray: { expected: [["test"], ["test1"], ["test2"], ["test3"]] }, + queryOneArray: { expected: ["test"] }, + queryManyArray: { + expected: [["test"], ["test1"], ["test2"], ["test3"]], + }, + }, + }); + }); + }); +} + +async function testQueriable( + { t, db, queries }: TestQueriableOptions, +): Promise { + await t.step("testQueriable", async (t) => { + await t.step("has properties", () => { + assertEquals(typeof db["execute"], "function"); + assertEquals(typeof db["query"], "function"); + assertEquals(typeof db["queryOne"], "function"); + assertEquals(typeof db["queryMany"], "function"); + assertEquals(typeof db["queryArray"], "function"); + assertEquals(typeof db["queryOneArray"], "function"); + assertEquals(typeof db["queryManyArray"], "function"); + assertEquals(typeof db["sql"], "function"); + assertEquals(typeof db["sqlArray"], "function"); + }); + + await t.step("should execute", async () => { + await db.execute(queries.selectManyFromTable); + }); + + await t.step("should query", async () => { + const res = await db.query(queries.selectManyFromTable); + assertEquals(res, [{ testcol: "test" }, { testcol: "test1" }, { + testcol: "test2", + }, { testcol: "test3" }]); + }); + + await t.step("should queryOne", async () => { + const res = await db.queryOne(queries.selectManyFromTable); + assertEquals(res, { testcol: "test" }); + }); + + await t.step("should queryMany", async () => { + const actual = []; + for await (const res of db.queryMany(queries.selectManyFromTable)) { + actual.push(res); + } + assertEquals(actual, [{ testcol: "test" }, { testcol: "test1" }, { + testcol: "test2", + }, { testcol: "test3" }]); + }); + + await t.step("should queryArray", async () => { + const res = await db.queryArray(queries.selectManyFromTable); + assertEquals(res, [["test"], ["test1"], ["test2"], ["test3"]]); + }); + + await t.step("should queryOneArray", async () => { + const res = await db.queryOneArray(queries.selectManyFromTable); + assertEquals(res, ["test"]); + }); + + await t.step("should queryManyArray", async () => { + const actual = []; + for await (const res of db.queryManyArray(queries.selectManyFromTable)) { + actual.push(res); + } + assertEquals(actual, [["test"], ["test1"], ["test2"], ["test3"]]); + }); + }); +} + +async function testTransactionable( + { t, db, queries }: TestTransactionableOptions, +): Promise { + await t.step("testTransactionable", async (t) => { + await t.step("has properties", () => { + assertEquals(typeof db["beginTransaction"], "function"); + assertEquals(typeof db["transaction"], "function"); + }); + + await testQueriable({ t, db, queries }); + + await t.step("transaction wrapper", async (t) => { + await db.transaction(async (c) => { + await testTransaction({ t, db: c, queries }); + }); + }); + + await t.step("get a transaction instance", async (t) => { + const res = await db.beginTransaction(); + + await testTransaction({ t, db: res, queries }); + }); + }); +} + +async function testTransaction( + { t, db, queries }: TestTransactionOptions, +): Promise { + await t.step("testTransaction", async (t) => { + await t.step("has properties", () => { + assertEquals(typeof db["commitTransaction"], "function"); + assertEquals(typeof db["rollbackTransaction"], "function"); + assertEquals(typeof db["createSavepoint"], "function"); + assertEquals(typeof db["releaseSavepoint"], "function"); + }); + + await testQueriable({ t, db, queries }); + + await t.step( + "createSavepoint, releaseSavepoint", + async () => { + await db.createSavepoint("savepoint1"); + await db.releaseSavepoint("savepoint1"); + }, + ); + }); +} + +export async function testSetupTable( + { t, db, queries }: TestQueriableOptions, +): Promise { + await t.step("drop table", async () => { + const res = await db.execute(queries.dropTable); + assertEquals(res, 0); + }); + + await t.step("setup table", async () => { + const res = await db.execute(queries.createTable); + assertEquals(res, 0); + }); + + await t.step("insert one", async () => { + const res = await db.execute(queries.insertOneToTable, ["test"]); + assertEquals(res, 1); + }); + + await t.step("insert many", async () => { + const res = await db.execute(queries.insertManyToTable, [ + "test1", + "test2", + "test3", + ]); + assertEquals(res, 3); + }); + + await t.step("select one", async () => { + const res = await db.queryOne(queries.selectOneFromTable, ["test"]); + assertEquals(res, { testcol: "test" }); + }); + + await t.step("select by match", async () => { + const res = await db.query(queries.selectByMatchFromTable, ["test"]); + assertEquals(res, [{ testcol: "test" }]); + }); + + await t.step("select many", async () => { + const res = await db.query(queries.selectManyFromTable); + assertEquals(res, [{ testcol: "test" }, { testcol: "test1" }, { + testcol: "test2", + }, { testcol: "test3" }]); + }); +} + +export async function connectionTest( + { t, connection }: ConnectionTestOptions, +): Promise { + await t.step("Connection Test", async (t) => { + await t.step("is instance of base", () => { + assertInstanceOf(connection, SqlBase); + }); + + await t.step("has properties", () => { + assertEquals(typeof connection.connectionUrl, "string"); + assertEquals(typeof connection.connectionOptions, "object"); + assertEquals(typeof connection.connected, "boolean"); + assertEquals(connection.connected, false); + assertEquals(typeof connection.connect, "function"); + assertEquals(typeof connection.close, "function"); + assertEquals(typeof connection[Symbol.asyncDispose], "function"); + }); + + await t.step("can connect and close", async () => { + assertEquals(connection.connected, false); + await connection.connect(); + assertEquals(connection.connected, true); + await connection.close(); + assertEquals(connection.connected, false); + }); + + await t.step("can reconnect", async () => { + assertEquals(connection.connected, false); + await connection.connect(); + assertEquals(connection.connected, true); + await connection.close(); + assertEquals(connection.connected, false); + }); + }); +} + +export async function sqlxBaseStaticTest( + { t, Base }: SqlBaseStaticTestOptions, +): Promise { + await t.step("SqlBase Static Test", async (t) => { + await t.step("has properties", () => { + assertEquals(typeof Base, "function"); + assertEquals(Base.sqlxVersion, VERSION); + }); + }); +} + +/** + * connectionConstructorTest + * + * Test the connection constructor. This is the test you want to include in your test suite. + * + * @example + * ``` + * Deno.test("Connection", async (t) => { + * ...other tests + * await connectionConstructorTest({ t, Connection, connectionOptions, connectionUrl }) + * ...other tests + * }) + * ``` + */ +export async function connectionConstructorTest( + { t, Connection, connectionOptions, connectionUrl }: + ConnectionConstructorTestOptions, +): Promise { + await t.step("SQLx Connection Constructor Test", async (t) => { + await t.step("is constructor", async (t) => { + assertEquals(typeof Connection, "function"); + await sqlxBaseStaticTest({ t, Base: Connection }); + }); + + await t.step("can construct", async (t) => { + const connection = new Connection(connectionUrl, connectionOptions); + + await connectionTest({ t, connection }); + }); + + await t.step("can connect with using and dispose", async () => { + await using connection = new Connection(connectionUrl, connectionOptions); + assertEquals(connection.connected, false); + await connection.connect(); + assertEquals(connection.connected, true); + }); + }); +} + +/** + * clientTest + * + * Test the client constructor. This is the test you want to include in your test suite. + * + * @example + * ``` + * Deno.test("Client", async (t) => { + * ...other tests + * await clientTest({ t, Client, connectionOptions, connectionUrl, queries }) + * ...other tests + * }) + * ``` + */ +export async function clientTest( + { t, Client, connectionUrl, connectionOptions, queries }: ClientTestOptions, +): Promise { + await t.step("SQLx Client Tests", async (t) => { + await t.step("is constructor", async (t) => { + assertEquals(typeof Client, "function"); + await sqlxBaseStaticTest({ t, Base: Client }); + }); + + await t.step("can construct", async (t) => { + const client = new Client(connectionUrl, connectionOptions); + + await connectionTest({ t, connection: client.connection }); + + await testConnectAndCloseClient({ + t, + Client, + connectionUrl, + connectionOptions, + queries, + }); + }); + + await t.step("can connect with using and dispose", async () => { + await using connection = new Client(connectionUrl, connectionOptions); + assertEquals(connection.connected, false); + await connection.connect(); + assertEquals(connection.connected, true); + }); + + await using db = new Client(connectionUrl, connectionOptions); + + await db.connect(); + + await testSetupTable({ t, db, queries }); + + await testPreparable({ t, db, queries }); + await testTransactionable({ t, db, queries }); + + await t.step("drop table", async () => { + await db.execute(queries.dropTable); + }); + }); +} + +/** + * clientPoolTest + * + * Test the client pool constructor. This is the test you want to include in your test suite. + * + * @example + * ``` + * Deno.test("Client", async (t) => { + * ...other tests + * await clientPoolTest({ t, Client, connectionOptions, connectionUrl, queries }) + * ...other tests + * }) + * ``` + */ +export async function clientPoolTest( + { t, Client, connectionUrl, connectionOptions, queries }: PoolTestOptions, +): Promise { + await t.step("SQLx Pool Tests", async (t) => { + const parsedConnectionOptions = { + ...connectionOptions, + maxSize: 3, + lazyInitialization: true, + }; + await t.step("is constructor", async (t) => { + assertEquals(typeof Client, "function"); + await sqlxBaseStaticTest({ t, Base: Client }); + }); + + await t.step("can construct", async (t) => { + const client = new Client(connectionUrl, parsedConnectionOptions); + await t.step("has properties", () => { + assertEquals(typeof client.deferredStack, "object"); + assertEquals(typeof client.acquire, "function"); + assertEquals(typeof client.release, "function"); + assertEquals(typeof client.connect, "function"); + assertEquals(typeof client.close, "function"); + assertInstanceOf(client.deferredStack, DeferredStack); + }); + }); + + await testConnectAndClosePoolClient({ + t: t, + Client, + connectionUrl, + connectionOptions: parsedConnectionOptions, + queries, + }); + + await t.step("acquire and release", async () => { + await using db = new Client(connectionUrl, parsedConnectionOptions); + + let aquireCalledCount = 0; + let releaseCalledCount = 0; + + db.eventTarget.addEventListener("acquire", () => { + aquireCalledCount++; + }); + db.eventTarget.addEventListener("release", () => { + releaseCalledCount++; + }); + + await db.connect(); + assertEquals( + db.deferredStack.maxSize, + parsedConnectionOptions.maxSize, + ); + assertEquals(db.deferredStack.availableCount, 3); + assertEquals(db.deferredStack.queuedCount, 0); + assertEquals(aquireCalledCount, 0); + assertEquals(releaseCalledCount, 0); + + const poolConnections: SqlPoolClient[] = []; + + let i = db.deferredStack.maxSize; + while (db.deferredStack.availableCount) { + const p = await db.acquire(); + assertEquals(db.deferredStack.availableCount, --i); + assertEquals(db.deferredStack.queuedCount, 0); + poolConnections.push(p); + } + + assertEquals(aquireCalledCount, 3); + assertEquals(releaseCalledCount, 0); + + let p1Resolved = false; + let p2Resolved = false; + const p1 = db.acquire().then((r) => { + p1Resolved = true; + return r; + }); + assertEquals(db.deferredStack.queuedCount, 1); + const p2 = db.acquire().then((r) => { + p2Resolved = true; + return r; + }); + assertEquals(db.deferredStack.queuedCount, 2); + + assertFalse(p1Resolved); + await poolConnections.pop()?.release(); + await p1; + assert(p1Resolved); + assertEquals(db.deferredStack.queuedCount, 1); + assertFalse(p2Resolved); + assertEquals(aquireCalledCount, 4); + assertEquals(releaseCalledCount, 1); + await poolConnections.pop()?.release(); + await p2; + assert(p2Resolved); + assertEquals(db.deferredStack.queuedCount, 0); + assertEquals(aquireCalledCount, 5); + assertEquals(releaseCalledCount, 2); + + poolConnections.push(await p1); + poolConnections.push(await p2); + + for (const [i, p] of poolConnections.entries()) { + await p.release(); + assertEquals(db.deferredStack.availableCount, i + 1); + assertEquals(db.deferredStack.queuedCount, 0); + } + assertEquals(aquireCalledCount, 5); + assertEquals(releaseCalledCount, 5); + + await db.close(); + }); + + await using db = new Client(connectionUrl, parsedConnectionOptions); + await db.connect(); + + const p = await db.acquire(); + await testSetupTable({ t, db: p, queries }); + await p.release(); + + await t.step("acquire and release with query", async (t) => { + for (let i = 0; i < db.deferredStack.maxSize; i++) { + const p = await db.acquire(); + await testTransactionable({ t, db: p, queries }); + p.release(); + } + }); + }); +} From e12113ed92f9bdda8966dea23ae03168e3fda25c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Thu, 30 May 2024 22:05:03 +0200 Subject: [PATCH 03/29] cleanup --- collections/deferred_stack.test.ts | 8 +------- sql/deno.json | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/collections/deferred_stack.test.ts b/collections/deferred_stack.test.ts index 95809d0..fcfc6cb 100644 --- a/collections/deferred_stack.test.ts +++ b/collections/deferred_stack.test.ts @@ -1,10 +1,4 @@ -import { - assert, - assertEquals, - assertFalse, - assertMatch, - assertThrows, -} from "@std/assert"; +import { assert, assertEquals, assertFalse, assertThrows } from "@std/assert"; import { DeferredStack } from "./deferred_stack.ts"; Deno.test("deferred", async (t) => { diff --git a/sql/deno.json b/sql/deno.json index 33f5fe4..7369bee 100644 --- a/sql/deno.json +++ b/sql/deno.json @@ -4,6 +4,6 @@ "lock": false, "exports": { ".": "./mod.ts", - "./testing": "./lib/testing.ts" + "./testing": "./testing.ts" } } From 8d2d6ba4e340f17cf31d5a9103c7f6225ec1a4f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Thu, 6 Jun 2024 10:27:43 +0200 Subject: [PATCH 04/29] Removed SqlBase with dedicated version --- sql/README.md | 6 ------ sql/connection.ts | 4 +--- sql/core.ts | 32 ++++---------------------------- sql/errors.ts | 4 +--- sql/events.ts | 13 ++++--------- sql/meta.ts | 6 ------ sql/mod.ts | 1 - 7 files changed, 10 insertions(+), 56 deletions(-) delete mode 100644 sql/meta.ts diff --git a/sql/README.md b/sql/README.md index 53655f2..ef5cb05 100644 --- a/sql/README.md +++ b/sql/README.md @@ -194,25 +194,19 @@ In most cases, these are the classes and the inheritance graph that should be implemented. Notice how almost everything extends `SqlBase` at the base. - SqlConnection - - SqlBase - SqlPreparedQueriable - - SqlBase - SqlTransactionQueriable - SqlPreparable - SqlQueriable - - SqlBase - SqlClient - SqlTransactionable - SqlPreparable - SqlQueriable - - SqlBase - SqlPoolClient - SqlTransactionable - SqlPreparable - SqlQueriable - - SqlBase - SqlClientPool - - SqlBase ### Other diff --git a/sql/connection.ts b/sql/connection.ts index aace091..ca9225d 100644 --- a/sql/connection.ts +++ b/sql/connection.ts @@ -1,5 +1,3 @@ -import type { SqlBase } from "./core.ts"; - /** * SqlConnectionOptions * @@ -30,7 +28,7 @@ export interface SqlConnectionOptions { */ export interface SqlConnection< ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, -> extends SqlBase, AsyncDisposable { +> extends AsyncDisposable { /** * Connection URL */ diff --git a/sql/core.ts b/sql/core.ts index 1fe9e03..47f279e 100644 --- a/sql/core.ts +++ b/sql/core.ts @@ -1,7 +1,6 @@ // deno-lint-ignore-file no-explicit-any import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; import type { DeferredStack as SqlDeferredStack } from "../collections/deferred_stack.ts"; -import { VERSION } from "./meta.ts"; /** * Row @@ -48,7 +47,7 @@ export interface SqlQueryOptions extends SqlConnectionOptions { */ export interface SqlConnectableBase< Connection extends SqlConnection = SqlConnection, -> extends SqlBase { +> { /** * The connection to the database */ @@ -464,13 +463,15 @@ export interface SqlTransactionable< * SqlPoolable * * Represents an object that can acquire a connection from a pool. + * + * TODO: check if this still works with new DeferredStack */ export interface SqlPoolable< Connectable extends SqlConnectablePoolBase = SqlConnectablePoolBase, DeferredStack extends SqlDeferredStack = SqlDeferredStack< Connectable >, -> extends SqlBase { +> { /** * The deferred stack of connections */ @@ -486,28 +487,3 @@ export interface SqlPoolable< */ release(connectable: Connectable): Promise; } - -/** - * Base class for all SQLx classes - * - * All SQLx implemented classes should have this class as its base class for inheritance, - * or at the least imlement it as an interface and manually include the static and dynamic properties - */ -export class SqlBase { - /** - * The version of SQLx - * @example - * ```ts - * import { VERSION } from "@halvardm/sqlx"; - * ``` - */ - readonly sqlxVersion: string = VERSION; - /** - * The version of SQLx - * @example - * ```ts - * import { VERSION } from "@halvardm/sqlx"; - * ``` - */ - static readonly sqlxVersion: string = VERSION; -} diff --git a/sql/errors.ts b/sql/errors.ts index b641875..f77eba5 100644 --- a/sql/errors.ts +++ b/sql/errors.ts @@ -6,9 +6,7 @@ import { VERSION } from "./meta.ts"; * * Base SQLx Error */ -export class SqlError extends Error implements SqlBase { - readonly sqlxVersion = VERSION; - static readonly sqlxVersion = VERSION; +export class SqlError extends Error { constructor(message: string) { super(message); this.name = this.constructor.name; diff --git a/sql/events.ts b/sql/events.ts index 85d7836..ae8f59a 100644 --- a/sql/events.ts +++ b/sql/events.ts @@ -2,8 +2,7 @@ * Events */ import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; -import type { SqlBase, SqlConnectableBase } from "./core.ts"; -import { VERSION } from "./meta.ts"; +import type { SqlConnectableBase } from "./core.ts"; /** * Event types @@ -55,9 +54,7 @@ export interface SqlConnectableEventInit< */ export class SqlErrorEvent< EventInit extends SqlErrorEventInit = SqlErrorEventInit, -> extends ErrorEvent implements SqlBase { - readonly sqlxVersion = VERSION; - static readonly sqlxVersion = VERSION; +> extends ErrorEvent { constructor(type: "error", eventInitDict?: EventInit) { super(type, eventInitDict); } @@ -69,9 +66,7 @@ export class SqlErrorEvent< export class SqlEvent< EventType extends SqlPoolConnectionEventType = SqlPoolConnectionEventType, EventInit extends SqlConnectableEventInit = SqlConnectableEventInit, -> extends Event implements SqlBase { - readonly sqlxVersion = VERSION; - static readonly sqlxVersion = VERSION; +> extends Event { constructor(type: EventType, eventInitDict?: EventInit) { super(type, eventInitDict); } @@ -188,7 +183,7 @@ export class SqlEventTarget< */ export interface SqlEventable< EventTarget extends SqlEventTarget = SqlEventTarget, -> extends SqlBase { +> { /** * The EventTarget to reduce inheritance */ diff --git a/sql/meta.ts b/sql/meta.ts deleted file mode 100644 index 0a88f0c..0000000 --- a/sql/meta.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * The version of SQLx - * - * This will stay 0 until the first stable release. - */ -export const VERSION: string = "0"; diff --git a/sql/mod.ts b/sql/mod.ts index f3b9a6f..1c4ad50 100644 --- a/sql/mod.ts +++ b/sql/mod.ts @@ -3,5 +3,4 @@ export * from "./connection.ts"; export * from "./core.ts"; export * from "./errors.ts"; export * from "./events.ts"; -export * from "./meta.ts"; export * from "./pool.ts"; From babbcd533e1a6fe90186304bf961320c2d634d3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Fri, 7 Jun 2024 16:15:36 +0200 Subject: [PATCH 05/29] fix(collections): added release and remove callbacks --- collections/deferred_stack.test.ts | 14 ++++----- collections/deferred_stack.ts | 48 ++++++++++++++++++++---------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/collections/deferred_stack.test.ts b/collections/deferred_stack.test.ts index fcfc6cb..22123fb 100644 --- a/collections/deferred_stack.test.ts +++ b/collections/deferred_stack.test.ts @@ -54,7 +54,7 @@ Deno.test("deferred", async (t) => { assertEquals(deferred.queuedCount, 0); assertEquals(e1.active, true); assertEquals(e1.value, 2); - e1.release(); + await e1.release(); assertEquals(deferred.maxSize, 2); assertEquals(deferred.elements.length, 2); assertEquals(deferred.stack.length, 2); @@ -123,7 +123,7 @@ Deno.test("deferred", async (t) => { assertEquals(deferred.availableCount, 0); assertEquals(deferred.queuedCount, 2); - e2.release(); + await e2.release(); await e4; assert(e4Resolved); assertFalse(e5Resolved); @@ -138,7 +138,7 @@ Deno.test("deferred", async (t) => { assertEquals(e1.active, false); assertEquals(e2.active, false); - e3.release(); + await e3.release(); await e5; assert(e5Resolved); assertEquals(deferred.maxSize, 2); @@ -153,7 +153,7 @@ Deno.test("deferred", async (t) => { assertEquals(e2.active, false); assertEquals(e3.active, false); - (await e4).release(); + await (await e4).release(); assertEquals(deferred.maxSize, 2); assertEquals(deferred.elements.length, 2); assertEquals(deferred.stack.length, 1); @@ -167,7 +167,7 @@ Deno.test("deferred", async (t) => { assertEquals(e3.active, false); assertEquals((await e4).active, false); - (await e5).release(); + await (await e5).release(); assertEquals(deferred.maxSize, 2); assertEquals(deferred.elements.length, 2); assertEquals(deferred.stack.length, 2); @@ -183,7 +183,7 @@ Deno.test("deferred", async (t) => { assertEquals((await e5).active, false); const e6 = await deferred.pop(); - e6.remove(); + await e6.remove(); assertEquals(deferred.maxSize, 2); assertEquals(deferred.elements.length, 1); assertEquals(deferred.stack.length, 1); @@ -200,7 +200,7 @@ Deno.test("deferred", async (t) => { assertEquals(e6.active, false); const e7 = await deferred.pop(); - e7.remove(); + await e7.remove(); assertEquals(deferred.maxSize, 2); assertEquals(deferred.elements.length, 0); assertEquals(deferred.stack.length, 0); diff --git a/collections/deferred_stack.ts b/collections/deferred_stack.ts index f8eda86..dbd7d3e 100644 --- a/collections/deferred_stack.ts +++ b/collections/deferred_stack.ts @@ -3,11 +3,19 @@ * * Options for DeferredStack */ -export type DeferredStackOptions = { +export type DeferredStackOptions = { /** * The maximum stack size to be allowed. */ maxSize?: number; + /** + * The release function to be called when the element is released + */ + releaseFn?: (element: DeferredStackElement) => Promise | void; + /** + * The remove function to be called when the element is removed + */ + removeFn?: (element: DeferredStackElement) => Promise | void; }; /** @@ -30,6 +38,8 @@ export class DeferredStack { * @default 10 */ readonly maxSize: number = 10; + #releaseFn?: DeferredStackOptions["releaseFn"]; + #removeFn?: DeferredStackOptions["removeFn"]; /** * The list of all elements @@ -90,8 +100,10 @@ export class DeferredStack { return this.queue.length; } - constructor(options: DeferredStackOptions) { - this.maxSize = options.maxSize ?? 10; + constructor(options?: DeferredStackOptions) { + this.maxSize = options?.maxSize ?? 10; + this.#releaseFn = options?.releaseFn; + this.#removeFn = options?.removeFn; } /** @@ -109,8 +121,14 @@ export class DeferredStack { const newElement = new DeferredStackElement({ value: element, - releaseFn: (element) => this.#release(element), - removeFn: (element) => this.#remove(element), + releaseFn: async (element) => { + await this.#release(element); + await this.#releaseFn?.(element); + }, + removeFn: async (element) => { + this.#remove(element); + await this.#removeFn?.(element); + }, }); this.#elements.push(newElement); @@ -157,9 +175,9 @@ export class DeferredStack { * To avoid that previous users of the element can still access it, * the element is removed from the stack, and added again. */ - #release(element: DeferredStackElement): void { + async #release(element: DeferredStackElement): Promise { const value = element.value; - element.remove(); + await element.remove(); this.add(value); } @@ -183,11 +201,11 @@ export type DeferredStackElementOptions = { /** * The release function to be called when the element is released */ - releaseFn: (element: DeferredStackElement) => void; + releaseFn: (element: DeferredStackElement) => Promise; /** * The remove function to be called when the element is removed */ - removeFn: (element: DeferredStackElement) => void; + removeFn: (element: DeferredStackElement) => Promise; }; /** @@ -201,7 +219,7 @@ export class DeferredStackElement { /** * The unique identifier of the element */ - _id = crypto.randomUUID(); + _id: string = crypto.randomUUID(); /** * Whether the element is in use @@ -216,7 +234,7 @@ export class DeferredStackElement { /** * The value of the element */ - #value: T; + _value: T; /** * The release function to be called when the element is released @@ -251,13 +269,13 @@ export class DeferredStackElement { get value(): T { if (!this.active) throw new Error("Element is not active"); if (this.#disposed) throw new Error("Element is disposed"); - return this.#value; + return this._value; } constructor( options: DeferredStackElementOptions, ) { - this.#value = options.value; + this._value = options.value; this.#releaseFn = options.releaseFn; this.#removeFn = options.removeFn; } @@ -274,14 +292,14 @@ export class DeferredStackElement { /** * Releases the element back to the DeferredStack */ - release(): void { + release(): ReturnType["releaseFn"]> { return this.#releaseFn(this); } /** * Removes the element from the DeferredStack */ - remove(): void { + remove(): ReturnType["removeFn"]> { this.#active = false; this.#disposed = true; return this.#removeFn(this); From 02e894738c17b1d98097b4bcce69073f15ae5509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Fri, 7 Jun 2024 16:39:51 +0200 Subject: [PATCH 06/29] Changed and simplified the structure for inheritance and flow --- sql/README.md | 167 +++---- sql/_assets/inheritance_flowchart.jpg | Bin 0 -> 230770 bytes sql/asserts.ts | 230 +++++++++ sql/client.ts | 36 +- sql/connection.ts | 52 +- sql/core.ts | 149 ++---- sql/errors.ts | 18 - sql/events.ts | 41 +- sql/mod.ts | 1 + sql/pool.ts | 113 +++-- sql/test.ts | 653 ++++++++++++++++++++++++++ sql/testing.ts | 505 +++++++++----------- 12 files changed, 1381 insertions(+), 584 deletions(-) create mode 100644 sql/_assets/inheritance_flowchart.jpg create mode 100644 sql/asserts.ts create mode 100644 sql/test.ts diff --git a/sql/README.md b/sql/README.md index ef5cb05..c539b5b 100644 --- a/sql/README.md +++ b/sql/README.md @@ -24,56 +24,74 @@ const res = await db.query("SELECT * FROM table"); ### Client Full compliance with `@stdext/std` provides a database client with the following -methods (see [SqlClient](./lib/client.ts)): - -- `connect` (See [SqlConnection](./lib/connection.ts)): Creates a connection to - the database -- `close` (See [SqlConnection](./lib/connection.ts)): Closes the connection to - the database -- `execute` (See [SqlQueriable](./lib/core.ts)): Executes a SQL statement -- `query` (See [SqlQueriable](./lib/core.ts)): Queries the database and returns - an array of object -- `queryOne` (See [SqlQueriable](./lib/core.ts)): Queries the database and - returns at most one entry as an object -- `queryMany` (See [SqlQueriable](./lib/core.ts)): Queries the database with an +methods (see [SqlClient](./client.ts)): + +- `connect` (See [SqlConnection](./connection.ts)): Creates a connection to the + database +- `close` (See [SqlConnection](./connection.ts)): Closes the connection to the + database +- `execute` (See [SqlQueriable](./core.ts)): Executes a SQL statement +- `query` (See [SqlQueriable](./core.ts)): Queries the database and returns an + array of object +- `queryOne` (See [SqlQueriable](./core.ts)): Queries the database and returns + at most one entry as an object +- `queryMany` (See [SqlQueriable](./core.ts)): Queries the database with an async generator and yields each entry as an object -- `queryArray` (See [SqlQueriable](./lib/core.ts)): Queries the database and - returns an array of arrays -- `queryOneArray` (See [SqlQueriable](./lib/core.ts)): Queries the database and +- `queryArray` (See [SqlQueriable](./core.ts)): Queries the database and returns + an array of arrays +- `queryOneArray` (See [SqlQueriable](./core.ts)): Queries the database and returns at most one entry as an array -- `queryManyArray` (See [SqlQueriable](./lib/core.ts)): Queries the database - with an async generator and yields each entry as an array -- `sql` (See [SqlQueriable](./lib/core.ts)): Allows you to create a query using +- `queryManyArray` (See [SqlQueriable](./core.ts)): Queries the database with an + async generator and yields each entry as an array +- `sql` (See [SqlQueriable](./core.ts)): Allows you to create a query using template literals, and returns the entries as an array of objects. This is a wrapper around `query` -- `sqlArray` (See [SqlQueriable](./lib/core.ts)): Allows you to create a query - using template literals, and returns the entries as an array of arrays. This - is a wrapper around `queryArray` -- `prepare` (See [SqlPreparable](./lib/core.ts)): Returns a prepared statement - class that contains a subset of the Queriable functions (see - [SqlPreparedQueriable](./lib/core.ts)) -- `beginTransaction` (See [SqlTransactionable](./lib/core.ts)): Returns a +- `sqlArray` (See [SqlQueriable](./core.ts)): Allows you to create a query using + template literals, and returns the entries as an array of arrays. This is a + wrapper around `queryArray` +- `prepare` (See [SqlPreparable](./core.ts)): Returns a prepared statement class + that contains a subset of the Queriable functions (see + [SqlPreparedQueriable](./core.ts)) +- `beginTransaction` (See [SqlTransactionable](./core.ts)): Returns a transaction class that contains implements the queriable functions, as well as - transaction related functions (see [SqlTransactionQueriable](./lib/core.ts)) -- `transaction` (See [SqlTransactionable](./lib/core.ts)): A wrapper function - for transactions, handles the logic of beginning, committing and rollback a + transaction related functions (see [SqlTransactionQueriable](./core.ts)) +- `transaction` (See [SqlTransactionable](./core.ts)): A wrapper function for + transactions, handles the logic of beginning, committing and rollback a transaction. +#### Events + +The following events can be subscribed to according to the specs (see +[events.ts](./events.ts)): + +- `connect`: Gets dispatched when a connection is established +- `close`: Gets dispatched when a connection is about to be closed +- `error`: Gets dispatched when an error is triggered + ### ClientPool Full compliance with `@stdext/std` provides a database client pool (a pool of -clients) with the following methods (see [SqlClientPool](./lib/pool.ts)): +clients) with the following methods (see [SqlClientPool](./pool.ts)): -- `connect` (See [SqlConnection](./lib/core.ts)): Creates the connection classes - and adds them to a connection pool, and optionally connects them to the - database -- `close` (See [SqlConnection](./lib/core.ts)): Closes all connections in the - pool -- `acquire` (See [SqlPoolable](./lib/core.ts)): Retrieves a - [SqlPoolClient](./lib/pool.ts) (a subset of [Client](#client)), and connects - it if not already connected -- `release` (See [SqlPoolable](./lib/core.ts)): Releases a connection back to - the pool +- `connect` (See [SqlConnection](./core.ts)): Creates the connection classes and + adds them to a connection pool, and optionally connects them to the database +- `close` (See [SqlConnection](./core.ts)): Closes all connections in the pool +- `acquire` (See [SqlPoolable](./core.ts)): Retrieves a + [SqlPoolClient](./pool.ts) (a subset of [Client](#client)), and connects it if + not already connected + +#### Events + +The following events can be subscribed to according to the specs (see +[events.ts](./events.ts)): + +- `connect`: Gets dispatched when a connection is established for a pool client + (with lazy initialization, it will only get triggered once a connection is + popped and activated) +- `close`: Gets dispatched when a connection is about to be closed +- `error`: Gets dispatched when an error is triggered +- `acquire`: Gets dispatched when a connection is acquired from the pool +- `release`: Gets dispatched when a connection is released back to the pool ### Examples @@ -163,62 +181,27 @@ console.log(res); To be fully compliant with `@stdext/std`, you will need to implement the following classes for your database driver: -- `Connection` ([SqlConnection](./lib/connection.ts)): This represents the +- `Connection` ([SqlConnection](./connection.ts)): This represents the connection to the database. This should preferably only contain the - functionality of containing a connection, and provide a minimum query method - to be used to query the database. The query method does not need to follow any - specific spec, but must be consumable by all query functions as well as - execute functions (see `SqlQueriable`) -- `Prepared` ([SqlPreparedQueriable](./lib/core.ts)): This represents a prepared - statement. All queriable methods must be implemented -- `Transaction` ([SqlTransactionQueriable](./lib/core.ts)): This represents a - transaction. All queriable methods must be implemented -- `Client` ([SqlClient](./lib/client.ts)): This represents a database client -- `ClientPool` ([SqlClientPool](./lib/pool.ts)): This represents a pool of - clients -- `PoolClient` ([SqlPoolClient](./lib/pool.ts)): This represents a client to be + functionality of containing a connection, and provide a minimum set of query + methods to be used to query the database +- `PreparedStatement` ([SqlPreparedStatement](./core.ts)): This represents a + prepared statement. All queriable methods must be implemented +- `Transaction` ([SqlTransaction](./core.ts)): This represents a transaction. + All queriable methods must be implemented +- `Client` ([SqlClient](./client.ts)): This represents a database client +- `ClientPool` ([SqlClientPool](./pool.ts)): This represents a pool of clients +- `PoolClient` ([SqlPoolClient](./pool.ts)): This represents a client to be provided by a pool -It is also however advisable to create the following sub classes to use in other -classes: - -- [SqlQueriable](./lib/core.ts): This represents a queriable base class that - contains the standard `@stdext/std` query methods. In most cases, this serves - as a base class for all other queriable classes -- [SqlEventTarget](./lib/events.ts): A typed event target class -- [SqlError](./lib/errors.ts): A typed error class +It is also however advisable to create additional helper classes for easier +inheritance. See [test.ts](./test.ts) for a minimum but functional example of +how to implement these interfaces. ### Inheritance graph -In most cases, these are the classes and the inheritance graph that should be -implemented. Notice how almost everything extends `SqlBase` at the base. - -- SqlConnection -- SqlPreparedQueriable -- SqlTransactionQueriable - - SqlPreparable - - SqlQueriable -- SqlClient - - SqlTransactionable - - SqlPreparable - - SqlQueriable -- SqlPoolClient - - SqlTransactionable - - SqlPreparable - - SqlQueriable -- SqlClientPool - -### Other - -There is also a [SqlDeferredStack](./lib//deferred.ts) that is used by the pool -as a helper for queuing and stacking the pool clients. - -### Base class - -`@stdext/std` uses implementable interfaces as well as base classes that needs -to be inherited for desired functionality. - -At the base layer, you will have to extend the `SqlBase` class for all -`@stdext/std` derived classes. Alternatively, if this is not possible, you can -implement SqlBase, and ensure that the dynamic and static properties are -accessible. +Here is an overview of the inheritance and flow of the different interfaces. In +most cases, these are the classes and the inheritance graph that should be +implemented. + +![inheritance flow](./_assets/inheritance_flowchart.jpg) diff --git a/sql/_assets/inheritance_flowchart.jpg b/sql/_assets/inheritance_flowchart.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5f7ef7fe90c9ce60891af58a46e621888885ff23 GIT binary patch literal 230770 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

*7D2?x_P5X-K@- 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 diff --git a/sql/asserts.ts b/sql/asserts.ts index e862422..33bf6c2 100644 --- a/sql/asserts.ts +++ b/sql/asserts.ts @@ -2,8 +2,7 @@ import { assertExists, assertInstanceOf, AssertionError } from "@std/assert"; import { SqlClient, SqlClientPool, - SqlClientQueriable, - SqlConnectableBase, + SqlConnectable, SqlConnection, SqlError, SqlEventable, @@ -11,8 +10,8 @@ import { SqlPreparedStatement, SqlQueriable, SqlTransaction, + SqlTransactionable, } from "./mod.ts"; -import { DeferredStack } from "../collections/deferred_stack.ts"; function hasProperty(obj: T, property: string | symbol | number): boolean { let currentProto = obj; @@ -91,7 +90,6 @@ export function assertIsSqlConnection( assertHasProperties( value, [ - "connectionUrl", "options", "connected", "connect", @@ -103,9 +101,10 @@ export function assertIsSqlConnection( ); } -export function assertIsSqlConnectableBase( +export function assertIsSqlConnectable( value: unknown, -): asserts value is SqlConnectableBase { +): asserts value is SqlConnectable { + assertIsAsyncDisposable(value); assertHasProperties( value, [ @@ -119,7 +118,7 @@ export function assertIsSqlConnectableBase( export function assertIsSqlPreparedStatement( value: unknown, ): asserts value is SqlPreparedStatement { - assertIsSqlConnectableBase(value); + assertIsSqlConnectable(value); assertHasProperties( value, [ @@ -139,7 +138,7 @@ export function assertIsSqlPreparedStatement( export function assertIsSqlQueriable( value: unknown, ): asserts value is SqlQueriable { - assertIsSqlConnectableBase(value); + assertIsSqlConnectable(value); assertHasProperties( value, [ @@ -154,11 +153,22 @@ export function assertIsSqlQueriable( ], ); } +export function assertIsSqlPreparable( + value: unknown, +): asserts value is SqlQueriable { + assertIsSqlQueriable(value); + assertHasProperties( + value, + [ + "prepare", + ], + ); +} export function assertIsSqlTransaction( value: unknown, ): asserts value is SqlTransaction { - assertIsSqlQueriable(value); + assertIsSqlPreparable(value); assertHasProperties( value, [ @@ -171,14 +181,13 @@ export function assertIsSqlTransaction( ); } -export function assertIsSqlClientQueriable( +export function assertIsSqlTransactionable( value: unknown, -): asserts value is SqlClientQueriable { - assertIsSqlQueriable(value); +): asserts value is SqlTransactionable { + assertIsSqlPreparable(value); assertHasProperties( value, [ - "prepare", "beginTransaction", "transaction", ], @@ -195,7 +204,7 @@ export function assertIsSqlEventable( export function assertIsSqlClient(value: unknown): asserts value is SqlClient { assertIsSqlConnection(value); assertIsSqlQueriable(value); - assertIsSqlClientQueriable(value); + assertIsSqlTransactionable(value); assertIsSqlEventable(value); assertHasProperties(value, ["options"]); } @@ -203,8 +212,8 @@ export function assertIsSqlClient(value: unknown): asserts value is SqlClient { export function assertIsSqlPoolClient( value: unknown, ): asserts value is SqlPoolClient { - assertIsSqlConnectableBase(value); - assertIsSqlClientQueriable(value); + assertIsSqlConnectable(value); + assertIsSqlTransactionable(value); assertHasProperties(value, [ "options", "disposed", @@ -226,5 +235,4 @@ export function assertIsSqlClientPool( "deferredStack", "acquire", ]); - assertInstanceOf(value.deferredStack, DeferredStack); } diff --git a/sql/client.ts b/sql/client.ts index d215afc..4706dd8 100644 --- a/sql/client.ts +++ b/sql/client.ts @@ -1,10 +1,9 @@ import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; import type { - SqlClientQueriable, SqlPreparedStatement, - SqlQueriable, SqlQueryOptions, SqlTransaction, + SqlTransactionable, SqlTransactionOptions, } from "./core.ts"; import type { SqlEventable, SqlEventTarget } from "./events.ts"; @@ -25,48 +24,47 @@ export interface SqlClient< ParameterType, QueryOptions > = SqlConnection, - Prepared extends SqlPreparedStatement< + PreparedStatement extends SqlPreparedStatement< ConnectionOptions, - Connection, ParameterType, - QueryOptions + QueryOptions, + Connection > = SqlPreparedStatement< ConnectionOptions, - Connection, ParameterType, - QueryOptions + QueryOptions, + Connection >, TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, Transaction extends SqlTransaction< ConnectionOptions, - Connection, ParameterType, QueryOptions, + Connection, + PreparedStatement, TransactionOptions > = SqlTransaction< ConnectionOptions, - Connection, ParameterType, QueryOptions, + Connection, + PreparedStatement, TransactionOptions >, > extends - SqlConnection, - SqlQueriable< - ConnectionOptions, - Connection, - ParameterType, - QueryOptions + Pick< + SqlConnection, + "close" | "connect" | "connected" >, - SqlClientQueriable< + SqlTransactionable< ConnectionOptions, - Connection, ParameterType, QueryOptions, - Prepared, + Connection, + PreparedStatement, TransactionOptions, Transaction >, - SqlEventable { - options: ConnectionOptions & QueryOptions & TransactionOptions; + SqlEventable, + AsyncDisposable { } diff --git a/sql/connection.ts b/sql/connection.ts index e78cfde..068e94c 100644 --- a/sql/connection.ts +++ b/sql/connection.ts @@ -104,3 +104,28 @@ export interface SqlConnection< options?: QueryOptions, ): AsyncGenerator; } + +/** + * SqlConnectable + * + * The base interface for everything that interracts with the connection like querying. + */ +export interface SqlConnectable< + Options extends SqlConnectionOptions = SqlConnectionOptions, + Connection extends SqlConnection = SqlConnection, +> extends AsyncDisposable { + /** + * The (global) options. + */ + readonly options: Options; + + /** + * The connection to the database + */ + readonly connection: Connection; + + /** + * Whether the connection is connected or not + */ + get connected(): boolean; +} diff --git a/sql/core.ts b/sql/core.ts index 25091fa..5b9c8b0 100644 --- a/sql/core.ts +++ b/sql/core.ts @@ -1,5 +1,9 @@ // deno-lint-ignore-file no-explicit-any -import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; +import type { + SqlConnectable, + SqlConnection, + SqlConnectionOptions, +} from "./connection.ts"; /** * Row @@ -39,25 +43,6 @@ export interface SqlQueryOptions extends SqlConnectionOptions { signal?: AbortSignal; } -/** - * SqlConnectableBase - * - * The base interface for everything that interracts with the connection like querying. - */ -export interface SqlConnectableBase< - Connection extends SqlConnection = SqlConnection, -> { - /** - * The connection to the database - */ - connection: Connection; - - /** - * Whether the connection is connected or not - */ - get connected(): boolean; -} - /** * SqlPreparedQueriable * @@ -65,20 +50,24 @@ export interface SqlConnectableBase< */ export interface SqlPreparedStatement< ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - Connection extends SqlConnection = SqlConnection< - ConnectionOptions - >, ParameterType extends unknown = unknown, QueryOptions extends SqlQueryOptions = SqlQueryOptions, -> extends SqlConnectableBase { + Connection extends SqlConnection< + ConnectionOptions, + ParameterType, + QueryOptions + > = SqlConnection< + ConnectionOptions, + ParameterType, + QueryOptions + >, +> extends SqlConnectable { + readonly options: ConnectionOptions & QueryOptions; + /** * The SQL statement */ readonly sql: string; - /** - * The (global) options to pass to the query method. - */ - readonly options: QueryOptions; /** * Executes the prepared statement @@ -170,16 +159,19 @@ export interface SqlPreparedStatement< */ export interface SqlQueriable< ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - Connection extends SqlConnection = SqlConnection< - ConnectionOptions - >, ParameterType extends unknown = unknown, QueryOptions extends SqlQueryOptions = SqlQueryOptions, -> extends SqlConnectableBase { - /** - * The (global) options to pass to the query method. - */ - readonly options: QueryOptions; + Connection extends SqlConnection< + ConnectionOptions, + ParameterType, + QueryOptions + > = SqlConnection< + ConnectionOptions, + ParameterType, + QueryOptions + >, +> extends SqlConnectable { + readonly options: ConnectionOptions & QueryOptions; /** * Execute a SQL statement @@ -298,25 +290,99 @@ export interface SqlQueriable< } /** - * SqlTransactionQueriable + * SqlPreparable + * + * Represents an object that can create a prepared statement. + */ +export interface SqlPreparable< + ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, + ParameterType extends unknown = unknown, + QueryOptions extends SqlQueryOptions = SqlQueryOptions, + Connection extends SqlConnection< + ConnectionOptions, + ParameterType, + QueryOptions + > = SqlConnection< + ConnectionOptions, + ParameterType, + QueryOptions + >, + PreparedStatement extends SqlPreparedStatement< + ConnectionOptions, + ParameterType, + QueryOptions, + Connection + > = SqlPreparedStatement< + ConnectionOptions, + ParameterType, + QueryOptions, + Connection + >, +> extends + SqlQueriable { + /** + * Create a prepared statement that can be executed multiple times. + * This is useful when you want to execute the same SQL statement multiple times with different parameters. + * + * @param sql the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns a prepared statement + * + * @example + * ```ts + * const stmt = db.prepare("SELECT * FROM table WHERE id = ?"); + * + * for (let i = 0; i < 10; i++) { + * const row of stmt.query([i]) + * console.log(row); + * } + * ``` + */ + prepare( + sql: string, + options?: QueryOptions, + ): PreparedStatement; +} + +/** + * SqlTransaction * * Represents a transaction. */ export interface SqlTransaction< ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - Connection extends SqlConnection = SqlConnection< - ConnectionOptions - >, ParameterType extends unknown = unknown, QueryOptions extends SqlQueryOptions = SqlQueryOptions, + Connection extends SqlConnection< + ConnectionOptions, + ParameterType, + QueryOptions + > = SqlConnection< + ConnectionOptions, + ParameterType, + QueryOptions + >, + PreparedStatement extends SqlPreparedStatement< + ConnectionOptions, + ParameterType, + QueryOptions, + Connection + > = SqlPreparedStatement< + ConnectionOptions, + ParameterType, + QueryOptions, + Connection + >, TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, > extends - SqlQueriable< + SqlPreparable< ConnectionOptions, - Connection, ParameterType, - QueryOptions + QueryOptions, + Connection, + PreparedStatement > { + readonly options: ConnectionOptions & QueryOptions; /** * Whether the connection is in an active transaction or not. */ @@ -349,7 +415,7 @@ export interface SqlTransaction< } /** - * SqlClientQueriable + * SqlTransactionable * * Represents an object that can create a transaction and a prepared statement. * @@ -357,63 +423,55 @@ export interface SqlTransaction< * A prepared statement should in most cases be unique to a connection, * and should not live after the related connection is closed. */ -export interface SqlClientQueriable< +export interface SqlTransactionable< ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - Connection extends SqlConnection = SqlConnection< - ConnectionOptions - >, ParameterType extends unknown = unknown, QueryOptions extends SqlQueryOptions = SqlQueryOptions, - Prepared extends SqlPreparedStatement< + Connection extends SqlConnection< ConnectionOptions, - Connection, ParameterType, QueryOptions - > = SqlPreparedStatement< + > = SqlConnection< ConnectionOptions, - Connection, ParameterType, QueryOptions >, + PreparedStatement extends SqlPreparedStatement< + ConnectionOptions, + ParameterType, + QueryOptions, + Connection + > = SqlPreparedStatement< + ConnectionOptions, + ParameterType, + QueryOptions, + Connection + >, TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, Transaction extends SqlTransaction< ConnectionOptions, - Connection, ParameterType, QueryOptions, + Connection, + PreparedStatement, TransactionOptions > = SqlTransaction< ConnectionOptions, - Connection, ParameterType, QueryOptions, + Connection, + PreparedStatement, TransactionOptions >, > extends - SqlQueriable { - /** - * Create a prepared statement that can be executed multiple times. - * This is useful when you want to execute the same SQL statement multiple times with different parameters. - * - * @param sql the SQL statement - * @param options the options to pass to the query method, will be merged with the global options - * @returns a prepared statement - * - * @example - * ```ts - * const stmt = db.prepare("SELECT * FROM table WHERE id = ?"); - * - * for (let i = 0; i < 10; i++) { - * const row of stmt.query([i]) - * console.log(row); - * } - * ``` - */ - prepare( - sql: string, - options?: QueryOptions, - ): Prepared; - + SqlPreparable< + ConnectionOptions, + ParameterType, + QueryOptions, + Connection, + PreparedStatement + > { + readonly options: ConnectionOptions & QueryOptions; /** * Starts a transaction */ diff --git a/sql/deno.json b/sql/deno.json index 7369bee..2266da1 100644 --- a/sql/deno.json +++ b/sql/deno.json @@ -1,5 +1,5 @@ { - "version": "0.0.5", + "version": "0.0.0-2", "name": "@stdext/sql", "lock": false, "exports": { diff --git a/sql/events.ts b/sql/events.ts index 8594ce3..e6aa4f5 100644 --- a/sql/events.ts +++ b/sql/events.ts @@ -2,7 +2,7 @@ * Events */ import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; -import type { SqlConnectableBase } from "./core.ts"; +import type { SqlConnectable } from "./connection.ts"; /** * Event types @@ -29,7 +29,7 @@ export type SqlPoolConnectionEventType = * SqlErrorEventInit */ export interface SqlErrorEventInit< - Connectable extends SqlConnectableBase = SqlConnectableBase, + Connectable extends SqlConnectable = SqlConnectable, > extends ErrorEventInit { connectable?: Connectable; } diff --git a/sql/pool.ts b/sql/pool.ts index 092a27c..8dbfb2e 100644 --- a/sql/pool.ts +++ b/sql/pool.ts @@ -1,16 +1,11 @@ import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; import type { - SqlClientQueriable, - SqlConnectableBase, SqlPreparedStatement, SqlQueryOptions, SqlTransaction, + SqlTransactionable, SqlTransactionOptions, } from "./core.ts"; -import type { - DeferredStack, - DeferredStackOptions, -} from "../collections/deferred_stack.ts"; import type { SqlEventable, SqlEventTarget } from "./events.ts"; /** @@ -30,8 +25,7 @@ export interface SqlPoolClientOptions { * * This represents the options for a connection pool. */ -export interface SqlClientPoolOptions - extends SqlConnectionOptions, DeferredStackOptions { +export interface SqlClientPoolOptions extends SqlConnectionOptions { /** * Whether to lazily initialize connections. * @@ -40,6 +34,10 @@ export interface SqlClientPoolOptions * acquiring a connection, and max pool size has not been reached. */ lazyInitialization?: boolean; + /** + * The maximum stack size to be allowed. + */ + maxSize?: number; } /** @@ -58,37 +56,38 @@ export interface SqlPoolClient< QueryOptions extends SqlQueryOptions = SqlQueryOptions, Prepared extends SqlPreparedStatement< ConnectionOptions, - Connection, ParameterType, - QueryOptions + QueryOptions, + Connection > = SqlPreparedStatement< ConnectionOptions, - Connection, ParameterType, - QueryOptions + QueryOptions, + Connection >, TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, Transaction extends SqlTransaction< ConnectionOptions, - Connection, ParameterType, QueryOptions, + Connection, + Prepared, TransactionOptions > = SqlTransaction< ConnectionOptions, - Connection, ParameterType, QueryOptions, + Connection, + Prepared, TransactionOptions >, PoolClientOptions extends SqlPoolClientOptions = SqlPoolClientOptions, > extends - SqlConnectableBase, - SqlClientQueriable< + SqlTransactionable< ConnectionOptions, - Connection, ParameterType, QueryOptions, + Connection, Prepared, TransactionOptions, Transaction @@ -99,7 +98,6 @@ export interface SqlPoolClient< readonly options: & ConnectionOptions & QueryOptions - & TransactionOptions & PoolClientOptions; /** * Whether the pool client is disposed and should not be available anymore @@ -133,27 +131,29 @@ export interface SqlClientPool< >, Prepared extends SqlPreparedStatement< ConnectionOptions, - Connection, ParameterType, - QueryOptions + QueryOptions, + Connection > = SqlPreparedStatement< ConnectionOptions, - Connection, ParameterType, - QueryOptions + QueryOptions, + Connection >, TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, Transaction extends SqlTransaction< ConnectionOptions, - Connection, ParameterType, QueryOptions, + Connection, + Prepared, TransactionOptions > = SqlTransaction< ConnectionOptions, - Connection, ParameterType, QueryOptions, + Connection, + Prepared, TransactionOptions >, PoolClient extends SqlPoolClient< @@ -182,12 +182,10 @@ export interface SqlClientPool< >, "execute" | "queryMany" | "queryManyArray" > { - readonly options: ConnectionOptions & QueryOptions & TransactionOptions; - - /** - * The deferred stack of connections - */ - deferredStack: DeferredStack; + readonly options: + & ConnectionOptions + & QueryOptions + & SqlClientPoolOptions; /** * Acquire a connection from the pool diff --git a/sql/test.ts b/sql/test.ts index c244e13..cd3486a 100644 --- a/sql/test.ts +++ b/sql/test.ts @@ -1,19 +1,12 @@ -// deno-lint-ignore-file no-unused-vars no-explicit-any import { DeferredStack } from "../collections/deferred_stack.ts"; import { SqlConnectionEventInit, SqlEvent, - SqlEventable, SqlEventTarget, SqlPoolConnectionEventType, } from "./events.ts"; +import { sqlIntegrationTestSuite, sqlUnitTestSuite } from "./testing.ts"; import * as Sql from "./mod.ts"; -import { - clientPoolTest, - clientTest, - connectionConstructorTest, - TestQueries, -} from "./testing.ts"; /** * Represents a database table for a test database @@ -135,25 +128,25 @@ class TestSqlConnection implements Sql.SqlConnection { execute( sql: string, params?: unknown[] | undefined, - options?: Sql.SqlQueryOptions | undefined, + _options?: Sql.SqlQueryOptions | undefined, ): Promise { const queryRes = testDbQueryParser(sql, params as string[]); return Promise.resolve(queryRes); } - async *queryMany = Sql.Row>( + async *queryMany( sql: string, params?: unknown[] | undefined, - options?: Sql.SqlQueryOptions | undefined, + _options?: Sql.SqlQueryOptions | undefined, ): AsyncGenerator { const queryRes = testDbQueryParser(sql, params as string[]); for (const row of queryRes) { yield row; } } - async *queryManyArray = Sql.ArrayRow>( + async *queryManyArray( sql: string, params?: unknown[] | undefined, - options?: Sql.SqlQueryOptions | undefined, + _options?: Sql.SqlQueryOptions | undefined, ): AsyncGenerator { const queryRes = testDbQueryParser(sql, params as string[]); for (const row of queryRes) { @@ -166,36 +159,45 @@ class TestSqlConnection implements Sql.SqlConnection { } } -class TestSqlConnectableBase - implements Sql.SqlConnectableBase { +class TestSqlConnectable + implements Sql.SqlConnectable { + options: TestSqlConnectionOptions; connection: TestSqlConnection; get connected(): boolean { return this.connection.connected; } - constructor(connection: TestSqlConnection) { + constructor( + connection: TestSqlConnectable["connection"], + options: TestSqlConnectable["options"] = {}, + ) { this.connection = connection; + this.options = options; + } + [Symbol.asyncDispose](): Promise { + return this.connection.close(); } } -class TestSqlPreparedStatement extends TestSqlConnectableBase +class TestSqlPreparedStatement extends TestSqlConnectable implements Sql.SqlPreparedStatement< TestSqlConnectionOptions, - TestSqlConnection, TestParameterType, - TestSqlQueryOptions + TestSqlQueryOptions, + TestSqlConnection > { + declare readonly options: + & TestSqlConnectionOptions + & TestSqlQueryOptions; sql: string; - options: TestSqlConnectionOptions & TestSqlQueryOptions; constructor( - connection: TestSqlConnection, + connection: TestSqlPreparedStatement["connection"], sql: string, - options: TestSqlConnectionOptions & TestSqlQueryOptions = {}, + options: TestSqlPreparedStatement["options"] = {}, ) { - super(connection); + super(connection, options); this.sql = sql; - this.options = options; } execute( params?: TestParameterType[] | undefined, @@ -245,26 +247,26 @@ class TestSqlPreparedStatement extends TestSqlConnectableBase } } -class TestSqlQueriable extends TestSqlConnectableBase - implements - Sql.SqlQueriable< - TestSqlConnectionOptions, - TestSqlConnection, - TestParameterType, - TestSqlQueryOptions - > { - options: TestSqlConnectionOptions & TestSqlQueryOptions; +class TestSqlQueriable extends TestSqlConnectable implements + Sql.SqlQueriable< + TestSqlConnectionOptions, + TestParameterType, + TestSqlQueryOptions, + TestSqlConnection + > { + declare readonly options: + & TestSqlConnectionOptions + & TestSqlQueryOptions; constructor( - connection: TestSqlConnection, - options: TestSqlConnectionOptions & TestSqlQueryOptions = {}, + connection: TestSqlQueriable["connection"], + options: TestSqlQueriable["options"] = {}, ) { - super(connection); - this.options = options; + super(connection, options); } execute( sql: string, params?: TestParameterType[] | undefined, - options?: TestSqlQueryOptions | undefined, + _options?: TestSqlQueryOptions | undefined, ): Promise { return Promise.resolve(testDbQueryParser(sql, params)); } @@ -314,96 +316,106 @@ class TestSqlQueriable extends TestSqlConnectableBase ): AsyncGenerator { return this.connection.queryManyArray(sql, params, options); } - sql = Sql.Row>( + sql( strings: TemplateStringsArray, ...parameters: string[] ): Promise { - return this.query(strings.join("?"), parameters); + return this.query(strings.join("?"), parameters); } - sqlArray = Sql.ArrayRow>( + sqlArray( strings: TemplateStringsArray, ...parameters: string[] ): Promise { - return this.queryArray(strings.join("?"), parameters); + return this.queryArray(strings.join("?"), parameters); } } -class TestSqlTransaction extends TestSqlQueriable implements - Sql.SqlTransaction< +class TestSqlPreparable extends TestSqlQueriable implements + Sql.SqlPreparable< TestSqlConnectionOptions, - TestSqlConnection, TestParameterType, TestSqlQueryOptions, - TestSqlTransactionOptions + TestSqlConnection, + TestSqlPreparedStatement > { - _inTransaction: boolean = false; - get inTransaction(): boolean { - return this._inTransaction; + constructor( + connection: TestSqlPreparable["connection"], + options: TestSqlPreparable["options"] = {}, + ) { + super(connection, options); } - options: + prepare( + sql: string, + options?: TestSqlQueryOptions | undefined, + ): TestSqlPreparedStatement { + return new TestSqlPreparedStatement(this.connection, sql, options); + } +} + +class TestSqlTransaction extends TestSqlPreparable + implements + Sql.SqlTransaction< + TestSqlConnectionOptions, + TestParameterType, + TestSqlQueryOptions, + TestSqlConnection, + TestSqlPreparedStatement, + TestSqlTransactionOptions + > { + declare readonly options: & TestSqlConnectionOptions & TestSqlQueryOptions & TestSqlTransactionOptions; + _inTransaction: boolean = false; + get inTransaction(): boolean { + return this._inTransaction; + } + constructor( - connection: TestSqlConnection, - options: - & TestSqlConnectionOptions - & TestSqlQueryOptions - & TestSqlTransactionOptions = {}, + connection: TestSqlTransaction["connection"], + options: TestSqlTransaction["options"] = {}, ) { super(connection, options); - this.options = options; } commitTransaction( - options?: Record | undefined, + _options?: Record | undefined, ): Promise { return Promise.resolve(); } rollbackTransaction( - options?: Record | undefined, + _options?: Record | undefined, ): Promise { return Promise.resolve(); } - createSavepoint(name?: string | undefined): Promise { + createSavepoint(_name?: string | undefined): Promise { return Promise.resolve(); } - releaseSavepoint(name?: string | undefined): Promise { + releaseSavepoint(_name?: string | undefined): Promise { return Promise.resolve(); } } -class TestSqlClientQueriable extends TestSqlQueriable +class TestSqlTransactionable extends TestSqlPreparable implements - Sql.SqlClientQueriable< + Sql.SqlPreparable< TestSqlConnectionOptions, - TestSqlConnection, TestParameterType, TestSqlQueryOptions, - TestSqlPreparedStatement, - TestSqlTransactionOptions + TestSqlConnection, + TestSqlPreparedStatement > { - options: + declare readonly options: & TestSqlConnectionOptions & TestSqlQueryOptions & TestSqlTransactionOptions; constructor( - connection: TestSqlConnection, - options: - & TestSqlConnectionOptions - & TestSqlQueryOptions - & TestSqlTransactionOptions = {}, + connection: TestSqlTransactionable["connection"], + options: TestSqlTransactionable["options"] = {}, ) { super(connection, options); - this.options = options; - } - prepare( - sql: string, - options?: TestSqlQueryOptions | undefined, - ): TestSqlPreparedStatement { - return new TestSqlPreparedStatement(this.connection, sql, options); } beginTransaction( - options?: Record | undefined, + _options?: Record | undefined, ): Promise { return Promise.resolve( new TestSqlTransaction(this.connection, this.options), @@ -432,17 +444,7 @@ class TestSqlEventTarget extends SqlEventTarget< > { } -class TestSqlEventable implements - SqlEventable< - TestSqlEventTarget - > { - eventTarget: TestSqlEventTarget; - constructor(eventTarget: TestSqlEventTarget) { - this.eventTarget = eventTarget; - } -} - -class TestSqlClient extends TestSqlClientQueriable implements +class TestSqlClient extends TestSqlTransactionable implements Sql.SqlClient< TestSqlEventTarget, TestSqlConnectionOptions, @@ -453,18 +455,16 @@ class TestSqlClient extends TestSqlClientQueriable implements TestSqlTransactionOptions, TestSqlTransaction > { - connectionUrl: string; - options: TestSqlConnectionOptions; + declare readonly options: + & TestSqlConnectionOptions + & TestSqlQueryOptions + & TestSqlTransactionOptions; eventTarget: TestSqlEventTarget; constructor( connectionUrl: string, - connectionOptions?: TestSqlConnectionOptions, + options: TestSqlClient["options"] = {}, ) { - super(new TestSqlConnection(connectionUrl, connectionOptions), { - test: "test", - }); - this.connectionUrl = connectionUrl; - this.options = connectionOptions ?? { test: "test" }; + super(new TestSqlConnection(connectionUrl, options), options); this.eventTarget = new TestSqlEventTarget(); } async connect(): Promise { @@ -479,18 +479,12 @@ class TestSqlClient extends TestSqlClientQueriable implements ); await this.connection.close(); } - [Symbol.asyncDispose](): Promise { - return this.close(); - } } -type TestSqlPoolClientOptions = - & TestSqlConnectionOptions - & TestSqlQueryOptions - & TestSqlTransactionOptions - & Sql.SqlPoolClientOptions; +interface TestSqlPoolClientOptions extends Sql.SqlPoolClientOptions { +} -class TestSqlPoolClient extends TestSqlClientQueriable +class TestSqlPoolClient extends TestSqlTransactionable implements Sql.SqlPoolClient< TestSqlConnectionOptions, @@ -499,10 +493,14 @@ class TestSqlPoolClient extends TestSqlClientQueriable TestSqlQueryOptions, TestSqlPreparedStatement, TestSqlTransactionOptions, - TestSqlTransaction + TestSqlTransaction, + TestSqlPoolClientOptions > { - readonly options: TestSqlPoolClientOptions; - + declare readonly options: + & TestSqlConnectionOptions + & TestSqlQueryOptions + & TestSqlTransactionOptions + & TestSqlPoolClientOptions; #releaseFn?: () => Promise; #disposed: boolean = false; @@ -511,12 +509,11 @@ class TestSqlPoolClient extends TestSqlClientQueriable } constructor( - connection: TestSqlConnection, - options: TestSqlPoolClientOptions, + connection: TestSqlPoolClient["connection"], + options: TestSqlPoolClient["options"] = {}, ) { super(connection, options); - this.options = options; - if (this.options.releaseFn) { + if (this.options?.releaseFn) { this.#releaseFn = this.options.releaseFn; } } @@ -542,20 +539,24 @@ class TestSqlClientPool implements TestSqlPoolClient, TestSqlEventTarget > { + declare readonly options: + & TestSqlConnectionOptions + & TestSqlQueryOptions + & TestSqlTransactionOptions + & TestSqlClientPoolOptions; deferredStack: DeferredStack; eventTarget: TestSqlEventTarget; connectionUrl: string; - options: TestSqlClientPoolOptions; _connected: boolean = false; get connected(): boolean { return this._connected; } constructor( connectionUrl: string, - options?: TestSqlClientPoolOptions, + options: TestSqlClientPoolOptions = {}, ) { this.connectionUrl = connectionUrl; - this.options = options ?? { test: "test" }; + this.options = options; this.deferredStack = new DeferredStack({ maxSize: 3, removeFn: async (element) => { @@ -608,46 +609,43 @@ class TestSqlClientPool implements } } -const queries: TestQueries = { - createTable: "CREATE TABLE sqlxtesttable", - dropTable: "DROP TABLE sqlxtesttable", - insertOneToTable: 'INSERT INTO sqlxtesttable VALUES [{"testcol":"?"}]', - insertManyToTable: - 'INSERT INTO sqlxtesttable VALUES [{"testcol":"?"},{"testcol":"?"},{"testcol":"?"}]', - selectOneFromTable: "SELECT * FROM sqlxtesttable WHERE testcol = ? LIMIT 1", - selectByMatchFromTable: "SELECT * FROM sqlxtesttable WHERE testcol = ?", - selectManyFromTable: "SELECT * FROM sqlxtesttable", - select1AsString: "RETURN '1'", - select1Plus1AsNumber: "RETURN 2", - deleteByMatchFromTable: "DELETE FROM sqlxtesttable WHERE testcol = ?", - deleteAllFromTable: "DELETE FROM sqlxtesttable", -}; +const connectionUrl = "test"; +const options: TestSqlTransaction["options"] = { test: "test" }; +const sql = "test"; -Deno.test("sql connection test", async (t) => { - await connectionConstructorTest({ - t, - Connection: TestSqlConnection, - connectionOptions: { test: "test" }, - connectionUrl: "test", - }); -}); +const connection = new TestSqlConnection(connectionUrl, options); +const preparedStatement = new TestSqlPreparedStatement( + connection, + sql, + options, +); +const transaction = new TestSqlTransaction(connection, options); +const eventTarget = new TestSqlEventTarget(); +const client = new TestSqlClient(connectionUrl, options); +const poolClient = new TestSqlPoolClient(connection, options); +const clientPool = new TestSqlClientPool(connectionUrl, options); -Deno.test("sql client test", async (t) => { - await clientTest({ - t, - Client: TestSqlClient, - connectionUrl: "test", - connectionOptions: { test: "test" }, - queries, - }); +sqlUnitTestSuite({ + testPrefix: "sql", + connectionClass: connection, + preparedStatementClass: preparedStatement, + transactionClass: transaction, + eventTargetClass: eventTarget, + clientClass: client, + poolClientClass: poolClient, + clientPoolClass: clientPool, + checks: { + connectionUrl, + options, + clientPoolOptions: options, + sql, + }, }); -Deno.test("sql client pool test", async (t) => { - await clientPoolTest({ - t, - Client: TestSqlClientPool, - connectionUrl: "test", - connectionOptions: { test: "test", test2: "test2" }, - queries, - }); +sqlIntegrationTestSuite({ + testPrefix: "sql", + Client: TestSqlClient, + ClientPool: TestSqlClientPool, + clientArguments: [connectionUrl, options], + clientPoolArguments: [connectionUrl, options], }); diff --git a/sql/testing.ts b/sql/testing.ts index 33b5b5b..44bbca8 100644 --- a/sql/testing.ts +++ b/sql/testing.ts @@ -1,188 +1,181 @@ -// deno-lint-ignore-file no-explicit-any -import { assert, assertEquals, assertFalse } from "@std/assert"; import { - type ArrayRow, - type Row, - type SqlClientQueriable, - type SqlPreparedStatement, - type SqlQueriable, - type SqlTransaction, -} from "./core.ts"; -import type { SqlClient } from "./client.ts"; -import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; -import type { - SqlClientPool, - SqlClientPoolOptions, - SqlPoolClient, -} from "./pool.ts"; -import type { SqlQueryOptions } from "./core.ts"; + assert, + assertEquals, + assertFalse, + assertInstanceOf, +} from "@std/assert"; import { assertIsSqlClient, assertIsSqlClientPool, - assertIsSqlClientQueriable, + assertIsSqlConnectable, assertIsSqlConnection, + assertIsSqlEventable, + assertIsSqlPoolClient, + assertIsSqlPreparable, assertIsSqlPreparedStatement, assertIsSqlQueriable, assertIsSqlTransaction, -} from "./asserts.ts"; + assertIsSqlTransactionable, + SqlClient, + SqlClientPool, + SqlConnectable, + SqlPoolClient, + SqlPreparedStatement, + SqlQueriable, + SqlTransaction, + SqlTransactionable, +} from "./mod.ts"; + +// deno-lint-ignore no-explicit-any +export type AnyConstructor = new (...args: A) => T; +export type ClientConstructorArguments = [ + string, + Client["options"], +]; +export type ClientPoolConstructorArguments< + Client extends SqlClientPool = SqlClientPool, +> = [string, Client["options"]]; +export type ClientConstructor = + AnyConstructor>; +export type ClientPoolConstructor< + Client extends SqlClientPool = SqlClientPool, +> = AnyConstructor>; + +export function testSqlConnection( + value: unknown, + checks: { + connectionUrl: string; + }, +) { + assertIsSqlConnection(value); + assertEquals(value.connectionUrl, checks.connectionUrl); +} -interface ConnectionConstructor { - new ( - ...args: any[] - ): SqlConnection; +export function _testSqlConnectable( + value: unknown, + checks: { + connectionUrl: string; + options: SqlConnectable["options"]; + }, +) { + assertIsSqlConnectable(value); + assertEquals(value.options, checks.options); + testSqlConnection(value.connection, checks); } -interface ClientConstructor { - new ( - ...args: any[] - ): SqlClient; +export function testSqlPreparedStatement( + value: unknown, + checks: { + connectionUrl: string; + options: SqlPreparedStatement["options"]; + sql: string; + }, +) { + assertIsSqlPreparedStatement(value); + _testSqlConnectable(value, checks); + assertEquals(value.sql, checks.sql); } -interface ClientPoolConstructor { - new ( - ...args: any[] - ): SqlClientPool; +export function _testSqlQueriable( + value: unknown, + checks: { + connectionUrl: string; + options: SqlQueriable["options"]; + }, +) { + assertIsSqlQueriable(value); + _testSqlConnectable(value, checks); +} +export function _testSqlPreparable( + value: unknown, + checks: { + connectionUrl: string; + options: SqlQueriable["options"]; + }, +) { + assertIsSqlPreparable(value); + _testSqlQueriable(value, checks); +} +export function testSqlTransaction( + value: unknown, + checks: { + connectionUrl: string; + options: SqlTransaction["options"]; + }, +) { + assertIsSqlTransaction(value); + _testSqlPreparable(value, checks); +} +export function _testSqlTransactionable( + value: unknown, + checks: { + connectionUrl: string; + options: SqlTransactionable["options"]; + }, +) { + assertIsSqlTransactionable(value); + _testSqlPreparable(value, checks); +} +export function testSqlEventTarget( + value: unknown, +) { + assertInstanceOf(value, EventTarget); } -export type TestQueries = { - /** - * Should create a table "sqlxtesttable" if not exist with a single column "testcol" of string type (min 10 characters) - */ - createTable: string; - /** - * Should drop the table "sqlxtesttable" if exists - */ - dropTable: string; - /** - * Should insert a single row into the table - */ - insertOneToTable: string; - /** - * Should insert multiple rows into the table - */ - insertManyToTable: string; - /** - * Should select a single row from the table - */ - selectOneFromTable: string; - /** - * Should select a single row by matching the testcol value - */ - selectByMatchFromTable: string; - /** - * Should select multiple rows from the table - */ - selectManyFromTable: string; - /** - * Should return "1" as "result" - */ - select1AsString: string; - /** - * Should return 1+1 as "result" - */ - select1Plus1AsNumber: string; - /** - * Should delete a single row by matching the testcol value - */ - deleteByMatchFromTable: string; - /** - * Should delete all rows from the table - */ - deleteAllFromTable: string; -}; - -export type BaseQueriableTestOptions = { - t: Deno.TestContext; - queries: TestQueries; -}; - -export type TestConnectAndClosePoolClient = PoolTestOptions; - -export type TestPreparedStatementOptions = - & Omit - & { - db: SqlPreparedStatement; - cases: { - execute: { - params?: any[]; - expected: number | undefined; - }; - query: { - params?: any[]; - expected: Row[]; - }; - queryOne: { - params?: any[]; - expected: Row | undefined; - }; - queryMany: { - params?: any[]; - expected: Row[]; - }; - queryArray: { - params?: any[]; - expected: ArrayRow[]; - }; - queryOneArray: { - params?: any[]; - expected: ArrayRow; - }; - queryManyArray: { - params?: any[]; - expected: ArrayRow[]; - }; - }; - }; +export function _testSqlEventable( + value: unknown, +) { + assertIsSqlEventable(value); + testSqlEventTarget(value.eventTarget); +} + +export function testSqlClient( + value: unknown, + checks: { + connectionUrl: string; + options: SqlClient["options"]; + }, +) { + assertIsSqlClient(value); + _testSqlTransactionable(value, checks); + _testSqlEventable(value); +} + +export function testSqlPoolClient( + value: unknown, + checks: { + connectionUrl: string; + options: SqlPoolClient["options"]; + }, +) { + assertIsSqlPoolClient(value); + _testSqlTransactionable(value, checks); +} -export type TestQueriableOptions = BaseQueriableTestOptions & { - db: SqlQueriable; -}; - -export type TestClientQueriableOptions = BaseQueriableTestOptions & { - db: SqlClientQueriable; -}; -export type TestTransactionOptions = BaseQueriableTestOptions & { - db: SqlTransaction; -}; - -export type ConnectionTestOptions = { - t: Deno.TestContext; - connection: SqlConnection; -}; - -export type ConnectionConstructorTestOptions = { - t: Deno.TestContext; - Connection: ConnectionConstructor; - connectionUrl: string | URL; - connectionOptions: SqlConnectionOptions & Record; -}; - -export type ClientTestOptions = BaseQueriableTestOptions & { - Client: ClientConstructor; - connectionUrl: string | URL; - connectionOptions: SqlConnectionOptions & SqlQueryOptions & Record; -}; - -export type PoolTestOptions = BaseQueriableTestOptions & { - Client: ClientPoolConstructor; - connectionUrl: string | URL; - connectionOptions: - & SqlConnectionOptions - & SqlQueryOptions - & SqlClientPoolOptions - & Record; -}; - -async function _testConnectAndCloseClient( - { t, Client, connectionUrl, connectionOptions }: ClientTestOptions, +export function testSqlClientPool( + value: unknown, + checks: { + connectionUrl: string; + options: SqlClientPool["options"]; + }, +) { + assertIsSqlClientPool(value); + _testSqlEventable(value); + assertEquals(value.connectionUrl, checks.connectionUrl); +} + +async function _testClientConnection( + t: Deno.TestContext, + Client: ClientConstructor, + clientArguments: ClientConstructorArguments, ): Promise { await t.step("testConnectAndClose", async (t) => { await t.step("should connect and close with using", async () => { - await using db = new Client(connectionUrl, connectionOptions); + await using db = new Client(...clientArguments); await db.connect(); }); await t.step("should connect and close", async () => { - const db = new Client(connectionUrl, connectionOptions); + const db = new Client(...clientArguments); await db.connect(); @@ -190,7 +183,7 @@ async function _testConnectAndCloseClient( }); await t.step("should connect and close with events", async () => { - const db = new Client(connectionUrl, connectionOptions); + const db = new Client(...clientArguments); let connectListenerCalled = false; let closeListenerCalled = false; @@ -223,13 +216,16 @@ async function _testConnectAndCloseClient( }); } -async function _testConnectAndClosePoolClient( - { t, Client, connectionUrl, connectionOptions }: - TestConnectAndClosePoolClient, +async function _testClientPoolConnection< + Client extends SqlClientPool = SqlClientPool, +>( + t: Deno.TestContext, + Client: ClientPoolConstructor, + clientArguments: ClientPoolConstructorArguments, ): Promise { await t.step("testConnectAndClose", async (t) => { await t.step("should connect and close", async () => { - const db = new Client(connectionUrl, connectionOptions); + const db = new Client(...clientArguments); assertEquals(db.connected, false); @@ -238,8 +234,8 @@ async function _testConnectAndClosePoolClient( await db.close(); }); await t.step("should connect and close with using", async () => { - await using db = new Client(connectionUrl, { - ...connectionOptions, + await using db = new Client(clientArguments[0], { + ...clientArguments[1], lazyInitialization: true, }); let connectListenerCalled = false; @@ -256,8 +252,8 @@ async function _testConnectAndClosePoolClient( ); }); await t.step("should connect and close with events", async () => { - const db = new Client(connectionUrl, { - ...connectionOptions, + const db = new Client(clientArguments[0], { + ...clientArguments[1], lazyInitialization: false, }); @@ -294,538 +290,77 @@ async function _testConnectAndClosePoolClient( }); } -async function _testPreparedStatement( - { t, db, cases }: TestPreparedStatementOptions, -): Promise { - await t.step("testPreparedStatement", async (t) => { - assertIsSqlPreparedStatement(db); - - await t.step("should execute", async () => { - if (cases.execute.expected) { - const res = await db.execute(cases.execute.params); - assertEquals(res, cases.execute.expected); - } - }); - - await t.step("should query", async () => { - const res = await db.query(cases.query.params); - assertEquals(res, cases.query.expected); - }); - - await t.step("should queryOne", async () => { - const res = await db.queryOne(cases.queryOne.params); - assertEquals(res, cases.queryOne.expected); - }); - - await t.step("should queryMany", async () => { - const actual = []; - for await (const res of db.queryMany(cases.queryMany.params)) { - actual.push(res); - } - assertEquals(actual, cases.queryMany.expected); - }); - - await t.step("should queryArray", async () => { - const res = await db.queryArray(cases.queryArray.params); - assertEquals(res, cases.queryArray.expected); - }); - - await t.step("should queryOneArray", async () => { - const res = await db.queryOneArray(cases.queryOneArray.params); - assertEquals(res, cases.queryOneArray.expected); - }); - - await t.step("should queryManyArray", async () => { - const actual = []; - for await (const res of db.queryManyArray(cases.queryManyArray.params)) { - actual.push(res); - } - assertEquals(actual, cases.queryManyArray.expected); - }); - }); -} - -async function _testQueriable( - { t, db, queries }: TestQueriableOptions, -): Promise { - await t.step("testQueriable", async (t) => { - assertIsSqlQueriable(db); - - await t.step("should execute", async () => { - await db.execute(queries.selectManyFromTable); - }); - - await t.step("should query", async () => { - const res = await db.query(queries.selectManyFromTable); - assertEquals(res, [{ testcol: "test" }, { testcol: "test1" }, { - testcol: "test2", - }, { testcol: "test3" }]); - }); - - await t.step("should queryOne", async () => { - const res = await db.queryOne(queries.selectManyFromTable); - assertEquals(res, { testcol: "test" }); - }); - - await t.step("should queryMany", async () => { - const actual = []; - for await (const res of db.queryMany(queries.selectManyFromTable)) { - actual.push(res); - } - assertEquals(actual, [{ testcol: "test" }, { testcol: "test1" }, { - testcol: "test2", - }, { testcol: "test3" }]); - }); - - await t.step("should queryArray", async () => { - const res = await db.queryArray(queries.selectManyFromTable); - assertEquals(res, [["test"], ["test1"], ["test2"], ["test3"]]); - }); - - await t.step("should queryOneArray", async () => { - const res = await db.queryOneArray(queries.selectManyFromTable); - assertEquals(res, ["test"]); - }); - - await t.step("should queryManyArray", async () => { - const actual = []; - for await (const res of db.queryManyArray(queries.selectManyFromTable)) { - actual.push(res); - } - assertEquals(actual, [["test"], ["test1"], ["test2"], ["test3"]]); - }); - }); -} - -async function _testClientQueriable( - { t, db, queries }: TestClientQueriableOptions, -): Promise { - await t.step("testClientQueriable", async (t) => { - assertIsSqlClientQueriable(db); - - await t.step("test prepared statements", async (t) => { - await t.step("prepare select1AsString", async (t) => { - const statement = db.prepare(queries.select1AsString); - await _testPreparedStatement({ - t, - db: statement, - cases: { - execute: { expected: undefined }, - query: { expected: [{ result: "1" }] }, - queryOne: { expected: { result: "1" } }, - queryMany: { expected: [{ result: "1" }] }, - queryArray: { expected: [["1"]] }, - queryOneArray: { expected: ["1"] }, - queryManyArray: { expected: [["1"]] }, - }, - }); - }); - await t.step("prepare select1Plus1AsNumber", async (t) => { - const statement = db.prepare(queries.select1Plus1AsNumber); - await _testPreparedStatement({ - t, - db: statement, - cases: { - execute: { expected: undefined }, - query: { expected: [{ result: 2 }] }, - queryOne: { expected: { result: 2 } }, - queryMany: { expected: [{ result: 2 }] }, - queryArray: { expected: [[2]] }, - queryOneArray: { expected: [2] }, - queryManyArray: { expected: [[2]] }, - }, - }); - }); - await t.step("prepare selectByMatchFromTable", async (t) => { - const statement = db.prepare(queries.selectOneFromTable); - await _testPreparedStatement({ - t, - db: statement, - cases: { - execute: { params: ["test"], expected: undefined }, - query: { params: ["test"], expected: [{ testcol: "test" }] }, - queryOne: { params: ["test"], expected: { testcol: "test" } }, - queryMany: { params: ["test"], expected: [{ testcol: "test" }] }, - queryArray: { params: ["test"], expected: [["test"]] }, - queryOneArray: { params: ["test"], expected: ["test"] }, - queryManyArray: { params: ["test"], expected: [["test"]] }, - }, - }); - }); - await t.step("prepare selectByMatchFromTable", async (t) => { - const statement = db.prepare(queries.selectByMatchFromTable); - await _testPreparedStatement({ - t, - db: statement, - cases: { - execute: { params: ["test"], expected: undefined }, - query: { params: ["test"], expected: [{ testcol: "test" }] }, - queryOne: { params: ["test"], expected: { testcol: "test" } }, - queryMany: { params: ["test"], expected: [{ testcol: "test" }] }, - queryArray: { params: ["test"], expected: [["test"]] }, - queryOneArray: { params: ["test"], expected: ["test"] }, - queryManyArray: { params: ["test"], expected: [["test"]] }, - }, - }); - }); - await t.step("prepare selectManyFromTable", async (t) => { - const statement = db.prepare(queries.selectManyFromTable); - await _testPreparedStatement({ - t, - db: statement, - cases: { - execute: { expected: undefined }, - query: { - expected: [{ testcol: "test" }, { testcol: "test1" }, { - testcol: "test2", - }, { testcol: "test3" }], - }, - queryOne: { expected: { testcol: "test" } }, - queryMany: { - expected: [{ testcol: "test" }, { testcol: "test1" }, { - testcol: "test2", - }, { testcol: "test3" }], - }, - queryArray: { - expected: [["test"], ["test1"], ["test2"], ["test3"]], - }, - queryOneArray: { expected: ["test"] }, - queryManyArray: { - expected: [["test"], ["test1"], ["test2"], ["test3"]], - }, - }, - }); - }); - }); - - await t.step("test transactions", async (t) => { - await _testQueriable({ t, db, queries }); - - await t.step("transaction wrapper", async (t) => { - await db.transaction(async (c) => { - await _testTransaction({ t, db: c, queries }); - }); - }); - - await t.step("get a transaction instance", async (t) => { - const res = await db.beginTransaction(); - - await _testTransaction({ t, db: res, queries }); - }); - }); - }); -} - -async function _testTransaction( - { t, db, queries }: TestTransactionOptions, -): Promise { - await t.step("testTransaction", async (t) => { - assertIsSqlTransaction(db); - - await _testQueriable({ t, db, queries }); - - await t.step( - "createSavepoint, releaseSavepoint", - async () => { - await db.createSavepoint("savepoint1"); - await db.releaseSavepoint("savepoint1"); - }, - ); - }); -} - -export async function _testSetupTable( - { t, db, queries }: TestQueriableOptions, -): Promise { - await t.step("drop table", async () => { - const res = await db.execute(queries.dropTable); - assertFalse(res); - }); - - await t.step("setup table", async () => { - const res = await db.execute(queries.createTable); - assertFalse(res); - }); +export function sqlUnitTestSuite(options: { + testPrefix?: string; + connectionClass: unknown; + preparedStatementClass: unknown; + transactionClass: unknown; + eventTargetClass: unknown; + clientClass: unknown; + poolClientClass: unknown; + clientPoolClass: unknown; + checks: { + connectionUrl: string; + options: SqlTransaction["options"]; + clientPoolOptions: SqlClientPool["options"]; + sql: string; + }; +}) { + const prefix = options.testPrefix ? options.testPrefix : "sql"; - await t.step("insert one", async () => { - const res = await db.execute(queries.insertOneToTable, ["test"]); - assertEquals(res, 1); + Deno.test(`${prefix}/SqlConnection`, () => { + testSqlConnection(options.connectionClass, options.checks); }); - await t.step("insert many", async () => { - const res = await db.execute(queries.insertManyToTable, [ - "test1", - "test2", - "test3", - ]); - assertEquals(res, 3); + Deno.test(`${prefix}/PreparedStatement`, () => { + testSqlPreparedStatement(options.preparedStatementClass, options.checks); }); - await t.step("select one", async () => { - const res = await db.queryOne(queries.selectOneFromTable, ["test"]); - assertEquals(res, { testcol: "test" }); + Deno.test(`${prefix}/SqlTransaction`, () => { + testSqlTransaction(options.transactionClass, options.checks); }); - await t.step("select by match", async () => { - const res = await db.query(queries.selectByMatchFromTable, ["test"]); - assertEquals(res, [{ testcol: "test" }]); + Deno.test(`${prefix}/SqlEventTarget`, () => { + testSqlEventTarget(options.eventTargetClass); }); - await t.step("select many", async () => { - const res = await db.query(queries.selectManyFromTable); - assertEquals(res, [{ testcol: "test" }, { testcol: "test1" }, { - testcol: "test2", - }, { testcol: "test3" }]); + Deno.test(`${prefix}/SqlClient`, () => { + testSqlClient(options.clientClass, options.checks); }); -} - -export async function _connectionTest( - { t, connection }: ConnectionTestOptions, -): Promise { - await t.step("Connection Test", async (t) => { - assertIsSqlConnection(connection); - - await t.step("can connect and close", async () => { - assertEquals(connection.connected, false); - await connection.connect(); - assertEquals(connection.connected, true); - await connection.close(); - assertEquals(connection.connected, false); - }); - await t.step("can reconnect", async () => { - assertEquals(connection.connected, false); - await connection.connect(); - assertEquals(connection.connected, true); - await connection.close(); - assertEquals(connection.connected, false); - }); + Deno.test(`${prefix}/SqlPoolClient`, () => { + testSqlPoolClient(options.poolClientClass, options.checks); }); -} - -/** - * connectionConstructorTest - * - * Test the connection constructor. This is the test you want to include in your test suite. - * - * @example - * ``` - * Deno.test("Connection", async (t) => { - * ...other tests - * await connectionConstructorTest({ t, Connection, connectionOptions, connectionUrl }) - * ...other tests - * }) - * ``` - */ -export async function connectionConstructorTest( - { t, Connection, connectionOptions, connectionUrl }: - ConnectionConstructorTestOptions, -): Promise { - await t.step("SQLx Connection Constructor Test", async (t) => { - assertEquals(typeof Connection, "function"); - - const connection = new Connection(connectionUrl, connectionOptions); - await _connectionTest({ t, connection }); - await t.step("can connect with using and dispose", async () => { - await using connection = new Connection(connectionUrl, connectionOptions); - assertEquals(connection.connected, false); - await connection.connect(); - assertEquals(connection.connected, true); - }); + Deno.test(`${prefix}/SqlClientPool`, () => { + testSqlClientPool(options.clientPoolClass, options.checks); }); } - -/** - * clientTest - * - * Test the client constructor. This is the test you want to include in your test suite. - * - * @example - * ``` - * Deno.test("Client", async (t) => { - * ...other tests - * await clientTest({ t, Client, connectionOptions, connectionUrl, queries }) - * ...other tests - * }) - * ``` - */ -export async function clientTest( - { t, Client, connectionUrl, connectionOptions, queries }: ClientTestOptions, -): Promise { - await t.step("SQLx Client Tests", async (t) => { - await t.step("is constructor", () => { - assertEquals(typeof Client, "function"); - }); - - await t.step("can construct", async (t) => { - const client = new Client(connectionUrl, connectionOptions); - - assertIsSqlClient(client); - - await _connectionTest({ t, connection: client.connection }); - - await _testConnectAndCloseClient({ - t, - Client, - connectionUrl, - connectionOptions, - queries, - }); - }); - - await t.step("can connect with using and dispose", async () => { - await using connection = new Client(connectionUrl, connectionOptions); - assertEquals(connection.connected, false); - await connection.connect(); - assertEquals(connection.connected, true); - }); - - await using db = new Client(connectionUrl, connectionOptions); - - await db.connect(); - - await _testSetupTable({ t, db, queries }); - - // await testClientQueriable({ t, db, queries }); - - await t.step("drop table", async () => { - await db.execute(queries.dropTable); - }); +export function sqlIntegrationTestSuite< + Client extends SqlClient = SqlClient, + ClientPool extends SqlClientPool = SqlClientPool, +>(options: { + testPrefix?: string; + Client: ClientConstructor; + ClientPool: ClientPoolConstructor; + clientArguments: ClientConstructorArguments; + clientPoolArguments: ClientPoolConstructorArguments; +}) { + const prefix = options.testPrefix ? options.testPrefix : "sql"; + + Deno.test(`${prefix}/SqlClient connection`, async (t) => { + await _testClientConnection( + t, + options.Client, + options.clientArguments, + ); }); -} - -/** - * clientPoolTest - * - * Test the client pool constructor. This is the test you want to include in your test suite. - * - * @example - * ``` - * Deno.test("Client", async (t) => { - * ...other tests - * await clientPoolTest({ t, Client, connectionOptions, connectionUrl, queries }) - * ...other tests - * }) - * ``` - */ -export async function clientPoolTest( - { t, Client, connectionUrl, connectionOptions, queries }: PoolTestOptions, -): Promise { - await t.step("SQLx Pool Tests", async (t) => { - const parsedConnectionOptions = { - ...connectionOptions, - maxSize: 3, - lazyInitialization: true, - }; - await t.step("is constructor", () => { - assertEquals(typeof Client, "function"); - }); - - await t.step("can construct", () => { - const client = new Client(connectionUrl, parsedConnectionOptions); - assertIsSqlClientPool(client); - }); - - await _testConnectAndClosePoolClient({ - t: t, - Client, - connectionUrl, - connectionOptions: parsedConnectionOptions, - queries, - }); - - await t.step("acquire and release", async () => { - await using db = new Client(connectionUrl, parsedConnectionOptions); - - let aquireCalledCount = 0; - let releaseCalledCount = 0; - - db.eventTarget.addEventListener("acquire", () => { - aquireCalledCount++; - }); - db.eventTarget.addEventListener("release", () => { - releaseCalledCount++; - }); - - await db.connect(); - assertEquals( - db.deferredStack.maxSize, - parsedConnectionOptions.maxSize, - ); - assertEquals(db.deferredStack.availableCount, 3); - assertEquals(db.deferredStack.queuedCount, 0); - assertEquals(aquireCalledCount, 0); - assertEquals(releaseCalledCount, 0); - - const poolConnections: SqlPoolClient[] = []; - - let i = db.deferredStack.maxSize; - while (db.deferredStack.availableCount) { - const p = await db.acquire(); - assertEquals(db.deferredStack.availableCount, --i); - assertEquals(db.deferredStack.queuedCount, 0); - poolConnections.push(p); - } - - assertEquals(aquireCalledCount, 3); - assertEquals(releaseCalledCount, 0); - let p1Resolved = false; - let p2Resolved = false; - const p1 = db.acquire().then((r) => { - p1Resolved = true; - return r; - }); - assertEquals(db.deferredStack.queuedCount, 1); - const p2 = db.acquire().then((r) => { - p2Resolved = true; - return r; - }); - assertEquals(db.deferredStack.queuedCount, 2); - - assertFalse(p1Resolved); - await poolConnections.pop()?.release(); - await p1; - assert(p1Resolved); - assertEquals(db.deferredStack.queuedCount, 1); - assertFalse(p2Resolved); - assertEquals(aquireCalledCount, 4); - assertEquals(releaseCalledCount, 1); - await poolConnections.pop()?.release(); - await p2; - assert(p2Resolved); - assertEquals(db.deferredStack.queuedCount, 0); - assertEquals(aquireCalledCount, 5); - assertEquals(releaseCalledCount, 2); - - poolConnections.push(await p1); - poolConnections.push(await p2); - - for (const [i, p] of poolConnections.entries()) { - await p.release(); - assertEquals(db.deferredStack.availableCount, i + 1); - assertEquals(db.deferredStack.queuedCount, 0); - } - assertEquals(aquireCalledCount, 5); - assertEquals(releaseCalledCount, 5); - - await db.close(); - }); - - await using db = new Client(connectionUrl, parsedConnectionOptions); - await db.connect(); - - const p = await db.acquire(); - await _testSetupTable({ t, db: p, queries }); - await p.release(); - - await t.step("acquire and release with query", async (t) => { - for (let i = 0; i < db.deferredStack.maxSize; i++) { - const p = await db.acquire(); - await _testClientQueriable({ t, db: p, queries }); - p.release(); - } - }); + Deno.test(`${prefix}/SqlClientPool connection`, async (t) => { + await _testClientPoolConnection( + t, + options.ClientPool, + options.clientPoolArguments, + ); }); } From 55ec4b4e680027cd2198d49cb33d4f9a38ad8d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Thu, 20 Jun 2024 15:49:05 +0200 Subject: [PATCH 08/29] Cleaned up test names --- collections/deferred_stack.test.ts | 2 +- crypto/hash.test.ts | 2 +- crypto/hash/argon2.test.ts | 2 +- crypto/hash/bcrypt.test.ts | 2 +- crypto/hash/scrypt.test.ts | 2 +- encoding/hex.test.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/collections/deferred_stack.test.ts b/collections/deferred_stack.test.ts index 22123fb..24f867b 100644 --- a/collections/deferred_stack.test.ts +++ b/collections/deferred_stack.test.ts @@ -1,7 +1,7 @@ import { assert, assertEquals, assertFalse, assertThrows } from "@std/assert"; import { DeferredStack } from "./deferred_stack.ts"; -Deno.test("deferred", async (t) => { +Deno.test("collections/deferred_stack/DeferredStack", async (t) => { await t.step("fill and empty x2", async () => { const deferred = new DeferredStack({ maxSize: 2 }); assertEquals(deferred.maxSize, 2); diff --git a/crypto/hash.test.ts b/crypto/hash.test.ts index 896613c..4317336 100644 --- a/crypto/hash.test.ts +++ b/crypto/hash.test.ts @@ -1,7 +1,7 @@ import { assert, assertMatch, assertThrows } from "@std/assert"; import { hash, verify } from "./hash.ts"; -Deno.test("hash", async (t) => { +Deno.test("crypto/hash/[hash|verify]", async (t) => { await t.step("unsupported", () => { // deno-lint-ignore ban-ts-comment // @ts-ignore diff --git a/crypto/hash/argon2.test.ts b/crypto/hash/argon2.test.ts index 53c9da6..4ad90c0 100644 --- a/crypto/hash/argon2.test.ts +++ b/crypto/hash/argon2.test.ts @@ -1,7 +1,7 @@ import { assert, assertMatch } from "@std/assert"; import { type Argon2Options, hash, verify } from "./argon2.ts"; -Deno.test("Argon2", async (t) => { +Deno.test("crypto/hash/Argon2", async (t) => { await t.step("defaults", () => { const h = hash("password", {}); assertMatch(h, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/); diff --git a/crypto/hash/bcrypt.test.ts b/crypto/hash/bcrypt.test.ts index 068bed4..af57b85 100644 --- a/crypto/hash/bcrypt.test.ts +++ b/crypto/hash/bcrypt.test.ts @@ -1,7 +1,7 @@ import { assert, assertMatch } from "@std/assert"; import { type BcryptOptions, hash, verify } from "./bcrypt.ts"; -Deno.test("Bcrypt", async (t) => { +Deno.test("crypto/hash/Bcrypt", async (t) => { await t.step("defaults", () => { const o = {} as BcryptOptions; const h = hash("password", o); diff --git a/crypto/hash/scrypt.test.ts b/crypto/hash/scrypt.test.ts index 0ab1dcd..9ccee38 100644 --- a/crypto/hash/scrypt.test.ts +++ b/crypto/hash/scrypt.test.ts @@ -1,7 +1,7 @@ import { assert, assertMatch } from "@std/assert"; import { hash, type ScryptOptions, verify } from "./scrypt.ts"; -Deno.test("Scrypt", async (t) => { +Deno.test("crypto/hash/Scrypt", async (t) => { await t.step("defaults", () => { const o = {} as ScryptOptions; const h = hash("password", o); diff --git a/encoding/hex.test.ts b/encoding/hex.test.ts index 0b8146f..19cc306 100644 --- a/encoding/hex.test.ts +++ b/encoding/hex.test.ts @@ -1,7 +1,7 @@ import { assertEquals } from "@std/assert"; import { dump } from "./hex.ts"; -Deno.test("dump", async (t) => { +Deno.test("encoding/hex/dump", async (t) => { const data = "This is a test string that is longer than 16 bytes and will be split into multiple lines for the hexdump. The quick brown fox jumps over the lazy dog. Foo bar baz."; const buffer8Compatible = new TextEncoder().encode(data); From a307addf50325add439325360ff7aade60c0e7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Thu, 20 Jun 2024 15:49:25 +0200 Subject: [PATCH 09/29] added coverage to exclude for lint and coverage --- deno.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deno.json b/deno.json index fca372d..b2dd5a1 100644 --- a/deno.json +++ b/deno.json @@ -24,6 +24,7 @@ "./sql" ], "exclude": [ - "./crypto/hash/_wasm/target" + "./crypto/hash/_wasm/target", + "coverage" ] } From a5586af0c8ac84b3560c8cc995557cd21bdaea83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sat, 22 Jun 2024 20:40:39 +0200 Subject: [PATCH 10/29] updated asserts and testing helpers --- sql/asserts.ts | 251 ++++++++++++++++++++++++++++++++++++++++++++++++- sql/deno.json | 2 +- sql/test.ts | 83 +++++++++++----- sql/testing.ts | 195 +++++++++++++++++++------------------- 4 files changed, 407 insertions(+), 124 deletions(-) diff --git a/sql/asserts.ts b/sql/asserts.ts index 33bf6c2..6eea0c7 100644 --- a/sql/asserts.ts +++ b/sql/asserts.ts @@ -13,6 +13,9 @@ import { SqlTransactionable, } from "./mod.ts"; +/** + * Check if an object has a property + */ function hasProperty(obj: T, property: string | symbol | number): boolean { let currentProto = obj; @@ -31,7 +34,29 @@ function hasProperty(obj: T, property: string | symbol | number): boolean { } /** - * Check if an object has properties, and returns the properties that are missing + * Check if an object has properties + */ +export function hasProperties( + value: unknown, + properties: Array, +): value is { [K in T]: unknown } { + assertExists(value); + + const missing: Array = []; + + for (const property of properties) { + if ( + !hasProperty(value as { [K in T]: unknown }, property) + ) { + missing.push(property); + } + } + + return missing.length === 0; +} + +/** + * Check if an object has properties and throws if not */ export function assertHasProperties( value: unknown, @@ -66,12 +91,24 @@ export function isSqlError(err: unknown): err is SqlError { } /** - * Check if an error is a SqlError + * Asserts that an error is a SqlError */ export function assertIsSqlError(err: unknown): asserts err is SqlError { assertInstanceOf(err, SqlError); } +/** + * Check if an object is an AsyncDisposable + */ +export function isAsyncDisposable( + value: unknown, +): value is AsyncDisposable { + return hasProperties(value, [Symbol.asyncDispose]); +} + +/** + * Asserts that an object is an AsyncDisposable + */ export function assertIsAsyncDisposable( value: unknown, ): asserts value is AsyncDisposable { @@ -83,6 +120,29 @@ export function assertIsAsyncDisposable( ); } +/** + * Check if an object is an SqlConnection + */ +export function isSqlConnection( + value: unknown, +): value is SqlConnection { + return isAsyncDisposable(value) && hasProperties( + value, + [ + "options", + "connected", + "connect", + "close", + "execute", + "queryMany", + "queryManyArray", + ], + ); +} + +/** + * Asserts that an object is an SqlConnection + */ export function assertIsSqlConnection( value: unknown, ): asserts value is SqlConnection { @@ -101,6 +161,24 @@ export function assertIsSqlConnection( ); } +/** + * Check if an object is an SqlConnectable + */ +export function isSqlConnectable( + value: unknown, +): value is SqlConnectable { + return isAsyncDisposable(value) && hasProperties( + value, + [ + "connection", + "connected", + ], + ) && isSqlConnection(value.connection); +} + +/** + * Asserts that an object is an SqlConnectable + */ export function assertIsSqlConnectable( value: unknown, ): asserts value is SqlConnectable { @@ -115,6 +193,31 @@ export function assertIsSqlConnectable( assertIsSqlConnection(value.connection); } +/** + * Check if an object is an SqlPreparedStatement + */ +export function isSqlPreparedStatement( + value: unknown, +): value is SqlPreparedStatement { + return isSqlConnectable(value) && hasProperties( + value, + [ + "sql", + "options", + "execute", + "query", + "queryOne", + "queryMany", + "queryArray", + "queryOneArray", + "queryManyArray", + ], + ); +} + +/** + * Asserts that an object is an SqlPreparedStatement + */ export function assertIsSqlPreparedStatement( value: unknown, ): asserts value is SqlPreparedStatement { @@ -135,6 +238,30 @@ export function assertIsSqlPreparedStatement( ); } +/** + * Check if an object is an SqlQueriable + */ +export function isSqlQueriable( + value: unknown, +): value is SqlQueriable { + return isSqlConnectable(value) && hasProperties( + value, + [ + "options", + "execute", + "query", + "queryOne", + "queryMany", + "queryArray", + "queryOneArray", + "queryManyArray", + ], + ); +} + +/** + * Asserts that an object is an SqlQueriable + */ export function assertIsSqlQueriable( value: unknown, ): asserts value is SqlQueriable { @@ -153,6 +280,24 @@ export function assertIsSqlQueriable( ], ); } + +/** + * Check if an object is an SqlTransaction + */ +export function isSqlPreparable( + value: unknown, +): value is SqlQueriable { + return isSqlQueriable(value) && hasProperties( + value, + [ + "prepare", + ], + ); +} + +/** + * Asserts that an object is an SqlTransaction + */ export function assertIsSqlPreparable( value: unknown, ): asserts value is SqlQueriable { @@ -165,6 +310,27 @@ export function assertIsSqlPreparable( ); } +/** + * Check if an object is an SqlTransaction + */ +export function isSqlTransaction( + value: unknown, +): value is SqlTransaction { + return isSqlPreparable(value) && hasProperties( + value, + [ + "inTransaction", + "commitTransaction", + "rollbackTransaction", + "createSavepoint", + "releaseSavepoint", + ], + ); +} + +/** + * Asserts that an object is an SqlTransaction + */ export function assertIsSqlTransaction( value: unknown, ): asserts value is SqlTransaction { @@ -181,6 +347,24 @@ export function assertIsSqlTransaction( ); } +/** + * Check if an object is an SqlTransactionable + */ +export function isSqlTransactionable( + value: unknown, +): value is SqlTransactionable { + return isSqlPreparable(value) && hasProperties( + value, + [ + "beginTransaction", + "transaction", + ], + ); +} + +/** + * Asserts that an object is an SqlTransactionable + */ export function assertIsSqlTransactionable( value: unknown, ): asserts value is SqlTransactionable { @@ -194,6 +378,19 @@ export function assertIsSqlTransactionable( ); } +/** + * Check if an object is an SqlEventable + */ +export function isSqlEventable( + value: unknown, +): value is SqlEventable { + return hasProperties(value, ["eventTarget"]) && + value.eventTarget instanceof EventTarget; +} + +/** + * Asserts that an object is an SqlEventable + */ export function assertIsSqlEventable( value: unknown, ): asserts value is SqlEventable { @@ -201,6 +398,18 @@ export function assertIsSqlEventable( assertInstanceOf(value.eventTarget, EventTarget); } +/** + * Check if an object is an SqlClient + */ +export function isSqlClient(value: unknown): value is SqlClient { + return isSqlConnection(value) && isSqlQueriable(value) && + isSqlTransactionable(value) && isSqlEventable(value) && + hasProperties(value, ["options"]); +} + +/** + * Asserts that an object is an SqlClient + */ export function assertIsSqlClient(value: unknown): asserts value is SqlClient { assertIsSqlConnection(value); assertIsSqlQueriable(value); @@ -209,6 +418,23 @@ export function assertIsSqlClient(value: unknown): asserts value is SqlClient { assertHasProperties(value, ["options"]); } +/** + * Check if an object is an SqlPoolClient + */ +export function isSqlPoolClient( + value: unknown, +): value is SqlPoolClient { + return isSqlConnectable(value) && isSqlTransactionable(value) && + hasProperties(value, [ + "options", + "disposed", + "release", + ]); +} + +/** + * Asserts that an object is an SqlPoolClient + */ export function assertIsSqlPoolClient( value: unknown, ): asserts value is SqlPoolClient { @@ -221,6 +447,27 @@ export function assertIsSqlPoolClient( ]); } +/** + * Check if an object is an SqlClientPool + */ +export function isSqlClientPool( + value: unknown, +): value is SqlClientPool { + return isSqlEventable(value) && isAsyncDisposable(value) && + hasProperties(value, [ + "connectionUrl", + "options", + "connected", + "connect", + "close", + "deferredStack", + "acquire", + ]); +} + +/** + * Asserts that an object is an SqlClientPool + */ export function assertIsSqlClientPool( value: unknown, ): asserts value is SqlClientPool { diff --git a/sql/deno.json b/sql/deno.json index 2266da1..27bf4af 100644 --- a/sql/deno.json +++ b/sql/deno.json @@ -1,5 +1,5 @@ { - "version": "0.0.0-2", + "version": "0.0.0-5", "name": "@stdext/sql", "lock": false, "exports": { diff --git a/sql/test.ts b/sql/test.ts index cd3486a..544f3fc 100644 --- a/sql/test.ts +++ b/sql/test.ts @@ -5,7 +5,17 @@ import { SqlEventTarget, SqlPoolConnectionEventType, } from "./events.ts"; -import { sqlIntegrationTestSuite, sqlUnitTestSuite } from "./testing.ts"; +import { + testClientConnection, + testClientPoolConnection, + testSqlClient, + testSqlClientPool, + testSqlConnection, + testSqlEventTarget, + testSqlPoolClient, + testSqlPreparedStatement, + testSqlTransaction, +} from "./testing.ts"; import * as Sql from "./mod.ts"; /** @@ -625,27 +635,56 @@ const client = new TestSqlClient(connectionUrl, options); const poolClient = new TestSqlPoolClient(connection, options); const clientPool = new TestSqlClientPool(connectionUrl, options); -sqlUnitTestSuite({ - testPrefix: "sql", - connectionClass: connection, - preparedStatementClass: preparedStatement, - transactionClass: transaction, - eventTargetClass: eventTarget, - clientClass: client, - poolClientClass: poolClient, - clientPoolClass: clientPool, - checks: { - connectionUrl, - options, - clientPoolOptions: options, - sql, - }, +const expects = { + connectionUrl, + options, + clientPoolOptions: options, + sql, +}; + +Deno.test(`sql/type test`, async (t) => { + await t.step("SqlConnection", () => { + testSqlConnection(connection, expects); + }); + + await t.step(`sql/PreparedStatement`, () => { + testSqlPreparedStatement(preparedStatement, expects); + }); + + await t.step(`sql/SqlTransaction`, () => { + testSqlTransaction(transaction, expects); + }); + + await t.step(`sql/SqlEventTarget`, () => { + testSqlEventTarget(eventTarget); + }); + + await t.step(`sql/SqlClient`, () => { + testSqlClient(client, expects); + }); + + await t.step(`sql/SqlPoolClient`, () => { + testSqlPoolClient(poolClient, expects); + }); + + await t.step(`sql/SqlClientPool`, () => { + testSqlClientPool(clientPool, expects); + }); }); -sqlIntegrationTestSuite({ - testPrefix: "sql", - Client: TestSqlClient, - ClientPool: TestSqlClientPool, - clientArguments: [connectionUrl, options], - clientPoolArguments: [connectionUrl, options], +Deno.test(`sql/connection test`, async (t) => { + await t.step("SqlClient", async (t) => { + await testClientConnection( + t, + TestSqlClient, + [connectionUrl, options], + ); + }); + await t.step("SqlPoolClient", async (t) => { + await testClientPoolConnection( + t, + TestSqlClientPool, + [connectionUrl, options], + ); + }); }); diff --git a/sql/testing.ts b/sql/testing.ts index 44bbca8..5079727 100644 --- a/sql/testing.ts +++ b/sql/testing.ts @@ -41,85 +41,134 @@ export type ClientPoolConstructor< Client extends SqlClientPool = SqlClientPool, > = AnyConstructor>; +/** + * Test the SqlConnection class + * @param value The SqlClient + * @param expects The values to test against + */ export function testSqlConnection( value: unknown, - checks: { + expects: { connectionUrl: string; }, ) { assertIsSqlConnection(value); - assertEquals(value.connectionUrl, checks.connectionUrl); + assertEquals(value.connectionUrl, expects.connectionUrl); } +/** + * Test the SqlConnectable class + * @param value The SqlConnectable + * @param expects The values to test against + */ export function _testSqlConnectable( value: unknown, - checks: { + expects: { connectionUrl: string; options: SqlConnectable["options"]; }, ) { assertIsSqlConnectable(value); - assertEquals(value.options, checks.options); - testSqlConnection(value.connection, checks); + assertEquals(value.options, expects.options); + testSqlConnection(value.connection, expects); } + +/** + * Test the SqlPreparedStatement class + * @param value The SqlPreparedStatement + * @param expects The values to test against + */ export function testSqlPreparedStatement( value: unknown, - checks: { + expects: { connectionUrl: string; options: SqlPreparedStatement["options"]; sql: string; }, ) { assertIsSqlPreparedStatement(value); - _testSqlConnectable(value, checks); - assertEquals(value.sql, checks.sql); + _testSqlConnectable(value, expects); + assertEquals(value.sql, expects.sql); } + +/** + * Test the SqlQueriable class + * @param value The SqlQueriable + * @param expects The values to test against + */ export function _testSqlQueriable( value: unknown, - checks: { + expects: { connectionUrl: string; options: SqlQueriable["options"]; }, ) { assertIsSqlQueriable(value); - _testSqlConnectable(value, checks); + _testSqlConnectable(value, expects); } + +/** + * Test the SqlPreparable class + * @param value The SqlPreparable + * @param expects The values to test against + */ export function _testSqlPreparable( value: unknown, - checks: { + expects: { connectionUrl: string; options: SqlQueriable["options"]; }, ) { assertIsSqlPreparable(value); - _testSqlQueriable(value, checks); + _testSqlQueriable(value, expects); } + +/** + * Test the SqlTransaction class + * @param value The SqlTransaction + * @param expects The values to test against + */ export function testSqlTransaction( value: unknown, - checks: { + expects: { connectionUrl: string; options: SqlTransaction["options"]; }, ) { assertIsSqlTransaction(value); - _testSqlPreparable(value, checks); + _testSqlPreparable(value, expects); } + +/** + * Test the SqlTransactionable class + * @param value The SqlTransactionable + * @param expects The values to test against + */ export function _testSqlTransactionable( value: unknown, - checks: { + expects: { connectionUrl: string; options: SqlTransactionable["options"]; }, ) { assertIsSqlTransactionable(value); - _testSqlPreparable(value, checks); + _testSqlPreparable(value, expects); } + +/** + * Test the SqlEventTarget class + * @param value The SqlEventTarget + */ export function testSqlEventTarget( value: unknown, ) { assertInstanceOf(value, EventTarget); } +/** + * Test the SqlEventable class + * @param value The SqlEventable + */ export function _testSqlEventable( value: unknown, ) { @@ -127,42 +176,62 @@ export function _testSqlEventable( testSqlEventTarget(value.eventTarget); } +/** + * Test the SqlClient class + * @param value The SqlClient + * @param expects The values to test against + */ export function testSqlClient( value: unknown, - checks: { + expects: { connectionUrl: string; options: SqlClient["options"]; }, ) { assertIsSqlClient(value); - _testSqlTransactionable(value, checks); + _testSqlTransactionable(value, expects); _testSqlEventable(value); } +/** + * Test the SqlPoolClient class + * @param value The SqlPoolClient + * @param expects The values to test against + */ export function testSqlPoolClient( value: unknown, - checks: { + expects: { connectionUrl: string; options: SqlPoolClient["options"]; }, ) { assertIsSqlPoolClient(value); - _testSqlTransactionable(value, checks); + _testSqlTransactionable(value, expects); } +/** + * Test the SqlClientPool class + * @param value The SqlClientPool + * @param expects The values to test against + */ export function testSqlClientPool( value: unknown, - checks: { + expects: { connectionUrl: string; options: SqlClientPool["options"]; }, ) { assertIsSqlClientPool(value); _testSqlEventable(value); - assertEquals(value.connectionUrl, checks.connectionUrl); + assertEquals(value.connectionUrl, expects.connectionUrl); } -async function _testClientConnection( +/** + * Tests the connection of a SqlClient + */ +export async function testClientConnection< + Client extends SqlClient = SqlClient, +>( t: Deno.TestContext, Client: ClientConstructor, clientArguments: ClientConstructorArguments, @@ -216,7 +285,10 @@ async function _testClientConnection( }); } -async function _testClientPoolConnection< +/** + * Tests the connection of a SqlClientPool + */ +export async function testClientPoolConnection< Client extends SqlClientPool = SqlClientPool, >( t: Deno.TestContext, @@ -289,78 +361,3 @@ async function _testClientPoolConnection< }); }); } - -export function sqlUnitTestSuite(options: { - testPrefix?: string; - connectionClass: unknown; - preparedStatementClass: unknown; - transactionClass: unknown; - eventTargetClass: unknown; - clientClass: unknown; - poolClientClass: unknown; - clientPoolClass: unknown; - checks: { - connectionUrl: string; - options: SqlTransaction["options"]; - clientPoolOptions: SqlClientPool["options"]; - sql: string; - }; -}) { - const prefix = options.testPrefix ? options.testPrefix : "sql"; - - Deno.test(`${prefix}/SqlConnection`, () => { - testSqlConnection(options.connectionClass, options.checks); - }); - - Deno.test(`${prefix}/PreparedStatement`, () => { - testSqlPreparedStatement(options.preparedStatementClass, options.checks); - }); - - Deno.test(`${prefix}/SqlTransaction`, () => { - testSqlTransaction(options.transactionClass, options.checks); - }); - - Deno.test(`${prefix}/SqlEventTarget`, () => { - testSqlEventTarget(options.eventTargetClass); - }); - - Deno.test(`${prefix}/SqlClient`, () => { - testSqlClient(options.clientClass, options.checks); - }); - - Deno.test(`${prefix}/SqlPoolClient`, () => { - testSqlPoolClient(options.poolClientClass, options.checks); - }); - - Deno.test(`${prefix}/SqlClientPool`, () => { - testSqlClientPool(options.clientPoolClass, options.checks); - }); -} -export function sqlIntegrationTestSuite< - Client extends SqlClient = SqlClient, - ClientPool extends SqlClientPool = SqlClientPool, ->(options: { - testPrefix?: string; - Client: ClientConstructor; - ClientPool: ClientPoolConstructor; - clientArguments: ClientConstructorArguments; - clientPoolArguments: ClientPoolConstructorArguments; -}) { - const prefix = options.testPrefix ? options.testPrefix : "sql"; - - Deno.test(`${prefix}/SqlClient connection`, async (t) => { - await _testClientConnection( - t, - options.Client, - options.clientArguments, - ); - }); - - Deno.test(`${prefix}/SqlClientPool connection`, async (t) => { - await _testClientPoolConnection( - t, - options.ClientPool, - options.clientPoolArguments, - ); - }); -} From 15be3cd15b6ad466ccc4c44eaa41d5da890c1dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Fri, 28 Jun 2024 14:39:38 +0200 Subject: [PATCH 11/29] Updated docs --- sql/README.md | 115 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 81 insertions(+), 34 deletions(-) diff --git a/sql/README.md b/sql/README.md index c22914a..c698778 100644 --- a/sql/README.md +++ b/sql/README.md @@ -36,13 +36,15 @@ methods (see [SqlClient](./client.ts)): - `queryOne` (See [SqlQueriable](./core.ts)): Queries the database and returns at most one entry as an object - `queryMany` (See [SqlQueriable](./core.ts)): Queries the database with an - async generator and yields each entry as an object + async generator and yields each entry as an object. This is good for when you + want to iterate over a massive amount of rows. - `queryArray` (See [SqlQueriable](./core.ts)): Queries the database and returns an array of arrays - `queryOneArray` (See [SqlQueriable](./core.ts)): Queries the database and returns at most one entry as an array - `queryManyArray` (See [SqlQueriable](./core.ts)): Queries the database with an - async generator and yields each entry as an array + async generator and yields each entry as an array. This is good for when you + want to iterate over a massive amount of rows. - `sql` (See [SqlQueriable](./core.ts)): Allows you to create a query using template literals, and returns the entries as an array of objects. This is a wrapper around `query` @@ -95,45 +97,99 @@ The following events can be subscribed to according to the specs (see ### Examples +Async dispose + +```ts +await using client = new Client(connectionUrl, connectionOptions); +await client.connect(); +await client.execute("SOME INSERT QUERY"); +const res = await client.query("SELECT * FROM table"); +``` + Using const (requires manual close at the end) ```ts -const db = new Client(connectionUrl, connectionOptions); -await db.connect(); -await db.execute("SOME INSERT QUERY"); -const res = await db.query("SELECT * FROM table"); -await db.close(); +const client = new Client(connectionUrl, connectionOptions); +await client.connect(); +await client.execute("SOME INSERT QUERY"); +const res = await client.query("SELECT * FROM table"); +await client.close(); ``` -Query object +Query objects ```ts -await using db = new Client(connectionUrl, connectionOptions); -await db.connect(); -await db.execute("SOME INSERT QUERY"); -const res = await db.query("SELECT * FROM table"); +const res = await client.query("SELECT * FROM table"); console.log(res); // [{ col1: "some value" }] ``` -Query array +Query one object ```ts -await using db = new Client(connectionUrl, connectionOptions); -await db.connect(); -await db.execute("SOME INSERT QUERY"); -const res = await db.queryArray("SELECT * FROM table"); +const res = await client.queryOne("SELECT * FROM table"); +console.log(res); +// { col1: "some value" } +``` + +Query many objects with an iterator + +```ts +const res = Array.fromAsync(client.queryMany("SELECT * FROM table")); +console.log(res); +// [{ col1: "some value" }] + +// OR + +for await (const iterator of client.queryMany("SELECT * FROM table")) { + console.log(res); + // { col1: "some value" } +} +``` + +Query as an array + +```ts +const res = await client.queryArray("SELECT * FROM table"); console.log(res); // [[ "some value" ]] ``` -Query with template literals +Query one as an array ```ts -await using db = new Client(connectionUrl, connectionOptions); -await db.connect(); -await db.execute("SOME INSERT QUERY"); -const res = await db.sqlArray`SELECT * FROM table where id = ${id}`; +const res = await client.queryOneArray("SELECT * FROM table"); +console.log(res); +// [[ "some value" ]] +``` + +Query many as array with an iterator + +```ts +const res = Array.fromAsync(client.queryManyArray("SELECT * FROM table")); +console.log(res); +// [[ "some value" ]] + +// OR + +for await (const iterator of client.queryManyArray("SELECT * FROM table")) { + console.log(res); + // [ "some value" ] +} +``` + +Query with template literals as an object + +```ts +const res = await client.sql`SELECT * FROM table where id = ${id}`; +console.log(res); +// [{ col1: "some value" }] +``` + +Query with template literals as an array + +```ts +const res = await client.sqlArray`SELECT * FROM table where id = ${id}`; console.log(res); // [[ "some value" ]] ``` @@ -141,22 +197,16 @@ console.log(res); Transaction ```ts -await using db = new Client(connectionUrl, connectionOptions); -await db.connect(); -await db.execute("SOME INSERT QUERY"); -const transaction = await db.beginTransaction(); +const transaction = await client.beginTransaction(); await transaction.execute("SOME INSERT QUERY"); await transaction.commitTransaction(); -// transaction can no longer be used +// `transaction` can no longer be used, and a new transaction needs to be created ``` Transaction wrapper ```ts -await using db = new Client(connectionUrl, connectionOptions); -await db.connect(); -await db.execute("SOME INSERT QUERY"); -const res = await db.transaction(async (t) => { +const res = await client.transaction(async (t) => { await t.execute("SOME INSERT QUERY"); return t.query("SOME SELECT QUERY"); }); @@ -167,9 +217,6 @@ console.log(res); Prepared statement ```ts -await using db = new Client(connectionUrl, connectionOptions); -await db.connect(); -await db.execute("SOME INSERT QUERY"); const prepared = db.prepare("SOME PREPARED STATEMENT"); await prepared.query([...params]); console.log(res); From c6356bf8a66282aef990146076be7f07dcbc8516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sat, 13 Jul 2024 12:59:12 +0200 Subject: [PATCH 12/29] Updated readme, and added constructor restrictions for clients --- sql/README.md | 89 +++++++++++++++++++++++++++++++++++++++++++++------ sql/client.ts | 58 +++++++++++++++++++++++++++++++++ sql/pool.ts | 80 +++++++++++++++++++++++++++++++++++++++++++++ sql/test.ts | 65 +++++++++++++++++++++++++------------ 4 files changed, 262 insertions(+), 30 deletions(-) diff --git a/sql/README.md b/sql/README.md index c698778..5d55e31 100644 --- a/sql/README.md +++ b/sql/README.md @@ -5,7 +5,7 @@ The SQL package contains a standard interface for SQL based databases Inspired by [rust sqlx](https://docs.rs/sqlx/latest/sqlx/index.html) and [go sql](https://pkg.go.dev/database/sql). -The goal for this package is to have a standard interface for SQL like database +The goal for this package is to have a standard interface for SQL-like database clients that can be used in Deno, Node and other JS runtimes. ## Usage @@ -19,12 +19,18 @@ await db.execute("SOME INSERT QUERY"); const res = await db.query("SELECT * FROM table"); ``` -`@stdext/std` provides a standard for interacting with a database. +Both the `Client` and `ClientPool` need to be connected using `connect()` before +the database can be queried. At the end of the script, this connection also +needs to be cleaned up by calling `close()`. If using the new +[AsyncDispose](https://github.com/tc39/proposal-explicit-resource-management), +there is no need to call `close()` manually as shown in the example above. + +See the [examples](#examples) section for more usage. ### Client -Full compliance with `@stdext/std` provides a database client with the following -methods (see [SqlClient](./client.ts)): +The Client provides a database client with the following methods (see +[SqlClient](./client.ts)): - `connect` (See [SqlConnection](./connection.ts)): Creates a connection to the database @@ -72,14 +78,14 @@ The following events can be subscribed to according to the specs (see ### ClientPool -Full compliance with `@stdext/std` provides a database client pool (a pool of -clients) with the following methods (see [SqlClientPool](./pool.ts)): +The ClientPool provides a database client pool (a pool of clients) with the +following methods (see [SqlClientPool](./pool.ts)): - `connect` (See [SqlConnection](./core.ts)): Creates the connection classes and adds them to a connection pool, and optionally connects them to the database - `close` (See [SqlConnection](./core.ts)): Closes all connections in the pool - `acquire` (See [SqlPoolable](./core.ts)): Retrieves a - [SqlPoolClient](./pool.ts) (a subset of [Client](#client)), and connects it if + [SqlPoolClient](./pool.ts) (a subset of [Client](#client)), and connects if not already connected #### Events @@ -225,8 +231,11 @@ console.log(res); ## Implementation -To be fully compliant with `@stdext/std`, you will need to implement the -following classes for your database driver: +> This section is for implementing the interface for database drivers. For +> general usage, read the [usage](#usage) section. + +To be fully compliant with the specs, you will need to implement the following +classes for your database driver: - `Connection` ([SqlConnection](./connection.ts)): This represents the connection to the database. This should preferably only contain the @@ -252,3 +261,65 @@ most cases, these are the classes and the inheritance graph that should be implemented. ![inheritance flow](./_assets/inheritance_flowchart.jpg) + +### Constructor Signature + +The constructor also must follow a strict signature. + +The constructor for both the Client and the ClientPool follows the same +signature: + +1. `connectionUrl`: string | URL +2. `options`?: ConnectionOptions & QueryOptions + +As `ConnectionOptions` and `QueryOptions` can be extended, the options can be +used to customize the settings, thus having a standard 2 argument signature of +the constructor. + +> The current way to specify a constructor using interfaces in TS, is to use a +> combination of `implements` and `satisfies`. This will be updated if anything +> changes. + +#### Client + +The Client must have a constructor following the signature specified by +`SqlClientConstructor`. + +```ts +export const Client = class extends Transactionable implements SqlClient<...> { // Transactionable is a class implementing `SqlTransactionable` + ... + // The constructor now has to satisfy `SqlClientConstructor` + constructor( + connectionUrl: string | URL, + options: ConnectionOptions & QueryOptions = {}, + ) { + ... + } + ... +} satisfies SqlClientConstructor<...>; + +// We need to also export the instance type of the client +export type Client = InstanceType; +``` + +#### ClientPool + +The ClientPool must have a constructor following the signature specified by +`SqlClientPoolConstructor`. + +```ts +const ClientPool = class extends Transactionable implements SqlClientPool<...> { // Transactionable is a class implementing `SqlTransactionable` + ... + // The constructor now has to satisfy `SqlClientPoolConstructor` + constructor( + connectionUrl: string | URL, + options: ConnectionOptions & QueryOptions = {}, + ) { + ... + } + ... +} satisfies SqlClientPoolConstructor<...>; + +// We need to also export the instance type of the client pool +export type ClientPool = InstanceType; +``` diff --git a/sql/client.ts b/sql/client.ts index 4706dd8..819ebc7 100644 --- a/sql/client.ts +++ b/sql/client.ts @@ -68,3 +68,61 @@ export interface SqlClient< SqlEventable, AsyncDisposable { } + +/** + * SqlClientConstructor + * + * The constructor for the SqlClient interface. + */ +export interface SqlClientConstructor< + EventTarget extends SqlEventTarget = SqlEventTarget, + ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, + ParameterType extends unknown = unknown, + QueryOptions extends SqlQueryOptions = SqlQueryOptions, + Connection extends SqlConnection< + ConnectionOptions, + ParameterType, + QueryOptions + > = SqlConnection, + PreparedStatement extends SqlPreparedStatement< + ConnectionOptions, + ParameterType, + QueryOptions, + Connection + > = SqlPreparedStatement< + ConnectionOptions, + ParameterType, + QueryOptions, + Connection + >, + TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, + Transaction extends SqlTransaction< + ConnectionOptions, + ParameterType, + QueryOptions, + Connection, + PreparedStatement, + TransactionOptions + > = SqlTransaction< + ConnectionOptions, + ParameterType, + QueryOptions, + Connection, + PreparedStatement, + TransactionOptions + >, +> { + new ( + connectionUrl: string | URL, + options?: ConnectionOptions & QueryOptions, + ): SqlClient< + EventTarget, + ConnectionOptions, + ParameterType, + QueryOptions, + Connection, + PreparedStatement, + TransactionOptions, + Transaction + >; +} diff --git a/sql/pool.ts b/sql/pool.ts index 8dbfb2e..4e4356e 100644 --- a/sql/pool.ts +++ b/sql/pool.ts @@ -192,3 +192,83 @@ export interface SqlClientPool< */ acquire(): Promise; } + +/** + * SqlClientPoolConstructor + * + * The constructor for the SqlClientPool interface. + */ +export interface SqlClientPoolConstructor< + ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, + ParameterType extends unknown = unknown, + QueryOptions extends SqlQueryOptions = SqlQueryOptions, + Connection extends SqlConnection< + ConnectionOptions, + ParameterType, + QueryOptions + > = SqlConnection< + ConnectionOptions, + ParameterType, + QueryOptions + >, + PreparedStatement extends SqlPreparedStatement< + ConnectionOptions, + ParameterType, + QueryOptions, + Connection + > = SqlPreparedStatement< + ConnectionOptions, + ParameterType, + QueryOptions, + Connection + >, + TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, + Transaction extends SqlTransaction< + ConnectionOptions, + ParameterType, + QueryOptions, + Connection, + PreparedStatement, + TransactionOptions + > = SqlTransaction< + ConnectionOptions, + ParameterType, + QueryOptions, + Connection, + PreparedStatement, + TransactionOptions + >, + PoolClient extends SqlPoolClient< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions, + PreparedStatement, + TransactionOptions, + Transaction + > = SqlPoolClient< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions, + PreparedStatement, + TransactionOptions, + Transaction + >, + EventTarget extends SqlEventTarget = SqlEventTarget, +> { + new ( + connectionUrl: string | URL, + options?: ConnectionOptions & QueryOptions, + ): SqlClientPool< + ConnectionOptions, + ParameterType, + QueryOptions, + Connection, + PreparedStatement, + TransactionOptions, + Transaction, + PoolClient, + EventTarget + >; +} diff --git a/sql/test.ts b/sql/test.ts index 544f3fc..f48bd53 100644 --- a/sql/test.ts +++ b/sql/test.ts @@ -454,27 +454,27 @@ class TestSqlEventTarget extends SqlEventTarget< > { } -class TestSqlClient extends TestSqlTransactionable implements - Sql.SqlClient< - TestSqlEventTarget, - TestSqlConnectionOptions, - TestParameterType, - TestSqlQueryOptions, - TestSqlConnection, - TestSqlPreparedStatement, - TestSqlTransactionOptions, - TestSqlTransaction - > { +const TestSqlClient = class extends TestSqlTransactionable + implements + Sql.SqlClient< + TestSqlEventTarget, + TestSqlConnectionOptions, + TestParameterType, + TestSqlQueryOptions, + TestSqlConnection, + TestSqlPreparedStatement, + TestSqlTransactionOptions, + TestSqlTransaction + > { declare readonly options: & TestSqlConnectionOptions - & TestSqlQueryOptions - & TestSqlTransactionOptions; + & TestSqlQueryOptions; eventTarget: TestSqlEventTarget; constructor( - connectionUrl: string, - options: TestSqlClient["options"] = {}, + connectionUrl: string | URL, + options: TestSqlConnectionOptions & TestSqlQueryOptions = {}, ) { - super(new TestSqlConnection(connectionUrl, options), options); + super(new TestSqlConnection(connectionUrl.toString(), options), options); this.eventTarget = new TestSqlEventTarget(); } async connect(): Promise { @@ -489,7 +489,18 @@ class TestSqlClient extends TestSqlTransactionable implements ); await this.connection.close(); } -} +} satisfies Sql.SqlClientConstructor< + TestSqlEventTarget, + TestSqlConnectionOptions, + TestParameterType, + TestSqlQueryOptions, + TestSqlConnection, + TestSqlPreparedStatement, + TestSqlTransactionOptions, + TestSqlTransaction +>; + +type TestSqlClient = InstanceType; interface TestSqlPoolClientOptions extends Sql.SqlPoolClientOptions { } @@ -537,7 +548,7 @@ class TestSqlPoolClient extends TestSqlTransactionable } } -class TestSqlClientPool implements +const TestSqlClientPool = class implements Sql.SqlClientPool< TestSqlClientPoolOptions, TestParameterType, @@ -562,10 +573,10 @@ class TestSqlClientPool implements return this._connected; } constructor( - connectionUrl: string, + connectionUrl: string | URL, options: TestSqlClientPoolOptions = {}, ) { - this.connectionUrl = connectionUrl; + this.connectionUrl = connectionUrl.toString(); this.options = options; this.deferredStack = new DeferredStack({ maxSize: 3, @@ -617,7 +628,19 @@ class TestSqlClientPool implements async [Symbol.asyncDispose](): Promise { await this.close(); } -} +} satisfies Sql.SqlClientPoolConstructor< + TestSqlClientPoolOptions, + TestParameterType, + TestSqlQueryOptions, + TestSqlConnection, + TestSqlPreparedStatement, + TestSqlTransactionOptions, + TestSqlTransaction, + TestSqlPoolClient, + TestSqlEventTarget +>; + +type TestSqlClientPool = InstanceType; const connectionUrl = "test"; const options: TestSqlTransaction["options"] = { test: "test" }; From 152773cc2ba8fcea6da0729b45ea9c9c13440183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sat, 13 Jul 2024 13:46:45 +0200 Subject: [PATCH 13/29] Fixed workspace breaking update --- _tools/bump_version.ts | 2 +- collections/deno.json | 1 - crypto/deno.json | 1 - deno.json | 2 +- encoding/deno.json | 1 - http/deno.json | 1 - sql/deno.json | 1 - 7 files changed, 2 insertions(+), 7 deletions(-) diff --git a/_tools/bump_version.ts b/_tools/bump_version.ts index adab23b..04aee99 100644 --- a/_tools/bump_version.ts +++ b/_tools/bump_version.ts @@ -18,7 +18,7 @@ async function updateMetaVersion(filepath: string, version: string) { ); } -const { workspaces } = meta; +const { workspace: workspaces } = meta; const version = Deno.env.get("VERSION"); diff --git a/collections/deno.json b/collections/deno.json index 26b4211..d8e7b9b 100644 --- a/collections/deno.json +++ b/collections/deno.json @@ -1,7 +1,6 @@ { "version": "0.0.5", "name": "@stdext/collections", - "lock": false, "exports": { ".": "./mod.ts", "./deferred-stack": "./deferred_stack.ts" diff --git a/crypto/deno.json b/crypto/deno.json index 24dbe99..a30f2b1 100644 --- a/crypto/deno.json +++ b/crypto/deno.json @@ -1,7 +1,6 @@ { "version": "0.0.5", "name": "@stdext/crypto", - "lock": false, "exports": { "./hash": "./hash.ts" }, diff --git a/deno.json b/deno.json index b2dd5a1..8129a4e 100644 --- a/deno.json +++ b/deno.json @@ -16,7 +16,7 @@ "format": "deno fmt && deno task --cwd crypto format", "format:check": "deno fmt --check && deno task --cwd crypto format:check" }, - "workspaces": [ + "workspace": [ "./collections", "./crypto", "./encoding", diff --git a/encoding/deno.json b/encoding/deno.json index 15ff8bd..439b462 100644 --- a/encoding/deno.json +++ b/encoding/deno.json @@ -1,7 +1,6 @@ { "version": "0.0.5", "name": "@stdext/encoding", - "lock": false, "exports": { "./hex": "./hex.ts" } diff --git a/http/deno.json b/http/deno.json index 230de17..8973c68 100644 --- a/http/deno.json +++ b/http/deno.json @@ -1,7 +1,6 @@ { "version": "0.0.5", "name": "@stdext/http", - "lock": false, "exports": { "./header": "./header.ts", "./method": "./method.ts" diff --git a/sql/deno.json b/sql/deno.json index 27bf4af..5378c68 100644 --- a/sql/deno.json +++ b/sql/deno.json @@ -1,7 +1,6 @@ { "version": "0.0.0-5", "name": "@stdext/sql", - "lock": false, "exports": { ".": "./mod.ts", "./testing": "./testing.ts" From 54ad3805d1ab81940d27c1f2ced33ac5e10a7c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sat, 13 Jul 2024 13:46:53 +0200 Subject: [PATCH 14/29] fixed type issues --- sql/asserts.ts | 20 ++++++++++---------- sql/connection.ts | 2 +- sql/test.ts | 6 +++--- sql/testing.ts | 16 ++++++++-------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/sql/asserts.ts b/sql/asserts.ts index 6eea0c7..d030a96 100644 --- a/sql/asserts.ts +++ b/sql/asserts.ts @@ -1,16 +1,16 @@ import { assertExists, assertInstanceOf, AssertionError } from "@std/assert"; import { - SqlClient, - SqlClientPool, - SqlConnectable, - SqlConnection, + type SqlClient, + type SqlClientPool, + type SqlConnectable, + type SqlConnection, SqlError, - SqlEventable, - SqlPoolClient, - SqlPreparedStatement, - SqlQueriable, - SqlTransaction, - SqlTransactionable, + type SqlEventable, + type SqlPoolClient, + type SqlPreparedStatement, + type SqlQueriable, + type SqlTransaction, + type SqlTransactionable, } from "./mod.ts"; /** diff --git a/sql/connection.ts b/sql/connection.ts index 068e94c..e9b0817 100644 --- a/sql/connection.ts +++ b/sql/connection.ts @@ -1,4 +1,4 @@ -import { ArrayRow, Row, SqlQueryOptions } from "./core.ts"; +import type { ArrayRow, Row, SqlQueryOptions } from "./core.ts"; /** * SqlConnectionOptions diff --git a/sql/test.ts b/sql/test.ts index f48bd53..d671f23 100644 --- a/sql/test.ts +++ b/sql/test.ts @@ -1,9 +1,9 @@ import { DeferredStack } from "../collections/deferred_stack.ts"; import { - SqlConnectionEventInit, - SqlEvent, + type SqlConnectionEventInit, + type SqlEvent, SqlEventTarget, - SqlPoolConnectionEventType, + type SqlPoolConnectionEventType, } from "./events.ts"; import { testClientConnection, diff --git a/sql/testing.ts b/sql/testing.ts index 5079727..6ac0a0b 100644 --- a/sql/testing.ts +++ b/sql/testing.ts @@ -16,14 +16,14 @@ import { assertIsSqlQueriable, assertIsSqlTransaction, assertIsSqlTransactionable, - SqlClient, - SqlClientPool, - SqlConnectable, - SqlPoolClient, - SqlPreparedStatement, - SqlQueriable, - SqlTransaction, - SqlTransactionable, + type SqlClient, + type SqlClientPool, + type SqlConnectable, + type SqlPoolClient, + type SqlPreparedStatement, + type SqlQueriable, + type SqlTransaction, + type SqlTransactionable, } from "./mod.ts"; // deno-lint-ignore no-explicit-any From 3f022b3b7748f8c80bdca3981173290ba41e3c4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Wed, 31 Jul 2024 00:01:20 +0200 Subject: [PATCH 15/29] Added deallocated to prepared statement --- sql/core.ts | 12 +++++++++++- sql/test.ts | 5 +++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/sql/core.ts b/sql/core.ts index 5b9c8b0..d089058 100644 --- a/sql/core.ts +++ b/sql/core.ts @@ -69,6 +69,16 @@ export interface SqlPreparedStatement< */ readonly sql: string; + /** + * Whether the prepared statement has been deallocated or not. + */ + deallocated: boolean; + + /** + * Deallocate the prepared statement + */ + deallocate(): Promise; + /** * Executes the prepared statement * @@ -386,7 +396,7 @@ export interface SqlTransaction< /** * Whether the connection is in an active transaction or not. */ - get inTransaction(): boolean; + inTransaction: boolean; /** * Commit the transaction diff --git a/sql/test.ts b/sql/test.ts index d671f23..3fe6159 100644 --- a/sql/test.ts +++ b/sql/test.ts @@ -209,6 +209,11 @@ class TestSqlPreparedStatement extends TestSqlConnectable super(connection, options); this.sql = sql; } + deallocated = false; + deallocate(): Promise { + this.deallocated = true; + return Promise.resolve(); + } execute( params?: TestParameterType[] | undefined, options?: TestSqlQueryOptions | undefined, From ae7fc9b53231420b54285acca3708eea7788519c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Wed, 31 Jul 2024 00:02:07 +0200 Subject: [PATCH 16/29] Updated readme --- sql/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sql/README.md b/sql/README.md index 5d55e31..04a14fa 100644 --- a/sql/README.md +++ b/sql/README.md @@ -13,10 +13,10 @@ clients that can be used in Deno, Node and other JS runtimes. Minimal usage example: ```ts -await using db = new Client(connectionUrl, connectionOptions); -await db.connect(); -await db.execute("SOME INSERT QUERY"); -const res = await db.query("SELECT * FROM table"); +await using client = new Client(connectionUrl, connectionOptions); +await client.connect(); +await client.execute("SOME INSERT QUERY"); +const res = await client.query("SELECT * FROM table"); ``` Both the `Client` and `ClientPool` need to be connected using `connect()` before From 1207bcb7c60de232145487d2a83beea082c1ffcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Thu, 3 Oct 2024 01:07:31 +0200 Subject: [PATCH 17/29] renamed directory from sql to database/sql --- {sql => database/sql}/README.md | 0 .../sql}/_assets/inheritance_flowchart.jpg | Bin {sql => database/sql}/asserts.ts | 0 {sql => database/sql}/client.ts | 0 {sql => database/sql}/connection.ts | 0 {sql => database/sql}/core.ts | 0 {sql => database/sql}/deno.json | 0 {sql => database/sql}/errors.ts | 0 {sql => database/sql}/events.ts | 0 {sql => database/sql}/mod.ts | 0 {sql => database/sql}/pool.ts | 0 {sql => database/sql}/test.ts | 0 {sql => database/sql}/testing.ts | 0 13 files changed, 0 insertions(+), 0 deletions(-) rename {sql => database/sql}/README.md (100%) rename {sql => database/sql}/_assets/inheritance_flowchart.jpg (100%) rename {sql => database/sql}/asserts.ts (100%) rename {sql => database/sql}/client.ts (100%) rename {sql => database/sql}/connection.ts (100%) rename {sql => database/sql}/core.ts (100%) rename {sql => database/sql}/deno.json (100%) rename {sql => database/sql}/errors.ts (100%) rename {sql => database/sql}/events.ts (100%) rename {sql => database/sql}/mod.ts (100%) rename {sql => database/sql}/pool.ts (100%) rename {sql => database/sql}/test.ts (100%) rename {sql => database/sql}/testing.ts (100%) diff --git a/sql/README.md b/database/sql/README.md similarity index 100% rename from sql/README.md rename to database/sql/README.md diff --git a/sql/_assets/inheritance_flowchart.jpg b/database/sql/_assets/inheritance_flowchart.jpg similarity index 100% rename from sql/_assets/inheritance_flowchart.jpg rename to database/sql/_assets/inheritance_flowchart.jpg diff --git a/sql/asserts.ts b/database/sql/asserts.ts similarity index 100% rename from sql/asserts.ts rename to database/sql/asserts.ts diff --git a/sql/client.ts b/database/sql/client.ts similarity index 100% rename from sql/client.ts rename to database/sql/client.ts diff --git a/sql/connection.ts b/database/sql/connection.ts similarity index 100% rename from sql/connection.ts rename to database/sql/connection.ts diff --git a/sql/core.ts b/database/sql/core.ts similarity index 100% rename from sql/core.ts rename to database/sql/core.ts diff --git a/sql/deno.json b/database/sql/deno.json similarity index 100% rename from sql/deno.json rename to database/sql/deno.json diff --git a/sql/errors.ts b/database/sql/errors.ts similarity index 100% rename from sql/errors.ts rename to database/sql/errors.ts diff --git a/sql/events.ts b/database/sql/events.ts similarity index 100% rename from sql/events.ts rename to database/sql/events.ts diff --git a/sql/mod.ts b/database/sql/mod.ts similarity index 100% rename from sql/mod.ts rename to database/sql/mod.ts diff --git a/sql/pool.ts b/database/sql/pool.ts similarity index 100% rename from sql/pool.ts rename to database/sql/pool.ts diff --git a/sql/test.ts b/database/sql/test.ts similarity index 100% rename from sql/test.ts rename to database/sql/test.ts diff --git a/sql/testing.ts b/database/sql/testing.ts similarity index 100% rename from sql/testing.ts rename to database/sql/testing.ts From 9627edb522f2ae5c02736a67f452336543006c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Thu, 3 Oct 2024 01:07:59 +0200 Subject: [PATCH 18/29] updated readmes and deno.json --- database/README.md | 25 +++++++++++++++++++++++++ database/deno.json | 8 ++++++++ database/sql/README.md | 2 +- database/sql/deno.json | 8 -------- deno.json | 4 ++-- 5 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 database/README.md create mode 100644 database/deno.json delete mode 100644 database/sql/deno.json diff --git a/database/README.md b/database/README.md new file mode 100644 index 0000000..f3afb2d --- /dev/null +++ b/database/README.md @@ -0,0 +1,25 @@ +# @stdext/database + +The database package contains interfaces and helpers for interracting with +databases. It draws inspiration from +[go std/database](https://pkg.go.dev/database). + +## Entrypoints + +### Sql + +The SQL package contains a standard interface for SQL based databases + +> The SQL entrypoint is not intended to be directly used in applications, but is +> meant to be implemented by database drivers. This is mainly for library +> authors. + +Databases implementing these interfaces can be used as following (see +[database/sql](./sql/README.md) for more details): + +```ts +await using client = new Client(connectionUrl, connectionOptions); +await client.connect(); +await client.execute("SOME INSERT QUERY"); +const res = await client.query("SELECT * FROM table"); +``` diff --git a/database/deno.json b/database/deno.json new file mode 100644 index 0000000..7daea2a --- /dev/null +++ b/database/deno.json @@ -0,0 +1,8 @@ +{ + "version": "0.0.0-5", + "name": "@stdext/database", + "exports": { + "./sql": "./sql/mod.ts", + "./sql/testing": "./sql/testing.ts" + } +} diff --git a/database/sql/README.md b/database/sql/README.md index 04a14fa..62f1b69 100644 --- a/database/sql/README.md +++ b/database/sql/README.md @@ -1,4 +1,4 @@ -# @stdext/sql +# @stdext/database/sql The SQL package contains a standard interface for SQL based databases diff --git a/database/sql/deno.json b/database/sql/deno.json deleted file mode 100644 index 5378c68..0000000 --- a/database/sql/deno.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "version": "0.0.0-5", - "name": "@stdext/sql", - "exports": { - ".": "./mod.ts", - "./testing": "./testing.ts" - } -} diff --git a/deno.json b/deno.json index 7bc43f4..11c895a 100644 --- a/deno.json +++ b/deno.json @@ -20,9 +20,9 @@ "workspace": [ "./collections", "./crypto", + "./database", "./encoding", - "./http", - "./sql" + "./http" ], "exclude": [ "./crypto/hash/_wasm/target", From 9ab2dedaaecc41cd6bc9b951c9e39e6617e24fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Thu, 3 Oct 2024 01:15:01 +0200 Subject: [PATCH 19/29] Added local workspaces to imports --- deno.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 11c895a..1638c58 100644 --- a/deno.json +++ b/deno.json @@ -3,7 +3,12 @@ "imports": { "@std/assert": "jsr:@std/assert@^1.0.2", "@std/path": "jsr:@std/path@^1.0.2", - "@std/encoding": "jsr:@std/encoding@^1.0.1" + "@std/encoding": "jsr:@std/encoding@^1.0.1", + "@stdext/collections": "jsr:@stdext/collections", + "@stdext/crypto": "jsr:@stdext/crypto", + "@stdext/database": "jsr:@stdext/database", + "@stdext/encoding": "jsr:@stdext/encoding", + "@stdext/http": "jsr:@stdext/http" }, "tasks": { "bump_version": "deno run --allow-env=VERSION --allow-read=. --allow-write=. ./_tools/bump_version.ts", From ad1db405551fdcbd5707e8edd40d7272a9f6fe17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Thu, 3 Oct 2024 01:15:10 +0200 Subject: [PATCH 20/29] updated tests --- crypto/totp.test.ts | 1 - database/sql/test.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crypto/totp.test.ts b/crypto/totp.test.ts index aafdab3..f5c43fd 100644 --- a/crypto/totp.test.ts +++ b/crypto/totp.test.ts @@ -5,7 +5,6 @@ const secret = "OCOMBLGUREYUXFQJIL75FQFCKYFCKLQP"; const t = 1704067200000; Deno.test("generateTotp()", async () => { - console.log(); assertEquals(await generateTotp(secret, 0, t), "342743"); assertEquals(await generateTotp(secret, 944996400, t), "149729"); assertEquals(await generateTotp(secret, 976618800, t), "372018"); diff --git a/database/sql/test.ts b/database/sql/test.ts index 3fe6159..30cbd91 100644 --- a/database/sql/test.ts +++ b/database/sql/test.ts @@ -1,4 +1,4 @@ -import { DeferredStack } from "../collections/deferred_stack.ts"; +import { DeferredStack } from "@stdext/collections"; import { type SqlConnectionEventInit, type SqlEvent, From 2bc309d1cc36e7fdcf003dbb023425c3301685ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Thu, 3 Oct 2024 22:37:55 +0200 Subject: [PATCH 21/29] Updated generics and types --- database/sql/asserts.ts | 43 ++-- database/sql/client.ts | 127 +++++++--- database/sql/connection.ts | 131 ---------- database/sql/core.ts | 218 +++++++++++------ database/sql/driver.test.ts | 193 +++++++++++++++ database/sql/driver.ts | 180 ++++++++++++++ database/sql/events.ts | 88 ++++++- database/sql/mod.ts | 3 +- database/sql/pool.ts | 249 +++++++++++++------ database/sql/test.ts | 460 +++++++++++++++++------------------- database/sql/testing.ts | 174 ++++++++++++-- database/sql/utils.test.ts | 82 +++++++ database/sql/utils.ts | 78 ++++++ deno.json | 8 +- 14 files changed, 1431 insertions(+), 603 deletions(-) delete mode 100644 database/sql/connection.ts create mode 100644 database/sql/driver.test.ts create mode 100644 database/sql/driver.ts create mode 100644 database/sql/utils.test.ts create mode 100644 database/sql/utils.ts diff --git a/database/sql/asserts.ts b/database/sql/asserts.ts index d030a96..bee7c58 100644 --- a/database/sql/asserts.ts +++ b/database/sql/asserts.ts @@ -1,9 +1,9 @@ import { assertExists, assertInstanceOf, AssertionError } from "@std/assert"; import { + type Driver, + type DriverConnectable, type SqlClient, type SqlClientPool, - type SqlConnectable, - type SqlConnection, SqlError, type SqlEventable, type SqlPoolClient, @@ -125,7 +125,7 @@ export function assertIsAsyncDisposable( */ export function isSqlConnection( value: unknown, -): value is SqlConnection { +): value is Driver { return isAsyncDisposable(value) && hasProperties( value, [ @@ -143,9 +143,9 @@ export function isSqlConnection( /** * Asserts that an object is an SqlConnection */ -export function assertIsSqlConnection( +export function assertIsDriver( value: unknown, -): asserts value is SqlConnection { +): asserts value is Driver { assertIsAsyncDisposable(value); assertHasProperties( value, @@ -154,19 +154,18 @@ export function assertIsSqlConnection( "connected", "connect", "close", - "execute", - "queryMany", - "queryManyArray", + "ping", + "query", ], ); } /** - * Check if an object is an SqlConnectable + * Check if an object is an DriverConnectable */ -export function isSqlConnectable( +export function isDriverConnectable( value: unknown, -): value is SqlConnectable { +): value is DriverConnectable { return isAsyncDisposable(value) && hasProperties( value, [ @@ -177,11 +176,11 @@ export function isSqlConnectable( } /** - * Asserts that an object is an SqlConnectable + * Asserts that an object is an DriverConnectable */ -export function assertIsSqlConnectable( +export function assertIsDriverConnectable( value: unknown, -): asserts value is SqlConnectable { +): asserts value is DriverConnectable { assertIsAsyncDisposable(value); assertHasProperties( value, @@ -190,7 +189,7 @@ export function assertIsSqlConnectable( "connected", ], ); - assertIsSqlConnection(value.connection); + assertIsDriver(value.connection); } /** @@ -199,7 +198,7 @@ export function assertIsSqlConnectable( export function isSqlPreparedStatement( value: unknown, ): value is SqlPreparedStatement { - return isSqlConnectable(value) && hasProperties( + return isDriverConnectable(value) && hasProperties( value, [ "sql", @@ -221,7 +220,7 @@ export function isSqlPreparedStatement( export function assertIsSqlPreparedStatement( value: unknown, ): asserts value is SqlPreparedStatement { - assertIsSqlConnectable(value); + assertIsDriverConnectable(value); assertHasProperties( value, [ @@ -244,7 +243,7 @@ export function assertIsSqlPreparedStatement( export function isSqlQueriable( value: unknown, ): value is SqlQueriable { - return isSqlConnectable(value) && hasProperties( + return isDriverConnectable(value) && hasProperties( value, [ "options", @@ -265,7 +264,7 @@ export function isSqlQueriable( export function assertIsSqlQueriable( value: unknown, ): asserts value is SqlQueriable { - assertIsSqlConnectable(value); + assertIsDriverConnectable(value); assertHasProperties( value, [ @@ -411,7 +410,7 @@ export function isSqlClient(value: unknown): value is SqlClient { * Asserts that an object is an SqlClient */ export function assertIsSqlClient(value: unknown): asserts value is SqlClient { - assertIsSqlConnection(value); + isDriverConnectable(value); assertIsSqlQueriable(value); assertIsSqlTransactionable(value); assertIsSqlEventable(value); @@ -424,7 +423,7 @@ export function assertIsSqlClient(value: unknown): asserts value is SqlClient { export function isSqlPoolClient( value: unknown, ): value is SqlPoolClient { - return isSqlConnectable(value) && isSqlTransactionable(value) && + return isDriverConnectable(value) && isSqlTransactionable(value) && hasProperties(value, [ "options", "disposed", @@ -438,7 +437,7 @@ export function isSqlPoolClient( export function assertIsSqlPoolClient( value: unknown, ): asserts value is SqlPoolClient { - assertIsSqlConnectable(value); + assertIsDriverConnectable(value); assertIsSqlTransactionable(value); assertHasProperties(value, [ "options", diff --git a/database/sql/client.ts b/database/sql/client.ts index 819ebc7..e4447b6 100644 --- a/database/sql/client.ts +++ b/database/sql/client.ts @@ -1,7 +1,13 @@ -import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; +import type { + Driver, + DriverConnectionOptions, + DriverParameterType, + DriverQueryMeta, + DriverQueryOptions, + DriverQueryValues, +} from "./driver.ts"; import type { SqlPreparedStatement, - SqlQueryOptions, SqlTransaction, SqlTransactionable, SqlTransactionOptions, @@ -15,51 +21,77 @@ import type { SqlEventable, SqlEventTarget } from "./events.ts"; * to the database, you will in most cases use this interface. */ export interface SqlClient< - EventTarget extends SqlEventTarget = SqlEventTarget, - ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - ParameterType extends unknown = unknown, - QueryOptions extends SqlQueryOptions = SqlQueryOptions, - Connection extends SqlConnection< + ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + QueryOptions extends DriverQueryOptions = DriverQueryOptions, + ParameterType extends DriverParameterType = DriverParameterType, + QueryValues extends DriverQueryValues = DriverQueryValues, + QueryMeta extends DriverQueryMeta = DriverQueryMeta, + Connection extends Driver< ConnectionOptions, + QueryOptions, ParameterType, - QueryOptions - > = SqlConnection, - PreparedStatement extends SqlPreparedStatement< + QueryValues, + QueryMeta + > = Driver< ConnectionOptions, + QueryOptions, ParameterType, + QueryValues, + QueryMeta + >, + PreparedStatement extends SqlPreparedStatement< + ConnectionOptions, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection > = SqlPreparedStatement< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection >, TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, Transaction extends SqlTransaction< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, PreparedStatement, TransactionOptions > = SqlTransaction< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, PreparedStatement, TransactionOptions >, + EventTarget extends SqlEventTarget = SqlEventTarget, > extends Pick< - SqlConnection, + Driver< + ConnectionOptions, + QueryOptions, + ParameterType, + QueryValues, + QueryMeta + >, "close" | "connect" | "connected" >, SqlTransactionable< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, PreparedStatement, TransactionOptions, @@ -75,54 +107,83 @@ export interface SqlClient< * The constructor for the SqlClient interface. */ export interface SqlClientConstructor< - EventTarget extends SqlEventTarget = SqlEventTarget, - ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - ParameterType extends unknown = unknown, - QueryOptions extends SqlQueryOptions = SqlQueryOptions, - Connection extends SqlConnection< + ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + QueryOptions extends DriverQueryOptions = DriverQueryOptions, + ParameterType extends DriverParameterType = DriverParameterType, + QueryValues extends DriverQueryValues = DriverQueryValues, + QueryMeta extends DriverQueryMeta = DriverQueryMeta, + Connection extends Driver< ConnectionOptions, + QueryOptions, ParameterType, - QueryOptions - > = SqlConnection, - PreparedStatement extends SqlPreparedStatement< + QueryValues, + QueryMeta + > = Driver< ConnectionOptions, + QueryOptions, ParameterType, + QueryValues, + QueryMeta + >, + PreparedStatement extends SqlPreparedStatement< + ConnectionOptions, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection > = SqlPreparedStatement< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection >, TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, Transaction extends SqlTransaction< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, PreparedStatement, TransactionOptions > = SqlTransaction< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, PreparedStatement, TransactionOptions >, -> { - new ( - connectionUrl: string | URL, - options?: ConnectionOptions & QueryOptions, - ): SqlClient< - EventTarget, + Client extends SqlClient< ConnectionOptions, + QueryOptions, ParameterType, + QueryValues, + QueryMeta, + Connection, + PreparedStatement, + TransactionOptions, + Transaction + > = SqlClient< + ConnectionOptions, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, PreparedStatement, TransactionOptions, Transaction - >; + >, +> { + new ( + connectionUrl: string | URL, + options?: Client["options"], + ): Client; } diff --git a/database/sql/connection.ts b/database/sql/connection.ts deleted file mode 100644 index e9b0817..0000000 --- a/database/sql/connection.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { ArrayRow, Row, SqlQueryOptions } from "./core.ts"; - -/** - * SqlConnectionOptions - * - * The options that will be used when connecting to the database. - */ -export interface SqlConnectionOptions { - /** - * Transforms the value that will be sent to the database - */ - transformInput?: (value: unknown) => unknown; - /** - * Transforms the value received from the database - */ - transformOutput?: (value: unknown) => unknown; -} - -/** - * SqlConnection - * - * This represents a connection to a database. - * When a user wants a single connection to the database, - * they should use a class implementing or using this interface. - * - * The class implementing this interface should be able to connect to the database, - * and have the following constructor arguments (if more options are needed, extend the SqlConnectionOptions): - * - connectionUrl: string|URL - * - connectionOptions?: SqlConnectionOptions; - */ -export interface SqlConnection< - ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - ParameterType extends unknown = unknown, - QueryOptions extends SqlQueryOptions = SqlQueryOptions, -> extends AsyncDisposable { - /** - * Connection URL - */ - readonly connectionUrl: string; - - /** - * Connection options - */ - readonly options: ConnectionOptions; - - /** - * Whether the connection is connected to the database - */ - get connected(): boolean; - - /** - * Create a connection to the database - */ - connect(): Promise; - - /** - * Close the connection to the database - */ - close(): Promise; - - /** - * Execute a SQL statement - * - * @param sql the SQL statement - * @param params the parameters to bind to the SQL statement - * @param options the options to pass to the query method, will be merged with the global options - * @returns the number of affected rows if any - */ - execute( - sql: string, - params?: ParameterType[], - options?: QueryOptions, - ): Promise; - - /** - * Query the database and return an iterator. - * Usefull when querying large datasets, as this should take advantage of data streams. - * - * @param sql the SQL statement - * @param params the parameters to bind to the SQL statement - * @param options the options to pass to the query method, will be merged with the global options - * @returns the rows returned by the query as object entries - */ - // deno-lint-ignore no-explicit-any - queryMany = Row>( - sql: string, - params?: ParameterType[], - options?: QueryOptions, - ): AsyncGenerator; - - /** - * Query the database and return an iterator. - * Usefull when querying large datasets, as this should take advantage of data streams. - * - * @param sql the SQL statement - * @param params the parameters to bind to the SQL statement - * @param options the options to pass to the query method, will be merged with the global options - * @returns the rows returned by the query as array entries - */ - // deno-lint-ignore no-explicit-any - queryManyArray = ArrayRow>( - sql: string, - params?: ParameterType[], - options?: QueryOptions, - ): AsyncGenerator; -} - -/** - * SqlConnectable - * - * The base interface for everything that interracts with the connection like querying. - */ -export interface SqlConnectable< - Options extends SqlConnectionOptions = SqlConnectionOptions, - Connection extends SqlConnection = SqlConnection, -> extends AsyncDisposable { - /** - * The (global) options. - */ - readonly options: Options; - - /** - * The connection to the database - */ - readonly connection: Connection; - - /** - * Whether the connection is connected or not - */ - get connected(): boolean; -} diff --git a/database/sql/core.ts b/database/sql/core.ts index d089058..a041427 100644 --- a/database/sql/core.ts +++ b/database/sql/core.ts @@ -1,9 +1,14 @@ // deno-lint-ignore-file no-explicit-any import type { - SqlConnectable, - SqlConnection, - SqlConnectionOptions, -} from "./connection.ts"; + Driver, + DriverConnectable, + DriverConnectionOptions, + DriverInternalOptions, + DriverParameterType, + DriverQueryMeta, + DriverQueryOptions, + DriverQueryValues, +} from "./driver.ts"; /** * Row @@ -31,16 +36,12 @@ export type SqlTransactionOptions = { rollbackTransactionOptions?: Record; }; -/** - * SqlQueryOptions - * - * Options to pass to the query methods. - */ -export interface SqlQueryOptions extends SqlConnectionOptions { - /** - * A signal to abort the query. - */ - signal?: AbortSignal; +export interface TransactionInternalOptions< + ConnectionOptions extends DriverConnectionOptions, + QueryOptions extends DriverQueryOptions, + TransactionOptions extends SqlTransactionOptions, +> extends DriverInternalOptions { + transactionOptions: TransactionOptions; } /** @@ -49,20 +50,34 @@ export interface SqlQueryOptions extends SqlConnectionOptions { * Represents a prepared statement to be executed separately from creation. */ export interface SqlPreparedStatement< - ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - ParameterType extends unknown = unknown, - QueryOptions extends SqlQueryOptions = SqlQueryOptions, - Connection extends SqlConnection< + ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + QueryOptions extends DriverQueryOptions = DriverQueryOptions, + ParameterType extends DriverParameterType = DriverParameterType, + QueryValues extends DriverQueryValues = DriverQueryValues, + QueryMeta extends DriverQueryMeta = DriverQueryMeta, + Connection extends Driver< ConnectionOptions, + QueryOptions, ParameterType, - QueryOptions - > = SqlConnection< + QueryValues, + QueryMeta + > = Driver< ConnectionOptions, + QueryOptions, ParameterType, - QueryOptions + QueryValues, + QueryMeta >, -> extends SqlConnectable { - readonly options: ConnectionOptions & QueryOptions; +> extends + DriverConnectable< + ConnectionOptions, + QueryOptions, + ParameterType, + QueryValues, + QueryMeta, + Connection + > { + readonly options: DriverInternalOptions; /** * The SQL statement @@ -72,7 +87,7 @@ export interface SqlPreparedStatement< /** * Whether the prepared statement has been deallocated or not. */ - deallocated: boolean; + readonly deallocated: boolean; /** * Deallocate the prepared statement @@ -168,20 +183,34 @@ export interface SqlPreparedStatement< * Represents an object that can execute SQL queries. */ export interface SqlQueriable< - ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - ParameterType extends unknown = unknown, - QueryOptions extends SqlQueryOptions = SqlQueryOptions, - Connection extends SqlConnection< + ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + QueryOptions extends DriverQueryOptions = DriverQueryOptions, + ParameterType extends DriverParameterType = DriverParameterType, + QueryValues extends DriverQueryValues = DriverQueryValues, + QueryMeta extends DriverQueryMeta = DriverQueryMeta, + Connection extends Driver< ConnectionOptions, + QueryOptions, ParameterType, - QueryOptions - > = SqlConnection< + QueryValues, + QueryMeta + > = Driver< ConnectionOptions, + QueryOptions, ParameterType, - QueryOptions + QueryValues, + QueryMeta >, -> extends SqlConnectable { - readonly options: ConnectionOptions & QueryOptions; +> extends + DriverConnectable< + ConnectionOptions, + QueryOptions, + ParameterType, + QueryValues, + QueryMeta, + Connection + > { + readonly options: DriverInternalOptions; /** * Execute a SQL statement @@ -305,31 +334,48 @@ export interface SqlQueriable< * Represents an object that can create a prepared statement. */ export interface SqlPreparable< - ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - ParameterType extends unknown = unknown, - QueryOptions extends SqlQueryOptions = SqlQueryOptions, - Connection extends SqlConnection< + ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + QueryOptions extends DriverQueryOptions = DriverQueryOptions, + ParameterType extends DriverParameterType = DriverParameterType, + QueryValues extends DriverQueryValues = DriverQueryValues, + QueryMeta extends DriverQueryMeta = DriverQueryMeta, + Connection extends Driver< ConnectionOptions, + QueryOptions, ParameterType, - QueryOptions - > = SqlConnection< + QueryValues, + QueryMeta + > = Driver< ConnectionOptions, + QueryOptions, ParameterType, - QueryOptions + QueryValues, + QueryMeta >, PreparedStatement extends SqlPreparedStatement< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection > = SqlPreparedStatement< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection >, > extends - SqlQueriable { + SqlQueriable< + ConnectionOptions, + QueryOptions, + ParameterType, + QueryValues, + QueryMeta, + Connection + > { /** * Create a prepared statement that can be executed multiple times. * This is useful when you want to execute the same SQL statement multiple times with different parameters. @@ -351,7 +397,7 @@ export interface SqlPreparable< prepare( sql: string, options?: QueryOptions, - ): PreparedStatement; + ): Promise; } /** @@ -360,39 +406,55 @@ export interface SqlPreparable< * Represents a transaction. */ export interface SqlTransaction< - ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - ParameterType extends unknown = unknown, - QueryOptions extends SqlQueryOptions = SqlQueryOptions, - Connection extends SqlConnection< + ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + QueryOptions extends DriverQueryOptions = DriverQueryOptions, + ParameterType extends DriverParameterType = DriverParameterType, + QueryValues extends DriverQueryValues = DriverQueryValues, + QueryMeta extends DriverQueryMeta = DriverQueryMeta, + Connection extends Driver< ConnectionOptions, + QueryOptions, ParameterType, - QueryOptions - > = SqlConnection< + QueryValues, + QueryMeta + > = Driver< ConnectionOptions, + QueryOptions, ParameterType, - QueryOptions + QueryValues, + QueryMeta >, PreparedStatement extends SqlPreparedStatement< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection > = SqlPreparedStatement< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection >, TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, > extends SqlPreparable< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, PreparedStatement > { - readonly options: ConnectionOptions & QueryOptions; + readonly options: TransactionInternalOptions< + ConnectionOptions, + QueryOptions, + TransactionOptions + >; /** * Whether the connection is in an active transaction or not. */ @@ -434,41 +496,55 @@ export interface SqlTransaction< * and should not live after the related connection is closed. */ export interface SqlTransactionable< - ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - ParameterType extends unknown = unknown, - QueryOptions extends SqlQueryOptions = SqlQueryOptions, - Connection extends SqlConnection< + ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + QueryOptions extends DriverQueryOptions = DriverQueryOptions, + ParameterType extends DriverParameterType = DriverParameterType, + QueryValues extends DriverQueryValues = DriverQueryValues, + QueryMeta extends DriverQueryMeta = DriverQueryMeta, + Connection extends Driver< ConnectionOptions, + QueryOptions, ParameterType, - QueryOptions - > = SqlConnection< + QueryValues, + QueryMeta + > = Driver< ConnectionOptions, + QueryOptions, ParameterType, - QueryOptions + QueryValues, + QueryMeta >, PreparedStatement extends SqlPreparedStatement< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection > = SqlPreparedStatement< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection >, TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, Transaction extends SqlTransaction< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, PreparedStatement, TransactionOptions > = SqlTransaction< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, PreparedStatement, TransactionOptions @@ -476,12 +552,18 @@ export interface SqlTransactionable< > extends SqlPreparable< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, PreparedStatement > { - readonly options: ConnectionOptions & QueryOptions; + readonly options: TransactionInternalOptions< + ConnectionOptions, + QueryOptions, + TransactionOptions + >; /** * Starts a transaction */ diff --git a/database/sql/driver.test.ts b/database/sql/driver.test.ts new file mode 100644 index 0000000..7cbe090 --- /dev/null +++ b/database/sql/driver.test.ts @@ -0,0 +1,193 @@ +import { testDriverConnectable, testDriverConnection } from "./testing.ts"; +import type { + Driver, + DriverConnectable, + DriverConnectionOptions, + DriverInternalOptions, + DriverParameterType, + DriverQueryMeta, + DriverQueryNext, + DriverQueryOptions, + DriverQueryValues, +} from "./driver.ts"; +import { SqlError } from "./errors.ts"; +import { assert, assertEquals, assertFalse, assertRejects } from "@std/assert"; + +const testDbQueryParser = (sql: string) => { + return JSON.parse(sql); +}; +type TestDriverParameterType = DriverParameterType; +type TestDriverQueryValues = DriverQueryValues>; +interface TestDriverQueryMeta extends DriverQueryMeta { + test?: string; +} +interface TestDriverConnectionOptions extends DriverConnectionOptions { + test?: string; +} +interface TestDriverQueryOptions extends DriverQueryOptions { + test?: string; +} + +class TestDriverConnection implements + Driver< + TestDriverConnectionOptions, + TestDriverQueryOptions, + TestDriverParameterType, + TestDriverQueryValues, + TestDriverQueryMeta + > { + connectionUrl: string; + options: DriverInternalOptions< + TestDriverConnectionOptions, + TestDriverQueryOptions + >; + _connected: boolean = false; + constructor( + connectionUrl: string, + options: TestDriverConnection["options"], + ) { + this.connectionUrl = connectionUrl; + this.options = options; + } + ping(): Promise { + if (!this._connected) { + throw new SqlError("not connected"); + } + return Promise.resolve(); + } + get connected(): boolean { + return this._connected; + } + connect(): Promise { + this._connected = true; + return Promise.resolve(); + } + close(): Promise { + this._connected = false; + return Promise.resolve(); + } + + async *query< + Values extends TestDriverQueryValues = TestDriverQueryValues, + Meta extends TestDriverQueryMeta = TestDriverQueryMeta, + >( + sql: string, + _params?: TestDriverParameterType[], + _options?: TestDriverQueryOptions, + ): AsyncGenerator> { + if (!this._connected) throw new SqlError("not connected"); + + const queryRes = testDbQueryParser(sql); + for (const row of queryRes) { + const res: DriverQueryNext = { + columns: Object.keys(row), + values: Object.values(row) as Values, + meta: {} as Meta, + }; + + yield res; + } + } + + async [Symbol.asyncDispose](): Promise { + await this.close(); + } +} + +class TestDriverConnectable implements + DriverConnectable< + TestDriverConnectionOptions, + TestDriverQueryOptions, + TestDriverParameterType, + TestDriverQueryValues, + TestDriverQueryMeta, + TestDriverConnection + > { + options: TestDriverConnection["options"]; + connection: TestDriverConnection; + get connected(): boolean { + return this.connection.connected; + } + + constructor( + connection: TestDriverConnectable["connection"], + options: TestDriverConnectable["options"], + ) { + this.connection = connection; + this.options = options; + } + [Symbol.asyncDispose](): Promise { + return this.connection.close(); + } +} + +const connectionUrl = "test"; +const options: TestDriverConnection["options"] = { + connectionOptions: {}, + queryOptions: {}, +}; +const sql = "test"; + +const connection = new TestDriverConnection(connectionUrl, options); +const connectable = new TestDriverConnectable( + connection, + connection.options, +); + +const expects = { + connectionUrl, + options, + clientPoolOptions: options, + sql, +}; + +// Type tester +// @ts-expect-error: qwer is not allowed +const _testingDriverQueryValues: DriverQueryValues<["asdf"]> = ["asdf", "qwer"]; + +Deno.test(`DriverConnection`, async (t) => { + await t.step("test suite", () => { + testDriverConnection(connection, expects); + }); + + await t.step("ping will throw if not connected", async () => { + await using conn = new TestDriverConnection(connectionUrl, options); + + await conn.connect(); + assert(conn.connected); + await conn.ping(); + + await conn.close(); + assertFalse(connection.connected); + assertRejects(async () => { + await conn.ping(); + }); + }); + + await t.step("can query using loop", async () => { + await using conn = new TestDriverConnection(connectionUrl, options); + await conn.connect(); + assert(conn.connected); + const rows: DriverQueryNext[] = []; + for await (const row of conn.query(JSON.stringify([{ a: "b" }]))) { + rows.push(row); + } + assertEquals(rows, [{ columns: ["a"], values: ["b"], meta: {} }]); + }); + + await t.step("can query using collect", async () => { + await using conn = new TestDriverConnection(connectionUrl, options); + await conn.connect(); + assert(conn.connected); + const rows: DriverQueryNext[] = await Array.fromAsync( + conn.query(JSON.stringify([{ a: "b" }])), + ); + assertEquals(rows, [{ columns: ["a"], values: ["b"], meta: {} }]); + }); +}); + +Deno.test(`DriverConnectable`, async (t) => { + await t.step("test suite", () => { + testDriverConnectable(connectable, expects); + }); +}); diff --git a/database/sql/driver.ts b/database/sql/driver.ts new file mode 100644 index 0000000..e0c9ce1 --- /dev/null +++ b/database/sql/driver.ts @@ -0,0 +1,180 @@ +/** + * Represents the supported value types the driver can handle + */ +// deno-lint-ignore no-explicit-any +export type DriverParameterType = T; + +/** + * Represents the values returned from the query + */ +// deno-lint-ignore no-explicit-any +export type DriverQueryValues = Array> = T; + +/** + * Represents the meta data returned from the query + */ +// deno-lint-ignore no-explicit-any +export type DriverQueryMeta = Record; + +/** + * DriverConnectionOptions + * + * The options that will be used when connecting to the database. + * + * This is a placeholder for future options. + */ +// deno-lint-ignore no-empty-interface +export interface DriverConnectionOptions {} + +/** + * DriverQueryOptions + * + * Options to pass to the query methods. + */ +export interface DriverQueryOptions { + /** + * A signal to abort the query. + */ + signal?: AbortSignal; + /** + * Transforms the value that will be sent to the database + */ + transformInput?: (value: unknown) => unknown; + /** + * Transforms the value received from the database + */ + transformOutput?: (value: unknown) => unknown; +} + +/** + * DriverQueryNext + * + * The representation of a query row. + */ +export type DriverQueryNext< + Values extends DriverQueryValues = DriverQueryValues, + Meta extends DriverQueryMeta = DriverQueryMeta, +> = { + /** + * The column headers in the same order as the values + */ + columns: string[]; + /** + * The values + */ + values: Values; + /** + * Aditional information from the query + */ + meta: Meta; +}; + +export interface DriverInternalOptions< + ConnectionOptions extends DriverConnectionOptions, + QueryOptions extends DriverQueryOptions, +> { + connectionOptions: ConnectionOptions; + queryOptions: QueryOptions; + // deno-lint-ignore no-explicit-any + [key: string | symbol | number]: any; +} + +/** + * DriverConnection + * + * This represents a connection to a database. + * When a user wants a single connection to the database, + * they should use a class implementing or using this interface. + * + * The class implementing this interface should be able to connect to the database, + * and have the following constructor arguments (if more options are needed, extend the DriverConnectionOptions): + * - connectionUrl: string|URL + * - connectionOptions?: DriverConnectionOptions; + */ +export interface Driver< + DConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + DQueryOptions extends DriverQueryOptions = DriverQueryOptions, + DParameterType extends DriverParameterType = DriverParameterType, + DQueryValues extends DriverQueryValues = DriverQueryValues, + DQueryMeta extends DriverQueryMeta = DriverQueryMeta, +> extends AsyncDisposable { + /** + * Connection URL + */ + readonly connectionUrl: string; + + /** + * Connection options + */ + readonly options: DriverInternalOptions; + + /** + * Whether the connection is connected to the database + */ + get connected(): boolean; + + /** + * Create a connection to the database + */ + connect(): Promise; + + /** + * Close the connection to the database + */ + close(): Promise; + + /** + * Pings the database connection to check that it's alive + * + * Throws an error if connection is not alive + */ + ping(): Promise; + + /** + * Query the database and return an iterator. + * + * @param sql the SQL statement + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the rows returned by the query + */ + query< + Values extends DQueryValues = DQueryValues, + Meta extends DQueryMeta = DQueryMeta, + >( + sql: string, + params?: DParameterType[], + options?: DQueryOptions, + ): AsyncGenerator>; +} + +/** + * DriverConnectable + * + * The base interface for everything that interracts with the connection like querying. + */ +export interface DriverConnectable< + DConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + DQueryOptions extends DriverQueryOptions = DriverQueryOptions, + DParameterType extends DriverParameterType = DriverParameterType, + DQueryValues extends DriverQueryValues = DriverQueryValues, + DQueryMeta extends DriverQueryMeta = DriverQueryMeta, + DConnection extends Driver< + DConnectionOptions, + DQueryOptions, + DParameterType, + DQueryValues, + DQueryMeta + > = Driver< + DConnectionOptions, + DQueryOptions, + DParameterType, + DQueryValues, + DQueryMeta + >, +> extends AsyncDisposable, Pick { + /** + * The connection to the database + */ + readonly connection: DConnection; +} diff --git a/database/sql/events.ts b/database/sql/events.ts index e6aa4f5..5b1dc47 100644 --- a/database/sql/events.ts +++ b/database/sql/events.ts @@ -1,8 +1,15 @@ /** * Events */ -import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; -import type { SqlConnectable } from "./connection.ts"; +import type { + Driver, + DriverConnectionOptions, + DriverParameterType, + DriverQueryMeta, + DriverQueryOptions, + DriverQueryValues, +} from "./driver.ts"; +import type { DriverConnectable } from "./driver.ts"; /** * Event types @@ -29,18 +36,35 @@ export type SqlPoolConnectionEventType = * SqlErrorEventInit */ export interface SqlErrorEventInit< - Connectable extends SqlConnectable = SqlConnectable, + Connectable extends DriverConnectable = DriverConnectable, > extends ErrorEventInit { connectable?: Connectable; } /** - * SqlConnectableEventInit + * DriverConnectableEventInit * * SQLx Connectable event init */ export interface SqlConnectionEventInit< - Connection extends SqlConnection = SqlConnection, + DConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + DQueryOptions extends DriverQueryOptions = DriverQueryOptions, + DParameterType extends DriverParameterType = DriverParameterType, + DQueryValues extends DriverQueryValues = DriverQueryValues, + DQueryMeta extends DriverQueryMeta = DriverQueryMeta, + Connection extends Driver< + DConnectionOptions, + DQueryOptions, + DParameterType, + DQueryValues, + DQueryMeta + > = Driver< + DConnectionOptions, + DQueryOptions, + DParameterType, + DQueryValues, + DQueryMeta + >, > extends EventInit { connection: Connection; } @@ -76,7 +100,39 @@ export class SqlEvent< * Gets dispatched when a connection is established */ export class SqlConnectEvent< - EventInit extends SqlConnectionEventInit = SqlConnectionEventInit, + ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + QueryOptions extends DriverQueryOptions = DriverQueryOptions, + ParameterType extends DriverParameterType = DriverParameterType, + QueryValues extends DriverQueryValues = DriverQueryValues, + QueryMeta extends DriverQueryMeta = DriverQueryMeta, + Connection extends Driver< + ConnectionOptions, + QueryOptions, + ParameterType, + QueryValues, + QueryMeta + > = Driver< + ConnectionOptions, + QueryOptions, + ParameterType, + QueryValues, + QueryMeta + >, + EventInit extends SqlConnectionEventInit< + ConnectionOptions, + QueryOptions, + ParameterType, + QueryValues, + QueryMeta, + Connection + > = SqlConnectionEventInit< + ConnectionOptions, + QueryOptions, + ParameterType, + QueryValues, + QueryMeta, + Connection + >, > extends SqlEvent<"connect", EventInit> { constructor(eventInitDict: EventInit) { super("connect", eventInitDict); @@ -126,9 +182,23 @@ export class SqlReleaseEvent< * The EventTarget to be used by SQLx */ export class SqlEventTarget< - ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - Connection extends SqlConnection = SqlConnection< - ConnectionOptions + ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + QueryOptions extends DriverQueryOptions = DriverQueryOptions, + ParameterType extends DriverParameterType = DriverParameterType, + QueryValues extends DriverQueryValues = DriverQueryValues, + QueryMeta extends DriverQueryMeta = DriverQueryMeta, + Connection extends Driver< + ConnectionOptions, + QueryOptions, + ParameterType, + QueryValues, + QueryMeta + > = Driver< + ConnectionOptions, + QueryOptions, + ParameterType, + QueryValues, + QueryMeta >, EventType extends SqlPoolConnectionEventType = SqlPoolConnectionEventType, EventInit extends SqlConnectionEventInit = SqlConnectionEventInit< diff --git a/database/sql/mod.ts b/database/sql/mod.ts index f93b657..ccd2b58 100644 --- a/database/sql/mod.ts +++ b/database/sql/mod.ts @@ -1,7 +1,8 @@ export * from "./asserts.ts"; export * from "./client.ts"; -export * from "./connection.ts"; export * from "./core.ts"; +export * from "./driver.ts"; export * from "./errors.ts"; export * from "./events.ts"; export * from "./pool.ts"; +export * from "./utils.ts"; diff --git a/database/sql/pool.ts b/database/sql/pool.ts index 4e4356e..64f7174 100644 --- a/database/sql/pool.ts +++ b/database/sql/pool.ts @@ -1,10 +1,17 @@ -import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; +import type { + Driver, + DriverConnectionOptions, + DriverParameterType, + DriverQueryMeta, + DriverQueryOptions, + DriverQueryValues, +} from "./driver.ts"; import type { SqlPreparedStatement, - SqlQueryOptions, SqlTransaction, SqlTransactionable, SqlTransactionOptions, + TransactionInternalOptions, } from "./core.ts"; import type { SqlEventable, SqlEventTarget } from "./events.ts"; @@ -20,12 +27,26 @@ export interface SqlPoolClientOptions { releaseFn?: () => Promise; } +export interface PoolClientInternalOptions< + ConnectionOptions extends DriverConnectionOptions, + QueryOptions extends DriverQueryOptions, + TransactionOptions extends SqlTransactionOptions, + PoolClientOptions extends SqlPoolClientOptions, +> extends + TransactionInternalOptions< + ConnectionOptions, + QueryOptions, + TransactionOptions + > { + poolClientOptions: PoolClientOptions; +} + /** * SqlClientPoolOptions * * This represents the options for a connection pool. */ -export interface SqlClientPoolOptions extends SqlConnectionOptions { +export interface SqlClientPoolOptions { /** * Whether to lazily initialize connections. * @@ -40,6 +61,22 @@ export interface SqlClientPoolOptions extends SqlConnectionOptions { maxSize?: number; } +export interface ClientPoolInternalOptions< + ConnectionOptions extends DriverConnectionOptions, + QueryOptions extends DriverQueryOptions, + TransactionOptions extends SqlTransactionOptions, + PoolClientOptions extends SqlPoolClientOptions, + ClientPoolOptions extends SqlClientPoolOptions, +> extends + PoolClientInternalOptions< + ConnectionOptions, + QueryOptions, + TransactionOptions, + PoolClientOptions + > { + clientPoolOptions: ClientPoolOptions; +} + /** * SqlPoolClient * @@ -48,61 +85,86 @@ export interface SqlClientPoolOptions extends SqlConnectionOptions { * they should use a class implementing this interface. */ export interface SqlPoolClient< - ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - Connection extends SqlConnection = SqlConnection< - ConnectionOptions - >, - ParameterType extends unknown = unknown, - QueryOptions extends SqlQueryOptions = SqlQueryOptions, - Prepared extends SqlPreparedStatement< + ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + QueryOptions extends DriverQueryOptions = DriverQueryOptions, + ParameterType extends DriverParameterType = DriverParameterType, + QueryValues extends DriverQueryValues = DriverQueryValues, + QueryMeta extends DriverQueryMeta = DriverQueryMeta, + Connection extends Driver< + ConnectionOptions, + QueryOptions, + ParameterType, + QueryValues, + QueryMeta + > = Driver< ConnectionOptions, + QueryOptions, ParameterType, + QueryValues, + QueryMeta + >, + PreparedStatement extends SqlPreparedStatement< + ConnectionOptions, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection > = SqlPreparedStatement< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection >, TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, Transaction extends SqlTransaction< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, - Prepared, + PreparedStatement, TransactionOptions > = SqlTransaction< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, - Prepared, + PreparedStatement, TransactionOptions >, PoolClientOptions extends SqlPoolClientOptions = SqlPoolClientOptions, > extends SqlTransactionable< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, - Prepared, + PreparedStatement, TransactionOptions, Transaction > { /** * The options used to create the pool client */ - readonly options: - & ConnectionOptions - & QueryOptions - & PoolClientOptions; + readonly options: PoolClientInternalOptions< + ConnectionOptions, + QueryOptions, + TransactionOptions, + PoolClientOptions + >; + /** * Whether the pool client is disposed and should not be available anymore */ - disposed: boolean; + readonly disposed: boolean; /** * Release the connection to the pool */ @@ -117,75 +179,100 @@ export interface SqlPoolClient< * they should use a class implementing this interface. */ export interface SqlClientPool< - ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - ParameterType extends unknown = unknown, - QueryOptions extends SqlQueryOptions = SqlQueryOptions, - Connection extends SqlConnection< + ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + QueryOptions extends DriverQueryOptions = DriverQueryOptions, + ParameterType extends DriverParameterType = DriverParameterType, + QueryValues extends DriverQueryValues = DriverQueryValues, + QueryMeta extends DriverQueryMeta = DriverQueryMeta, + Connection extends Driver< ConnectionOptions, + QueryOptions, ParameterType, - QueryOptions - > = SqlConnection< + QueryValues, + QueryMeta + > = Driver< ConnectionOptions, + QueryOptions, ParameterType, - QueryOptions + QueryValues, + QueryMeta >, - Prepared extends SqlPreparedStatement< + PreparedStatement extends SqlPreparedStatement< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection > = SqlPreparedStatement< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection >, TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, Transaction extends SqlTransaction< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, - Prepared, + PreparedStatement, TransactionOptions > = SqlTransaction< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, - Prepared, + PreparedStatement, TransactionOptions >, + PoolClientOptions extends SqlPoolClientOptions = SqlPoolClientOptions, PoolClient extends SqlPoolClient< ConnectionOptions, - Connection, - ParameterType, QueryOptions, - Prepared, + ParameterType, + QueryValues, + QueryMeta, + Connection, + PreparedStatement, TransactionOptions, - Transaction + Transaction, + PoolClientOptions > = SqlPoolClient< ConnectionOptions, - Connection, - ParameterType, QueryOptions, - Prepared, + ParameterType, + QueryValues, + QueryMeta, + Connection, + PreparedStatement, TransactionOptions, - Transaction + Transaction, + PoolClientOptions >, + ClientPoolOptions extends SqlClientPoolOptions = SqlClientPoolOptions, EventTarget extends SqlEventTarget = SqlEventTarget, > extends SqlEventable, Omit< - SqlConnection< + Driver< ConnectionOptions >, - "execute" | "queryMany" | "queryManyArray" + "query" | "ping" > { - readonly options: - & ConnectionOptions - & QueryOptions - & SqlClientPoolOptions; + readonly options: ClientPoolInternalOptions< + ConnectionOptions, + QueryOptions, + TransactionOptions, + PoolClientOptions, + ClientPoolOptions + >; /** * Acquire a connection from the pool @@ -199,62 +286,84 @@ export interface SqlClientPool< * The constructor for the SqlClientPool interface. */ export interface SqlClientPoolConstructor< - ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - ParameterType extends unknown = unknown, - QueryOptions extends SqlQueryOptions = SqlQueryOptions, - Connection extends SqlConnection< + ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + QueryOptions extends DriverQueryOptions = DriverQueryOptions, + ParameterType extends DriverParameterType = DriverParameterType, + QueryValues extends DriverQueryValues = DriverQueryValues, + QueryMeta extends DriverQueryMeta = DriverQueryMeta, + Connection extends Driver< ConnectionOptions, + QueryOptions, ParameterType, - QueryOptions - > = SqlConnection< + QueryValues, + QueryMeta + > = Driver< ConnectionOptions, + QueryOptions, ParameterType, - QueryOptions + QueryValues, + QueryMeta >, PreparedStatement extends SqlPreparedStatement< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection > = SqlPreparedStatement< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection >, TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, Transaction extends SqlTransaction< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, PreparedStatement, TransactionOptions > = SqlTransaction< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, PreparedStatement, TransactionOptions >, + PoolClientOptions extends SqlPoolClientOptions = SqlPoolClientOptions, PoolClient extends SqlPoolClient< ConnectionOptions, - Connection, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, + Connection, PreparedStatement, TransactionOptions, - Transaction + Transaction, + PoolClientOptions > = SqlPoolClient< ConnectionOptions, - Connection, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, + Connection, PreparedStatement, TransactionOptions, - Transaction + Transaction, + PoolClientOptions >, + ClientPoolOptions extends SqlClientPoolOptions = SqlClientPoolOptions, EventTarget extends SqlEventTarget = SqlEventTarget, > { new ( @@ -262,13 +371,17 @@ export interface SqlClientPoolConstructor< options?: ConnectionOptions & QueryOptions, ): SqlClientPool< ConnectionOptions, - ParameterType, QueryOptions, + ParameterType, + QueryValues, + QueryMeta, Connection, PreparedStatement, TransactionOptions, Transaction, + PoolClientOptions, PoolClient, + ClientPoolOptions, EventTarget >; } diff --git a/database/sql/test.ts b/database/sql/test.ts index 30cbd91..b637a40 100644 --- a/database/sql/test.ts +++ b/database/sql/test.ts @@ -1,16 +1,11 @@ import { DeferredStack } from "@stdext/collections"; -import { - type SqlConnectionEventInit, - type SqlEvent, - SqlEventTarget, - type SqlPoolConnectionEventType, -} from "./events.ts"; +import { deepMerge } from "@std/collections"; import { testClientConnection, testClientPoolConnection, + testDriverConnection, testSqlClient, testSqlClientPool, - testSqlConnection, testSqlEventTarget, testSqlPoolClient, testSqlPreparedStatement, @@ -18,108 +13,46 @@ import { } from "./testing.ts"; import * as Sql from "./mod.ts"; -/** - * Represents a database table for a test database - */ -type TestDbTable = Array>; -/** - * Represents a database for testing - */ -type TestDb = Record; - -const testDb: TestDb = {}; - -const testDbQueryParser = (sql: string, parameters: string[] = []) => { - const replaceWildcards = (query: string, values: string[]) => { - for (const v of values) { - query = query.replace("?", v); - } - - return query; - }; - - const querySegments = sql.toLowerCase().split(" "); - if (querySegments[0] === "create") { - if (querySegments[1] === "table") { - const tableName = querySegments[2]; - testDb[tableName] = []; - return; - } - } else if (querySegments[0] === "drop") { - if (querySegments[1] === "table") { - const tableName = querySegments[2]; - delete testDb[tableName]; - return; - } - } else if (querySegments[0] === "insert") { - if (querySegments[1] === "into") { - const tableName = querySegments[2]; - const values = querySegments[4]; - const parsedValues = replaceWildcards(values, parameters); - const jsonValues = JSON.parse(parsedValues); - for (const v of jsonValues) { - testDb[tableName].push(v); - } - return jsonValues.length; - } - } else if (querySegments[0] === "select") { - if (querySegments[1] === "*") { - if (querySegments[2] === "from") { - const tableName = querySegments[3]; - if (querySegments[4]) { - if (querySegments[4] === "where") { - const columnName = querySegments[5]; - const value = parameters[0]; - return testDb[tableName].filter((row) => row[columnName] === value); - } - } else { - return testDb[tableName]; - } - } - } - } else if (querySegments[0] === "return") { - if (querySegments[1].startsWith("'")) { - return [{ result: querySegments[1].replace(/'/g, "") }]; - } else { - return [{ result: JSON.parse(querySegments[1]) }]; - } - } else if (querySegments[0] === "delete") { - if (querySegments[1] === "from") { - const tableName = querySegments[2]; - if (querySegments[3] === "where") { - const columnName = querySegments[4]; - const value = parameters[0]; - testDb[tableName] = testDb[tableName].filter((row) => - row[columnName] !== value - ); - } else { - testDb[tableName] = []; - } - return; - } - } +const testDbQueryParser = (sql: string) => { + return JSON.parse(sql); }; +type TestQueryValues = Sql.DriverQueryValues; +interface TestQueryMeta extends Sql.DriverQueryMeta { + test?: string; +} + type TestRow = Sql.Row; type TestArrayRow = Sql.ArrayRow; type TestParameterType = string; type TestSqlTransactionOptions = Sql.SqlTransactionOptions; -interface TestSqlQueryOptions extends Sql.SqlQueryOptions { + +interface TestSqlQueryOptions extends Sql.DriverQueryOptions { test?: string; } -interface TestSqlConnectionOptions extends Sql.SqlConnectionOptions { +interface TestSqlConnectionOptions extends Sql.DriverConnectionOptions { test?: string; } interface TestSqlClientPoolOptions extends Sql.SqlClientPoolOptions, TestSqlConnectionOptions { } -class TestSqlConnection implements Sql.SqlConnection { - connectionUrl: string; - options: TestSqlConnectionOptions; +class TestDriver implements + Sql.Driver< + TestSqlConnectionOptions, + TestSqlQueryOptions, + TestParameterType, + TestQueryValues, + TestQueryMeta + > { + readonly connectionUrl: string; + readonly options: Sql.DriverInternalOptions< + TestSqlConnectionOptions, + TestSqlQueryOptions + >; _connected: boolean = false; constructor( connectionUrl: string, - options: TestSqlConnectionOptions = {}, + options: TestDriver["options"], ) { this.connectionUrl = connectionUrl; this.options = options; @@ -127,6 +60,10 @@ class TestSqlConnection implements Sql.SqlConnection { get connected(): boolean { return this._connected; } + ping(): Promise { + if (!this.connected) throw new Sql.SqlError("not connected"); + return Promise.resolve(); + } connect(): Promise { this._connected = true; return Promise.resolve(); @@ -135,51 +72,53 @@ class TestSqlConnection implements Sql.SqlConnection { this._connected = false; return Promise.resolve(); } - execute( - sql: string, - params?: unknown[] | undefined, - _options?: Sql.SqlQueryOptions | undefined, - ): Promise { - const queryRes = testDbQueryParser(sql, params as string[]); - return Promise.resolve(queryRes); - } - async *queryMany( - sql: string, - params?: unknown[] | undefined, - _options?: Sql.SqlQueryOptions | undefined, - ): AsyncGenerator { - const queryRes = testDbQueryParser(sql, params as string[]); - for (const row of queryRes) { - yield row; - } - } - async *queryManyArray( + + async *query< + Values extends TestQueryValues = TestQueryValues, + Meta extends Sql.DriverQueryMeta = Sql.DriverQueryMeta, + >( sql: string, - params?: unknown[] | undefined, - _options?: Sql.SqlQueryOptions | undefined, - ): AsyncGenerator { - const queryRes = testDbQueryParser(sql, params as string[]); + _params?: unknown[] | undefined, + _options?: Sql.DriverQueryOptions | undefined, + ): AsyncGenerator> { + const queryRes = testDbQueryParser(sql); for (const row of queryRes) { - const values = Object.values(row); - yield values as T; + const res: Sql.DriverQueryNext = { + columns: Object.keys(row), + values: Object.values(row) as Values, + meta: {} as Meta, + }; + + yield res; } } + async [Symbol.asyncDispose](): Promise { await this.close(); } } -class TestSqlConnectable - implements Sql.SqlConnectable { - options: TestSqlConnectionOptions; - connection: TestSqlConnection; +class TestSqlConnectable implements + Sql.DriverConnectable< + TestSqlConnectionOptions, + TestSqlQueryOptions, + TestParameterType, + TestQueryValues, + TestQueryMeta, + TestDriver + > { + readonly options: Sql.DriverInternalOptions< + TestSqlConnectionOptions, + TestSqlQueryOptions + >; + readonly connection: TestDriver; get connected(): boolean { return this.connection.connected; } constructor( connection: TestSqlConnectable["connection"], - options: TestSqlConnectable["options"] = {}, + options: TestSqlConnectable["options"], ) { this.connection = connection; this.options = options; @@ -193,18 +132,17 @@ class TestSqlPreparedStatement extends TestSqlConnectable implements Sql.SqlPreparedStatement< TestSqlConnectionOptions, - TestParameterType, TestSqlQueryOptions, - TestSqlConnection + TestParameterType, + TestQueryValues, + TestQueryMeta, + TestDriver > { - declare readonly options: - & TestSqlConnectionOptions - & TestSqlQueryOptions; sql: string; constructor( connection: TestSqlPreparedStatement["connection"], sql: string, - options: TestSqlPreparedStatement["options"] = {}, + options: TestSqlPreparedStatement["options"], ) { super(connection, options); this.sql = sql; @@ -215,10 +153,10 @@ class TestSqlPreparedStatement extends TestSqlConnectable return Promise.resolve(); } execute( - params?: TestParameterType[] | undefined, - options?: TestSqlQueryOptions | undefined, + _params?: TestParameterType[] | undefined, + _options?: TestSqlQueryOptions | undefined, ): Promise { - return this.connection.execute(this.sql, params, options); + return Promise.resolve(testDbQueryParser(this.sql)); } query( params?: TestParameterType[] | undefined, @@ -238,7 +176,9 @@ class TestSqlPreparedStatement extends TestSqlConnectable params?: TestParameterType[] | undefined, options?: TestSqlQueryOptions | undefined, ): AsyncGenerator { - return this.connection.queryMany(this.sql, params, options); + return Sql.mapObjectIterable( + this.connection.query(this.sql, params, options), + ); } queryArray( params?: TestParameterType[] | undefined, @@ -258,32 +198,33 @@ class TestSqlPreparedStatement extends TestSqlConnectable params?: TestParameterType[] | undefined, options?: TestSqlQueryOptions | undefined, ): AsyncGenerator { - return this.connection.queryManyArray(this.sql, params, options); + return Sql.mapArrayIterable( + this.connection.query(this.sql, params, options), + ); } } class TestSqlQueriable extends TestSqlConnectable implements Sql.SqlQueriable< TestSqlConnectionOptions, - TestParameterType, TestSqlQueryOptions, - TestSqlConnection + TestParameterType, + TestQueryValues, + TestQueryMeta, + TestDriver > { - declare readonly options: - & TestSqlConnectionOptions - & TestSqlQueryOptions; constructor( connection: TestSqlQueriable["connection"], - options: TestSqlQueriable["options"] = {}, + options: TestSqlQueriable["options"], ) { super(connection, options); } execute( sql: string, - params?: TestParameterType[] | undefined, + _params?: TestParameterType[] | undefined, _options?: TestSqlQueryOptions | undefined, ): Promise { - return Promise.resolve(testDbQueryParser(sql, params)); + return Promise.resolve(testDbQueryParser(sql)); } query( sql: string, @@ -306,7 +247,9 @@ class TestSqlQueriable extends TestSqlConnectable implements params?: TestParameterType[] | undefined, options?: TestSqlQueryOptions | undefined, ): AsyncGenerator { - return this.connection.queryMany(sql, params, options); + return Sql.mapObjectIterable( + this.connection.query(sql, params, options), + ); } queryArray( sql: string, @@ -329,7 +272,9 @@ class TestSqlQueriable extends TestSqlConnectable implements params?: TestParameterType[] | undefined, options?: TestSqlQueryOptions | undefined, ): AsyncGenerator { - return this.connection.queryManyArray(sql, params, options); + return Sql.mapArrayIterable( + this.connection.query(sql, params, options), + ); } sql( strings: TemplateStringsArray, @@ -348,22 +293,32 @@ class TestSqlQueriable extends TestSqlConnectable implements class TestSqlPreparable extends TestSqlQueriable implements Sql.SqlPreparable< TestSqlConnectionOptions, - TestParameterType, TestSqlQueryOptions, - TestSqlConnection, + TestParameterType, + TestQueryValues, + TestQueryMeta, + TestDriver, TestSqlPreparedStatement > { constructor( connection: TestSqlPreparable["connection"], - options: TestSqlPreparable["options"] = {}, + options: TestSqlPreparable["options"], ) { super(connection, options); } prepare( sql: string, options?: TestSqlQueryOptions | undefined, - ): TestSqlPreparedStatement { - return new TestSqlPreparedStatement(this.connection, sql, options); + ): Promise { + return Promise.resolve( + new TestSqlPreparedStatement( + this.connection, + sql, + deepMerge(this.options, { + queryOptions: options, + }), + ), + ); } } @@ -371,16 +326,19 @@ class TestSqlTransaction extends TestSqlPreparable implements Sql.SqlTransaction< TestSqlConnectionOptions, - TestParameterType, TestSqlQueryOptions, - TestSqlConnection, + TestParameterType, + TestQueryValues, + TestQueryMeta, + TestDriver, TestSqlPreparedStatement, TestSqlTransactionOptions > { - declare readonly options: - & TestSqlConnectionOptions - & TestSqlQueryOptions - & TestSqlTransactionOptions; + declare readonly options: Sql.TransactionInternalOptions< + TestSqlConnectionOptions, + TestSqlQueryOptions, + TestSqlTransactionOptions + >; _inTransaction: boolean = false; get inTransaction(): boolean { return this._inTransaction; @@ -388,7 +346,7 @@ class TestSqlTransaction extends TestSqlPreparable constructor( connection: TestSqlTransaction["connection"], - options: TestSqlTransaction["options"] = {}, + options: TestSqlTransaction["options"], ) { super(connection, options); } @@ -414,18 +372,22 @@ class TestSqlTransactionable extends TestSqlPreparable implements Sql.SqlPreparable< TestSqlConnectionOptions, - TestParameterType, TestSqlQueryOptions, - TestSqlConnection, + TestParameterType, + TestQueryValues, + TestQueryMeta, + TestDriver, TestSqlPreparedStatement > { - declare readonly options: - & TestSqlConnectionOptions - & TestSqlQueryOptions - & TestSqlTransactionOptions; + declare readonly options: Sql.TransactionInternalOptions< + TestSqlConnectionOptions, + TestSqlQueryOptions, + TestSqlTransactionOptions + >; + constructor( connection: TestSqlTransactionable["connection"], - options: TestSqlTransactionable["options"] = {}, + options: TestSqlTransactionable["options"], ) { super(connection, options); } @@ -445,41 +407,44 @@ class TestSqlTransactionable extends TestSqlPreparable } } -type TestSqlConnectionEventInit = SqlConnectionEventInit; +type TestSqlConnectionEventInit = Sql.SqlConnectionEventInit; -class TestSqlEventTarget extends SqlEventTarget< +class TestSqlEventTarget extends Sql.SqlEventTarget< TestSqlConnectionOptions, - TestSqlConnection, - SqlPoolConnectionEventType, + TestSqlQueryOptions, + TestParameterType, + TestQueryValues, + TestQueryMeta, + TestDriver, + Sql.SqlPoolConnectionEventType, TestSqlConnectionEventInit, - SqlEvent, + Sql.SqlEvent, EventListenerOrEventListenerObject, AddEventListenerOptions, EventListenerOptions > { } -const TestSqlClient = class extends TestSqlTransactionable - implements - Sql.SqlClient< - TestSqlEventTarget, - TestSqlConnectionOptions, - TestParameterType, - TestSqlQueryOptions, - TestSqlConnection, - TestSqlPreparedStatement, - TestSqlTransactionOptions, - TestSqlTransaction - > { - declare readonly options: - & TestSqlConnectionOptions - & TestSqlQueryOptions; +class TestSqlClient extends TestSqlTransactionable implements + Sql.SqlClient< + TestSqlConnectionOptions, + TestSqlQueryOptions, + TestParameterType, + TestQueryValues, + TestQueryMeta, + TestDriver, + TestSqlPreparedStatement, + TestSqlTransactionOptions, + TestSqlTransaction, + TestSqlEventTarget + > { eventTarget: TestSqlEventTarget; constructor( connectionUrl: string | URL, - options: TestSqlConnectionOptions & TestSqlQueryOptions = {}, + options: TestSqlTransactionable["options"], ) { - super(new TestSqlConnection(connectionUrl.toString(), options), options); + const driver = new TestDriver(connectionUrl.toString(), options); + super(driver, options); this.eventTarget = new TestSqlEventTarget(); } async connect(): Promise { @@ -494,18 +459,7 @@ const TestSqlClient = class extends TestSqlTransactionable ); await this.connection.close(); } -} satisfies Sql.SqlClientConstructor< - TestSqlEventTarget, - TestSqlConnectionOptions, - TestParameterType, - TestSqlQueryOptions, - TestSqlConnection, - TestSqlPreparedStatement, - TestSqlTransactionOptions, - TestSqlTransaction ->; - -type TestSqlClient = InstanceType; +} interface TestSqlPoolClientOptions extends Sql.SqlPoolClientOptions { } @@ -514,19 +468,23 @@ class TestSqlPoolClient extends TestSqlTransactionable implements Sql.SqlPoolClient< TestSqlConnectionOptions, - TestSqlConnection, - TestParameterType, TestSqlQueryOptions, + TestParameterType, + TestQueryValues, + TestQueryMeta, + TestDriver, TestSqlPreparedStatement, TestSqlTransactionOptions, TestSqlTransaction, TestSqlPoolClientOptions > { - declare readonly options: - & TestSqlConnectionOptions - & TestSqlQueryOptions - & TestSqlTransactionOptions - & TestSqlPoolClientOptions; + declare readonly options: Sql.PoolClientInternalOptions< + TestSqlConnectionOptions, + TestSqlQueryOptions, + TestSqlTransactionOptions, + TestSqlPoolClientOptions + >; + #releaseFn?: () => Promise; #disposed: boolean = false; @@ -536,11 +494,11 @@ class TestSqlPoolClient extends TestSqlTransactionable constructor( connection: TestSqlPoolClient["connection"], - options: TestSqlPoolClient["options"] = {}, + options: TestSqlPoolClient["options"], ) { super(connection, options); - if (this.options?.releaseFn) { - this.#releaseFn = this.options.releaseFn; + if (this.options?.poolClientOptions.releaseFn) { + this.#releaseFn = this.options?.poolClientOptions.releaseFn; } } async release() { @@ -553,24 +511,29 @@ class TestSqlPoolClient extends TestSqlTransactionable } } -const TestSqlClientPool = class implements +class TestSqlClientPool implements Sql.SqlClientPool< - TestSqlClientPoolOptions, - TestParameterType, + TestSqlConnectionOptions, TestSqlQueryOptions, - TestSqlConnection, + TestParameterType, + TestQueryValues, + TestQueryMeta, + TestDriver, TestSqlPreparedStatement, TestSqlTransactionOptions, TestSqlTransaction, - TestSqlPoolClient, - TestSqlEventTarget + TestSqlPoolClientOptions, + TestSqlPoolClient > { - declare readonly options: - & TestSqlConnectionOptions - & TestSqlQueryOptions - & TestSqlTransactionOptions - & TestSqlClientPoolOptions; - deferredStack: DeferredStack; + declare readonly options: Sql.ClientPoolInternalOptions< + TestSqlConnectionOptions, + TestSqlQueryOptions, + TestSqlTransactionOptions, + TestSqlPoolClientOptions, + TestSqlClientPoolOptions + >; + + deferredStack: DeferredStack; eventTarget: TestSqlEventTarget; connectionUrl: string; _connected: boolean = false; @@ -579,11 +542,11 @@ const TestSqlClientPool = class implements } constructor( connectionUrl: string | URL, - options: TestSqlClientPoolOptions = {}, + options: TestSqlClientPool["options"], ) { this.connectionUrl = connectionUrl.toString(); this.options = options; - this.deferredStack = new DeferredStack({ + this.deferredStack = new DeferredStack({ maxSize: 3, removeFn: async (element) => { await element._value.close(); @@ -593,11 +556,11 @@ const TestSqlClientPool = class implements } async connect(): Promise { for (let i = 0; i < this.deferredStack.maxSize; i++) { - const conn = new TestSqlConnection( + const conn = new TestDriver( this.connectionUrl, this.options, ); - if (!this.options.lazyInitialization) { + if (!this.options.clientPoolOptions.lazyInitialization) { await conn.connect(); this.eventTarget.dispatchEvent( new Sql.SqlConnectEvent({ connection: conn }), @@ -619,39 +582,40 @@ const TestSqlClientPool = class implements this.eventTarget.dispatchEvent( new Sql.SqlAcquireEvent({ connection: el.value }), ); - const c = new TestSqlPoolClient(el.value, { - ...this.options, - releaseFn: async () => { - this.eventTarget.dispatchEvent( - new Sql.SqlReleaseEvent({ connection: el._value }), - ); - await el.release(); - }, - }); + const c = new TestSqlPoolClient( + el.value, + deepMerge( + this.options, + { + poolClientOptions: { + releaseFn: async () => { + this.eventTarget.dispatchEvent( + new Sql.SqlReleaseEvent({ connection: el._value }), + ); + await el.release(); + }, + }, + }, + ), + ); return c; } async [Symbol.asyncDispose](): Promise { await this.close(); } -} satisfies Sql.SqlClientPoolConstructor< - TestSqlClientPoolOptions, - TestParameterType, - TestSqlQueryOptions, - TestSqlConnection, - TestSqlPreparedStatement, - TestSqlTransactionOptions, - TestSqlTransaction, - TestSqlPoolClient, - TestSqlEventTarget ->; - -type TestSqlClientPool = InstanceType; +} const connectionUrl = "test"; -const options: TestSqlTransaction["options"] = { test: "test" }; +const options: TestSqlClientPool["options"] = { + clientPoolOptions: {}, + connectionOptions: {}, + poolClientOptions: {}, + queryOptions: {}, + transactionOptions: {}, +}; const sql = "test"; -const connection = new TestSqlConnection(connectionUrl, options); +const connection = new TestDriver(connectionUrl, options); const preparedStatement = new TestSqlPreparedStatement( connection, sql, @@ -672,7 +636,7 @@ const expects = { Deno.test(`sql/type test`, async (t) => { await t.step("SqlConnection", () => { - testSqlConnection(connection, expects); + testDriverConnection(connection, expects); }); await t.step(`sql/PreparedStatement`, () => { diff --git a/database/sql/testing.ts b/database/sql/testing.ts index 6ac0a0b..e5a0852 100644 --- a/database/sql/testing.ts +++ b/database/sql/testing.ts @@ -3,12 +3,13 @@ import { assertEquals, assertFalse, assertInstanceOf, + assertRejects, } from "@std/assert"; import { + assertIsDriver, + assertIsDriverConnectable, assertIsSqlClient, assertIsSqlClientPool, - assertIsSqlConnectable, - assertIsSqlConnection, assertIsSqlEventable, assertIsSqlPoolClient, assertIsSqlPreparable, @@ -16,27 +17,31 @@ import { assertIsSqlQueriable, assertIsSqlTransaction, assertIsSqlTransactionable, + type DriverConnectable, type SqlClient, type SqlClientPool, - type SqlConnectable, type SqlPoolClient, type SqlPreparedStatement, type SqlQueriable, type SqlTransaction, type SqlTransactionable, } from "./mod.ts"; +import { deepMerge } from "@std/collections"; // deno-lint-ignore no-explicit-any export type AnyConstructor = new (...args: A) => T; -export type ClientConstructorArguments = [ +export type ClientConstructorArguments< + Client extends DriverConnectable = DriverConnectable, +> = [ string, Client["options"], ]; export type ClientPoolConstructorArguments< Client extends SqlClientPool = SqlClientPool, > = [string, Client["options"]]; -export type ClientConstructor = - AnyConstructor>; +export type ClientConstructor< + Client extends DriverConnectable = DriverConnectable, +> = AnyConstructor>; export type ClientPoolConstructor< Client extends SqlClientPool = SqlClientPool, > = AnyConstructor>; @@ -46,31 +51,90 @@ export type ClientPoolConstructor< * @param value The SqlClient * @param expects The values to test against */ -export function testSqlConnection( +export function testDriverConnection( value: unknown, expects: { connectionUrl: string; }, ) { - assertIsSqlConnection(value); + assertIsDriver(value); assertEquals(value.connectionUrl, expects.connectionUrl); } /** - * Test the SqlConnectable class - * @param value The SqlConnectable + * Tests the connection of a SqlClient + */ +export async function testDriverConnectionConstructor< + Client extends SqlClient = SqlClient, +>( + t: Deno.TestContext, + Client: ClientConstructor, + clientArguments: ClientConstructorArguments, +): Promise { + await t.step("testConnectAndClose", async (t) => { + await t.step("should connect and close with using", async () => { + await using db = new Client(...clientArguments); + + await db.connect(); + }); + + await t.step("should connect and close", async () => { + const db = new Client(...clientArguments); + + await db.connect(); + + await db.close(); + }); + + await t.step("should connect and close with events", async () => { + const db = new Client(...clientArguments); + + let connectListenerCalled = false; + let closeListenerCalled = false; + let error: Error | undefined = undefined; + + try { + db.eventTarget.addEventListener("connect", () => { + connectListenerCalled = true; + }); + + db.eventTarget.addEventListener("close", () => { + closeListenerCalled = true; + }); + + await db.connect(); + await db.close(); + } catch (e) { + error = e; + } + + assert( + connectListenerCalled, + "Connect listener not called: " + error?.message, + ); + assert( + closeListenerCalled, + "Close listener not called: " + error?.message, + ); + }); + }); +} + +/** + * Test the DriverConnectable class + * @param value The DriverConnectable * @param expects The values to test against */ -export function _testSqlConnectable( +export function testDriverConnectable( value: unknown, expects: { connectionUrl: string; - options: SqlConnectable["options"]; + options: DriverConnectable["options"]; }, ) { - assertIsSqlConnectable(value); + assertIsDriverConnectable(value); assertEquals(value.options, expects.options); - testSqlConnection(value.connection, expects); + testDriverConnection(value.connection, expects); } /** @@ -87,7 +151,7 @@ export function testSqlPreparedStatement( }, ) { assertIsSqlPreparedStatement(value); - _testSqlConnectable(value, expects); + testDriverConnectable(value, expects); assertEquals(value.sql, expects.sql); } @@ -104,7 +168,7 @@ export function _testSqlQueriable( }, ) { assertIsSqlQueriable(value); - _testSqlConnectable(value, expects); + testDriverConnectable(value, expects); } /** @@ -306,10 +370,20 @@ export async function testClientPoolConnection< await db.close(); }); await t.step("should connect and close with using", async () => { - await using db = new Client(clientArguments[0], { - ...clientArguments[1], - lazyInitialization: true, - }); + const opts = deepMerge( + clientArguments[1], + // deno-lint-ignore ban-ts-comment + // @ts-ignore + { + clientPoolOptions: { + lazyInitialization: true, + }, + }, + ); + await using db = new Client( + clientArguments[0], + opts, + ); let connectListenerCalled = false; db.eventTarget.addEventListener("connect", () => { @@ -361,3 +435,63 @@ export async function testClientPoolConnection< }); }); } + +export async function testClientSanity< + Client extends SqlClient = SqlClient, +>( + t: Deno.TestContext, + Client: ClientConstructor, + clientArguments: ClientConstructorArguments, +): Promise { + await testClientConnection(t, Client, clientArguments); + + const client = new Client(...clientArguments); + + await client.connect(); + + // Testing prepared statements + + const stmt1 = client.prepare("select 1 as one;"); + + assertIsSqlPreparedStatement(stmt1); + assertFalse(stmt1.deallocated); + + const stmt2 = client.prepare("select 1 as one;"); + + assertIsSqlPreparedStatement(stmt2); + assertFalse(stmt2.deallocated); + + await stmt1.execute(); + await stmt1.deallocate(); + + assert(stmt1.deallocated); + + assertRejects(async () => { + await stmt1.execute(); + }); + + await stmt2.execute(); + await stmt2.deallocate(); + + assert(stmt2.deallocated); + + assertRejects(async () => { + await stmt2.execute(); + }); + + // Testing transactions + + const transaction = await client.beginTransaction(); + + assert(transaction.inTransaction); + + await transaction.execute("select 1 as one;"); + + await transaction.commitTransaction(); + + assertFalse(transaction.inTransaction); + + assertRejects(async () => { + await transaction.execute("select 1 as one;"); + }); +} diff --git a/database/sql/utils.test.ts b/database/sql/utils.test.ts new file mode 100644 index 0000000..09d6833 --- /dev/null +++ b/database/sql/utils.test.ts @@ -0,0 +1,82 @@ +import { assertEquals } from "@std/assert"; +import { + getObjectFromRow, + mapArrayIterable, + mapObjectIterable, +} from "./utils.ts"; +import type { DriverQueryNext } from "./driver.ts"; + +Deno.test("getObjectFromRow", async (t) => { + await t.step("empty row", () => { + assertEquals(getObjectFromRow({ columns: [], values: [], meta: {} }), {}); + }); + + await t.step("filled row", () => { + assertEquals( + getObjectFromRow({ columns: ["a", "b"], values: ["c", 1], meta: {} }), + { a: "c", b: 1 }, + ); + }); + + await t.step("more columns row", () => { + assertEquals( + getObjectFromRow({ columns: ["a", "b"], values: ["c"], meta: {} }), + { a: "c", b: undefined }, + ); + }); + + await t.step("more values row", () => { + assertEquals( + getObjectFromRow({ columns: ["a"], values: ["c", 1], meta: {} }), + { a: "c" }, + ); + }); +}); + +Deno.test("mapArrayIterable", async (t) => { + await t.step("empty row", async () => { + const itt = async function* () { + yield { columns: [], values: [], meta: {} } as DriverQueryNext; + }; + + const actual = await Array.fromAsync(mapArrayIterable(itt())); + assertEquals(actual, [[]]); + }); + + await t.step("filled row", async () => { + const itt = async function* () { + yield { + columns: ["a", "b"], + values: ["c", 1], + meta: {}, + } as DriverQueryNext; + }; + + const actual = await Array.fromAsync(mapArrayIterable(itt())); + assertEquals(actual, [["c", 1]]); + }); +}); + +Deno.test("mapObjectIterable", async (t) => { + await t.step("empty row", async () => { + const itt = async function* () { + yield { columns: [], values: [], meta: {} } as DriverQueryNext; + }; + + const actual = await Array.fromAsync(mapObjectIterable(itt())); + assertEquals(actual, [{}]); + }); + + await t.step("filled row", async () => { + const itt = async function* () { + yield { + columns: ["a", "b"], + values: ["c", 1], + meta: {}, + } as DriverQueryNext; + }; + + const actual = await Array.fromAsync(mapObjectIterable(itt())); + assertEquals(actual, [{ a: "c", b: 1 }]); + }); +}); diff --git a/database/sql/utils.ts b/database/sql/utils.ts new file mode 100644 index 0000000..6dd2152 --- /dev/null +++ b/database/sql/utils.ts @@ -0,0 +1,78 @@ +import type { DriverQueryNext } from "./driver.ts"; + +/** + * Takes a row object and returns a mapped object + */ +export function getObjectFromRow< + Output extends Record = Record, + Row extends DriverQueryNext = DriverQueryNext, +>(row: Row): Output { + const rowObject: Output = {} as Output; + + for (let i = 0; i < row.columns.length; i++) { + // deno-lint-ignore ban-ts-comment + // @ts-ignore + rowObject[row.columns[i]] = row.values[i]; + } + + return rowObject; +} + +/** + * Takes a row iterable and maps only the values + */ +export async function* mapArrayIterable< + Output extends Array = Array, + Row extends DriverQueryNext = DriverQueryNext, +>(q: AsyncIterable): AsyncGenerator { + for await (const row of q) { + yield row.values as Output; + } +} + +/** + * Takes a row iterable and maps an object + */ +export async function* mapObjectIterable< + Output extends Record = Record, + Row extends DriverQueryNext = DriverQueryNext, +>(q: AsyncIterable): AsyncGenerator { + for await (const row of q) { + yield getObjectFromRow(row); + } +} + +// export function getObjectsFromRows< +// Output extends Record = Record, +// Row extends DriverQueryNext = DriverQueryNext, +// >(rows: Row[]): Output[] { +// return rows.map(getObjectFromRow); +// } + +// export function getObjectsFromIterable< +// Output extends Record = Record, +// Row extends DriverQueryNext = DriverQueryNext, +// >(q: AsyncIterable): Promise { +// return Array.fromAsync(mapObjectIterable(q)); +// } + +// export async function getFirstFromIterable< +// Row extends DriverQueryNext = DriverQueryNext, +// >(q: AsyncIterable): Promise { +// const res = await Array.fromAsync(q); + +// return res[0] ?? null; +// } + +// export async function getFirstObjectFromIterable< +// Output extends Record = Record, +// Row extends DriverQueryNext = DriverQueryNext, +// >(q: AsyncIterable): Promise { +// const res = await getFirstFromIterable(q); + +// if (!res) { +// return null; +// } + +// return getObjectFromRow(res); +// } diff --git a/deno.json b/deno.json index 1638c58..cd3e71c 100644 --- a/deno.json +++ b/deno.json @@ -1,9 +1,10 @@ { "lock": false, "imports": { - "@std/assert": "jsr:@std/assert@^1.0.2", - "@std/path": "jsr:@std/path@^1.0.2", - "@std/encoding": "jsr:@std/encoding@^1.0.1", + "@std/assert": "jsr:@std/assert@^1", + "@std/collections": "jsr:@std/collections@^1", + "@std/encoding": "jsr:@std/encoding@^1", + "@std/path": "jsr:@std/path@^1", "@stdext/collections": "jsr:@stdext/collections", "@stdext/crypto": "jsr:@stdext/crypto", "@stdext/database": "jsr:@stdext/database", @@ -14,6 +15,7 @@ "bump_version": "deno run --allow-env=VERSION --allow-read=. --allow-write=. ./_tools/bump_version.ts", "check": "deno task format:check && deno lint && deno check **/*.ts", "test": "RUST_BACKTRACE=1 deno test --unstable-http --unstable-webgpu --allow-all --parallel --coverage --trace-leaks", + "test:inspect": "RUST_BACKTRACE=1 deno test --unstable-http --unstable-webgpu --allow-all --parallel --coverage --trace-leaks --inspect-wait", "cov:gen": "deno coverage coverage --lcov --output=cov.lcov", "build": "deno task build:wasm", "build:check": "deno task build:wasm:check", From a9718db659f5ba66e46844949121072d936d3c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sun, 6 Oct 2024 18:40:30 +0200 Subject: [PATCH 22/29] Updated names and type signatures --- database/sql/asserts.ts | 166 ++++++----- database/sql/client.ts | 237 +++++----------- database/sql/core.ts | 481 ++++++++++++++++---------------- database/sql/driver.test.ts | 26 +- database/sql/driver.ts | 67 ++--- database/sql/errors.ts | 2 +- database/sql/events.ts | 211 ++++++-------- database/sql/pool.ts | 538 ++++++++++++++++++------------------ database/sql/test.ts | 384 +++++++++++++------------ database/sql/testing.ts | 216 +++++++-------- database/sql/utils.ts | 35 --- 11 files changed, 1123 insertions(+), 1240 deletions(-) diff --git a/database/sql/asserts.ts b/database/sql/asserts.ts index bee7c58..4a6c173 100644 --- a/database/sql/asserts.ts +++ b/database/sql/asserts.ts @@ -1,17 +1,15 @@ import { assertExists, assertInstanceOf, AssertionError } from "@std/assert"; -import { - type Driver, - type DriverConnectable, - type SqlClient, - type SqlClientPool, - SqlError, - type SqlEventable, - type SqlPoolClient, - type SqlPreparedStatement, - type SqlQueriable, - type SqlTransaction, - type SqlTransactionable, -} from "./mod.ts"; +import type { Driver, DriverConnectable } from "./driver.ts"; +import type { Client } from "./client.ts"; +import type { + PreparedStatement, + Queriable, + Transaction, + Transactionable, +} from "./core.ts"; +import type { Eventable } from "./events.ts"; +import type { ClientPool, PoolClient } from "./pool.ts"; +import { SqlError } from "./errors.ts"; /** * Check if an object has a property @@ -121,9 +119,9 @@ export function assertIsAsyncDisposable( } /** - * Check if an object is an SqlConnection + * Check if an object is a Driver */ -export function isSqlConnection( +export function isDriver( value: unknown, ): value is Driver { return isAsyncDisposable(value) && hasProperties( @@ -141,7 +139,7 @@ export function isSqlConnection( } /** - * Asserts that an object is an SqlConnection + * Asserts that an object is a Driver */ export function assertIsDriver( value: unknown, @@ -172,7 +170,7 @@ export function isDriverConnectable( "connection", "connected", ], - ) && isSqlConnection(value.connection); + ) && isDriver(value.connection); } /** @@ -193,11 +191,11 @@ export function assertIsDriverConnectable( } /** - * Check if an object is an SqlPreparedStatement + * Check if an object is an PreparedStatement */ -export function isSqlPreparedStatement( +export function isPreparedStatement( value: unknown, -): value is SqlPreparedStatement { +): value is PreparedStatement { return isDriverConnectable(value) && hasProperties( value, [ @@ -215,11 +213,11 @@ export function isSqlPreparedStatement( } /** - * Asserts that an object is an SqlPreparedStatement + * Asserts that an object is an PreparedStatement */ -export function assertIsSqlPreparedStatement( +export function assertIsPreparedStatement( value: unknown, -): asserts value is SqlPreparedStatement { +): asserts value is PreparedStatement { assertIsDriverConnectable(value); assertHasProperties( value, @@ -238,11 +236,11 @@ export function assertIsSqlPreparedStatement( } /** - * Check if an object is an SqlQueriable + * Check if an object is an Queriable */ -export function isSqlQueriable( +export function isQueriable( value: unknown, -): value is SqlQueriable { +): value is Queriable { return isDriverConnectable(value) && hasProperties( value, [ @@ -259,11 +257,11 @@ export function isSqlQueriable( } /** - * Asserts that an object is an SqlQueriable + * Asserts that an object is an Queriable */ -export function assertIsSqlQueriable( +export function assertIsQueriable( value: unknown, -): asserts value is SqlQueriable { +): asserts value is Queriable { assertIsDriverConnectable(value); assertHasProperties( value, @@ -281,12 +279,12 @@ export function assertIsSqlQueriable( } /** - * Check if an object is an SqlTransaction + * Check if an object is an Transaction */ -export function isSqlPreparable( +export function isPreparable( value: unknown, -): value is SqlQueriable { - return isSqlQueriable(value) && hasProperties( +): value is Queriable { + return isQueriable(value) && hasProperties( value, [ "prepare", @@ -295,12 +293,12 @@ export function isSqlPreparable( } /** - * Asserts that an object is an SqlTransaction + * Asserts that an object is an Transaction */ -export function assertIsSqlPreparable( +export function assertIsPreparable( value: unknown, -): asserts value is SqlQueriable { - assertIsSqlQueriable(value); +): asserts value is Queriable { + assertIsQueriable(value); assertHasProperties( value, [ @@ -310,12 +308,12 @@ export function assertIsSqlPreparable( } /** - * Check if an object is an SqlTransaction + * Check if an object is an Transaction */ -export function isSqlTransaction( +export function isTransaction( value: unknown, -): value is SqlTransaction { - return isSqlPreparable(value) && hasProperties( +): value is Transaction { + return isPreparable(value) && hasProperties( value, [ "inTransaction", @@ -328,12 +326,12 @@ export function isSqlTransaction( } /** - * Asserts that an object is an SqlTransaction + * Asserts that an object is an Transaction */ -export function assertIsSqlTransaction( +export function assertIsTransaction( value: unknown, -): asserts value is SqlTransaction { - assertIsSqlPreparable(value); +): asserts value is Transaction { + assertIsPreparable(value); assertHasProperties( value, [ @@ -347,12 +345,12 @@ export function assertIsSqlTransaction( } /** - * Check if an object is an SqlTransactionable + * Check if an object is an Transactionable */ -export function isSqlTransactionable( +export function isTransactionable( value: unknown, -): value is SqlTransactionable { - return isSqlPreparable(value) && hasProperties( +): value is Transactionable { + return isPreparable(value) && hasProperties( value, [ "beginTransaction", @@ -362,12 +360,12 @@ export function isSqlTransactionable( } /** - * Asserts that an object is an SqlTransactionable + * Asserts that an object is an Transactionable */ -export function assertIsSqlTransactionable( +export function assertIsTransactionable( value: unknown, -): asserts value is SqlTransactionable { - assertIsSqlPreparable(value); +): asserts value is Transactionable { + assertIsPreparable(value); assertHasProperties( value, [ @@ -378,52 +376,52 @@ export function assertIsSqlTransactionable( } /** - * Check if an object is an SqlEventable + * Check if an object is an Eventable */ -export function isSqlEventable( +export function isEventable( value: unknown, -): value is SqlEventable { +): value is Eventable { return hasProperties(value, ["eventTarget"]) && value.eventTarget instanceof EventTarget; } /** - * Asserts that an object is an SqlEventable + * Asserts that an object is an Eventable */ -export function assertIsSqlEventable( +export function assertIsEventable( value: unknown, -): asserts value is SqlEventable { +): asserts value is Eventable { assertHasProperties(value, ["eventTarget"]); assertInstanceOf(value.eventTarget, EventTarget); } /** - * Check if an object is an SqlClient + * Check if an object is an Client */ -export function isSqlClient(value: unknown): value is SqlClient { - return isSqlConnection(value) && isSqlQueriable(value) && - isSqlTransactionable(value) && isSqlEventable(value) && +export function isClient(value: unknown): value is Client { + return isDriver(value) && isQueriable(value) && + isTransactionable(value) && isEventable(value) && hasProperties(value, ["options"]); } /** - * Asserts that an object is an SqlClient + * Asserts that an object is an Client */ -export function assertIsSqlClient(value: unknown): asserts value is SqlClient { +export function assertIsClient(value: unknown): asserts value is Client { isDriverConnectable(value); - assertIsSqlQueriable(value); - assertIsSqlTransactionable(value); - assertIsSqlEventable(value); + assertIsQueriable(value); + assertIsTransactionable(value); + assertIsEventable(value); assertHasProperties(value, ["options"]); } /** - * Check if an object is an SqlPoolClient + * Check if an object is an PoolClient */ -export function isSqlPoolClient( +export function isPoolClient( value: unknown, -): value is SqlPoolClient { - return isDriverConnectable(value) && isSqlTransactionable(value) && +): value is PoolClient { + return isDriverConnectable(value) && isTransactionable(value) && hasProperties(value, [ "options", "disposed", @@ -432,13 +430,13 @@ export function isSqlPoolClient( } /** - * Asserts that an object is an SqlPoolClient + * Asserts that an object is an PoolClient */ -export function assertIsSqlPoolClient( +export function assertIsPoolClient( value: unknown, -): asserts value is SqlPoolClient { +): asserts value is PoolClient { assertIsDriverConnectable(value); - assertIsSqlTransactionable(value); + assertIsTransactionable(value); assertHasProperties(value, [ "options", "disposed", @@ -447,12 +445,12 @@ export function assertIsSqlPoolClient( } /** - * Check if an object is an SqlClientPool + * Check if an object is an ClientPool */ -export function isSqlClientPool( +export function isClientPool( value: unknown, -): value is SqlClientPool { - return isSqlEventable(value) && isAsyncDisposable(value) && +): value is ClientPool { + return isEventable(value) && isAsyncDisposable(value) && hasProperties(value, [ "connectionUrl", "options", @@ -465,12 +463,12 @@ export function isSqlClientPool( } /** - * Asserts that an object is an SqlClientPool + * Asserts that an object is an ClientPool */ -export function assertIsSqlClientPool( +export function assertIsClientPool( value: unknown, -): asserts value is SqlClientPool { - assertIsSqlEventable(value); +): asserts value is ClientPool { + assertIsEventable(value); assertIsAsyncDisposable(value); assertHasProperties(value, [ "connectionUrl", diff --git a/database/sql/client.ts b/database/sql/client.ts index e4447b6..edfe5f1 100644 --- a/database/sql/client.ts +++ b/database/sql/client.ts @@ -1,3 +1,9 @@ +import type { + PreparedStatement, + Transaction, + Transactionable, + TransactionOptions, +} from "./core.ts"; import type { Driver, DriverConnectionOptions, @@ -6,184 +12,91 @@ import type { DriverQueryOptions, DriverQueryValues, } from "./driver.ts"; -import type { - SqlPreparedStatement, - SqlTransaction, - SqlTransactionable, - SqlTransactionOptions, -} from "./core.ts"; -import type { SqlEventable, SqlEventTarget } from "./events.ts"; +import type { Eventable, SqlEventTarget } from "./events.ts"; /** - * SqlClient + * Client * * This represents a database client. When you need a single connection * to the database, you will in most cases use this interface. */ -export interface SqlClient< - ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, - QueryOptions extends DriverQueryOptions = DriverQueryOptions, - ParameterType extends DriverParameterType = DriverParameterType, - QueryValues extends DriverQueryValues = DriverQueryValues, - QueryMeta extends DriverQueryMeta = DriverQueryMeta, - Connection extends Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta +export interface Client< + IConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions = DriverQueryOptions, + IParameterType extends DriverParameterType = DriverParameterType, + IQueryValues extends DriverQueryValues = DriverQueryValues, + IQueryMeta extends DriverQueryMeta = DriverQueryMeta, + IDriver extends Driver< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta > = Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta >, - PreparedStatement extends SqlPreparedStatement< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection - > = SqlPreparedStatement< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection + IPreparedStatement extends PreparedStatement< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver + > = PreparedStatement< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver >, - TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, - Transaction extends SqlTransaction< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions - > = SqlTransaction< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions + ITransactionOptions extends TransactionOptions = TransactionOptions, + ITransaction extends Transaction< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement, + ITransactionOptions + > = Transaction< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement, + ITransactionOptions >, - EventTarget extends SqlEventTarget = SqlEventTarget, + IEventTarget extends SqlEventTarget = SqlEventTarget, > extends Pick< Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta >, "close" | "connect" | "connected" >, - SqlTransactionable< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions, - Transaction + Transactionable< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement, + ITransactionOptions, + ITransaction >, - SqlEventable, + Eventable, AsyncDisposable { } - -/** - * SqlClientConstructor - * - * The constructor for the SqlClient interface. - */ -export interface SqlClientConstructor< - ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, - QueryOptions extends DriverQueryOptions = DriverQueryOptions, - ParameterType extends DriverParameterType = DriverParameterType, - QueryValues extends DriverQueryValues = DriverQueryValues, - QueryMeta extends DriverQueryMeta = DriverQueryMeta, - Connection extends Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta - > = Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta - >, - PreparedStatement extends SqlPreparedStatement< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection - > = SqlPreparedStatement< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection - >, - TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, - Transaction extends SqlTransaction< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions - > = SqlTransaction< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions - >, - Client extends SqlClient< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions, - Transaction - > = SqlClient< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions, - Transaction - >, -> { - new ( - connectionUrl: string | URL, - options?: Client["options"], - ): Client; -} diff --git a/database/sql/core.ts b/database/sql/core.ts index a041427..8442c19 100644 --- a/database/sql/core.ts +++ b/database/sql/core.ts @@ -25,59 +25,62 @@ export type Row = Record; export type ArrayRow = T[]; /** - * SqlTransactionOptions + * TransactionOptions * * Core transaction options * Used to type the options for the transaction methods */ -export type SqlTransactionOptions = { +export type TransactionOptions = { beginTransactionOptions?: Record; commitTransactionOptions?: Record; rollbackTransactionOptions?: Record; }; +/** + * Internal Transaction options + */ export interface TransactionInternalOptions< - ConnectionOptions extends DriverConnectionOptions, - QueryOptions extends DriverQueryOptions, - TransactionOptions extends SqlTransactionOptions, -> extends DriverInternalOptions { - transactionOptions: TransactionOptions; + IConnectionOptions extends DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions, + ITransactionOptions extends TransactionOptions, +> extends DriverInternalOptions { + transactionOptions: ITransactionOptions; } /** - * SqlPreparedQueriable + * PreparedQueriable * * Represents a prepared statement to be executed separately from creation. */ -export interface SqlPreparedStatement< - ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, - QueryOptions extends DriverQueryOptions = DriverQueryOptions, - ParameterType extends DriverParameterType = DriverParameterType, - QueryValues extends DriverQueryValues = DriverQueryValues, - QueryMeta extends DriverQueryMeta = DriverQueryMeta, - Connection extends Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta +export interface PreparedStatement< + IConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions = DriverQueryOptions, + IParameterType extends DriverParameterType = DriverParameterType, + IQueryValues extends DriverQueryValues = DriverQueryValues, + IQueryMeta extends DriverQueryMeta = DriverQueryMeta, + IDriver extends Driver< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta > = Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta >, > extends DriverConnectable< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver > { - readonly options: DriverInternalOptions; + readonly options: DriverInternalOptions; /** * The SQL statement @@ -102,8 +105,8 @@ export interface SqlPreparedStatement< * @returns the number of affected rows if any */ execute( - params?: ParameterType[], - options?: QueryOptions, + params?: IParameterType[], + options?: IQueryOptions, ): Promise; /** * Query the database with the prepared statement @@ -113,8 +116,8 @@ export interface SqlPreparedStatement< * @returns the rows returned by the query as object entries */ query = Row>( - params?: ParameterType[], - options?: QueryOptions, + params?: IParameterType[], + options?: IQueryOptions, ): Promise; /** * Query the database with the prepared statement, and return at most one row @@ -124,8 +127,8 @@ export interface SqlPreparedStatement< * @returns the row returned by the query as an object entry, or undefined if no row is returned */ queryOne = Row>( - params?: ParameterType[], - options?: QueryOptions, + params?: IParameterType[], + options?: IQueryOptions, ): Promise; /** * Query the database with the prepared statement, and return an iterator. @@ -136,8 +139,8 @@ export interface SqlPreparedStatement< * @returns the rows returned by the query as object entries */ queryMany = Row>( - params?: ParameterType[], - options?: QueryOptions, + params?: IParameterType[], + options?: IQueryOptions, ): AsyncGenerator; /** * Query the database with the prepared statement @@ -147,8 +150,8 @@ export interface SqlPreparedStatement< * @returns the rows returned by the query as array entries */ queryArray = ArrayRow>( - params?: ParameterType[], - options?: QueryOptions, + params?: IParameterType[], + options?: IQueryOptions, ): Promise; /** * Query the database with the prepared statement, and return at most one row @@ -159,8 +162,8 @@ export interface SqlPreparedStatement< * @returns the row returned by the query as an array entry, or undefined if no row is returned */ queryOneArray = ArrayRow>( - params?: ParameterType[], - options?: QueryOptions, + params?: IParameterType[], + options?: IQueryOptions, ): Promise; /** @@ -172,45 +175,45 @@ export interface SqlPreparedStatement< * @returns the rows returned by the query as array entries */ queryManyArray = ArrayRow>( - params?: ParameterType[], - options?: QueryOptions, + params?: IParameterType[], + options?: IQueryOptions, ): AsyncGenerator; } /** - * SqlQueriable + * Queriable * * Represents an object that can execute SQL queries. */ -export interface SqlQueriable< - ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, - QueryOptions extends DriverQueryOptions = DriverQueryOptions, - ParameterType extends DriverParameterType = DriverParameterType, - QueryValues extends DriverQueryValues = DriverQueryValues, - QueryMeta extends DriverQueryMeta = DriverQueryMeta, - Connection extends Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta +export interface Queriable< + IConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions = DriverQueryOptions, + IParameterType extends DriverParameterType = DriverParameterType, + IQueryValues extends DriverQueryValues = DriverQueryValues, + IQueryMeta extends DriverQueryMeta = DriverQueryMeta, + IDriver extends Driver< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta > = Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta >, > extends DriverConnectable< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver > { - readonly options: DriverInternalOptions; + readonly options: DriverInternalOptions; /** * Execute a SQL statement @@ -222,8 +225,8 @@ export interface SqlQueriable< */ execute( sql: string, - params?: ParameterType[], - options?: QueryOptions, + params?: IParameterType[], + options?: IQueryOptions, ): Promise; /** * Query the database @@ -235,8 +238,8 @@ export interface SqlQueriable< */ query = Row>( sql: string, - params?: ParameterType[], - options?: QueryOptions, + params?: IParameterType[], + options?: IQueryOptions, ): Promise; /** * Query the database and return at most one row @@ -248,8 +251,8 @@ export interface SqlQueriable< */ queryOne = Row>( sql: string, - params?: ParameterType[], - options?: QueryOptions, + params?: IParameterType[], + options?: IQueryOptions, ): Promise; /** * Query the database and return an iterator. @@ -262,8 +265,8 @@ export interface SqlQueriable< */ queryMany = Row>( sql: string, - params?: ParameterType[], - options?: QueryOptions, + params?: IParameterType[], + options?: IQueryOptions, ): AsyncGenerator; /** * Query the database @@ -275,8 +278,8 @@ export interface SqlQueriable< */ queryArray = ArrayRow>( sql: string, - params?: ParameterType[], - options?: QueryOptions, + params?: IParameterType[], + options?: IQueryOptions, ): Promise; /** * Query the database and return at most one row @@ -288,8 +291,8 @@ export interface SqlQueriable< */ queryOneArray = ArrayRow>( sql: string, - params?: ParameterType[], - options?: QueryOptions, + params?: IParameterType[], + options?: IQueryOptions, ): Promise; /** @@ -303,8 +306,8 @@ export interface SqlQueriable< */ queryManyArray = ArrayRow>( sql: string, - params?: ParameterType[], - options?: QueryOptions, + params?: IParameterType[], + options?: IQueryOptions, ): AsyncGenerator; /** @@ -314,7 +317,7 @@ export interface SqlQueriable< */ sql = Row>( strings: TemplateStringsArray, - ...parameters: ParameterType[] + ...parameters: IParameterType[] ): Promise; /** @@ -324,57 +327,57 @@ export interface SqlQueriable< */ sqlArray = ArrayRow>( strings: TemplateStringsArray, - ...parameters: ParameterType[] + ...parameters: IParameterType[] ): Promise; } /** - * SqlPreparable + * Preparable * * Represents an object that can create a prepared statement. */ -export interface SqlPreparable< - ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, - QueryOptions extends DriverQueryOptions = DriverQueryOptions, - ParameterType extends DriverParameterType = DriverParameterType, - QueryValues extends DriverQueryValues = DriverQueryValues, - QueryMeta extends DriverQueryMeta = DriverQueryMeta, - Connection extends Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta +export interface Preparable< + IConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions = DriverQueryOptions, + IParameterType extends DriverParameterType = DriverParameterType, + IQueryValues extends DriverQueryValues = DriverQueryValues, + IQueryMeta extends DriverQueryMeta = DriverQueryMeta, + IDriver extends Driver< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta > = Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta >, - PreparedStatement extends SqlPreparedStatement< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection - > = SqlPreparedStatement< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection + IPreparedStatement extends PreparedStatement< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver + > = PreparedStatement< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver >, > extends - SqlQueriable< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection + Queriable< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver > { /** * Create a prepared statement that can be executed multiple times. @@ -396,64 +399,64 @@ export interface SqlPreparable< */ prepare( sql: string, - options?: QueryOptions, - ): Promise; + options?: IQueryOptions, + ): Promise; } /** - * SqlTransaction + * Transaction * * Represents a transaction. */ -export interface SqlTransaction< - ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, - QueryOptions extends DriverQueryOptions = DriverQueryOptions, - ParameterType extends DriverParameterType = DriverParameterType, - QueryValues extends DriverQueryValues = DriverQueryValues, - QueryMeta extends DriverQueryMeta = DriverQueryMeta, - Connection extends Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta +export interface Transaction< + IConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions = DriverQueryOptions, + IParameterType extends DriverParameterType = DriverParameterType, + IQueryValues extends DriverQueryValues = DriverQueryValues, + IQueryMeta extends DriverQueryMeta = DriverQueryMeta, + IDriver extends Driver< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta > = Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta >, - PreparedStatement extends SqlPreparedStatement< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection - > = SqlPreparedStatement< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection + IPreparedStatement extends PreparedStatement< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver + > = PreparedStatement< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver >, - TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, + ITransactionOptions extends TransactionOptions = TransactionOptions, > extends - SqlPreparable< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement + Preparable< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement > { readonly options: TransactionInternalOptions< - ConnectionOptions, - QueryOptions, - TransactionOptions + IConnectionOptions, + IQueryOptions, + ITransactionOptions >; /** * Whether the connection is in an active transaction or not. @@ -464,13 +467,13 @@ export interface SqlTransaction< * Commit the transaction */ commitTransaction( - options?: TransactionOptions["commitTransactionOptions"], + options?: ITransactionOptions["commitTransactionOptions"], ): Promise; /** * Rollback the transaction */ rollbackTransaction( - options?: TransactionOptions["rollbackTransactionOptions"], + options?: ITransactionOptions["rollbackTransactionOptions"], ): Promise; /** * Create a save point @@ -487,7 +490,7 @@ export interface SqlTransaction< } /** - * SqlTransactionable + * Transactionable * * Represents an object that can create a transaction and a prepared statement. * @@ -495,81 +498,81 @@ export interface SqlTransaction< * A prepared statement should in most cases be unique to a connection, * and should not live after the related connection is closed. */ -export interface SqlTransactionable< - ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, - QueryOptions extends DriverQueryOptions = DriverQueryOptions, - ParameterType extends DriverParameterType = DriverParameterType, - QueryValues extends DriverQueryValues = DriverQueryValues, - QueryMeta extends DriverQueryMeta = DriverQueryMeta, - Connection extends Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta +export interface Transactionable< + IConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions = DriverQueryOptions, + IParameterType extends DriverParameterType = DriverParameterType, + IQueryValues extends DriverQueryValues = DriverQueryValues, + IQueryMeta extends DriverQueryMeta = DriverQueryMeta, + IDriver extends Driver< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta > = Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta >, - PreparedStatement extends SqlPreparedStatement< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection - > = SqlPreparedStatement< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection + IPreparedStatement extends PreparedStatement< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver + > = PreparedStatement< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver >, - TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, - Transaction extends SqlTransaction< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions - > = SqlTransaction< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions + ITransactionOptions extends TransactionOptions = TransactionOptions, + ITransaction extends Transaction< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement, + ITransactionOptions + > = Transaction< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement, + ITransactionOptions >, > extends - SqlPreparable< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement + Preparable< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement > { readonly options: TransactionInternalOptions< - ConnectionOptions, - QueryOptions, - TransactionOptions + IConnectionOptions, + IQueryOptions, + ITransactionOptions >; /** * Starts a transaction */ beginTransaction( - options?: TransactionOptions["beginTransactionOptions"], - ): Promise; + options?: ITransactionOptions["beginTransactionOptions"], + ): Promise; /** * Transaction wrapper @@ -583,6 +586,6 @@ export interface SqlTransactionable< * @returns the result of the callback function */ transaction( - fn: (t: Transaction) => Promise, + fn: (t: ITransaction) => Promise, ): Promise; } diff --git a/database/sql/driver.test.ts b/database/sql/driver.test.ts index 7cbe090..1491036 100644 --- a/database/sql/driver.test.ts +++ b/database/sql/driver.test.ts @@ -1,4 +1,4 @@ -import { testDriverConnectable, testDriverConnection } from "./testing.ts"; +import { testDriver, testDriverConnectable } from "./testing.ts"; import type { Driver, DriverConnectable, @@ -28,7 +28,7 @@ interface TestDriverQueryOptions extends DriverQueryOptions { test?: string; } -class TestDriverConnection implements +class TestDriver implements Driver< TestDriverConnectionOptions, TestDriverQueryOptions, @@ -44,7 +44,7 @@ class TestDriverConnection implements _connected: boolean = false; constructor( connectionUrl: string, - options: TestDriverConnection["options"], + options: TestDriver["options"], ) { this.connectionUrl = connectionUrl; this.options = options; @@ -101,10 +101,10 @@ class TestDriverConnectable implements TestDriverParameterType, TestDriverQueryValues, TestDriverQueryMeta, - TestDriverConnection + TestDriver > { - options: TestDriverConnection["options"]; - connection: TestDriverConnection; + options: TestDriver["options"]; + connection: TestDriver; get connected(): boolean { return this.connection.connected; } @@ -122,13 +122,13 @@ class TestDriverConnectable implements } const connectionUrl = "test"; -const options: TestDriverConnection["options"] = { +const options: TestDriver["options"] = { connectionOptions: {}, queryOptions: {}, }; const sql = "test"; -const connection = new TestDriverConnection(connectionUrl, options); +const connection = new TestDriver(connectionUrl, options); const connectable = new TestDriverConnectable( connection, connection.options, @@ -147,11 +147,11 @@ const _testingDriverQueryValues: DriverQueryValues<["asdf"]> = ["asdf", "qwer"]; Deno.test(`DriverConnection`, async (t) => { await t.step("test suite", () => { - testDriverConnection(connection, expects); + testDriver(connection, expects); }); await t.step("ping will throw if not connected", async () => { - await using conn = new TestDriverConnection(connectionUrl, options); + await using conn = new TestDriver(connectionUrl, options); await conn.connect(); assert(conn.connected); @@ -159,13 +159,13 @@ Deno.test(`DriverConnection`, async (t) => { await conn.close(); assertFalse(connection.connected); - assertRejects(async () => { + await assertRejects(async () => { await conn.ping(); }); }); await t.step("can query using loop", async () => { - await using conn = new TestDriverConnection(connectionUrl, options); + await using conn = new TestDriver(connectionUrl, options); await conn.connect(); assert(conn.connected); const rows: DriverQueryNext[] = []; @@ -176,7 +176,7 @@ Deno.test(`DriverConnection`, async (t) => { }); await t.step("can query using collect", async () => { - await using conn = new TestDriverConnection(connectionUrl, options); + await using conn = new TestDriver(connectionUrl, options); await conn.connect(); assert(conn.connected); const rows: DriverQueryNext[] = await Array.fromAsync( diff --git a/database/sql/driver.ts b/database/sql/driver.ts index e0c9ce1..034adc1 100644 --- a/database/sql/driver.ts +++ b/database/sql/driver.ts @@ -69,12 +69,15 @@ export type DriverQueryNext< meta: Meta; }; +/** + * Internal Driver Options + */ export interface DriverInternalOptions< - ConnectionOptions extends DriverConnectionOptions, - QueryOptions extends DriverQueryOptions, + IConnectionOptions extends DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions, > { - connectionOptions: ConnectionOptions; - queryOptions: QueryOptions; + connectionOptions: IConnectionOptions; + queryOptions: IQueryOptions; // deno-lint-ignore no-explicit-any [key: string | symbol | number]: any; } @@ -92,11 +95,11 @@ export interface DriverInternalOptions< * - connectionOptions?: DriverConnectionOptions; */ export interface Driver< - DConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, - DQueryOptions extends DriverQueryOptions = DriverQueryOptions, - DParameterType extends DriverParameterType = DriverParameterType, - DQueryValues extends DriverQueryValues = DriverQueryValues, - DQueryMeta extends DriverQueryMeta = DriverQueryMeta, + IConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions = DriverQueryOptions, + IParameterType extends DriverParameterType = DriverParameterType, + IQueryValues extends DriverQueryValues = DriverQueryValues, + IQueryMeta extends DriverQueryMeta = DriverQueryMeta, > extends AsyncDisposable { /** * Connection URL @@ -106,7 +109,7 @@ export interface Driver< /** * Connection options */ - readonly options: DriverInternalOptions; + readonly options: DriverInternalOptions; /** * Whether the connection is connected to the database @@ -139,12 +142,12 @@ export interface Driver< * @returns the rows returned by the query */ query< - Values extends DQueryValues = DQueryValues, - Meta extends DQueryMeta = DQueryMeta, + Values extends IQueryValues = IQueryValues, + Meta extends IQueryMeta = IQueryMeta, >( sql: string, - params?: DParameterType[], - options?: DQueryOptions, + params?: IParameterType[], + options?: IQueryOptions, ): AsyncGenerator>; } @@ -154,27 +157,27 @@ export interface Driver< * The base interface for everything that interracts with the connection like querying. */ export interface DriverConnectable< - DConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, - DQueryOptions extends DriverQueryOptions = DriverQueryOptions, - DParameterType extends DriverParameterType = DriverParameterType, - DQueryValues extends DriverQueryValues = DriverQueryValues, - DQueryMeta extends DriverQueryMeta = DriverQueryMeta, - DConnection extends Driver< - DConnectionOptions, - DQueryOptions, - DParameterType, - DQueryValues, - DQueryMeta + IConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions = DriverQueryOptions, + IParameterType extends DriverParameterType = DriverParameterType, + IQueryValues extends DriverQueryValues = DriverQueryValues, + IQueryMeta extends DriverQueryMeta = DriverQueryMeta, + IDriver extends Driver< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta > = Driver< - DConnectionOptions, - DQueryOptions, - DParameterType, - DQueryValues, - DQueryMeta + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta >, -> extends AsyncDisposable, Pick { +> extends AsyncDisposable, Pick { /** * The connection to the database */ - readonly connection: DConnection; + readonly connection: IDriver; } diff --git a/database/sql/errors.ts b/database/sql/errors.ts index 72a95c6..57b1941 100644 --- a/database/sql/errors.ts +++ b/database/sql/errors.ts @@ -1,7 +1,7 @@ /** * SqlError * - * Base SQLx Error + * Base Error */ export class SqlError extends Error { constructor(message: string) { diff --git a/database/sql/events.ts b/database/sql/events.ts index 5b1dc47..38b1600 100644 --- a/database/sql/events.ts +++ b/database/sql/events.ts @@ -1,6 +1,3 @@ -/** - * Events - */ import type { Driver, DriverConnectionOptions, @@ -16,15 +13,15 @@ import type { DriverConnectable } from "./driver.ts"; */ /** - * SQLx Client event types + * Client event types */ -export type SqlClientEventType = "connect" | "close" | "error"; +export type ClientEventType = "connect" | "close" | "error"; /** - * SQLx Pool Connection event types + * Pool connection event types */ -export type SqlPoolConnectionEventType = - | SqlClientEventType +export type PoolConnectionEventType = + | ClientEventType | "acquire" | "release"; @@ -36,37 +33,37 @@ export type SqlPoolConnectionEventType = * SqlErrorEventInit */ export interface SqlErrorEventInit< - Connectable extends DriverConnectable = DriverConnectable, + IConnectable extends DriverConnectable = DriverConnectable, > extends ErrorEventInit { - connectable?: Connectable; + connectable?: IConnectable; } /** * DriverConnectableEventInit * - * SQLx Connectable event init - */ -export interface SqlConnectionEventInit< - DConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, - DQueryOptions extends DriverQueryOptions = DriverQueryOptions, - DParameterType extends DriverParameterType = DriverParameterType, - DQueryValues extends DriverQueryValues = DriverQueryValues, - DQueryMeta extends DriverQueryMeta = DriverQueryMeta, - Connection extends Driver< - DConnectionOptions, - DQueryOptions, - DParameterType, - DQueryValues, - DQueryMeta + * IConnectable event init + */ +export interface DriverEventInit< + IConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions = DriverQueryOptions, + IParameterType extends DriverParameterType = DriverParameterType, + IQueryValues extends DriverQueryValues = DriverQueryValues, + IQueryMeta extends DriverQueryMeta = DriverQueryMeta, + IDriver extends Driver< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta > = Driver< - DConnectionOptions, - DQueryOptions, - DParameterType, - DQueryValues, - DQueryMeta + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta >, > extends EventInit { - connection: Connection; + connection: IDriver; } /** @@ -74,24 +71,24 @@ export interface SqlConnectionEventInit< */ /** - * Base SQLx error event class + * Base error event class */ export class SqlErrorEvent< - EventInit extends SqlErrorEventInit = SqlErrorEventInit, + IEventInit extends SqlErrorEventInit = SqlErrorEventInit, > extends ErrorEvent { - constructor(type: "error", eventInitDict?: EventInit) { + constructor(type: "error", eventInitDict?: IEventInit) { super(type, eventInitDict); } } /** - * Base SQLx event class + * Base event class */ export class SqlEvent< - EventType extends SqlPoolConnectionEventType = SqlPoolConnectionEventType, - EventInit extends SqlConnectionEventInit = SqlConnectionEventInit, + IEventType extends PoolConnectionEventType = PoolConnectionEventType, + IEventInit extends DriverEventInit = DriverEventInit, > extends Event { - constructor(type: EventType, eventInitDict?: EventInit) { + constructor(type: IEventType, eventInitDict?: IEventInit) { super(type, eventInitDict); } } @@ -99,42 +96,10 @@ export class SqlEvent< /** * Gets dispatched when a connection is established */ -export class SqlConnectEvent< - ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, - QueryOptions extends DriverQueryOptions = DriverQueryOptions, - ParameterType extends DriverParameterType = DriverParameterType, - QueryValues extends DriverQueryValues = DriverQueryValues, - QueryMeta extends DriverQueryMeta = DriverQueryMeta, - Connection extends Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta - > = Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta - >, - EventInit extends SqlConnectionEventInit< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection - > = SqlConnectionEventInit< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection - >, -> extends SqlEvent<"connect", EventInit> { - constructor(eventInitDict: EventInit) { +export class ConnectEvent< + IEventInit extends DriverEventInit = DriverEventInit, +> extends SqlEvent<"connect", IEventInit> { + constructor(eventInitDict: IEventInit) { super("connect", eventInitDict); } } @@ -142,10 +107,10 @@ export class SqlConnectEvent< /** * Gets dispatched when a connection is about to be closed */ -export class SqlCloseEvent< - EventInit extends SqlConnectionEventInit = SqlConnectionEventInit, -> extends SqlEvent<"close", EventInit> { - constructor(eventInitDict: EventInit) { +export class CloseEvent< + IEventInit extends DriverEventInit = DriverEventInit, +> extends SqlEvent<"close", IEventInit> { + constructor(eventInitDict: IEventInit) { super("close", eventInitDict); } } @@ -153,10 +118,10 @@ export class SqlCloseEvent< /** * Gets dispatched when a connection is acquired from the pool */ -export class SqlAcquireEvent< - EventInit extends SqlConnectionEventInit = SqlConnectionEventInit, -> extends SqlEvent<"acquire", EventInit> { - constructor(eventInitDict: EventInit) { +export class AcquireEvent< + IEventInit extends DriverEventInit = DriverEventInit, +> extends SqlEvent<"acquire", IEventInit> { + constructor(eventInitDict: IEventInit) { super("acquire", eventInitDict); } } @@ -164,10 +129,10 @@ export class SqlAcquireEvent< /** * Gets dispatched when a connection is released back to the pool */ -export class SqlReleaseEvent< - EventInit extends SqlConnectionEventInit = SqlConnectionEventInit, -> extends SqlEvent<"release", EventInit> { - constructor(eventInitDict: EventInit) { +export class ReleaseEvent< + IEventInit extends DriverEventInit = DriverEventInit, +> extends SqlEvent<"release", IEventInit> { + constructor(eventInitDict: IEventInit) { super("release", eventInitDict); } } @@ -177,41 +142,41 @@ export class SqlReleaseEvent< */ /** - * SqlEventTarget + * EventTarget * - * The EventTarget to be used by SQLx + * The EventTarget to be used */ export class SqlEventTarget< - ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, - QueryOptions extends DriverQueryOptions = DriverQueryOptions, - ParameterType extends DriverParameterType = DriverParameterType, - QueryValues extends DriverQueryValues = DriverQueryValues, - QueryMeta extends DriverQueryMeta = DriverQueryMeta, - Connection extends Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta + IConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions = DriverQueryOptions, + IParameterType extends DriverParameterType = DriverParameterType, + IQueryValues extends DriverQueryValues = DriverQueryValues, + IQueryMeta extends DriverQueryMeta = DriverQueryMeta, + IDriver extends Driver< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta > = Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta >, - EventType extends SqlPoolConnectionEventType = SqlPoolConnectionEventType, - EventInit extends SqlConnectionEventInit = SqlConnectionEventInit< - Connection + IEventType extends PoolConnectionEventType = PoolConnectionEventType, + IEventInit extends DriverEventInit = DriverEventInit< + IDriver >, - Event extends SqlEvent = SqlEvent< - EventType, - EventInit + IEvent extends SqlEvent = SqlEvent< + IEventType, + IEventInit >, - Listener extends EventListenerOrEventListenerObject = + IListener extends EventListenerOrEventListenerObject = EventListenerOrEventListenerObject, - ListenerOptions extends AddEventListenerOptions = AddEventListenerOptions, - RemoveListenerOptions extends EventListenerOptions = EventListenerOptions, + IListenerOptions extends AddEventListenerOptions = AddEventListenerOptions, + IRemoveListenerOptions extends EventListenerOptions = EventListenerOptions, > extends EventTarget { /** * With typed events. @@ -219,9 +184,9 @@ export class SqlEventTarget< * @inheritdoc */ addEventListener( - type: EventType, - listener: Listener | null, - options?: boolean | ListenerOptions, + type: IEventType, + listener: IListener | null, + options?: boolean | IListenerOptions, ): void { return super.addEventListener(type, listener, options); } @@ -231,7 +196,7 @@ export class SqlEventTarget< * * @inheritdoc */ - dispatchEvent(event: Event): boolean { + dispatchEvent(event: IEvent): boolean { return super.dispatchEvent(event); } @@ -241,22 +206,22 @@ export class SqlEventTarget< * @inheritdoc */ removeEventListener( - type: EventType, - callback: Listener | null, - options?: boolean | RemoveListenerOptions, + type: IEventType, + callback: IListener | null, + options?: boolean | IRemoveListenerOptions, ): void { return super.removeEventListener(type, callback, options); } } /** - * SqlEventable + * Eventable */ -export interface SqlEventable< - EventTarget extends SqlEventTarget = SqlEventTarget, +export interface Eventable< + IEventTarget extends SqlEventTarget = SqlEventTarget, > { /** * The EventTarget to reduce inheritance */ - eventTarget: EventTarget; + eventTarget: IEventTarget; } diff --git a/database/sql/pool.ts b/database/sql/pool.ts index 64f7174..915fa0c 100644 --- a/database/sql/pool.ts +++ b/database/sql/pool.ts @@ -7,20 +7,20 @@ import type { DriverQueryValues, } from "./driver.ts"; import type { - SqlPreparedStatement, - SqlTransaction, - SqlTransactionable, - SqlTransactionOptions, + PreparedStatement, + Transaction, + Transactionable, TransactionInternalOptions, + TransactionOptions, } from "./core.ts"; -import type { SqlEventable, SqlEventTarget } from "./events.ts"; +import type { Eventable, SqlEventTarget } from "./events.ts"; /** - * SqlPoolClientOptions + * PoolClientOptions * * This represents the options for a pool client. */ -export interface SqlPoolClientOptions { +export interface PoolClientOptions { /** * The function to call when releasing the connection. */ @@ -28,25 +28,25 @@ export interface SqlPoolClientOptions { } export interface PoolClientInternalOptions< - ConnectionOptions extends DriverConnectionOptions, - QueryOptions extends DriverQueryOptions, - TransactionOptions extends SqlTransactionOptions, - PoolClientOptions extends SqlPoolClientOptions, + IConnectionOptions extends DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions, + ITransactionOptions extends TransactionOptions, + IPoolClientOptions extends PoolClientOptions, > extends TransactionInternalOptions< - ConnectionOptions, - QueryOptions, - TransactionOptions + IConnectionOptions, + IQueryOptions, + ITransactionOptions > { - poolClientOptions: PoolClientOptions; + poolClientOptions: IPoolClientOptions; } /** - * SqlClientPoolOptions + * ClientPoolOptions * * This represents the options for a connection pool. */ -export interface SqlClientPoolOptions { +export interface ClientPoolOptions { /** * Whether to lazily initialize connections. * @@ -62,103 +62,103 @@ export interface SqlClientPoolOptions { } export interface ClientPoolInternalOptions< - ConnectionOptions extends DriverConnectionOptions, - QueryOptions extends DriverQueryOptions, - TransactionOptions extends SqlTransactionOptions, - PoolClientOptions extends SqlPoolClientOptions, - ClientPoolOptions extends SqlClientPoolOptions, + IConnectionOptions extends DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions, + ITransactionOptions extends TransactionOptions, + IPoolClientOptions extends PoolClientOptions, + IClientPoolOptions extends ClientPoolOptions, > extends PoolClientInternalOptions< - ConnectionOptions, - QueryOptions, - TransactionOptions, - PoolClientOptions + IConnectionOptions, + IQueryOptions, + ITransactionOptions, + IPoolClientOptions > { - clientPoolOptions: ClientPoolOptions; + clientPoolOptions: IClientPoolOptions; } /** - * SqlPoolClient + * PoolClient * * This represents a connection to a database from a pool. * When a user wants to use a connection from a pool, * they should use a class implementing this interface. */ -export interface SqlPoolClient< - ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, - QueryOptions extends DriverQueryOptions = DriverQueryOptions, - ParameterType extends DriverParameterType = DriverParameterType, - QueryValues extends DriverQueryValues = DriverQueryValues, - QueryMeta extends DriverQueryMeta = DriverQueryMeta, - Connection extends Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta +export interface PoolClient< + IConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions = DriverQueryOptions, + IParameterType extends DriverParameterType = DriverParameterType, + IQueryValues extends DriverQueryValues = DriverQueryValues, + IQueryMeta extends DriverQueryMeta = DriverQueryMeta, + IDriver extends Driver< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta > = Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta >, - PreparedStatement extends SqlPreparedStatement< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection - > = SqlPreparedStatement< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection + IPreparedStatement extends PreparedStatement< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver + > = PreparedStatement< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver >, - TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, - Transaction extends SqlTransaction< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions - > = SqlTransaction< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions + ITransactionOptions extends TransactionOptions = TransactionOptions, + ITransaction extends Transaction< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement, + ITransactionOptions + > = Transaction< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement, + ITransactionOptions >, - PoolClientOptions extends SqlPoolClientOptions = SqlPoolClientOptions, + IPoolClientOptions extends PoolClientOptions = PoolClientOptions, > extends - SqlTransactionable< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions, - Transaction + Transactionable< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement, + ITransactionOptions, + ITransaction > { /** * The options used to create the pool client */ readonly options: PoolClientInternalOptions< - ConnectionOptions, - QueryOptions, - TransactionOptions, - PoolClientOptions + IConnectionOptions, + IQueryOptions, + ITransactionOptions, + IPoolClientOptions >; /** @@ -172,216 +172,216 @@ export interface SqlPoolClient< } /** - * SqlClientPool + * ClientPool * * This represents a pool of connections to a database. * When a user wants to use a pool of connections to the database, * they should use a class implementing this interface. */ -export interface SqlClientPool< - ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, - QueryOptions extends DriverQueryOptions = DriverQueryOptions, - ParameterType extends DriverParameterType = DriverParameterType, - QueryValues extends DriverQueryValues = DriverQueryValues, - QueryMeta extends DriverQueryMeta = DriverQueryMeta, - Connection extends Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta +export interface ClientPool< + IConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions = DriverQueryOptions, + IParameterType extends DriverParameterType = DriverParameterType, + IQueryValues extends DriverQueryValues = DriverQueryValues, + IQueryMeta extends DriverQueryMeta = DriverQueryMeta, + IDriver extends Driver< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta > = Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta >, - PreparedStatement extends SqlPreparedStatement< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection - > = SqlPreparedStatement< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection + IPreparedStatement extends PreparedStatement< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver + > = PreparedStatement< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver >, - TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, - Transaction extends SqlTransaction< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions - > = SqlTransaction< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions + ITransactionOptions extends TransactionOptions = TransactionOptions, + ITransaction extends Transaction< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement, + ITransactionOptions + > = Transaction< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement, + ITransactionOptions >, - PoolClientOptions extends SqlPoolClientOptions = SqlPoolClientOptions, - PoolClient extends SqlPoolClient< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions, - Transaction, - PoolClientOptions - > = SqlPoolClient< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions, - Transaction, - PoolClientOptions + IPoolClientOptions extends PoolClientOptions = PoolClientOptions, + IPoolClient extends PoolClient< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement, + ITransactionOptions, + ITransaction, + IPoolClientOptions + > = PoolClient< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement, + ITransactionOptions, + ITransaction, + IPoolClientOptions >, - ClientPoolOptions extends SqlClientPoolOptions = SqlClientPoolOptions, - EventTarget extends SqlEventTarget = SqlEventTarget, + IClientPoolOptions extends ClientPoolOptions = ClientPoolOptions, + IEventTarget extends SqlEventTarget = SqlEventTarget, > extends - SqlEventable, + Eventable, Omit< Driver< - ConnectionOptions + IConnectionOptions >, "query" | "ping" > { readonly options: ClientPoolInternalOptions< - ConnectionOptions, - QueryOptions, - TransactionOptions, - PoolClientOptions, - ClientPoolOptions + IConnectionOptions, + IQueryOptions, + ITransactionOptions, + IPoolClientOptions, + IClientPoolOptions >; /** * Acquire a connection from the pool */ - acquire(): Promise; + acquire(): Promise; } /** - * SqlClientPoolConstructor + * ClientPoolConstructor * - * The constructor for the SqlClientPool interface. + * The constructor for the ClientPool interface. */ -export interface SqlClientPoolConstructor< - ConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, - QueryOptions extends DriverQueryOptions = DriverQueryOptions, - ParameterType extends DriverParameterType = DriverParameterType, - QueryValues extends DriverQueryValues = DriverQueryValues, - QueryMeta extends DriverQueryMeta = DriverQueryMeta, - Connection extends Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta +export interface ClientPoolConstructor< + IConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions = DriverQueryOptions, + IParameterType extends DriverParameterType = DriverParameterType, + IQueryValues extends DriverQueryValues = DriverQueryValues, + IQueryMeta extends DriverQueryMeta = DriverQueryMeta, + IDriver extends Driver< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta > = Driver< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta >, - PreparedStatement extends SqlPreparedStatement< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection - > = SqlPreparedStatement< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection + IPreparedStatement extends PreparedStatement< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver + > = PreparedStatement< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver >, - TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, - Transaction extends SqlTransaction< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions - > = SqlTransaction< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions + ITransactionOptions extends TransactionOptions = TransactionOptions, + ITransaction extends Transaction< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement, + ITransactionOptions + > = Transaction< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement, + ITransactionOptions >, - PoolClientOptions extends SqlPoolClientOptions = SqlPoolClientOptions, - PoolClient extends SqlPoolClient< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions, - Transaction, - PoolClientOptions - > = SqlPoolClient< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions, - Transaction, - PoolClientOptions + IPoolClientOptions extends PoolClientOptions = PoolClientOptions, + IPoolClient extends PoolClient< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement, + ITransactionOptions, + ITransaction, + IPoolClientOptions + > = PoolClient< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement, + ITransactionOptions, + ITransaction, + IPoolClientOptions >, - ClientPoolOptions extends SqlClientPoolOptions = SqlClientPoolOptions, - EventTarget extends SqlEventTarget = SqlEventTarget, + IClientPoolOptions extends ClientPoolOptions = ClientPoolOptions, + IEventTarget extends SqlEventTarget = SqlEventTarget, > { new ( connectionUrl: string | URL, - options?: ConnectionOptions & QueryOptions, - ): SqlClientPool< - ConnectionOptions, - QueryOptions, - ParameterType, - QueryValues, - QueryMeta, - Connection, - PreparedStatement, - TransactionOptions, - Transaction, - PoolClientOptions, - PoolClient, - ClientPoolOptions, - EventTarget + options?: IConnectionOptions & IQueryOptions, + ): ClientPool< + IConnectionOptions, + IQueryOptions, + IParameterType, + IQueryValues, + IQueryMeta, + IDriver, + IPreparedStatement, + ITransactionOptions, + ITransaction, + IPoolClientOptions, + IPoolClient, + IClientPoolOptions, + IEventTarget >; } diff --git a/database/sql/test.ts b/database/sql/test.ts index b637a40..76a6610 100644 --- a/database/sql/test.ts +++ b/database/sql/test.ts @@ -1,20 +1,25 @@ import { DeferredStack } from "@stdext/collections"; import { deepMerge } from "@std/collections"; import { + testClient, testClientConnection, + testClientPool, testClientPoolConnection, - testDriverConnection, - testSqlClient, - testSqlClientPool, - testSqlEventTarget, - testSqlPoolClient, - testSqlPreparedStatement, - testSqlTransaction, + testClientSanity, + testDriver, + testEventTarget, + testPoolClient, + testPreparedStatement, + testTransaction, } from "./testing.ts"; import * as Sql from "./mod.ts"; const testDbQueryParser = (sql: string) => { - return JSON.parse(sql); + try { + return JSON.parse(sql); + } catch { + return ""; + } }; type TestQueryValues = Sql.DriverQueryValues; @@ -25,29 +30,28 @@ interface TestQueryMeta extends Sql.DriverQueryMeta { type TestRow = Sql.Row; type TestArrayRow = Sql.ArrayRow; type TestParameterType = string; -type TestSqlTransactionOptions = Sql.SqlTransactionOptions; +type TestTransactionOptions = Sql.TransactionOptions; -interface TestSqlQueryOptions extends Sql.DriverQueryOptions { +interface TestDriverQueryOptions extends Sql.DriverQueryOptions { test?: string; } -interface TestSqlConnectionOptions extends Sql.DriverConnectionOptions { +interface TestDriverConnectionOptions extends Sql.DriverConnectionOptions { test?: string; } -interface TestSqlClientPoolOptions - extends Sql.SqlClientPoolOptions, TestSqlConnectionOptions { +interface TestClientPoolOptions extends Sql.ClientPoolOptions { } class TestDriver implements Sql.Driver< - TestSqlConnectionOptions, - TestSqlQueryOptions, + TestDriverConnectionOptions, + TestDriverQueryOptions, TestParameterType, TestQueryValues, TestQueryMeta > { readonly connectionUrl: string; readonly options: Sql.DriverInternalOptions< - TestSqlConnectionOptions, - TestSqlQueryOptions + TestDriverConnectionOptions, + TestDriverQueryOptions >; _connected: boolean = false; constructor( @@ -100,27 +104,31 @@ class TestDriver implements class TestSqlConnectable implements Sql.DriverConnectable< - TestSqlConnectionOptions, - TestSqlQueryOptions, + TestDriverConnectionOptions, + TestDriverQueryOptions, TestParameterType, TestQueryValues, TestQueryMeta, TestDriver > { readonly options: Sql.DriverInternalOptions< - TestSqlConnectionOptions, - TestSqlQueryOptions + TestDriverConnectionOptions, + TestDriverQueryOptions >; - readonly connection: TestDriver; + readonly _connection: TestDriver; get connected(): boolean { return this.connection.connected; } + get connection(): TestDriver { + return this._connection; + } + constructor( connection: TestSqlConnectable["connection"], options: TestSqlConnectable["options"], ) { - this.connection = connection; + this._connection = connection; this.options = options; } [Symbol.asyncDispose](): Promise { @@ -128,11 +136,11 @@ class TestSqlConnectable implements } } -class TestSqlPreparedStatement extends TestSqlConnectable +class TestPreparedStatement extends TestSqlConnectable implements - Sql.SqlPreparedStatement< - TestSqlConnectionOptions, - TestSqlQueryOptions, + Sql.PreparedStatement< + TestDriverConnectionOptions, + TestDriverQueryOptions, TestParameterType, TestQueryValues, TestQueryMeta, @@ -140,33 +148,40 @@ class TestSqlPreparedStatement extends TestSqlConnectable > { sql: string; constructor( - connection: TestSqlPreparedStatement["connection"], + connection: TestPreparedStatement["connection"], sql: string, - options: TestSqlPreparedStatement["options"], + options: TestPreparedStatement["options"], ) { super(connection, options); this.sql = sql; } deallocated = false; + + get connection(): TestDriver { + if (this.deallocated) throw new Sql.SqlError("deallocated"); + return this._connection; + } + deallocate(): Promise { this.deallocated = true; return Promise.resolve(); } execute( _params?: TestParameterType[] | undefined, - _options?: TestSqlQueryOptions | undefined, + _options?: TestDriverQueryOptions | undefined, ): Promise { + this.connection; return Promise.resolve(testDbQueryParser(this.sql)); } query( params?: TestParameterType[] | undefined, - options?: TestSqlQueryOptions | undefined, + options?: TestDriverQueryOptions | undefined, ): Promise { return Array.fromAsync(this.queryMany(params, options)); } queryOne( params?: TestParameterType[] | undefined, - options?: TestSqlQueryOptions | undefined, + options?: TestDriverQueryOptions | undefined, ): Promise { return this.query(params, options).then((res) => res[0]) as Promise< T | undefined @@ -174,7 +189,7 @@ class TestSqlPreparedStatement extends TestSqlConnectable } queryMany( params?: TestParameterType[] | undefined, - options?: TestSqlQueryOptions | undefined, + options?: TestDriverQueryOptions | undefined, ): AsyncGenerator { return Sql.mapObjectIterable( this.connection.query(this.sql, params, options), @@ -182,13 +197,13 @@ class TestSqlPreparedStatement extends TestSqlConnectable } queryArray( params?: TestParameterType[] | undefined, - options?: TestSqlQueryOptions | undefined, + options?: TestDriverQueryOptions | undefined, ): Promise { return Array.fromAsync(this.queryManyArray(params, options)); } queryOneArray( params?: TestParameterType[] | undefined, - options?: TestSqlQueryOptions | undefined, + options?: TestDriverQueryOptions | undefined, ): Promise { return this.queryArray(params, options).then((res) => res[0]) as Promise< T | undefined @@ -196,18 +211,21 @@ class TestSqlPreparedStatement extends TestSqlConnectable } queryManyArray( params?: TestParameterType[] | undefined, - options?: TestSqlQueryOptions | undefined, + options?: TestDriverQueryOptions | undefined, ): AsyncGenerator { return Sql.mapArrayIterable( this.connection.query(this.sql, params, options), ); } + [Symbol.asyncDispose](): Promise { + return this.deallocate(); + } } class TestSqlQueriable extends TestSqlConnectable implements - Sql.SqlQueriable< - TestSqlConnectionOptions, - TestSqlQueryOptions, + Sql.Queriable< + TestDriverConnectionOptions, + TestDriverQueryOptions, TestParameterType, TestQueryValues, TestQueryMeta, @@ -222,21 +240,22 @@ class TestSqlQueriable extends TestSqlConnectable implements execute( sql: string, _params?: TestParameterType[] | undefined, - _options?: TestSqlQueryOptions | undefined, + _options?: TestDriverQueryOptions | undefined, ): Promise { + this.connection; return Promise.resolve(testDbQueryParser(sql)); } query( sql: string, params?: TestParameterType[] | undefined, - options?: TestSqlQueryOptions | undefined, + options?: TestDriverQueryOptions | undefined, ): Promise { return Array.fromAsync(this.queryMany(sql, params, options)); } queryOne( sql: string, params?: TestParameterType[] | undefined, - options?: TestSqlQueryOptions | undefined, + options?: TestDriverQueryOptions | undefined, ): Promise { return this.query(sql, params, options).then((res) => res[0]) as Promise< T | undefined @@ -245,7 +264,7 @@ class TestSqlQueriable extends TestSqlConnectable implements queryMany( sql: string, params?: TestParameterType[] | undefined, - options?: TestSqlQueryOptions | undefined, + options?: TestDriverQueryOptions | undefined, ): AsyncGenerator { return Sql.mapObjectIterable( this.connection.query(sql, params, options), @@ -254,14 +273,14 @@ class TestSqlQueriable extends TestSqlConnectable implements queryArray( sql: string, params?: TestParameterType[] | undefined, - options?: TestSqlQueryOptions | undefined, + options?: TestDriverQueryOptions | undefined, ): Promise { return Array.fromAsync(this.queryManyArray(sql, params, options)); } queryOneArray( sql: string, params?: TestParameterType[] | undefined, - options?: TestSqlQueryOptions | undefined, + options?: TestDriverQueryOptions | undefined, ): Promise { return this.queryArray(sql, params, options).then((res) => res[0] @@ -270,7 +289,7 @@ class TestSqlQueriable extends TestSqlConnectable implements queryManyArray( sql: string, params?: TestParameterType[] | undefined, - options?: TestSqlQueryOptions | undefined, + options?: TestDriverQueryOptions | undefined, ): AsyncGenerator { return Sql.mapArrayIterable( this.connection.query(sql, params, options), @@ -291,14 +310,14 @@ class TestSqlQueriable extends TestSqlConnectable implements } class TestSqlPreparable extends TestSqlQueriable implements - Sql.SqlPreparable< - TestSqlConnectionOptions, - TestSqlQueryOptions, + Sql.Preparable< + TestDriverConnectionOptions, + TestDriverQueryOptions, TestParameterType, TestQueryValues, TestQueryMeta, TestDriver, - TestSqlPreparedStatement + TestPreparedStatement > { constructor( connection: TestSqlPreparable["connection"], @@ -308,10 +327,10 @@ class TestSqlPreparable extends TestSqlQueriable implements } prepare( sql: string, - options?: TestSqlQueryOptions | undefined, - ): Promise { + options?: TestDriverQueryOptions | undefined, + ): Promise { return Promise.resolve( - new TestSqlPreparedStatement( + new TestPreparedStatement( this.connection, sql, deepMerge(this.options, { @@ -322,42 +341,51 @@ class TestSqlPreparable extends TestSqlQueriable implements } } -class TestSqlTransaction extends TestSqlPreparable - implements - Sql.SqlTransaction< - TestSqlConnectionOptions, - TestSqlQueryOptions, - TestParameterType, - TestQueryValues, - TestQueryMeta, - TestDriver, - TestSqlPreparedStatement, - TestSqlTransactionOptions - > { +class TestTransaction extends TestSqlPreparable implements + Sql.Transaction< + TestDriverConnectionOptions, + TestDriverQueryOptions, + TestParameterType, + TestQueryValues, + TestQueryMeta, + TestDriver, + TestPreparedStatement, + TestTransactionOptions + > { declare readonly options: Sql.TransactionInternalOptions< - TestSqlConnectionOptions, - TestSqlQueryOptions, - TestSqlTransactionOptions + TestDriverConnectionOptions, + TestDriverQueryOptions, + TestTransactionOptions >; _inTransaction: boolean = false; get inTransaction(): boolean { return this._inTransaction; } + get connection(): TestDriver { + if (!this.inTransaction) { + throw new Sql.SqlError("not in transaction"); + } + return super.connection; + } + constructor( - connection: TestSqlTransaction["connection"], - options: TestSqlTransaction["options"], + connection: TestTransaction["connection"], + options: TestTransaction["options"], ) { super(connection, options); + this._inTransaction = true; } commitTransaction( _options?: Record | undefined, ): Promise { + this._inTransaction = false; return Promise.resolve(); } rollbackTransaction( _options?: Record | undefined, ): Promise { + this._inTransaction = false; return Promise.resolve(); } createSavepoint(_name?: string | undefined): Promise { @@ -368,56 +396,55 @@ class TestSqlTransaction extends TestSqlPreparable } } -class TestSqlTransactionable extends TestSqlPreparable - implements - Sql.SqlPreparable< - TestSqlConnectionOptions, - TestSqlQueryOptions, - TestParameterType, - TestQueryValues, - TestQueryMeta, - TestDriver, - TestSqlPreparedStatement - > { +class TestTransactionable extends TestSqlPreparable implements + Sql.Preparable< + TestDriverConnectionOptions, + TestDriverQueryOptions, + TestParameterType, + TestQueryValues, + TestQueryMeta, + TestDriver, + TestPreparedStatement + > { declare readonly options: Sql.TransactionInternalOptions< - TestSqlConnectionOptions, - TestSqlQueryOptions, - TestSqlTransactionOptions + TestDriverConnectionOptions, + TestDriverQueryOptions, + TestTransactionOptions >; constructor( - connection: TestSqlTransactionable["connection"], - options: TestSqlTransactionable["options"], + connection: TestTransactionable["connection"], + options: TestTransactionable["options"], ) { super(connection, options); } beginTransaction( _options?: Record | undefined, - ): Promise { + ): Promise { return Promise.resolve( - new TestSqlTransaction(this.connection, this.options), + new TestTransaction(this.connection, this.options), ); } transaction( fn: ( - t: TestSqlTransaction, + t: TestTransaction, ) => Promise, ): Promise { - return fn(new TestSqlTransaction(this.connection, this.options)); + return fn(new TestTransaction(this.connection, this.options)); } } -type TestSqlConnectionEventInit = Sql.SqlConnectionEventInit; +type TestConnectionEventInit = Sql.DriverEventInit; class TestSqlEventTarget extends Sql.SqlEventTarget< - TestSqlConnectionOptions, - TestSqlQueryOptions, + TestDriverConnectionOptions, + TestDriverQueryOptions, TestParameterType, TestQueryValues, TestQueryMeta, TestDriver, - Sql.SqlPoolConnectionEventType, - TestSqlConnectionEventInit, + Sql.PoolConnectionEventType, + TestConnectionEventInit, Sql.SqlEvent, EventListenerOrEventListenerObject, AddEventListenerOptions, @@ -425,23 +452,23 @@ class TestSqlEventTarget extends Sql.SqlEventTarget< > { } -class TestSqlClient extends TestSqlTransactionable implements - Sql.SqlClient< - TestSqlConnectionOptions, - TestSqlQueryOptions, +class TestClient extends TestTransactionable implements + Sql.Client< + TestDriverConnectionOptions, + TestDriverQueryOptions, TestParameterType, TestQueryValues, TestQueryMeta, TestDriver, - TestSqlPreparedStatement, - TestSqlTransactionOptions, - TestSqlTransaction, + TestPreparedStatement, + TestTransactionOptions, + TestTransaction, TestSqlEventTarget > { eventTarget: TestSqlEventTarget; constructor( connectionUrl: string | URL, - options: TestSqlTransactionable["options"], + options: TestTransactionable["options"], ) { const driver = new TestDriver(connectionUrl.toString(), options); super(driver, options); @@ -450,39 +477,38 @@ class TestSqlClient extends TestSqlTransactionable implements async connect(): Promise { await this.connection.connect(); this.eventTarget.dispatchEvent( - new Sql.SqlConnectEvent({ connection: this.connection }), + new Sql.ConnectEvent({ connection: this.connection }), ); } async close(): Promise { this.eventTarget.dispatchEvent( - new Sql.SqlCloseEvent({ connection: this.connection }), + new Sql.CloseEvent({ connection: this.connection }), ); await this.connection.close(); } } -interface TestSqlPoolClientOptions extends Sql.SqlPoolClientOptions { +interface TestPoolClientOptions extends Sql.PoolClientOptions { } -class TestSqlPoolClient extends TestSqlTransactionable - implements - Sql.SqlPoolClient< - TestSqlConnectionOptions, - TestSqlQueryOptions, - TestParameterType, - TestQueryValues, - TestQueryMeta, - TestDriver, - TestSqlPreparedStatement, - TestSqlTransactionOptions, - TestSqlTransaction, - TestSqlPoolClientOptions - > { +class TestPoolClient extends TestTransactionable implements + Sql.PoolClient< + TestDriverConnectionOptions, + TestDriverQueryOptions, + TestParameterType, + TestQueryValues, + TestQueryMeta, + TestDriver, + TestPreparedStatement, + TestTransactionOptions, + TestTransaction, + TestPoolClientOptions + > { declare readonly options: Sql.PoolClientInternalOptions< - TestSqlConnectionOptions, - TestSqlQueryOptions, - TestSqlTransactionOptions, - TestSqlPoolClientOptions + TestDriverConnectionOptions, + TestDriverQueryOptions, + TestTransactionOptions, + TestPoolClientOptions >; #releaseFn?: () => Promise; @@ -493,8 +519,8 @@ class TestSqlPoolClient extends TestSqlTransactionable } constructor( - connection: TestSqlPoolClient["connection"], - options: TestSqlPoolClient["options"], + connection: TestPoolClient["connection"], + options: TestPoolClient["options"], ) { super(connection, options); if (this.options?.poolClientOptions.releaseFn) { @@ -511,26 +537,26 @@ class TestSqlPoolClient extends TestSqlTransactionable } } -class TestSqlClientPool implements - Sql.SqlClientPool< - TestSqlConnectionOptions, - TestSqlQueryOptions, +class TestClientPool implements + Sql.ClientPool< + TestDriverConnectionOptions, + TestDriverQueryOptions, TestParameterType, TestQueryValues, TestQueryMeta, TestDriver, - TestSqlPreparedStatement, - TestSqlTransactionOptions, - TestSqlTransaction, - TestSqlPoolClientOptions, - TestSqlPoolClient + TestPreparedStatement, + TestTransactionOptions, + TestTransaction, + TestPoolClientOptions, + TestPoolClient > { declare readonly options: Sql.ClientPoolInternalOptions< - TestSqlConnectionOptions, - TestSqlQueryOptions, - TestSqlTransactionOptions, - TestSqlPoolClientOptions, - TestSqlClientPoolOptions + TestDriverConnectionOptions, + TestDriverQueryOptions, + TestTransactionOptions, + TestPoolClientOptions, + TestClientPoolOptions >; deferredStack: DeferredStack; @@ -542,7 +568,7 @@ class TestSqlClientPool implements } constructor( connectionUrl: string | URL, - options: TestSqlClientPool["options"], + options: TestClientPool["options"], ) { this.connectionUrl = connectionUrl.toString(); this.options = options; @@ -563,7 +589,7 @@ class TestSqlClientPool implements if (!this.options.clientPoolOptions.lazyInitialization) { await conn.connect(); this.eventTarget.dispatchEvent( - new Sql.SqlConnectEvent({ connection: conn }), + new Sql.ConnectEvent({ connection: conn }), ); } this.deferredStack.add(conn); @@ -572,25 +598,25 @@ class TestSqlClientPool implements async close(): Promise { for (const el of this.deferredStack.elements) { this.eventTarget.dispatchEvent( - new Sql.SqlCloseEvent({ connection: el._value }), + new Sql.CloseEvent({ connection: el._value }), ); await el.remove(); } } - async acquire(): Promise { + async acquire(): Promise { const el = await this.deferredStack.pop(); this.eventTarget.dispatchEvent( - new Sql.SqlAcquireEvent({ connection: el.value }), + new Sql.AcquireEvent({ connection: el.value }), ); - const c = new TestSqlPoolClient( + const c = new TestPoolClient( el.value, - deepMerge( + deepMerge( this.options, { poolClientOptions: { releaseFn: async () => { this.eventTarget.dispatchEvent( - new Sql.SqlReleaseEvent({ connection: el._value }), + new Sql.ReleaseEvent({ connection: el._value }), ); await el.release(); }, @@ -606,7 +632,7 @@ class TestSqlClientPool implements } const connectionUrl = "test"; -const options: TestSqlClientPool["options"] = { +const options: TestClientPool["options"] = { clientPoolOptions: {}, connectionOptions: {}, poolClientOptions: {}, @@ -616,16 +642,16 @@ const options: TestSqlClientPool["options"] = { const sql = "test"; const connection = new TestDriver(connectionUrl, options); -const preparedStatement = new TestSqlPreparedStatement( +const preparedStatement = new TestPreparedStatement( connection, sql, options, ); -const transaction = new TestSqlTransaction(connection, options); +const transaction = new TestTransaction(connection, options); const eventTarget = new TestSqlEventTarget(); -const client = new TestSqlClient(connectionUrl, options); -const poolClient = new TestSqlPoolClient(connection, options); -const clientPool = new TestSqlClientPool(connectionUrl, options); +const client = new TestClient(connectionUrl, options); +const poolClient = new TestPoolClient(connection, options); +const clientPool = new TestClientPool(connectionUrl, options); const expects = { connectionUrl, @@ -634,48 +660,58 @@ const expects = { sql, }; -Deno.test(`sql/type test`, async (t) => { - await t.step("SqlConnection", () => { - testDriverConnection(connection, expects); +Deno.test(`sql static test`, async (t) => { + await t.step("Driver", () => { + testDriver(connection, expects); }); await t.step(`sql/PreparedStatement`, () => { - testSqlPreparedStatement(preparedStatement, expects); + testPreparedStatement(preparedStatement, expects); }); - await t.step(`sql/SqlTransaction`, () => { - testSqlTransaction(transaction, expects); + await t.step(`sql/Transaction`, () => { + testTransaction(transaction, expects); }); await t.step(`sql/SqlEventTarget`, () => { - testSqlEventTarget(eventTarget); + testEventTarget(eventTarget); }); - await t.step(`sql/SqlClient`, () => { - testSqlClient(client, expects); + await t.step(`sql/Client`, () => { + testClient(client, expects); }); - await t.step(`sql/SqlPoolClient`, () => { - testSqlPoolClient(poolClient, expects); + await t.step(`sql/PoolClient`, () => { + testPoolClient(poolClient, expects); }); - await t.step(`sql/SqlClientPool`, () => { - testSqlClientPool(clientPool, expects); + await t.step(`sql/ClientPool`, () => { + testClientPool(clientPool, expects); }); }); -Deno.test(`sql/connection test`, async (t) => { - await t.step("SqlClient", async (t) => { - await testClientConnection( +Deno.test(`sql connection test`, async (t) => { + await t.step("Client", async (t) => { + await testClientConnection( t, - TestSqlClient, + TestClient, [connectionUrl, options], ); }); - await t.step("SqlPoolClient", async (t) => { - await testClientPoolConnection( + await t.step("Client", async (t) => { + await testClientPoolConnection( + t, + TestClientPool, + [connectionUrl, options], + ); + }); +}); + +Deno.test(`sql sanity test`, async (t) => { + await t.step("Client", async (t) => { + await testClientSanity( t, - TestSqlClientPool, + TestClient, [connectionUrl, options], ); }); diff --git a/database/sql/testing.ts b/database/sql/testing.ts index e5a0852..4e5194a 100644 --- a/database/sql/testing.ts +++ b/database/sql/testing.ts @@ -6,52 +6,52 @@ import { assertRejects, } from "@std/assert"; import { + assertIsClient, + assertIsClientPool, assertIsDriver, assertIsDriverConnectable, - assertIsSqlClient, - assertIsSqlClientPool, - assertIsSqlEventable, - assertIsSqlPoolClient, - assertIsSqlPreparable, - assertIsSqlPreparedStatement, - assertIsSqlQueriable, - assertIsSqlTransaction, - assertIsSqlTransactionable, + assertIsEventable, + assertIsPoolClient, + assertIsPreparable, + assertIsPreparedStatement, + assertIsQueriable, + assertIsTransaction, + assertIsTransactionable, + type Client, + type ClientPool, type DriverConnectable, - type SqlClient, - type SqlClientPool, - type SqlPoolClient, - type SqlPreparedStatement, - type SqlQueriable, - type SqlTransaction, - type SqlTransactionable, + type PoolClient, + type PreparedStatement, + type Queriable, + type Transaction, + type Transactionable, } from "./mod.ts"; import { deepMerge } from "@std/collections"; // deno-lint-ignore no-explicit-any export type AnyConstructor = new (...args: A) => T; export type ClientConstructorArguments< - Client extends DriverConnectable = DriverConnectable, + IClient extends DriverConnectable = DriverConnectable, > = [ string, - Client["options"], + IClient["options"], ]; export type ClientPoolConstructorArguments< - Client extends SqlClientPool = SqlClientPool, -> = [string, Client["options"]]; + IClient extends ClientPool = ClientPool, +> = [string, IClient["options"]]; export type ClientConstructor< - Client extends DriverConnectable = DriverConnectable, -> = AnyConstructor>; + IClient extends DriverConnectable = DriverConnectable, +> = AnyConstructor>; export type ClientPoolConstructor< - Client extends SqlClientPool = SqlClientPool, -> = AnyConstructor>; + IClient extends ClientPool = ClientPool, +> = AnyConstructor>; /** - * Test the SqlConnection class - * @param value The SqlClient + * Test the Driver class + * @param value The Client * @param expects The values to test against */ -export function testDriverConnection( +export function testDriver( value: unknown, expects: { connectionUrl: string; @@ -62,14 +62,14 @@ export function testDriverConnection( } /** - * Tests the connection of a SqlClient + * Tests the connection of a Client */ -export async function testDriverConnectionConstructor< - Client extends SqlClient = SqlClient, +export async function testDriverConstructor< + IClient extends Client = Client, >( t: Deno.TestContext, - Client: ClientConstructor, - clientArguments: ClientConstructorArguments, + Client: ClientConstructor, + clientArguments: ClientConstructorArguments, ): Promise { await t.step("testConnectAndClose", async (t) => { await t.step("should connect and close with using", async () => { @@ -134,171 +134,171 @@ export function testDriverConnectable( ) { assertIsDriverConnectable(value); assertEquals(value.options, expects.options); - testDriverConnection(value.connection, expects); + testDriver(value.connection, expects); } /** - * Test the SqlPreparedStatement class - * @param value The SqlPreparedStatement + * Test the PreparedStatement class + * @param value The PreparedStatement * @param expects The values to test against */ -export function testSqlPreparedStatement( +export function testPreparedStatement( value: unknown, expects: { connectionUrl: string; - options: SqlPreparedStatement["options"]; + options: PreparedStatement["options"]; sql: string; }, ) { - assertIsSqlPreparedStatement(value); + assertIsPreparedStatement(value); testDriverConnectable(value, expects); assertEquals(value.sql, expects.sql); } /** - * Test the SqlQueriable class - * @param value The SqlQueriable + * Test the Queriable class + * @param value The Queriable * @param expects The values to test against */ -export function _testSqlQueriable( +export function testQueriable( value: unknown, expects: { connectionUrl: string; - options: SqlQueriable["options"]; + options: Queriable["options"]; }, ) { - assertIsSqlQueriable(value); + assertIsQueriable(value); testDriverConnectable(value, expects); } /** - * Test the SqlPreparable class - * @param value The SqlPreparable + * Test the Preparable class + * @param value The Preparable * @param expects The values to test against */ -export function _testSqlPreparable( +export function testPreparable( value: unknown, expects: { connectionUrl: string; - options: SqlQueriable["options"]; + options: Queriable["options"]; }, ) { - assertIsSqlPreparable(value); - _testSqlQueriable(value, expects); + assertIsPreparable(value); + testQueriable(value, expects); } /** - * Test the SqlTransaction class - * @param value The SqlTransaction + * Test the Transaction class + * @param value The Transaction * @param expects The values to test against */ -export function testSqlTransaction( +export function testTransaction( value: unknown, expects: { connectionUrl: string; - options: SqlTransaction["options"]; + options: Transaction["options"]; }, ) { - assertIsSqlTransaction(value); - _testSqlPreparable(value, expects); + assertIsTransaction(value); + testPreparable(value, expects); } /** - * Test the SqlTransactionable class - * @param value The SqlTransactionable + * Test the Transactionable class + * @param value The Transactionable * @param expects The values to test against */ -export function _testSqlTransactionable( +export function testTransactionable( value: unknown, expects: { connectionUrl: string; - options: SqlTransactionable["options"]; + options: Transactionable["options"]; }, ) { - assertIsSqlTransactionable(value); - _testSqlPreparable(value, expects); + assertIsTransactionable(value); + testPreparable(value, expects); } /** - * Test the SqlEventTarget class - * @param value The SqlEventTarget + * Test the EventTarget class + * @param value The EventTarget */ -export function testSqlEventTarget( +export function testEventTarget( value: unknown, ) { assertInstanceOf(value, EventTarget); } /** - * Test the SqlEventable class - * @param value The SqlEventable + * Test the Eventable class + * @param value The Eventable */ -export function _testSqlEventable( +export function testEventable( value: unknown, ) { - assertIsSqlEventable(value); - testSqlEventTarget(value.eventTarget); + assertIsEventable(value); + testEventTarget(value.eventTarget); } /** - * Test the SqlClient class - * @param value The SqlClient + * Test the Client class + * @param value The Client * @param expects The values to test against */ -export function testSqlClient( +export function testClient( value: unknown, expects: { connectionUrl: string; - options: SqlClient["options"]; + options: Client["options"]; }, ) { - assertIsSqlClient(value); - _testSqlTransactionable(value, expects); - _testSqlEventable(value); + assertIsClient(value); + testTransactionable(value, expects); + testEventable(value); } /** - * Test the SqlPoolClient class - * @param value The SqlPoolClient + * Test the PoolClient class + * @param value The PoolClient * @param expects The values to test against */ -export function testSqlPoolClient( +export function testPoolClient( value: unknown, expects: { connectionUrl: string; - options: SqlPoolClient["options"]; + options: PoolClient["options"]; }, ) { - assertIsSqlPoolClient(value); - _testSqlTransactionable(value, expects); + assertIsPoolClient(value); + testTransactionable(value, expects); } /** - * Test the SqlClientPool class - * @param value The SqlClientPool + * Test the ClientPool class + * @param value The ClientPool * @param expects The values to test against */ -export function testSqlClientPool( +export function testClientPool( value: unknown, expects: { connectionUrl: string; - options: SqlClientPool["options"]; + options: ClientPool["options"]; }, ) { - assertIsSqlClientPool(value); - _testSqlEventable(value); + assertIsClientPool(value); + testEventable(value); assertEquals(value.connectionUrl, expects.connectionUrl); } /** - * Tests the connection of a SqlClient + * Tests the connection of a Client */ export async function testClientConnection< - Client extends SqlClient = SqlClient, + IClient extends Client = Client, >( t: Deno.TestContext, - Client: ClientConstructor, - clientArguments: ClientConstructorArguments, + Client: ClientConstructor, + clientArguments: ClientConstructorArguments, ): Promise { await t.step("testConnectAndClose", async (t) => { await t.step("should connect and close with using", async () => { @@ -350,14 +350,14 @@ export async function testClientConnection< } /** - * Tests the connection of a SqlClientPool + * Tests the connection of a ClientPool */ export async function testClientPoolConnection< - Client extends SqlClientPool = SqlClientPool, + IClient extends ClientPool = ClientPool, >( t: Deno.TestContext, - Client: ClientPoolConstructor, - clientArguments: ClientPoolConstructorArguments, + Client: ClientPoolConstructor, + clientArguments: ClientPoolConstructorArguments, ): Promise { await t.step("testConnectAndClose", async (t) => { await t.step("should connect and close", async () => { @@ -370,7 +370,7 @@ export async function testClientPoolConnection< await db.close(); }); await t.step("should connect and close with using", async () => { - const opts = deepMerge( + const opts = deepMerge( clientArguments[1], // deno-lint-ignore ban-ts-comment // @ts-ignore @@ -437,11 +437,11 @@ export async function testClientPoolConnection< } export async function testClientSanity< - Client extends SqlClient = SqlClient, + IClient extends Client = Client, >( t: Deno.TestContext, - Client: ClientConstructor, - clientArguments: ClientConstructorArguments, + Client: ClientConstructor, + clientArguments: ClientConstructorArguments, ): Promise { await testClientConnection(t, Client, clientArguments); @@ -451,14 +451,14 @@ export async function testClientSanity< // Testing prepared statements - const stmt1 = client.prepare("select 1 as one;"); + const stmt1 = await client.prepare("select 1 as one;"); - assertIsSqlPreparedStatement(stmt1); + assertIsPreparedStatement(stmt1); assertFalse(stmt1.deallocated); - const stmt2 = client.prepare("select 1 as one;"); + const stmt2 = await client.prepare("select 1 as one;"); - assertIsSqlPreparedStatement(stmt2); + assertIsPreparedStatement(stmt2); assertFalse(stmt2.deallocated); await stmt1.execute(); @@ -466,7 +466,7 @@ export async function testClientSanity< assert(stmt1.deallocated); - assertRejects(async () => { + await assertRejects(async () => { await stmt1.execute(); }); @@ -475,7 +475,7 @@ export async function testClientSanity< assert(stmt2.deallocated); - assertRejects(async () => { + await assertRejects(async () => { await stmt2.execute(); }); @@ -483,7 +483,7 @@ export async function testClientSanity< const transaction = await client.beginTransaction(); - assert(transaction.inTransaction); + assert(transaction.inTransaction, "Transaction is not in transaction"); await transaction.execute("select 1 as one;"); @@ -491,7 +491,7 @@ export async function testClientSanity< assertFalse(transaction.inTransaction); - assertRejects(async () => { + await assertRejects(async () => { await transaction.execute("select 1 as one;"); }); } diff --git a/database/sql/utils.ts b/database/sql/utils.ts index 6dd2152..25d6976 100644 --- a/database/sql/utils.ts +++ b/database/sql/utils.ts @@ -41,38 +41,3 @@ export async function* mapObjectIterable< yield getObjectFromRow(row); } } - -// export function getObjectsFromRows< -// Output extends Record = Record, -// Row extends DriverQueryNext = DriverQueryNext, -// >(rows: Row[]): Output[] { -// return rows.map(getObjectFromRow); -// } - -// export function getObjectsFromIterable< -// Output extends Record = Record, -// Row extends DriverQueryNext = DriverQueryNext, -// >(q: AsyncIterable): Promise { -// return Array.fromAsync(mapObjectIterable(q)); -// } - -// export async function getFirstFromIterable< -// Row extends DriverQueryNext = DriverQueryNext, -// >(q: AsyncIterable): Promise { -// const res = await Array.fromAsync(q); - -// return res[0] ?? null; -// } - -// export async function getFirstObjectFromIterable< -// Output extends Record = Record, -// Row extends DriverQueryNext = DriverQueryNext, -// >(q: AsyncIterable): Promise { -// const res = await getFirstFromIterable(q); - -// if (!res) { -// return null; -// } - -// return getObjectFromRow(res); -// } From 633ba26d56b50b3f097dd62efb0e369fc9a90b28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 15 Oct 2024 23:09:09 +0200 Subject: [PATCH 23/29] Improved readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 002b0e4..cb475b2 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,14 @@ console.log(dump(buffer)); ## Packages +- [assert](https://jsr.io/@stdext/assert): The assert package, contains + validators and assertions - [collections](https://jsr.io/@stdext/collections): The collections package contains commonly used utilities and structures - [crypto](https://jsr.io/@stdext/crypto): The crypto package contains utility for crypto and hashing +- [database](https://jsr.io/@stdext/database): The database package contains + interfaces and helpers for interracting with databases - [encoding](https://jsr.io/@stdext/encoding): The encoding package contains utility for text encoding. - [http](https://jsr.io/@stdext/http): The http package contains utility for @@ -58,8 +62,6 @@ console.log(dump(buffer)); purpose lexers/tokenizers - [types](https://jsr.io/@stdext/types): The types package, contains general purpose type helpers -- [sql](https://jsr.io/@stdext/sql): The SQL package contains a standard - interface for SQL based databases ## Versioning From 4646cce59e5ba88cfc59d3915578227cc043ca93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 15 Oct 2024 23:15:00 +0200 Subject: [PATCH 24/29] Added override to inherited class properties --- database/sql/events.ts | 6 +++--- database/sql/test.ts | 8 ++++---- database/sql/testing.ts | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/database/sql/events.ts b/database/sql/events.ts index 38b1600..064a8ad 100644 --- a/database/sql/events.ts +++ b/database/sql/events.ts @@ -183,7 +183,7 @@ export class SqlEventTarget< * * @inheritdoc */ - addEventListener( + override addEventListener( type: IEventType, listener: IListener | null, options?: boolean | IListenerOptions, @@ -196,7 +196,7 @@ export class SqlEventTarget< * * @inheritdoc */ - dispatchEvent(event: IEvent): boolean { + override dispatchEvent(event: IEvent): boolean { return super.dispatchEvent(event); } @@ -205,7 +205,7 @@ export class SqlEventTarget< * * @inheritdoc */ - removeEventListener( + override removeEventListener( type: IEventType, callback: IListener | null, options?: boolean | IRemoveListenerOptions, diff --git a/database/sql/test.ts b/database/sql/test.ts index 76a6610..9e338e6 100644 --- a/database/sql/test.ts +++ b/database/sql/test.ts @@ -157,7 +157,7 @@ class TestPreparedStatement extends TestSqlConnectable } deallocated = false; - get connection(): TestDriver { + override get connection(): TestDriver { if (this.deallocated) throw new Sql.SqlError("deallocated"); return this._connection; } @@ -217,7 +217,7 @@ class TestPreparedStatement extends TestSqlConnectable this.connection.query(this.sql, params, options), ); } - [Symbol.asyncDispose](): Promise { + override [Symbol.asyncDispose](): Promise { return this.deallocate(); } } @@ -362,7 +362,7 @@ class TestTransaction extends TestSqlPreparable implements return this._inTransaction; } - get connection(): TestDriver { + override get connection(): TestDriver { if (!this.inTransaction) { throw new Sql.SqlError("not in transaction"); } @@ -532,7 +532,7 @@ class TestPoolClient extends TestTransactionable implements await this.#releaseFn?.(); } - [Symbol.asyncDispose](): Promise { + override [Symbol.asyncDispose](): Promise { return this.release(); } } diff --git a/database/sql/testing.ts b/database/sql/testing.ts index 4e5194a..90940bd 100644 --- a/database/sql/testing.ts +++ b/database/sql/testing.ts @@ -105,7 +105,7 @@ export async function testDriverConstructor< await db.connect(); await db.close(); } catch (e) { - error = e; + error = e as Error; } assert( @@ -334,7 +334,7 @@ export async function testClientConnection< await db.connect(); await db.close(); } catch (e) { - error = e; + error = e as Error; } assert( @@ -419,7 +419,7 @@ export async function testClientPoolConnection< await db.connect(); await db.close(); } catch (e) { - error = e; + error = e as Error; } assertEquals( From 614475c6574fb1d44002cda662b3db97a693d428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 15 Oct 2024 23:17:45 +0200 Subject: [PATCH 25/29] Added version contraints for packages --- deno.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/deno.json b/deno.json index cd6d29d..3b30ce9 100644 --- a/deno.json +++ b/deno.json @@ -7,15 +7,15 @@ "@std/json": "jsr:@std/json@^1", "@std/path": "jsr:@std/path@^1", "@std/toml": "jsr:@std/toml@^1", - "@stdext/assert": "jsr:@stdext/assert", - "@stdext/collections": "jsr:@stdext/collections", - "@stdext/crypto": "jsr:@stdext/crypto", - "@stdext/database": "jsr:@stdext/database", - "@stdext/encoding": "jsr:@stdext/encoding", - "@stdext/http": "jsr:@stdext/http", - "@stdext/json": "jsr:@stdext/json", - "@stdext/lexer": "jsr:@stdext/lexer", - "@stdext/types": "jsr:@stdext/types" + "@stdext/assert": "jsr:@stdext/assert@^0", + "@stdext/collections": "jsr:@stdext/collections@^0", + "@stdext/crypto": "jsr:@stdext/crypto@^0", + "@stdext/database": "jsr:@stdext/database@^0", + "@stdext/encoding": "jsr:@stdext/encoding@^0", + "@stdext/http": "jsr:@stdext/http@^0", + "@stdext/json": "jsr:@stdext/json@^0", + "@stdext/lexer": "jsr:@stdext/lexer@^0", + "@stdext/types": "jsr:@stdext/types@^0" }, "tasks": { "check": "deno task format:check && deno lint && deno check **/*.ts", From a2b0817dab54a211090299f9b098d616b6bf8187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Wed, 16 Oct 2024 18:42:08 +0200 Subject: [PATCH 26/29] feat(types): added type and improved documentation --- types/mod.ts | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/types/mod.ts b/types/mod.ts index cb941fd..4cbc2d9 100644 --- a/types/mod.ts +++ b/types/mod.ts @@ -8,11 +8,23 @@ export type FlipMap> = { /** * Make properties K in T optional + * + * @example + * ```ts + * type A = { a: string, b: string } + * type B = PartialBy // { a: string, b?: string } + * ``` */ export type PartialBy = Omit & Partial>; /** * Make properties K in T required + * + * @example + * ```ts + * type A = { a?: string, b?: string } + * type B = RequiredBy // { a?: string, b: string } + * ``` */ export type RequiredBy = & Omit @@ -20,6 +32,12 @@ export type RequiredBy = /** * Make properties K in T required, and the rest partial + * + * @example + * ```ts + * type A = { a: string, b?: string, c?: string } + * type B = RequiredPartialBy // { a?: string, b: string, c?: string } + * ``` */ export type RequiredPartialBy = & RequiredBy< @@ -48,6 +66,40 @@ export type WriteableBy = & Writeable>; /** - * Gets the values of an object + * Gets the values of a Record + * + * @example With type + * ```ts + * type A = { a: "hello", b: "world" } + * type B = ValueOf // "hello"|"world" + * ``` + * + * @example With object + * ```ts + * const a = { a: "hello", b: "world" } as const + * type B = ValueOf // "hello"|"world" + * ``` */ export type ValueOf = T[keyof T]; + +/** + * Represents a generic constructor + * + * @example As argument + * ```ts + * import type { AnyConstructor } from "@stdext/typings"; + * + * function(SomeClass: AnyConstructor){ + * const c = new SomeClass() + * } + * ``` + * + * @example For other type + * ```ts + * import type { AnyConstructor } from "@stdext/typings"; + * + * type SomeConstructor = AnyConstructor + * ``` + */ +// deno-lint-ignore no-explicit-any +export type AnyConstructor = new (...args: A) => T; From ec2c55ed3510f2e42856a4ef9fbeace6e7650bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Wed, 16 Oct 2024 21:52:07 +0200 Subject: [PATCH 27/29] fix(assert): Added message as option argument to asserts, and added object asserts --- assert/is_number.ts | 9 +- assert/is_numeric.ts | 9 +- assert/is_object.test.ts | 42 +++++++++ assert/is_object.ts | 27 ++++++ assert/is_record.ts | 13 ++- assert/is_string.ts | 9 +- assert/mod.ts | 2 + assert/object_has_properties.test.ts | 93 +++++++++++++++++++ assert/object_has_properties.ts | 130 +++++++++++++++++++++++++++ 9 files changed, 321 insertions(+), 13 deletions(-) create mode 100644 assert/is_object.test.ts create mode 100644 assert/is_object.ts create mode 100644 assert/object_has_properties.test.ts create mode 100644 assert/object_has_properties.ts diff --git a/assert/is_number.ts b/assert/is_number.ts index 00d56b4..b9efc09 100644 --- a/assert/is_number.ts +++ b/assert/is_number.ts @@ -10,8 +10,13 @@ export function isNumber(value: unknown): value is number { /** * Asserts that a value is a number */ -export function assertIsNumber(value: unknown): asserts value is number { +export function assertIsNumber( + value: unknown, + msg?: string, +): asserts value is number { if (!isNumber(value)) { - throw new AssertionError(`Value is not a number, was '${value}'`); + const msgSuffix = msg ? `: ${msg}` : "."; + const message = `Value is not a number, was '${value}'${msgSuffix}`; + throw new AssertionError(message); } } diff --git a/assert/is_numeric.ts b/assert/is_numeric.ts index 5693528..dc9a4a7 100644 --- a/assert/is_numeric.ts +++ b/assert/is_numeric.ts @@ -11,8 +11,13 @@ export function isNumeric(value: unknown): value is number { /** * Asserts that a value is a number and not NaN */ -export function assertIsNumeric(value: unknown): asserts value is number { +export function assertIsNumeric( + value: unknown, + msg?: string, +): asserts value is number { if (!isNumeric(value)) { - throw new AssertionError(`Value is not a numeric, was '${value}'`); + const msgSuffix = msg ? `: ${msg}` : "."; + const message = `Value is not a numeric, was '${value}'${msgSuffix}`; + throw new AssertionError(message); } } diff --git a/assert/is_object.test.ts b/assert/is_object.test.ts new file mode 100644 index 0000000..6ee878f --- /dev/null +++ b/assert/is_object.test.ts @@ -0,0 +1,42 @@ +import { assert, assertFalse, AssertionError, assertThrows } from "@std/assert"; +import { assertIsObject, isObject } from "./is_object.ts"; + +const VALID = [ + {}, + { a: 1 }, + { 1: "a" }, + { [Symbol.dispose]: "" }, +]; + +const INVALID = [ + "", + 1, + undefined, + null, + [], + [""], + new Map(), +]; + +Deno.test("isObject > can detect records", () => { + for (const v of VALID) { + assert(isObject(v), `Value of '${v}' is not valid`); + } + for (const v of INVALID) { + assertFalse(isObject(v), `Value of '${v}' is not invalid`); + } +}); + +Deno.test("assertIsObject > can detect records", () => { + for (const v of VALID) { + assertIsObject(v); + } + for (const v of INVALID) { + assertThrows( + () => assertIsObject(v), + AssertionError, + undefined, + `Value of '${v}' did not throw`, + ); + } +}); diff --git a/assert/is_object.ts b/assert/is_object.ts new file mode 100644 index 0000000..cd81847 --- /dev/null +++ b/assert/is_object.ts @@ -0,0 +1,27 @@ +import { AssertionError } from "@std/assert"; +import { objectToStringEquals } from "./utils.ts"; + +/** + * Checks if a value is an object + */ +export function isObject(value: unknown): value is object { + if (!objectToStringEquals("Object", value)) { + return false; + } + + return true; +} + +/** + * Asserts that a value is an object + */ +export function assertIsObject( + value: unknown, + msg?: string, +): asserts value is object { + if (!isObject(value)) { + const msgSuffix = msg ? `: ${msg}` : "."; + const message = `Value is not a object, was '${value}'${msgSuffix}`; + throw new AssertionError(message); + } +} diff --git a/assert/is_record.ts b/assert/is_record.ts index 3561f68..a044f9f 100644 --- a/assert/is_record.ts +++ b/assert/is_record.ts @@ -1,15 +1,11 @@ import { AssertionError } from "@std/assert"; -import { objectToStringEquals } from "./utils.ts"; +import { isObject } from "./is_object.ts"; /** * Checks if a value is a Record */ export function isRecord(value: unknown): value is Record { - if (!objectToStringEquals("Object", value)) { - return false; - } - - if (typeof value !== "object") { + if (!isObject(value)) { return false; } @@ -29,8 +25,11 @@ export function isRecord(value: unknown): value is Record { */ export function assertIsRecord( value: unknown, + msg?: string, ): asserts value is Record { if (!isRecord(value)) { - throw new AssertionError(`Value is not a Record, was '${value}'`); + const msgSuffix = msg ? `: ${msg}` : "."; + const message = `Value is not a Record, was '${value}'${msgSuffix}`; + throw new AssertionError(message); } } diff --git a/assert/is_string.ts b/assert/is_string.ts index f7b9c29..780fe3f 100644 --- a/assert/is_string.ts +++ b/assert/is_string.ts @@ -10,8 +10,13 @@ export function isString(value: unknown): value is string { /** * Asserts that a value is a string */ -export function assertIsString(value: unknown): asserts value is string { +export function assertIsString( + value: unknown, + msg?: string, +): asserts value is string { if (!isString(value)) { - throw new AssertionError(`Value is not a string, was '${value}'`); + const msgSuffix = msg ? `: ${msg}` : "."; + const message = `Value is not a string, was '${value}'${msgSuffix}`; + throw new AssertionError(message); } } diff --git a/assert/mod.ts b/assert/mod.ts index a766d00..9cda69a 100644 --- a/assert/mod.ts +++ b/assert/mod.ts @@ -1,4 +1,6 @@ export * from "./is_number.ts"; export * from "./is_numeric.ts"; +export * from "./is_object.ts"; export * from "./is_record.ts"; export * from "./is_string.ts"; +export * from "./object_has_properties.ts"; diff --git a/assert/object_has_properties.test.ts b/assert/object_has_properties.test.ts new file mode 100644 index 0000000..c5a0012 --- /dev/null +++ b/assert/object_has_properties.test.ts @@ -0,0 +1,93 @@ +import { assert, assertFalse, AssertionError, assertThrows } from "@std/assert"; +import { + assertObjectHasProperties, + assertObjectHasPropertiesDeep, + objectHasProperties, + objectHasPropertiesDeep, +} from "./object_has_properties.ts"; + +const VALID: Array<[object, Array]> = [ + [{ [Symbol.for("test")]: 0 }, [Symbol.for("test")]], + [{ 1: 0 }, [1]], + [{ 0: 0 }, [0]], + [{ "": 0 }, [""]], + [{ "test": 0 }, ["test"]], +]; + +const INVALID: Array<[object, Array]> = [ + [{}, [Symbol.for("test")]], + [{ [Symbol.for("test2")]: 0 }, [Symbol.for("test")]], + [{}, [1]], + [{}, [""]], + [{}, ["test"]], +]; + +class NestedClass extends (class { + get test() { + return "test"; + } +}) {} +const nestedClass = new NestedClass(); + +const INVALID_NOT_DEEP: Array<[object, Array]> = [ + ...INVALID, + [nestedClass, ["test"]], +]; +const VALID_DEEP: Array<[object, Array]> = [ + ...VALID, + [nestedClass, ["test"]], +]; + +Deno.test("objectHasProperties > can detect all property keys", () => { + for (const v of VALID) { + assert( + objectHasProperties(v[0], v[1]), + `Value of '${JSON.stringify(v)}' is not valid`, + ); + } + for (const v of INVALID_NOT_DEEP) { + assertFalse( + objectHasProperties(v[0], v[1]), + `Value of '${JSON.stringify(v)}' is not invalid`, + ); + } +}); + +Deno.test("assertObjectHasProperties > can detect all property keys", () => { + for (const v of VALID) { + assertObjectHasProperties(v[0], v[1]); + } + for (const v of INVALID) { + assertThrows( + () => assertObjectHasProperties(v[0], v[1]), + AssertionError, + ); + } +}); + +Deno.test("objectHasPropertiesDeep > can detect all property keys", () => { + for (const v of VALID_DEEP) { + assert( + objectHasPropertiesDeep(v[0], v[1]), + `Value of '${JSON.stringify(v)}' is not valid`, + ); + } + for (const v of INVALID) { + assertFalse( + objectHasPropertiesDeep(v[0], v[1]), + `Value of '${JSON.stringify(v)}' is not invalid`, + ); + } +}); + +Deno.test("assertObjectHasPropertiesDeep > can detect all property keys", () => { + for (const v of VALID) { + assertObjectHasPropertiesDeep(v[0], v[1]); + } + for (const v of INVALID) { + assertThrows( + () => assertObjectHasPropertiesDeep(v[0], v[1]), + AssertionError, + ); + } +}); diff --git a/assert/object_has_properties.ts b/assert/object_has_properties.ts new file mode 100644 index 0000000..430ef22 --- /dev/null +++ b/assert/object_has_properties.ts @@ -0,0 +1,130 @@ +import { AssertionError } from "@std/assert"; +import { assertIsObject, isObject } from "./is_object.ts"; + +/** + * Check if an object has a property + */ +function hasProperty( + obj: T, + property: PropertyKey, + deep: boolean, +): boolean { + let currentProto = obj; + + while (currentProto !== null && currentProto !== undefined) { + if (Object.hasOwn(currentProto, property)) { + return true; + } + const descriptor = Object.getOwnPropertyDescriptor( + currentProto, + property, + ); + if (descriptor !== undefined) { + return true; + } + if (!deep) { + return false; + } + currentProto = Object.getPrototypeOf(currentProto); + } + + return false; +} + +export function getKeyDiff( + value: object, + keys: Array, + deep: boolean, +): Array { + const diff: PropertyKey[] = []; + + for (const key of keys) { + if (!hasProperty(value, key, deep)) { + diff.push(key); + } + } + + return diff; +} + +/** + * Checks if an object has given property keys + */ +export function objectHasProperties( + value: unknown, + keys: Array, +): value is Record { + if (!isObject(value)) { + return false; + } + + const diff = getKeyDiff(value, keys, false); + + return diff.length < 1; +} + +/** + * Asserts that an object has given property keys + */ +export function assertObjectHasProperties( + value: unknown, + keys: Array, + msg?: string, +): asserts value is Record { + assertIsObject(value); + + const diff = getKeyDiff(value, keys, false); + + if (diff.length > 0) { + const msgSuffix = msg ? `: ${msg}` : "."; + const message = `The object is missing the following keys: [${ + keys.map(String).join(",") + }]${msgSuffix}`; + throw new AssertionError(message); + } +} + +/** + * Checks deeply if an object has given property keys + * + * Use when wanting to check for getters and other prototype + * properties on multilevel inheritance + */ +export function objectHasPropertiesDeep( + value: unknown, + keys: Array, +): value is Record { + if (!isObject(value)) { + return false; + } + + const diff = getKeyDiff(value, keys, true); + + return diff.length < 1; +} + +/** + * Asserts that an object has given property keys + * + * Use when wanting to check for getters and other prototype + * properties on multilevel inheritance + */ +export function assertObjectHasPropertiesDeep< + T extends PropertyKey = PropertyKey, +>( + value: unknown, + keys: Array, + msg?: string, +): asserts value is Record { + assertIsObject(value); + + const diff = getKeyDiff(value, keys, true); + + if (diff.length > 0) { + const msgSuffix = msg ? `: ${msg}` : "."; + const message = `The object is missing the following keys: [${ + keys.map(String).join(",") + }]${msgSuffix}`; + throw new AssertionError(message); + } +} From ecf2c53b6f6cd62781f6f3e59c9502ebfcab49ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Wed, 16 Oct 2024 21:52:39 +0200 Subject: [PATCH 28/29] chore(crypto): change ts-ignore to ts-expect-error --- crypto/hash.test.ts | 6 ++---- crypto/hash/argon2.test.ts | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/crypto/hash.test.ts b/crypto/hash.test.ts index c2fc339..276b475 100644 --- a/crypto/hash.test.ts +++ b/crypto/hash.test.ts @@ -2,11 +2,9 @@ import { assert, assertMatch, assertThrows } from "@std/assert"; import { hash, verify } from "./hash.ts"; Deno.test("hash() and verify() with unsupported", () => { - // deno-lint-ignore ban-ts-comment - // @ts-ignore + // @ts-expect-error: ts-inference assertThrows(() => hash("unsupported", "password")); - // deno-lint-ignore ban-ts-comment - // @ts-ignore + // @ts-expect-error: ts-inference assertThrows(() => verify("unsupported", "password", "")); }); diff --git a/crypto/hash/argon2.test.ts b/crypto/hash/argon2.test.ts index 82e85ad..fc2508b 100644 --- a/crypto/hash/argon2.test.ts +++ b/crypto/hash/argon2.test.ts @@ -22,8 +22,7 @@ Deno.test("hash() and verify() with argon2d", () => { }); Deno.test("hash() and verify() with wrong algorithm", () => { - // deno-lint-ignore ban-ts-comment - // @ts-ignore + // @ts-expect-error: ts-inference const o = { algorithm: "asdfasdf" } as Argon2Options; const h = hash("password", o); assertMatch(h, /^\$argon2id\$v=19\$m=19456,t=2,p=1\$/); From 87a1f4be7cb3fbb5fab5b4e40c0cb569b073513b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Wed, 16 Oct 2024 21:53:57 +0200 Subject: [PATCH 29/29] fix(database/sql): cleaned up asserts, improved interfaces, and improved tests --- database/sql/README.md | 127 +++++++++++++++----------- database/sql/asserts.ts | 141 ++++++++++++++--------------- database/sql/driver.test.ts | 47 +++++----- database/sql/driver.ts | 16 +++- database/sql/test.ts | 13 ++- database/sql/testing.ts | 175 +++++++++++++++++++++++++++--------- database/sql/utils.ts | 3 +- 7 files changed, 316 insertions(+), 206 deletions(-) diff --git a/database/sql/README.md b/database/sql/README.md index 62f1b69..e0a77ef 100644 --- a/database/sql/README.md +++ b/database/sql/README.md @@ -30,40 +30,38 @@ See the [examples](#examples) section for more usage. ### Client The Client provides a database client with the following methods (see -[SqlClient](./client.ts)): +[Client](./client.ts)): -- `connect` (See [SqlConnection](./connection.ts)): Creates a connection to the - database -- `close` (See [SqlConnection](./connection.ts)): Closes the connection to the - database -- `execute` (See [SqlQueriable](./core.ts)): Executes a SQL statement -- `query` (See [SqlQueriable](./core.ts)): Queries the database and returns an +- `connect` (See [Driver](./driver.ts)): Creates a connection to the database +- `close` (See [Driver](./driver.ts)): Closes the connection to the database +- `execute` (See [Queriable](./core.ts)): Executes a SQL statement +- `query` (See [Queriable](./core.ts)): Queries the database and returns an array of object -- `queryOne` (See [SqlQueriable](./core.ts)): Queries the database and returns - at most one entry as an object -- `queryMany` (See [SqlQueriable](./core.ts)): Queries the database with an - async generator and yields each entry as an object. This is good for when you - want to iterate over a massive amount of rows. -- `queryArray` (See [SqlQueriable](./core.ts)): Queries the database and returns - an array of arrays -- `queryOneArray` (See [SqlQueriable](./core.ts)): Queries the database and - returns at most one entry as an array -- `queryManyArray` (See [SqlQueriable](./core.ts)): Queries the database with an +- `queryOne` (See [Queriable](./core.ts)): Queries the database and returns at + most one entry as an object +- `queryMany` (See [Queriable](./core.ts)): Queries the database with an async + generator and yields each entry as an object. This is good for when you want + to iterate over a massive amount of rows. +- `queryArray` (See [Queriable](./core.ts)): Queries the database and returns an + array of arrays +- `queryOneArray` (See [Queriable](./core.ts)): Queries the database and returns + at most one entry as an array +- `queryManyArray` (See [Queriable](./core.ts)): Queries the database with an async generator and yields each entry as an array. This is good for when you want to iterate over a massive amount of rows. -- `sql` (See [SqlQueriable](./core.ts)): Allows you to create a query using +- `sql` (See [Queriable](./core.ts)): Allows you to create a query using template literals, and returns the entries as an array of objects. This is a wrapper around `query` -- `sqlArray` (See [SqlQueriable](./core.ts)): Allows you to create a query using +- `sqlArray` (See [Queriable](./core.ts)): Allows you to create a query using template literals, and returns the entries as an array of arrays. This is a wrapper around `queryArray` -- `prepare` (See [SqlPreparable](./core.ts)): Returns a prepared statement class +- `prepare` (See [Preparable](./core.ts)): Returns a prepared statement class that contains a subset of the Queriable functions (see - [SqlPreparedQueriable](./core.ts)) -- `beginTransaction` (See [SqlTransactionable](./core.ts)): Returns a - transaction class that contains implements the queriable functions, as well as - transaction related functions (see [SqlTransactionQueriable](./core.ts)) -- `transaction` (See [SqlTransactionable](./core.ts)): A wrapper function for + [PreparedQueriable](./core.ts)) +- `beginTransaction` (See [Transactionable](./core.ts)): Returns a transaction + class that contains implements the queriable functions, as well as transaction + related functions (see [TransactionQueriable](./core.ts)) +- `transaction` (See [Transactionable](./core.ts)): A wrapper function for transactions, handles the logic of beginning, committing and rollback a transaction. @@ -79,14 +77,13 @@ The following events can be subscribed to according to the specs (see ### ClientPool The ClientPool provides a database client pool (a pool of clients) with the -following methods (see [SqlClientPool](./pool.ts)): +following methods (see [ClientPool](./pool.ts)): -- `connect` (See [SqlConnection](./core.ts)): Creates the connection classes and - adds them to a connection pool, and optionally connects them to the database -- `close` (See [SqlConnection](./core.ts)): Closes all connections in the pool -- `acquire` (See [SqlPoolable](./core.ts)): Retrieves a - [SqlPoolClient](./pool.ts) (a subset of [Client](#client)), and connects if - not already connected +- `connect` (See [Driver](./driver.ts)): Creates the connection classes and adds + them to a connection pool, and optionally connects them to the database +- `close` (See [Driver](./driver.ts)): Closes all connections in the pool +- `acquire` (See [Poolable](./core.ts)): Retrieves a [PoolClient](./pool.ts) (a + subset of [Client](#client)), and connects if not already connected #### Events @@ -207,6 +204,12 @@ const transaction = await client.beginTransaction(); await transaction.execute("SOME INSERT QUERY"); await transaction.commitTransaction(); // `transaction` can no longer be used, and a new transaction needs to be created + +// OR + +await using transaction = await client.beginTransaction(); +await transaction.execute("SOME INSERT QUERY"); +// If commit is not called and the resource is cleaned up, rollback will be called automatically ``` Transaction wrapper @@ -223,10 +226,20 @@ console.log(res); Prepared statement ```ts -const prepared = db.prepare("SOME PREPARED STATEMENT"); +const prepared = await db.prepare("SOME PREPARED STATEMENT"); +await prepared.query([...params]); +console.log(res); +// [{ col1: "some value" }] +await prepared.dealocate(); +// `prepared` can no longer be used, and a new prepared statement needs to be created + +// OR + +await using prepared = await db.prepare("SOME PREPARED STATEMENT"); await prepared.query([...params]); console.log(res); // [{ col1: "some value" }] +// If dealocate is not called and the resource is cleaned up, dealocate will be called automatically ``` ## Implementation @@ -237,21 +250,28 @@ console.log(res); To be fully compliant with the specs, you will need to implement the following classes for your database driver: -- `Connection` ([SqlConnection](./connection.ts)): This represents the - connection to the database. This should preferably only contain the - functionality of containing a connection, and provide a minimum set of query - methods to be used to query the database -- `PreparedStatement` ([SqlPreparedStatement](./core.ts)): This represents a +- `Driver` ([Driver](./driver.ts)): This represents the connection to the + database. This should preferably only contain the functionality of containing + a connection, and provide a minimum set of methods to be used to call the + database +- `PreparedStatement` ([PreparedStatement](./core.ts)): This represents a prepared statement. All queriable methods must be implemented -- `Transaction` ([SqlTransaction](./core.ts)): This represents a transaction. - All queriable methods must be implemented -- `Client` ([SqlClient](./client.ts)): This represents a database client -- `ClientPool` ([SqlClientPool](./pool.ts)): This represents a pool of clients -- `PoolClient` ([SqlPoolClient](./pool.ts)): This represents a client to be +- `Transaction` ([Transaction](./core.ts)): This represents a transaction. All + queriable methods must be implemented +- `Client` ([Client](./client.ts)): This represents a database client +- `ClientPool` ([ClientPool](./pool.ts)): This represents a pool of clients +- `PoolClient` ([PoolClient](./pool.ts)): This represents a client to be provided by a pool It is also however advisable to create additional helper classes for easier -inheritance. See [test.ts](./test.ts) for a minimum but functional example of +inheritance. + +There are also some utility functions available in [utils.ts](./utils.ts) to +work with the iterables. + +### Testing + +See the bottom of [test.ts](./test.ts) for a minimum but functional example of how to implement these interfaces into intermediate classes. ### Inheritance graph @@ -282,21 +302,20 @@ the constructor. #### Client -The Client must have a constructor following the signature specified by -`SqlClientConstructor`. +The Client must have a constructor signature as follows. ```ts -export const Client = class extends Transactionable implements SqlClient<...> { // Transactionable is a class implementing `SqlTransactionable` +class DbClient extends Transactionable implements Client<...> { // Transactionable is a class implementing `Transactionable` ... - // The constructor now has to satisfy `SqlClientConstructor` + // The constructor now has to satisfy the following signature constructor( connectionUrl: string | URL, - options: ConnectionOptions & QueryOptions = {}, + options: Client["options"], ) { ... } ... -} satisfies SqlClientConstructor<...>; +} satisfies ClientConstructor<...>; // We need to also export the instance type of the client export type Client = InstanceType; @@ -305,12 +324,12 @@ export type Client = InstanceType; #### ClientPool The ClientPool must have a constructor following the signature specified by -`SqlClientPoolConstructor`. +`ClientPoolConstructor`. ```ts -const ClientPool = class extends Transactionable implements SqlClientPool<...> { // Transactionable is a class implementing `SqlTransactionable` +const ClientPool = class extends Transactionable implements ClientPool<...> { // Transactionable is a class implementing `Transactionable` ... - // The constructor now has to satisfy `SqlClientPoolConstructor` + // The constructor now has to satisfy `ClientPoolConstructor` constructor( connectionUrl: string | URL, options: ConnectionOptions & QueryOptions = {}, @@ -318,7 +337,7 @@ const ClientPool = class extends Transactionable implements SqlClientPool<...> { ... } ... -} satisfies SqlClientPoolConstructor<...>; +} satisfies ClientPoolConstructor<...>; // We need to also export the instance type of the client pool export type ClientPool = InstanceType; diff --git a/database/sql/asserts.ts b/database/sql/asserts.ts index 4a6c173..eb74a37 100644 --- a/database/sql/asserts.ts +++ b/database/sql/asserts.ts @@ -1,5 +1,9 @@ -import { assertExists, assertInstanceOf, AssertionError } from "@std/assert"; -import type { Driver, DriverConnectable } from "./driver.ts"; +import { assertInstanceOf, AssertionError } from "@std/assert"; +import type { + Driver, + DriverConnectable, + DriverInternalOptions, +} from "./driver.ts"; import type { Client } from "./client.ts"; import type { PreparedStatement, @@ -10,72 +14,59 @@ import type { import type { Eventable } from "./events.ts"; import type { ClientPool, PoolClient } from "./pool.ts"; import { SqlError } from "./errors.ts"; +import { + assertObjectHasPropertiesDeep, + isString, + objectHasProperties, + objectHasPropertiesDeep, +} from "@stdext/assert"; /** - * Check if an object has a property + * Check if a value is a connectionUrl */ -function hasProperty(obj: T, property: string | symbol | number): boolean { - let currentProto = obj; - - while (currentProto !== null && currentProto !== undefined) { - if (Object.hasOwn(currentProto, property)) { - return true; - } - const descriptor = Object.getOwnPropertyDescriptor(currentProto, property); - if (descriptor !== undefined) { - return true; - } - currentProto = Object.getPrototypeOf(currentProto); - } - - return false; +export function isConnectionUrl(value: unknown): value is string | URL { + return isString(value) || value instanceof URL; } /** - * Check if an object has properties + * Assert that a value is a connectionUrl */ -export function hasProperties( +export function assertIsConnectionUrl( value: unknown, - properties: Array, -): value is { [K in T]: unknown } { - assertExists(value); - - const missing: Array = []; - - for (const property of properties) { - if ( - !hasProperty(value as { [K in T]: unknown }, property) - ) { - missing.push(property); - } +): asserts value is string | URL { + if (!isConnectionUrl(value)) { + throw new AssertionError( + `The given value is not a valid connection url, must be 'string' or 'URL', but was '${value}'`, + ); } +} - return missing.length === 0; +/** + * Check if a value is driver options + */ +export function isDriverOptions< + T extends DriverInternalOptions = DriverInternalOptions, +>(value: unknown, otherKeys: string[] = []): value is T { + const driverOptionsKeys = ["connectionOptions", "queryOptions", ...otherKeys]; + + return objectHasProperties(value, driverOptionsKeys); } /** - * Check if an object has properties and throws if not + * Assert that a value is driver options */ -export function assertHasProperties( +export function assertIsDriverOptions< + T extends DriverInternalOptions = DriverInternalOptions, +>( value: unknown, - properties: Array, -): asserts value is { [K in T]: unknown } { - assertExists(value); - - const missing: Array = []; - - for (const property of properties) { - if ( - !hasProperty(value as { [K in T]: unknown }, property) - ) { - missing.push(property); - } - } + otherKeys: string[] = [], +): asserts value is T { + const driverOptionsKeys = ["connectionOptions", "queryOptions", ...otherKeys]; - if (missing.length) { + if (!isDriverOptions(value, otherKeys)) { throw new AssertionError( - `Object is missing properties: ${ - missing.map((e) => e.toString()).join(", ") + `The given value is not valid driver options, must contain the following keys ${ + driverOptionsKeys.map(String).join(",") }`, ); } @@ -101,7 +92,7 @@ export function assertIsSqlError(err: unknown): asserts err is SqlError { export function isAsyncDisposable( value: unknown, ): value is AsyncDisposable { - return hasProperties(value, [Symbol.asyncDispose]); + return objectHasPropertiesDeep(value, [Symbol.asyncDispose]); } /** @@ -110,7 +101,7 @@ export function isAsyncDisposable( export function assertIsAsyncDisposable( value: unknown, ): asserts value is AsyncDisposable { - assertHasProperties( + assertObjectHasPropertiesDeep( value, [ Symbol.asyncDispose, @@ -124,7 +115,7 @@ export function assertIsAsyncDisposable( export function isDriver( value: unknown, ): value is Driver { - return isAsyncDisposable(value) && hasProperties( + return isAsyncDisposable(value) && objectHasPropertiesDeep( value, [ "options", @@ -145,7 +136,7 @@ export function assertIsDriver( value: unknown, ): asserts value is Driver { assertIsAsyncDisposable(value); - assertHasProperties( + assertObjectHasPropertiesDeep( value, [ "options", @@ -164,7 +155,7 @@ export function assertIsDriver( export function isDriverConnectable( value: unknown, ): value is DriverConnectable { - return isAsyncDisposable(value) && hasProperties( + return isAsyncDisposable(value) && objectHasPropertiesDeep( value, [ "connection", @@ -180,7 +171,7 @@ export function assertIsDriverConnectable( value: unknown, ): asserts value is DriverConnectable { assertIsAsyncDisposable(value); - assertHasProperties( + assertObjectHasPropertiesDeep( value, [ "connection", @@ -196,7 +187,7 @@ export function assertIsDriverConnectable( export function isPreparedStatement( value: unknown, ): value is PreparedStatement { - return isDriverConnectable(value) && hasProperties( + return isDriverConnectable(value) && objectHasPropertiesDeep( value, [ "sql", @@ -219,7 +210,7 @@ export function assertIsPreparedStatement( value: unknown, ): asserts value is PreparedStatement { assertIsDriverConnectable(value); - assertHasProperties( + assertObjectHasPropertiesDeep( value, [ "sql", @@ -241,7 +232,7 @@ export function assertIsPreparedStatement( export function isQueriable( value: unknown, ): value is Queriable { - return isDriverConnectable(value) && hasProperties( + return isDriverConnectable(value) && objectHasPropertiesDeep( value, [ "options", @@ -263,7 +254,7 @@ export function assertIsQueriable( value: unknown, ): asserts value is Queriable { assertIsDriverConnectable(value); - assertHasProperties( + assertObjectHasPropertiesDeep( value, [ "options", @@ -284,7 +275,7 @@ export function assertIsQueriable( export function isPreparable( value: unknown, ): value is Queriable { - return isQueriable(value) && hasProperties( + return isQueriable(value) && objectHasPropertiesDeep( value, [ "prepare", @@ -299,7 +290,7 @@ export function assertIsPreparable( value: unknown, ): asserts value is Queriable { assertIsQueriable(value); - assertHasProperties( + assertObjectHasPropertiesDeep( value, [ "prepare", @@ -313,7 +304,7 @@ export function assertIsPreparable( export function isTransaction( value: unknown, ): value is Transaction { - return isPreparable(value) && hasProperties( + return isPreparable(value) && objectHasPropertiesDeep( value, [ "inTransaction", @@ -332,7 +323,7 @@ export function assertIsTransaction( value: unknown, ): asserts value is Transaction { assertIsPreparable(value); - assertHasProperties( + assertObjectHasPropertiesDeep( value, [ "inTransaction", @@ -350,7 +341,7 @@ export function assertIsTransaction( export function isTransactionable( value: unknown, ): value is Transactionable { - return isPreparable(value) && hasProperties( + return isPreparable(value) && objectHasPropertiesDeep( value, [ "beginTransaction", @@ -366,7 +357,7 @@ export function assertIsTransactionable( value: unknown, ): asserts value is Transactionable { assertIsPreparable(value); - assertHasProperties( + assertObjectHasPropertiesDeep( value, [ "beginTransaction", @@ -381,7 +372,7 @@ export function assertIsTransactionable( export function isEventable( value: unknown, ): value is Eventable { - return hasProperties(value, ["eventTarget"]) && + return objectHasPropertiesDeep(value, ["eventTarget"]) && value.eventTarget instanceof EventTarget; } @@ -391,7 +382,7 @@ export function isEventable( export function assertIsEventable( value: unknown, ): asserts value is Eventable { - assertHasProperties(value, ["eventTarget"]); + assertObjectHasPropertiesDeep(value, ["eventTarget"]); assertInstanceOf(value.eventTarget, EventTarget); } @@ -401,7 +392,7 @@ export function assertIsEventable( export function isClient(value: unknown): value is Client { return isDriver(value) && isQueriable(value) && isTransactionable(value) && isEventable(value) && - hasProperties(value, ["options"]); + objectHasPropertiesDeep(value, ["options"]); } /** @@ -412,7 +403,7 @@ export function assertIsClient(value: unknown): asserts value is Client { assertIsQueriable(value); assertIsTransactionable(value); assertIsEventable(value); - assertHasProperties(value, ["options"]); + assertObjectHasPropertiesDeep(value, ["options"]); } /** @@ -422,7 +413,7 @@ export function isPoolClient( value: unknown, ): value is PoolClient { return isDriverConnectable(value) && isTransactionable(value) && - hasProperties(value, [ + objectHasPropertiesDeep(value, [ "options", "disposed", "release", @@ -437,7 +428,7 @@ export function assertIsPoolClient( ): asserts value is PoolClient { assertIsDriverConnectable(value); assertIsTransactionable(value); - assertHasProperties(value, [ + assertObjectHasPropertiesDeep(value, [ "options", "disposed", "release", @@ -451,7 +442,7 @@ export function isClientPool( value: unknown, ): value is ClientPool { return isEventable(value) && isAsyncDisposable(value) && - hasProperties(value, [ + objectHasPropertiesDeep(value, [ "connectionUrl", "options", "connected", @@ -470,7 +461,7 @@ export function assertIsClientPool( ): asserts value is ClientPool { assertIsEventable(value); assertIsAsyncDisposable(value); - assertHasProperties(value, [ + assertObjectHasPropertiesDeep(value, [ "connectionUrl", "options", "connected", diff --git a/database/sql/driver.test.ts b/database/sql/driver.test.ts index 1491036..95e0939 100644 --- a/database/sql/driver.test.ts +++ b/database/sql/driver.test.ts @@ -1,4 +1,7 @@ -import { testDriver, testDriverConnectable } from "./testing.ts"; +import { + testDriverConnectable, + testDriverConstructorIntegration, +} from "./testing.ts"; import type { Driver, DriverConnectable, @@ -11,11 +14,12 @@ import type { DriverQueryValues, } from "./driver.ts"; import { SqlError } from "./errors.ts"; -import { assert, assertEquals, assertFalse, assertRejects } from "@std/assert"; +import { assert, assertEquals } from "@std/assert"; const testDbQueryParser = (sql: string) => { return JSON.parse(sql); }; + type TestDriverParameterType = DriverParameterType; type TestDriverQueryValues = DriverQueryValues>; interface TestDriverQueryMeta extends DriverQueryMeta { @@ -36,14 +40,14 @@ class TestDriver implements TestDriverQueryValues, TestDriverQueryMeta > { - connectionUrl: string; + connectionUrl: string | URL; options: DriverInternalOptions< TestDriverConnectionOptions, TestDriverQueryOptions >; _connected: boolean = false; constructor( - connectionUrl: string, + connectionUrl: string | URL, options: TestDriver["options"], ) { this.connectionUrl = connectionUrl; @@ -128,12 +132,6 @@ const options: TestDriver["options"] = { }; const sql = "test"; -const connection = new TestDriver(connectionUrl, options); -const connectable = new TestDriverConnectable( - connection, - connection.options, -); - const expects = { connectionUrl, options, @@ -145,23 +143,12 @@ const expects = { // @ts-expect-error: qwer is not allowed const _testingDriverQueryValues: DriverQueryValues<["asdf"]> = ["asdf", "qwer"]; -Deno.test(`DriverConnection`, async (t) => { - await t.step("test suite", () => { - testDriver(connection, expects); - }); - - await t.step("ping will throw if not connected", async () => { - await using conn = new TestDriver(connectionUrl, options); - - await conn.connect(); - assert(conn.connected); - await conn.ping(); - - await conn.close(); - assertFalse(connection.connected); - await assertRejects(async () => { - await conn.ping(); - }); +Deno.test(`Driver`, async (t) => { + await t.step("test suite", async (t) => { + await testDriverConstructorIntegration(t, TestDriver, [ + connectionUrl, + options, + ]); }); await t.step("can query using loop", async () => { @@ -187,6 +174,12 @@ Deno.test(`DriverConnection`, async (t) => { }); Deno.test(`DriverConnectable`, async (t) => { + const connection = new TestDriver(connectionUrl, options); + const connectable = new TestDriverConnectable( + connection, + connection.options, + ); + await t.step("test suite", () => { testDriverConnectable(connectable, expects); }); diff --git a/database/sql/driver.ts b/database/sql/driver.ts index 034adc1..89ba938 100644 --- a/database/sql/driver.ts +++ b/database/sql/driver.ts @@ -73,8 +73,8 @@ export type DriverQueryNext< * Internal Driver Options */ export interface DriverInternalOptions< - IConnectionOptions extends DriverConnectionOptions, - IQueryOptions extends DriverQueryOptions, + IConnectionOptions extends DriverConnectionOptions = DriverConnectionOptions, + IQueryOptions extends DriverQueryOptions = DriverQueryOptions, > { connectionOptions: IConnectionOptions; queryOptions: IQueryOptions; @@ -104,7 +104,7 @@ export interface Driver< /** * Connection URL */ - readonly connectionUrl: string; + readonly connectionUrl: string | URL; /** * Connection options @@ -151,6 +151,16 @@ export interface Driver< ): AsyncGenerator>; } +/** + * The driver constructor + */ +export interface DriverConstructor< + IDriver extends Driver = Driver, + IOptions extends IDriver["options"] = IDriver["options"], +> { + new (connectionUrl: string, options: IOptions): IDriver; +} + /** * DriverConnectable * diff --git a/database/sql/test.ts b/database/sql/test.ts index 9e338e6..36940df 100644 --- a/database/sql/test.ts +++ b/database/sql/test.ts @@ -3,6 +3,7 @@ import { deepMerge } from "@std/collections"; import { testClient, testClientConnection, + testClientConstructorIntegration, testClientPool, testClientPoolConnection, testClientSanity, @@ -692,14 +693,14 @@ Deno.test(`sql static test`, async (t) => { Deno.test(`sql connection test`, async (t) => { await t.step("Client", async (t) => { - await testClientConnection( + await testClientConnection( t, TestClient, [connectionUrl, options], ); }); await t.step("Client", async (t) => { - await testClientPoolConnection( + await testClientPoolConnection( t, TestClientPool, [connectionUrl, options], @@ -709,7 +710,13 @@ Deno.test(`sql connection test`, async (t) => { Deno.test(`sql sanity test`, async (t) => { await t.step("Client", async (t) => { - await testClientSanity( + await t.step("test suite", async (t) => { + await testClientConstructorIntegration(t, TestClient, [ + connectionUrl, + options, + ]); + }); + await testClientSanity( t, TestClient, [connectionUrl, options], diff --git a/database/sql/testing.ts b/database/sql/testing.ts index 90940bd..cfd4fff 100644 --- a/database/sql/testing.ts +++ b/database/sql/testing.ts @@ -20,6 +20,7 @@ import { type Client, type ClientPool, type DriverConnectable, + type DriverConstructor, type PoolClient, type PreparedStatement, type Queriable, @@ -27,9 +28,9 @@ import { type Transactionable, } from "./mod.ts"; import { deepMerge } from "@std/collections"; +import type { AnyConstructor } from "@stdext/types"; +import { assertIsConnectionUrl, assertIsDriverOptions } from "./asserts.ts"; -// deno-lint-ignore no-explicit-any -export type AnyConstructor = new (...args: A) => T; export type ClientConstructorArguments< IClient extends DriverConnectable = DriverConnectable, > = [ @@ -54,32 +55,141 @@ export type ClientPoolConstructor< export function testDriver( value: unknown, expects: { - connectionUrl: string; + connectionUrl: string | URL; }, ) { assertIsDriver(value); assertEquals(value.connectionUrl, expects.connectionUrl); } +/** + * Test the Driver class + * @param value The Client + * @param expects The values to test against + */ +export function testDriverConstructor< + IDriverConstructor extends DriverConstructor = DriverConstructor, +>( + DriverC: IDriverConstructor, + args: ConstructorParameters, +) { + assert( + args.length < 3, + "Number of arguments for the driver constructor has to be max 2.", + ); + assertIsConnectionUrl(args[0]); + assertIsDriverOptions(args[1]); + // @ts-expect-error: ts inference + const d = new DriverC(...args); + testDriver(d, { connectionUrl: args[0] }); +} + +/** + * Test the Driver class + * @param value The Client + * @param expects The values to test against + */ +export async function testDriverConstructorIntegration< + IDriverConstructor extends DriverConstructor = DriverConstructor, +>( + t: Deno.TestContext, + D: IDriverConstructor, + args: ConstructorParameters, +) { + testDriverConstructor(D, args); + + await t.step("testConnectAndClose", async (t) => { + await t.step("should connect and close with using", async () => { + // @ts-expect-error: ts-inference + await using d = new D(...args); + + await d.connect(); + }); + + await t.step("should connect and close", async () => { + // @ts-expect-error: ts-inference + const d = new D(...args); + + await d.connect(); + await d.close(); + }); + + await t.step("ping should work while connected", async () => { + // @ts-expect-error: ts-inference + await using d = new D(...args); + + await d.connect(); + assert(d.connected); + + await d.ping(); + + await d.close(); + + assertFalse(d.connected); + + await assertRejects(async () => { + await d.ping(); + }); + }); + }); +} + +/** + * Test the DriverConnectable class + * @param value The DriverConnectable + * @param expects The values to test against + */ +export function testDriverConnectable( + value: unknown, + expects: { + connectionUrl: string; + options: DriverConnectable["options"]; + }, +) { + assertIsDriverConnectable(value); + assertEquals(value.options, expects.options); + testDriver(value.connection, expects); +} + /** * Tests the connection of a Client */ -export async function testDriverConstructor< +export function testClientConstructor< + IClient extends Client = Client, +>( + ClientC: ClientConstructor, + args: ClientConstructorArguments, +): void { + assert( + args.length < 3, + "Number of arguments for the client constructor has to be max 2.", + ); + assertIsConnectionUrl(args[0]); + assertEquals(typeof args[1], "object"); + const d = new ClientC(...args); + testClient(d, { connectionUrl: args[0], options: args[1] }); +} +/** + * Tests the connection of a Client + */ +export async function testClientConstructorIntegration< IClient extends Client = Client, >( t: Deno.TestContext, Client: ClientConstructor, - clientArguments: ClientConstructorArguments, + args: ClientConstructorArguments, ): Promise { + testClientConstructor(Client, args); + await t.step("testConnectAndClose", async (t) => { await t.step("should connect and close with using", async () => { - await using db = new Client(...clientArguments); + await using db = new Client(...args); await db.connect(); }); await t.step("should connect and close", async () => { - const db = new Client(...clientArguments); + const db = new Client(...args); await db.connect(); @@ -87,7 +197,7 @@ export async function testDriverConstructor< }); await t.step("should connect and close with events", async () => { - const db = new Client(...clientArguments); + const db = new Client(...args); let connectListenerCalled = false; let closeListenerCalled = false; @@ -120,23 +230,6 @@ export async function testDriverConstructor< }); } -/** - * Test the DriverConnectable class - * @param value The DriverConnectable - * @param expects The values to test against - */ -export function testDriverConnectable( - value: unknown, - expects: { - connectionUrl: string; - options: DriverConnectable["options"]; - }, -) { - assertIsDriverConnectable(value); - assertEquals(value.options, expects.options); - testDriver(value.connection, expects); -} - /** * Test the PreparedStatement class * @param value The PreparedStatement @@ -372,8 +465,7 @@ export async function testClientPoolConnection< await t.step("should connect and close with using", async () => { const opts = deepMerge( clientArguments[1], - // deno-lint-ignore ban-ts-comment - // @ts-ignore + // @ts-expect-error: ts-inference { clientPoolOptions: { lazyInitialization: true, @@ -456,7 +548,7 @@ export async function testClientSanity< assertIsPreparedStatement(stmt1); assertFalse(stmt1.deallocated); - const stmt2 = await client.prepare("select 1 as one;"); + await using stmt2 = await client.prepare("select 1 as one;"); assertIsPreparedStatement(stmt2); assertFalse(stmt2.deallocated); @@ -471,27 +563,26 @@ export async function testClientSanity< }); await stmt2.execute(); - await stmt2.deallocate(); - - assert(stmt2.deallocated); - - await assertRejects(async () => { - await stmt2.execute(); - }); // Testing transactions - const transaction = await client.beginTransaction(); + const transaction1 = await client.beginTransaction(); - assert(transaction.inTransaction, "Transaction is not in transaction"); + assert(transaction1.inTransaction, "Transaction is not in transaction"); - await transaction.execute("select 1 as one;"); + await transaction1.execute("select 1 as one;"); - await transaction.commitTransaction(); - - assertFalse(transaction.inTransaction); + await transaction1.commitTransaction(); await assertRejects(async () => { - await transaction.execute("select 1 as one;"); + await transaction2.execute("select 1 as one;"); }); + + assertFalse(transaction1.inTransaction); + + await using transaction2 = await client.beginTransaction(); + + assert(transaction2.inTransaction, "Transaction is not in transaction"); + + await transaction2.execute("select 1 as one;"); } diff --git a/database/sql/utils.ts b/database/sql/utils.ts index 25d6976..3c44e5d 100644 --- a/database/sql/utils.ts +++ b/database/sql/utils.ts @@ -10,8 +10,7 @@ export function getObjectFromRow< const rowObject: Output = {} as Output; for (let i = 0; i < row.columns.length; i++) { - // deno-lint-ignore ban-ts-comment - // @ts-ignore + // @ts-expect-error: ts-inference rowObject[row.columns[i]] = row.values[i]; }

*7D2?x_P5X-K@- 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 literal 0 HcmV?d00001 diff --git a/sql/asserts.ts b/sql/asserts.ts new file mode 100644 index 0000000..e862422 --- /dev/null +++ b/sql/asserts.ts @@ -0,0 +1,230 @@ +import { assertExists, assertInstanceOf, AssertionError } from "@std/assert"; +import { + SqlClient, + SqlClientPool, + SqlClientQueriable, + SqlConnectableBase, + SqlConnection, + SqlError, + SqlEventable, + SqlPoolClient, + SqlPreparedStatement, + SqlQueriable, + SqlTransaction, +} from "./mod.ts"; +import { DeferredStack } from "../collections/deferred_stack.ts"; + +function hasProperty(obj: T, property: string | symbol | number): boolean { + let currentProto = obj; + + while (currentProto !== null && currentProto !== undefined) { + if (Object.hasOwn(currentProto, property)) { + return true; + } + const descriptor = Object.getOwnPropertyDescriptor(currentProto, property); + if (descriptor !== undefined) { + return true; + } + currentProto = Object.getPrototypeOf(currentProto); + } + + return false; +} + +/** + * Check if an object has properties, and returns the properties that are missing + */ +export function assertHasProperties( + value: unknown, + properties: Array, +): asserts value is { [K in T]: unknown } { + assertExists(value); + + const missing: Array = []; + + for (const property of properties) { + if ( + !hasProperty(value as { [K in T]: unknown }, property) + ) { + missing.push(property); + } + } + + if (missing.length) { + throw new AssertionError( + `Object is missing properties: ${ + missing.map((e) => e.toString()).join(", ") + }`, + ); + } +} + +/** + * Check if an error is a SqlError + */ +export function isSqlError(err: unknown): err is SqlError { + return err instanceof SqlError; +} + +/** + * Check if an error is a SqlError + */ +export function assertIsSqlError(err: unknown): asserts err is SqlError { + assertInstanceOf(err, SqlError); +} + +export function assertIsAsyncDisposable( + value: unknown, +): asserts value is AsyncDisposable { + assertHasProperties( + value, + [ + Symbol.asyncDispose, + ], + ); +} + +export function assertIsSqlConnection( + value: unknown, +): asserts value is SqlConnection { + assertIsAsyncDisposable(value); + assertHasProperties( + value, + [ + "connectionUrl", + "options", + "connected", + "connect", + "close", + "execute", + "queryMany", + "queryManyArray", + ], + ); +} + +export function assertIsSqlConnectableBase( + value: unknown, +): asserts value is SqlConnectableBase { + assertHasProperties( + value, + [ + "connection", + "connected", + ], + ); + assertIsSqlConnection(value.connection); +} + +export function assertIsSqlPreparedStatement( + value: unknown, +): asserts value is SqlPreparedStatement { + assertIsSqlConnectableBase(value); + assertHasProperties( + value, + [ + "sql", + "options", + "execute", + "query", + "queryOne", + "queryMany", + "queryArray", + "queryOneArray", + "queryManyArray", + ], + ); +} + +export function assertIsSqlQueriable( + value: unknown, +): asserts value is SqlQueriable { + assertIsSqlConnectableBase(value); + assertHasProperties( + value, + [ + "options", + "execute", + "query", + "queryOne", + "queryMany", + "queryArray", + "queryOneArray", + "queryManyArray", + ], + ); +} + +export function assertIsSqlTransaction( + value: unknown, +): asserts value is SqlTransaction { + assertIsSqlQueriable(value); + assertHasProperties( + value, + [ + "inTransaction", + "commitTransaction", + "rollbackTransaction", + "createSavepoint", + "releaseSavepoint", + ], + ); +} + +export function assertIsSqlClientQueriable( + value: unknown, +): asserts value is SqlClientQueriable { + assertIsSqlQueriable(value); + assertHasProperties( + value, + [ + "prepare", + "beginTransaction", + "transaction", + ], + ); +} + +export function assertIsSqlEventable( + value: unknown, +): asserts value is SqlEventable { + assertHasProperties(value, ["eventTarget"]); + assertInstanceOf(value.eventTarget, EventTarget); +} + +export function assertIsSqlClient(value: unknown): asserts value is SqlClient { + assertIsSqlConnection(value); + assertIsSqlQueriable(value); + assertIsSqlClientQueriable(value); + assertIsSqlEventable(value); + assertHasProperties(value, ["options"]); +} + +export function assertIsSqlPoolClient( + value: unknown, +): asserts value is SqlPoolClient { + assertIsSqlConnectableBase(value); + assertIsSqlClientQueriable(value); + assertHasProperties(value, [ + "options", + "disposed", + "release", + ]); +} + +export function assertIsSqlClientPool( + value: unknown, +): asserts value is SqlClientPool { + assertIsSqlEventable(value); + assertIsAsyncDisposable(value); + assertHasProperties(value, [ + "connectionUrl", + "options", + "connected", + "connect", + "close", + "deferredStack", + "acquire", + ]); + assertInstanceOf(value.deferredStack, DeferredStack); +} diff --git a/sql/client.ts b/sql/client.ts index 55b2321..d215afc 100644 --- a/sql/client.ts +++ b/sql/client.ts @@ -1,12 +1,11 @@ import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; import type { - SqlPreparable, - SqlPreparedQueriable, + SqlClientQueriable, + SqlPreparedStatement, SqlQueriable, SqlQueryOptions, - SqlTransactionable, + SqlTransaction, SqlTransactionOptions, - SqlTransactionQueriable, } from "./core.ts"; import type { SqlEventable, SqlEventTarget } from "./events.ts"; @@ -19,30 +18,32 @@ import type { SqlEventable, SqlEventTarget } from "./events.ts"; export interface SqlClient< EventTarget extends SqlEventTarget = SqlEventTarget, ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - Connection extends SqlConnection = SqlConnection< - ConnectionOptions - >, ParameterType extends unknown = unknown, QueryOptions extends SqlQueryOptions = SqlQueryOptions, - Prepared extends SqlPreparedQueriable< + Connection extends SqlConnection< + ConnectionOptions, + ParameterType, + QueryOptions + > = SqlConnection, + Prepared extends SqlPreparedStatement< ConnectionOptions, Connection, ParameterType, QueryOptions - > = SqlPreparedQueriable< + > = SqlPreparedStatement< ConnectionOptions, Connection, ParameterType, QueryOptions >, TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, - Transaction extends SqlTransactionQueriable< + Transaction extends SqlTransaction< ConnectionOptions, Connection, ParameterType, QueryOptions, TransactionOptions - > = SqlTransactionQueriable< + > = SqlTransaction< ConnectionOptions, Connection, ParameterType, @@ -50,27 +51,22 @@ export interface SqlClient< TransactionOptions >, > extends - SqlConnection, + SqlConnection, SqlQueriable< ConnectionOptions, Connection, ParameterType, QueryOptions >, - SqlPreparable< - ConnectionOptions, - Connection, - ParameterType, - QueryOptions, - Prepared - >, - SqlTransactionable< + SqlClientQueriable< ConnectionOptions, Connection, ParameterType, QueryOptions, + Prepared, TransactionOptions, Transaction >, SqlEventable { + options: ConnectionOptions & QueryOptions & TransactionOptions; } diff --git a/sql/connection.ts b/sql/connection.ts index ca9225d..e78cfde 100644 --- a/sql/connection.ts +++ b/sql/connection.ts @@ -1,3 +1,5 @@ +import { ArrayRow, Row, SqlQueryOptions } from "./core.ts"; + /** * SqlConnectionOptions * @@ -28,6 +30,8 @@ export interface SqlConnectionOptions { */ export interface SqlConnection< ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, + ParameterType extends unknown = unknown, + QueryOptions extends SqlQueryOptions = SqlQueryOptions, > extends AsyncDisposable { /** * Connection URL @@ -37,7 +41,7 @@ export interface SqlConnection< /** * Connection options */ - readonly connectionOptions: ConnectionOptions; + readonly options: ConnectionOptions; /** * Whether the connection is connected to the database @@ -53,4 +57,50 @@ export interface SqlConnection< * Close the connection to the database */ close(): Promise; + + /** + * Execute a SQL statement + * + * @param sql the SQL statement + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the number of affected rows if any + */ + execute( + sql: string, + params?: ParameterType[], + options?: QueryOptions, + ): Promise; + + /** + * Query the database and return an iterator. + * Usefull when querying large datasets, as this should take advantage of data streams. + * + * @param sql the SQL statement + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the rows returned by the query as object entries + */ + // deno-lint-ignore no-explicit-any + queryMany = Row>( + sql: string, + params?: ParameterType[], + options?: QueryOptions, + ): AsyncGenerator; + + /** + * Query the database and return an iterator. + * Usefull when querying large datasets, as this should take advantage of data streams. + * + * @param sql the SQL statement + * @param params the parameters to bind to the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns the rows returned by the query as array entries + */ + // deno-lint-ignore no-explicit-any + queryManyArray = ArrayRow>( + sql: string, + params?: ParameterType[], + options?: QueryOptions, + ): AsyncGenerator; } diff --git a/sql/core.ts b/sql/core.ts index 47f279e..25091fa 100644 --- a/sql/core.ts +++ b/sql/core.ts @@ -1,6 +1,5 @@ // deno-lint-ignore-file no-explicit-any import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; -import type { DeferredStack as SqlDeferredStack } from "../collections/deferred_stack.ts"; /** * Row @@ -59,22 +58,12 @@ export interface SqlConnectableBase< get connected(): boolean; } -/** - * SqlConnectablePoolBase - * - * The base interface for pool clients that interracts with the connection like querying. - */ -export interface SqlConnectablePoolBase< - Connection extends SqlConnection = SqlConnection, -> extends SqlConnectableBase, AsyncDisposable { -} - /** * SqlPreparedQueriable * * Represents a prepared statement to be executed separately from creation. */ -export interface SqlPreparedQueriable< +export interface SqlPreparedStatement< ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, Connection extends SqlConnection = SqlConnection< ConnectionOptions @@ -89,7 +78,7 @@ export interface SqlPreparedQueriable< /** * The (global) options to pass to the query method. */ - readonly queryOptions: QueryOptions; + readonly options: QueryOptions; /** * Executes the prepared statement @@ -190,7 +179,7 @@ export interface SqlQueriable< /** * The (global) options to pass to the query method. */ - readonly queryOptions: QueryOptions; + readonly options: QueryOptions; /** * Execute a SQL statement @@ -308,62 +297,12 @@ export interface SqlQueriable< ): Promise; } -/** - * SqlPreparable - * - * This interface is to be implemented by any class that supports creating a prepared statement. - * A prepared statement should in most cases be unique to a connection, - * and should not live after the related connection is closed. - */ -export interface SqlPreparable< - ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - Connection extends SqlConnection = SqlConnection< - ConnectionOptions - >, - ParameterType extends unknown = unknown, - QueryOptions extends SqlQueryOptions = SqlQueryOptions, - Prepared extends SqlPreparedQueriable< - ConnectionOptions, - Connection, - ParameterType, - QueryOptions - > = SqlPreparedQueriable< - ConnectionOptions, - Connection, - ParameterType, - QueryOptions - >, -> extends SqlConnectableBase { - /** - * Create a prepared statement that can be executed multiple times. - * This is useful when you want to execute the same SQL statement multiple times with different parameters. - * - * @param sql the SQL statement - * @param options the options to pass to the query method, will be merged with the global options - * @returns a prepared statement - * - * @example - * ```ts - * const stmt = db.prepare("SELECT * FROM table WHERE id = ?"); - * - * for (let i = 0; i < 10; i++) { - * const row of stmt.query([i]) - * console.log(row); - * } - * ``` - */ - prepare( - sql: string, - options?: QueryOptions, - ): Prepared; -} - /** * SqlTransactionQueriable * * Represents a transaction. */ -export interface SqlTransactionQueriable< +export interface SqlTransaction< ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, Connection extends SqlConnection = SqlConnection< ConnectionOptions @@ -410,32 +349,71 @@ export interface SqlTransactionQueriable< } /** - * SqlTransactionable + * SqlClientQueriable + * + * Represents an object that can create a transaction and a prepared statement. * - * Represents an object that can create a transaction. + * This interface is to be implemented by any class that supports creating a prepared statement. + * A prepared statement should in most cases be unique to a connection, + * and should not live after the related connection is closed. */ -export interface SqlTransactionable< +export interface SqlClientQueriable< ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, Connection extends SqlConnection = SqlConnection< ConnectionOptions >, ParameterType extends unknown = unknown, QueryOptions extends SqlQueryOptions = SqlQueryOptions, + Prepared extends SqlPreparedStatement< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions + > = SqlPreparedStatement< + ConnectionOptions, + Connection, + ParameterType, + QueryOptions + >, TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, - Transaction extends SqlTransactionQueriable< + Transaction extends SqlTransaction< ConnectionOptions, Connection, ParameterType, QueryOptions, TransactionOptions - > = SqlTransactionQueriable< + > = SqlTransaction< ConnectionOptions, Connection, ParameterType, QueryOptions, TransactionOptions >, -> extends SqlConnectableBase { +> extends + SqlQueriable { + /** + * Create a prepared statement that can be executed multiple times. + * This is useful when you want to execute the same SQL statement multiple times with different parameters. + * + * @param sql the SQL statement + * @param options the options to pass to the query method, will be merged with the global options + * @returns a prepared statement + * + * @example + * ```ts + * const stmt = db.prepare("SELECT * FROM table WHERE id = ?"); + * + * for (let i = 0; i < 10; i++) { + * const row of stmt.query([i]) + * console.log(row); + * } + * ``` + */ + prepare( + sql: string, + options?: QueryOptions, + ): Prepared; + /** * Starts a transaction */ @@ -458,32 +436,3 @@ export interface SqlTransactionable< fn: (t: Transaction) => Promise, ): Promise; } - -/** - * SqlPoolable - * - * Represents an object that can acquire a connection from a pool. - * - * TODO: check if this still works with new DeferredStack - */ -export interface SqlPoolable< - Connectable extends SqlConnectablePoolBase = SqlConnectablePoolBase, - DeferredStack extends SqlDeferredStack = SqlDeferredStack< - Connectable - >, -> { - /** - * The deferred stack of connections - */ - deferredStack: DeferredStack; - - /** - * Acquire a connection from the pool - */ - acquire(): Promise; - - /** - * Releases the connection to the pool - */ - release(connectable: Connectable): Promise; -} diff --git a/sql/errors.ts b/sql/errors.ts index f77eba5..72a95c6 100644 --- a/sql/errors.ts +++ b/sql/errors.ts @@ -1,6 +1,3 @@ -import type { SqlBase } from "./core.ts"; -import { VERSION } from "./meta.ts"; - /** * SqlError * @@ -12,18 +9,3 @@ export class SqlError extends Error { this.name = this.constructor.name; } } - -/** - * SqlDeferredError - * - * Error that is thrown by DeferredStack - */ -export class SqlDeferredError extends SqlError { -} - -/** - * Check if an error is a SqlError - */ -export function isSqlError(err: unknown): err is SqlError { - return err instanceof SqlError; -} diff --git a/sql/events.ts b/sql/events.ts index ae8f59a..8594ce3 100644 --- a/sql/events.ts +++ b/sql/events.ts @@ -35,14 +35,14 @@ export interface SqlErrorEventInit< } /** - * SqlPoolEventInit + * SqlConnectableEventInit * - * SQLx Pool event init + * SQLx Connectable event init */ -export interface SqlConnectableEventInit< - Connectable extends SqlConnectableBase = SqlConnectableBase, +export interface SqlConnectionEventInit< + Connection extends SqlConnection = SqlConnection, > extends EventInit { - connectable: Connectable; + connection: Connection; } /** @@ -65,7 +65,7 @@ export class SqlErrorEvent< */ export class SqlEvent< EventType extends SqlPoolConnectionEventType = SqlPoolConnectionEventType, - EventInit extends SqlConnectableEventInit = SqlConnectableEventInit, + EventInit extends SqlConnectionEventInit = SqlConnectionEventInit, > extends Event { constructor(type: EventType, eventInitDict?: EventInit) { super(type, eventInitDict); @@ -73,10 +73,10 @@ export class SqlEvent< } /** - * SqlClientConnectEvent class + * Gets dispatched when a connection is established */ -export class SqlConnectableConnectEvent< - EventInit extends SqlConnectableEventInit = SqlConnectableEventInit, +export class SqlConnectEvent< + EventInit extends SqlConnectionEventInit = SqlConnectionEventInit, > extends SqlEvent<"connect", EventInit> { constructor(eventInitDict: EventInit) { super("connect", eventInitDict); @@ -84,10 +84,10 @@ export class SqlConnectableConnectEvent< } /** - * SqlConnectionCloseEvent class + * Gets dispatched when a connection is about to be closed */ -export class SqlConnectableCloseEvent< - EventInit extends SqlConnectableEventInit = SqlConnectableEventInit, +export class SqlCloseEvent< + EventInit extends SqlConnectionEventInit = SqlConnectionEventInit, > extends SqlEvent<"close", EventInit> { constructor(eventInitDict: EventInit) { super("close", eventInitDict); @@ -95,10 +95,10 @@ export class SqlConnectableCloseEvent< } /** - * SqlPoolConnectionAcquireEvent class + * Gets dispatched when a connection is acquired from the pool */ -export class SqlPoolConnectableAcquireEvent< - EventInit extends SqlConnectableEventInit = SqlConnectableEventInit, +export class SqlAcquireEvent< + EventInit extends SqlConnectionEventInit = SqlConnectionEventInit, > extends SqlEvent<"acquire", EventInit> { constructor(eventInitDict: EventInit) { super("acquire", eventInitDict); @@ -106,10 +106,10 @@ export class SqlPoolConnectableAcquireEvent< } /** - * SqlPoolConnectionReleaseEvent class + * Gets dispatched when a connection is released back to the pool */ -export class SqlPoolConnectableReleaseEvent< - EventInit extends SqlConnectableEventInit = SqlConnectableEventInit, +export class SqlReleaseEvent< + EventInit extends SqlConnectionEventInit = SqlConnectionEventInit, > extends SqlEvent<"release", EventInit> { constructor(eventInitDict: EventInit) { super("release", eventInitDict); @@ -131,8 +131,9 @@ export class SqlEventTarget< ConnectionOptions >, EventType extends SqlPoolConnectionEventType = SqlPoolConnectionEventType, - EventInit extends SqlConnectableEventInit> = - SqlConnectableEventInit>, + EventInit extends SqlConnectionEventInit = SqlConnectionEventInit< + Connection + >, Event extends SqlEvent = SqlEvent< EventType, EventInit diff --git a/sql/mod.ts b/sql/mod.ts index 1c4ad50..f93b657 100644 --- a/sql/mod.ts +++ b/sql/mod.ts @@ -1,3 +1,4 @@ +export * from "./asserts.ts"; export * from "./client.ts"; export * from "./connection.ts"; export * from "./core.ts"; diff --git a/sql/pool.ts b/sql/pool.ts index 24d4aa1..092a27c 100644 --- a/sql/pool.ts +++ b/sql/pool.ts @@ -1,28 +1,37 @@ import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; import type { - SqlConnectablePoolBase, - SqlPoolable, - SqlPreparable, - SqlPreparedQueriable, - SqlQueriable, + SqlClientQueriable, + SqlConnectableBase, + SqlPreparedStatement, SqlQueryOptions, - SqlTransactionable, + SqlTransaction, SqlTransactionOptions, - SqlTransactionQueriable, } from "./core.ts"; import type { - DeferredStack as SqlDeferredStack, + DeferredStack, DeferredStackOptions, } from "../collections/deferred_stack.ts"; import type { SqlEventable, SqlEventTarget } from "./events.ts"; +/** + * SqlPoolClientOptions + * + * This represents the options for a pool client. + */ +export interface SqlPoolClientOptions { + /** + * The function to call when releasing the connection. + */ + releaseFn?: () => Promise; +} + /** * SqlClientPoolOptions * * This represents the options for a connection pool. */ export interface SqlClientPoolOptions - extends SqlConnectionOptions, DeferredStackOptions { + extends SqlConnectionOptions, DeferredStackOptions { /** * Whether to lazily initialize connections. * @@ -47,54 +56,55 @@ export interface SqlPoolClient< >, ParameterType extends unknown = unknown, QueryOptions extends SqlQueryOptions = SqlQueryOptions, - Prepared extends SqlPreparedQueriable< + Prepared extends SqlPreparedStatement< ConnectionOptions, Connection, ParameterType, QueryOptions - > = SqlPreparedQueriable< + > = SqlPreparedStatement< ConnectionOptions, Connection, ParameterType, QueryOptions >, TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, - Transaction extends SqlTransactionQueriable< + Transaction extends SqlTransaction< ConnectionOptions, Connection, ParameterType, QueryOptions, TransactionOptions - > = SqlTransactionQueriable< + > = SqlTransaction< ConnectionOptions, Connection, ParameterType, QueryOptions, TransactionOptions >, + PoolClientOptions extends SqlPoolClientOptions = SqlPoolClientOptions, > extends - SqlConnectablePoolBase, - SqlQueriable< - ConnectionOptions, - Connection, - ParameterType, - QueryOptions - >, - SqlPreparable< - ConnectionOptions, - Connection, - ParameterType, - QueryOptions, - Prepared - >, - SqlTransactionable< + SqlConnectableBase, + SqlClientQueriable< ConnectionOptions, Connection, ParameterType, QueryOptions, + Prepared, TransactionOptions, Transaction > { + /** + * The options used to create the pool client + */ + readonly options: + & ConnectionOptions + & QueryOptions + & TransactionOptions + & PoolClientOptions; + /** + * Whether the pool client is disposed and should not be available anymore + */ + disposed: boolean; /** * Release the connection to the pool */ @@ -110,30 +120,36 @@ export interface SqlPoolClient< */ export interface SqlClientPool< ConnectionOptions extends SqlConnectionOptions = SqlConnectionOptions, - Connection extends SqlConnection = SqlConnection< - ConnectionOptions - >, ParameterType extends unknown = unknown, QueryOptions extends SqlQueryOptions = SqlQueryOptions, - Prepared extends SqlPreparedQueriable< + Connection extends SqlConnection< + ConnectionOptions, + ParameterType, + QueryOptions + > = SqlConnection< + ConnectionOptions, + ParameterType, + QueryOptions + >, + Prepared extends SqlPreparedStatement< ConnectionOptions, Connection, ParameterType, QueryOptions - > = SqlPreparedQueriable< + > = SqlPreparedStatement< ConnectionOptions, Connection, ParameterType, QueryOptions >, TransactionOptions extends SqlTransactionOptions = SqlTransactionOptions, - Transaction extends SqlTransactionQueriable< + Transaction extends SqlTransaction< ConnectionOptions, Connection, ParameterType, QueryOptions, TransactionOptions - > = SqlTransactionQueriable< + > = SqlTransaction< ConnectionOptions, Connection, ParameterType, @@ -157,15 +173,24 @@ export interface SqlClientPool< TransactionOptions, Transaction >, - DeferredStack extends SqlDeferredStack = SqlDeferredStack< - PoolClient - >, EventTarget extends SqlEventTarget = SqlEventTarget, > extends - SqlConnection, - SqlPoolable< - PoolClient, - DeferredStack - >, - SqlEventable { + SqlEventable, + Omit< + SqlConnection< + ConnectionOptions + >, + "execute" | "queryMany" | "queryManyArray" + > { + readonly options: ConnectionOptions & QueryOptions & TransactionOptions; + + /** + * The deferred stack of connections + */ + deferredStack: DeferredStack; + + /** + * Acquire a connection from the pool + */ + acquire(): Promise; } diff --git a/sql/test.ts b/sql/test.ts new file mode 100644 index 0000000..c244e13 --- /dev/null +++ b/sql/test.ts @@ -0,0 +1,653 @@ +// deno-lint-ignore-file no-unused-vars no-explicit-any +import { DeferredStack } from "../collections/deferred_stack.ts"; +import { + SqlConnectionEventInit, + SqlEvent, + SqlEventable, + SqlEventTarget, + SqlPoolConnectionEventType, +} from "./events.ts"; +import * as Sql from "./mod.ts"; +import { + clientPoolTest, + clientTest, + connectionConstructorTest, + TestQueries, +} from "./testing.ts"; + +/** + * Represents a database table for a test database + */ +type TestDbTable = Array>; +/** + * Represents a database for testing + */ +type TestDb = Record; + +const testDb: TestDb = {}; + +const testDbQueryParser = (sql: string, parameters: string[] = []) => { + const replaceWildcards = (query: string, values: string[]) => { + for (const v of values) { + query = query.replace("?", v); + } + + return query; + }; + + const querySegments = sql.toLowerCase().split(" "); + if (querySegments[0] === "create") { + if (querySegments[1] === "table") { + const tableName = querySegments[2]; + testDb[tableName] = []; + return; + } + } else if (querySegments[0] === "drop") { + if (querySegments[1] === "table") { + const tableName = querySegments[2]; + delete testDb[tableName]; + return; + } + } else if (querySegments[0] === "insert") { + if (querySegments[1] === "into") { + const tableName = querySegments[2]; + const values = querySegments[4]; + const parsedValues = replaceWildcards(values, parameters); + const jsonValues = JSON.parse(parsedValues); + for (const v of jsonValues) { + testDb[tableName].push(v); + } + return jsonValues.length; + } + } else if (querySegments[0] === "select") { + if (querySegments[1] === "*") { + if (querySegments[2] === "from") { + const tableName = querySegments[3]; + if (querySegments[4]) { + if (querySegments[4] === "where") { + const columnName = querySegments[5]; + const value = parameters[0]; + return testDb[tableName].filter((row) => row[columnName] === value); + } + } else { + return testDb[tableName]; + } + } + } + } else if (querySegments[0] === "return") { + if (querySegments[1].startsWith("'")) { + return [{ result: querySegments[1].replace(/'/g, "") }]; + } else { + return [{ result: JSON.parse(querySegments[1]) }]; + } + } else if (querySegments[0] === "delete") { + if (querySegments[1] === "from") { + const tableName = querySegments[2]; + if (querySegments[3] === "where") { + const columnName = querySegments[4]; + const value = parameters[0]; + testDb[tableName] = testDb[tableName].filter((row) => + row[columnName] !== value + ); + } else { + testDb[tableName] = []; + } + return; + } + } +}; + +type TestRow = Sql.Row; +type TestArrayRow = Sql.ArrayRow; +type TestParameterType = string; +type TestSqlTransactionOptions = Sql.SqlTransactionOptions; +interface TestSqlQueryOptions extends Sql.SqlQueryOptions { + test?: string; +} +interface TestSqlConnectionOptions extends Sql.SqlConnectionOptions { + test?: string; +} +interface TestSqlClientPoolOptions + extends Sql.SqlClientPoolOptions, TestSqlConnectionOptions { +} +class TestSqlConnection implements Sql.SqlConnection { + connectionUrl: string; + options: TestSqlConnectionOptions; + _connected: boolean = false; + constructor( + connectionUrl: string, + options: TestSqlConnectionOptions = {}, + ) { + this.connectionUrl = connectionUrl; + this.options = options; + } + get connected(): boolean { + return this._connected; + } + connect(): Promise { + this._connected = true; + return Promise.resolve(); + } + close(): Promise { + this._connected = false; + return Promise.resolve(); + } + execute( + sql: string, + params?: unknown[] | undefined, + options?: Sql.SqlQueryOptions | undefined, + ): Promise { + const queryRes = testDbQueryParser(sql, params as string[]); + return Promise.resolve(queryRes); + } + async *queryMany = Sql.Row>( + sql: string, + params?: unknown[] | undefined, + options?: Sql.SqlQueryOptions | undefined, + ): AsyncGenerator { + const queryRes = testDbQueryParser(sql, params as string[]); + for (const row of queryRes) { + yield row; + } + } + async *queryManyArray = Sql.ArrayRow>( + sql: string, + params?: unknown[] | undefined, + options?: Sql.SqlQueryOptions | undefined, + ): AsyncGenerator { + const queryRes = testDbQueryParser(sql, params as string[]); + for (const row of queryRes) { + const values = Object.values(row); + yield values as T; + } + } + async [Symbol.asyncDispose](): Promise { + await this.close(); + } +} + +class TestSqlConnectableBase + implements Sql.SqlConnectableBase { + connection: TestSqlConnection; + get connected(): boolean { + return this.connection.connected; + } + + constructor(connection: TestSqlConnection) { + this.connection = connection; + } +} + +class TestSqlPreparedStatement extends TestSqlConnectableBase + implements + Sql.SqlPreparedStatement< + TestSqlConnectionOptions, + TestSqlConnection, + TestParameterType, + TestSqlQueryOptions + > { + sql: string; + options: TestSqlConnectionOptions & TestSqlQueryOptions; + constructor( + connection: TestSqlConnection, + sql: string, + options: TestSqlConnectionOptions & TestSqlQueryOptions = {}, + ) { + super(connection); + this.sql = sql; + this.options = options; + } + execute( + params?: TestParameterType[] | undefined, + options?: TestSqlQueryOptions | undefined, + ): Promise { + return this.connection.execute(this.sql, params, options); + } + query( + params?: TestParameterType[] | undefined, + options?: TestSqlQueryOptions | undefined, + ): Promise { + return Array.fromAsync(this.queryMany(params, options)); + } + queryOne( + params?: TestParameterType[] | undefined, + options?: TestSqlQueryOptions | undefined, + ): Promise { + return this.query(params, options).then((res) => res[0]) as Promise< + T | undefined + >; + } + queryMany( + params?: TestParameterType[] | undefined, + options?: TestSqlQueryOptions | undefined, + ): AsyncGenerator { + return this.connection.queryMany(this.sql, params, options); + } + queryArray( + params?: TestParameterType[] | undefined, + options?: TestSqlQueryOptions | undefined, + ): Promise { + return Array.fromAsync(this.queryManyArray(params, options)); + } + queryOneArray( + params?: TestParameterType[] | undefined, + options?: TestSqlQueryOptions | undefined, + ): Promise { + return this.queryArray(params, options).then((res) => res[0]) as Promise< + T | undefined + >; + } + queryManyArray( + params?: TestParameterType[] | undefined, + options?: TestSqlQueryOptions | undefined, + ): AsyncGenerator { + return this.connection.queryManyArray(this.sql, params, options); + } +} + +class TestSqlQueriable extends TestSqlConnectableBase + implements + Sql.SqlQueriable< + TestSqlConnectionOptions, + TestSqlConnection, + TestParameterType, + TestSqlQueryOptions + > { + options: TestSqlConnectionOptions & TestSqlQueryOptions; + constructor( + connection: TestSqlConnection, + options: TestSqlConnectionOptions & TestSqlQueryOptions = {}, + ) { + super(connection); + this.options = options; + } + execute( + sql: string, + params?: TestParameterType[] | undefined, + options?: TestSqlQueryOptions | undefined, + ): Promise { + return Promise.resolve(testDbQueryParser(sql, params)); + } + query( + sql: string, + params?: TestParameterType[] | undefined, + options?: TestSqlQueryOptions | undefined, + ): Promise { + return Array.fromAsync(this.queryMany(sql, params, options)); + } + queryOne( + sql: string, + params?: TestParameterType[] | undefined, + options?: TestSqlQueryOptions | undefined, + ): Promise { + return this.query(sql, params, options).then((res) => res[0]) as Promise< + T | undefined + >; + } + queryMany( + sql: string, + params?: TestParameterType[] | undefined, + options?: TestSqlQueryOptions | undefined, + ): AsyncGenerator { + return this.connection.queryMany(sql, params, options); + } + queryArray( + sql: string, + params?: TestParameterType[] | undefined, + options?: TestSqlQueryOptions | undefined, + ): Promise { + return Array.fromAsync(this.queryManyArray(sql, params, options)); + } + queryOneArray( + sql: string, + params?: TestParameterType[] | undefined, + options?: TestSqlQueryOptions | undefined, + ): Promise { + return this.queryArray(sql, params, options).then((res) => + res[0] + ) as Promise; + } + queryManyArray( + sql: string, + params?: TestParameterType[] | undefined, + options?: TestSqlQueryOptions | undefined, + ): AsyncGenerator { + return this.connection.queryManyArray(sql, params, options); + } + sql = Sql.Row>( + strings: TemplateStringsArray, + ...parameters: string[] + ): Promise { + return this.query(strings.join("?"), parameters); + } + sqlArray = Sql.ArrayRow>( + strings: TemplateStringsArray, + ...parameters: string[] + ): Promise { + return this.queryArray(strings.join("?"), parameters); + } +} + +class TestSqlTransaction extends TestSqlQueriable implements + Sql.SqlTransaction< + TestSqlConnectionOptions, + TestSqlConnection, + TestParameterType, + TestSqlQueryOptions, + TestSqlTransactionOptions + > { + _inTransaction: boolean = false; + get inTransaction(): boolean { + return this._inTransaction; + } + options: + & TestSqlConnectionOptions + & TestSqlQueryOptions + & TestSqlTransactionOptions; + constructor( + connection: TestSqlConnection, + options: + & TestSqlConnectionOptions + & TestSqlQueryOptions + & TestSqlTransactionOptions = {}, + ) { + super(connection, options); + this.options = options; + } + commitTransaction( + options?: Record | undefined, + ): Promise { + return Promise.resolve(); + } + rollbackTransaction( + options?: Record | undefined, + ): Promise { + return Promise.resolve(); + } + createSavepoint(name?: string | undefined): Promise { + return Promise.resolve(); + } + releaseSavepoint(name?: string | undefined): Promise { + return Promise.resolve(); + } +} + +class TestSqlClientQueriable extends TestSqlQueriable + implements + Sql.SqlClientQueriable< + TestSqlConnectionOptions, + TestSqlConnection, + TestParameterType, + TestSqlQueryOptions, + TestSqlPreparedStatement, + TestSqlTransactionOptions + > { + options: + & TestSqlConnectionOptions + & TestSqlQueryOptions + & TestSqlTransactionOptions; + constructor( + connection: TestSqlConnection, + options: + & TestSqlConnectionOptions + & TestSqlQueryOptions + & TestSqlTransactionOptions = {}, + ) { + super(connection, options); + this.options = options; + } + prepare( + sql: string, + options?: TestSqlQueryOptions | undefined, + ): TestSqlPreparedStatement { + return new TestSqlPreparedStatement(this.connection, sql, options); + } + beginTransaction( + options?: Record | undefined, + ): Promise { + return Promise.resolve( + new TestSqlTransaction(this.connection, this.options), + ); + } + transaction( + fn: ( + t: TestSqlTransaction, + ) => Promise, + ): Promise { + return fn(new TestSqlTransaction(this.connection, this.options)); + } +} + +type TestSqlConnectionEventInit = SqlConnectionEventInit; + +class TestSqlEventTarget extends SqlEventTarget< + TestSqlConnectionOptions, + TestSqlConnection, + SqlPoolConnectionEventType, + TestSqlConnectionEventInit, + SqlEvent, + EventListenerOrEventListenerObject, + AddEventListenerOptions, + EventListenerOptions +> { +} + +class TestSqlEventable implements + SqlEventable< + TestSqlEventTarget + > { + eventTarget: TestSqlEventTarget; + constructor(eventTarget: TestSqlEventTarget) { + this.eventTarget = eventTarget; + } +} + +class TestSqlClient extends TestSqlClientQueriable implements + Sql.SqlClient< + TestSqlEventTarget, + TestSqlConnectionOptions, + TestParameterType, + TestSqlQueryOptions, + TestSqlConnection, + TestSqlPreparedStatement, + TestSqlTransactionOptions, + TestSqlTransaction + > { + connectionUrl: string; + options: TestSqlConnectionOptions; + eventTarget: TestSqlEventTarget; + constructor( + connectionUrl: string, + connectionOptions?: TestSqlConnectionOptions, + ) { + super(new TestSqlConnection(connectionUrl, connectionOptions), { + test: "test", + }); + this.connectionUrl = connectionUrl; + this.options = connectionOptions ?? { test: "test" }; + this.eventTarget = new TestSqlEventTarget(); + } + async connect(): Promise { + await this.connection.connect(); + this.eventTarget.dispatchEvent( + new Sql.SqlConnectEvent({ connection: this.connection }), + ); + } + async close(): Promise { + this.eventTarget.dispatchEvent( + new Sql.SqlCloseEvent({ connection: this.connection }), + ); + await this.connection.close(); + } + [Symbol.asyncDispose](): Promise { + return this.close(); + } +} + +type TestSqlPoolClientOptions = + & TestSqlConnectionOptions + & TestSqlQueryOptions + & TestSqlTransactionOptions + & Sql.SqlPoolClientOptions; + +class TestSqlPoolClient extends TestSqlClientQueriable + implements + Sql.SqlPoolClient< + TestSqlConnectionOptions, + TestSqlConnection, + TestParameterType, + TestSqlQueryOptions, + TestSqlPreparedStatement, + TestSqlTransactionOptions, + TestSqlTransaction + > { + readonly options: TestSqlPoolClientOptions; + + #releaseFn?: () => Promise; + + #disposed: boolean = false; + get disposed(): boolean { + return this.#disposed; + } + + constructor( + connection: TestSqlConnection, + options: TestSqlPoolClientOptions, + ) { + super(connection, options); + this.options = options; + if (this.options.releaseFn) { + this.#releaseFn = this.options.releaseFn; + } + } + async release() { + this.#disposed = true; + await this.#releaseFn?.(); + } + + [Symbol.asyncDispose](): Promise { + return this.release(); + } +} + +class TestSqlClientPool implements + Sql.SqlClientPool< + TestSqlClientPoolOptions, + TestParameterType, + TestSqlQueryOptions, + TestSqlConnection, + TestSqlPreparedStatement, + TestSqlTransactionOptions, + TestSqlTransaction, + TestSqlPoolClient, + TestSqlEventTarget + > { + deferredStack: DeferredStack; + eventTarget: TestSqlEventTarget; + connectionUrl: string; + options: TestSqlClientPoolOptions; + _connected: boolean = false; + get connected(): boolean { + return this._connected; + } + constructor( + connectionUrl: string, + options?: TestSqlClientPoolOptions, + ) { + this.connectionUrl = connectionUrl; + this.options = options ?? { test: "test" }; + this.deferredStack = new DeferredStack({ + maxSize: 3, + removeFn: async (element) => { + await element._value.close(); + }, + }); + this.eventTarget = new TestSqlEventTarget(); + } + async connect(): Promise { + for (let i = 0; i < this.deferredStack.maxSize; i++) { + const conn = new TestSqlConnection( + this.connectionUrl, + this.options, + ); + if (!this.options.lazyInitialization) { + await conn.connect(); + this.eventTarget.dispatchEvent( + new Sql.SqlConnectEvent({ connection: conn }), + ); + } + this.deferredStack.add(conn); + } + } + async close(): Promise { + for (const el of this.deferredStack.elements) { + this.eventTarget.dispatchEvent( + new Sql.SqlCloseEvent({ connection: el._value }), + ); + await el.remove(); + } + } + async acquire(): Promise { + const el = await this.deferredStack.pop(); + this.eventTarget.dispatchEvent( + new Sql.SqlAcquireEvent({ connection: el.value }), + ); + const c = new TestSqlPoolClient(el.value, { + ...this.options, + releaseFn: async () => { + this.eventTarget.dispatchEvent( + new Sql.SqlReleaseEvent({ connection: el._value }), + ); + await el.release(); + }, + }); + return c; + } + async [Symbol.asyncDispose](): Promise { + await this.close(); + } +} + +const queries: TestQueries = { + createTable: "CREATE TABLE sqlxtesttable", + dropTable: "DROP TABLE sqlxtesttable", + insertOneToTable: 'INSERT INTO sqlxtesttable VALUES [{"testcol":"?"}]', + insertManyToTable: + 'INSERT INTO sqlxtesttable VALUES [{"testcol":"?"},{"testcol":"?"},{"testcol":"?"}]', + selectOneFromTable: "SELECT * FROM sqlxtesttable WHERE testcol = ? LIMIT 1", + selectByMatchFromTable: "SELECT * FROM sqlxtesttable WHERE testcol = ?", + selectManyFromTable: "SELECT * FROM sqlxtesttable", + select1AsString: "RETURN '1'", + select1Plus1AsNumber: "RETURN 2", + deleteByMatchFromTable: "DELETE FROM sqlxtesttable WHERE testcol = ?", + deleteAllFromTable: "DELETE FROM sqlxtesttable", +}; + +Deno.test("sql connection test", async (t) => { + await connectionConstructorTest({ + t, + Connection: TestSqlConnection, + connectionOptions: { test: "test" }, + connectionUrl: "test", + }); +}); + +Deno.test("sql client test", async (t) => { + await clientTest({ + t, + Client: TestSqlClient, + connectionUrl: "test", + connectionOptions: { test: "test" }, + queries, + }); +}); + +Deno.test("sql client pool test", async (t) => { + await clientPoolTest({ + t, + Client: TestSqlClientPool, + connectionUrl: "test", + connectionOptions: { test: "test", test2: "test2" }, + queries, + }); +}); diff --git a/sql/testing.ts b/sql/testing.ts index 55308c5..33b5b5b 100644 --- a/sql/testing.ts +++ b/sql/testing.ts @@ -1,19 +1,12 @@ // deno-lint-ignore-file no-explicit-any -import { - assert, - assertEquals, - assertFalse, - assertInstanceOf, -} from "@std/assert"; +import { assert, assertEquals, assertFalse } from "@std/assert"; import { type ArrayRow, type Row, - SqlBase, - type SqlPreparable, - type SqlPreparedQueriable, + type SqlClientQueriable, + type SqlPreparedStatement, type SqlQueriable, - type SqlTransactionable, - type SqlTransactionQueriable, + type SqlTransaction, } from "./core.ts"; import type { SqlClient } from "./client.ts"; import type { SqlConnection, SqlConnectionOptions } from "./connection.ts"; @@ -22,86 +15,91 @@ import type { SqlClientPoolOptions, SqlPoolClient, } from "./pool.ts"; -import { VERSION } from "./meta.ts"; import type { SqlQueryOptions } from "./core.ts"; -import { DeferredStack } from "../collections/deferred_stack.ts"; - -interface ConnectionConstructor extends SqlBase { +import { + assertIsSqlClient, + assertIsSqlClientPool, + assertIsSqlClientQueriable, + assertIsSqlConnection, + assertIsSqlPreparedStatement, + assertIsSqlQueriable, + assertIsSqlTransaction, +} from "./asserts.ts"; + +interface ConnectionConstructor { new ( ...args: any[] ): SqlConnection; } -interface ClientConstructor extends SqlBase { +interface ClientConstructor { new ( ...args: any[] ): SqlClient; } -interface ClientPoolConstructor extends SqlBase { +interface ClientPoolConstructor { new ( ...args: any[] ): SqlClientPool; } +export type TestQueries = { + /** + * Should create a table "sqlxtesttable" if not exist with a single column "testcol" of string type (min 10 characters) + */ + createTable: string; + /** + * Should drop the table "sqlxtesttable" if exists + */ + dropTable: string; + /** + * Should insert a single row into the table + */ + insertOneToTable: string; + /** + * Should insert multiple rows into the table + */ + insertManyToTable: string; + /** + * Should select a single row from the table + */ + selectOneFromTable: string; + /** + * Should select a single row by matching the testcol value + */ + selectByMatchFromTable: string; + /** + * Should select multiple rows from the table + */ + selectManyFromTable: string; + /** + * Should return "1" as "result" + */ + select1AsString: string; + /** + * Should return 1+1 as "result" + */ + select1Plus1AsNumber: string; + /** + * Should delete a single row by matching the testcol value + */ + deleteByMatchFromTable: string; + /** + * Should delete all rows from the table + */ + deleteAllFromTable: string; +}; + export type BaseQueriableTestOptions = { t: Deno.TestContext; - queries: { - /** - * Should create a table "sqlxtesttable" if not exist with a single column "testcol" of string type (min 10 characters) - */ - createTable: string; - /** - * Should drop the table "sqlxtesttable" if exists - */ - dropTable: string; - /** - * Should insert a single row into the table - */ - insertOneToTable: string; - /** - * Should insert multiple rows into the table - */ - insertManyToTable: string; - /** - * Should select a single row from the table - */ - selectOneFromTable: string; - /** - * Should select a single row by matching the testcol value - */ - selectByMatchFromTable: string; - /** - * Should select multiple rows from the table - */ - selectManyFromTable: string; - /** - * Should return "1" as "result" - */ - select1AsString: string; - /** - * Should return 1+1 as "result" - */ - select1Plus1AsNumber: string; - /** - * Should delete a single row by matching the testcol value - */ - deleteByMatchFromTable: string; - /** - * Should delete all rows from the table - */ - deleteAllFromTable: string; - }; + queries: TestQueries; }; export type TestConnectAndClosePoolClient = PoolTestOptions; -export type TestPreparableOptions = BaseQueriableTestOptions & { - db: SqlPreparable; -}; - -export type TestPreparedQueriableOptions = +export type TestPreparedStatementOptions = & Omit & { - db: SqlPreparedQueriable; + db: SqlPreparedStatement; cases: { execute: { params?: any[]; @@ -138,16 +136,11 @@ export type TestQueriableOptions = BaseQueriableTestOptions & { db: SqlQueriable; }; -export type TestTransactionableOptions = BaseQueriableTestOptions & { - db: SqlTransactionable & SqlQueriable; +export type TestClientQueriableOptions = BaseQueriableTestOptions & { + db: SqlClientQueriable; }; export type TestTransactionOptions = BaseQueriableTestOptions & { - db: SqlTransactionQueriable; -}; - -export type SqlBaseStaticTestOptions = { - t: Deno.TestContext; - Base: typeof SqlBase; + db: SqlTransaction; }; export type ConnectionTestOptions = { @@ -159,13 +152,13 @@ export type ConnectionConstructorTestOptions = { t: Deno.TestContext; Connection: ConnectionConstructor; connectionUrl: string | URL; - connectionOptions: SqlConnectionOptions; + connectionOptions: SqlConnectionOptions & Record; }; export type ClientTestOptions = BaseQueriableTestOptions & { Client: ClientConstructor; connectionUrl: string | URL; - connectionOptions: SqlConnectionOptions & SqlQueryOptions; + connectionOptions: SqlConnectionOptions & SqlQueryOptions & Record; }; export type PoolTestOptions = BaseQueriableTestOptions & { @@ -174,10 +167,11 @@ export type PoolTestOptions = BaseQueriableTestOptions & { connectionOptions: & SqlConnectionOptions & SqlQueryOptions - & SqlClientPoolOptions; + & SqlClientPoolOptions + & Record; }; -async function testConnectAndCloseClient( +async function _testConnectAndCloseClient( { t, Client, connectionUrl, connectionOptions }: ClientTestOptions, ): Promise { await t.step("testConnectAndClose", async (t) => { @@ -217,21 +211,19 @@ async function testConnectAndCloseClient( error = e; } - assertEquals( + assert( connectListenerCalled, - true, "Connect listener not called: " + error?.message, ); - assertEquals( + assert( closeListenerCalled, - true, "Close listener not called: " + error?.message, ); }); }); } -async function testConnectAndClosePoolClient( +async function _testConnectAndClosePoolClient( { t, Client, connectionUrl, connectionOptions }: TestConnectAndClosePoolClient, ): Promise { @@ -302,19 +294,11 @@ async function testConnectAndClosePoolClient( }); } -async function testPreparedQueriable( - { t, db, cases }: TestPreparedQueriableOptions, +async function _testPreparedStatement( + { t, db, cases }: TestPreparedStatementOptions, ): Promise { - await t.step("testPreparedQueriable", async (t) => { - await t.step("has properties", () => { - assertEquals(typeof db["execute"], "function"); - assertEquals(typeof db["query"], "function"); - assertEquals(typeof db["queryOne"], "function"); - assertEquals(typeof db["queryMany"], "function"); - assertEquals(typeof db["queryArray"], "function"); - assertEquals(typeof db["queryOneArray"], "function"); - assertEquals(typeof db["queryManyArray"], "function"); - }); + await t.step("testPreparedStatement", async (t) => { + assertIsSqlPreparedStatement(db); await t.step("should execute", async () => { if (cases.execute.expected) { @@ -361,120 +345,11 @@ async function testPreparedQueriable( }); } -async function testPreparable( - { t, db, queries }: TestPreparableOptions, -): Promise { - await t.step("testPreparable", async (t) => { - assertEquals(typeof db["prepare"], "function"); - - await t.step("prepare select1AsString", async (t) => { - const statement = db.prepare(queries.select1AsString); - await testPreparedQueriable({ - t, - db: statement, - cases: { - execute: { expected: undefined }, - query: { expected: [{ result: "1" }] }, - queryOne: { expected: { result: "1" } }, - queryMany: { expected: [{ result: "1" }] }, - queryArray: { expected: [["1"]] }, - queryOneArray: { expected: ["1"] }, - queryManyArray: { expected: [["1"]] }, - }, - }); - }); - await t.step("prepare select1Plus1AsNumber", async (t) => { - const statement = db.prepare(queries.select1Plus1AsNumber); - await testPreparedQueriable({ - t, - db: statement, - cases: { - execute: { expected: undefined }, - query: { expected: [{ result: 2 }] }, - queryOne: { expected: { result: 2 } }, - queryMany: { expected: [{ result: 2 }] }, - queryArray: { expected: [[2]] }, - queryOneArray: { expected: [2] }, - queryManyArray: { expected: [[2]] }, - }, - }); - }); - await t.step("prepare selectByMatchFromTable", async (t) => { - const statement = db.prepare(queries.selectOneFromTable); - await testPreparedQueriable({ - t, - db: statement, - cases: { - execute: { params: ["test"], expected: undefined }, - query: { params: ["test"], expected: [{ testcol: "test" }] }, - queryOne: { params: ["test"], expected: { testcol: "test" } }, - queryMany: { params: ["test"], expected: [{ testcol: "test" }] }, - queryArray: { params: ["test"], expected: [["test"]] }, - queryOneArray: { params: ["test"], expected: ["test"] }, - queryManyArray: { params: ["test"], expected: [["test"]] }, - }, - }); - }); - await t.step("prepare selectByMatchFromTable", async (t) => { - const statement = db.prepare(queries.selectByMatchFromTable); - await testPreparedQueriable({ - t, - db: statement, - cases: { - execute: { params: ["test"], expected: undefined }, - query: { params: ["test"], expected: [{ testcol: "test" }] }, - queryOne: { params: ["test"], expected: { testcol: "test" } }, - queryMany: { params: ["test"], expected: [{ testcol: "test" }] }, - queryArray: { params: ["test"], expected: [["test"]] }, - queryOneArray: { params: ["test"], expected: ["test"] }, - queryManyArray: { params: ["test"], expected: [["test"]] }, - }, - }); - }); - await t.step("prepare selectManyFromTable", async (t) => { - const statement = db.prepare(queries.selectManyFromTable); - await testPreparedQueriable({ - t, - db: statement, - cases: { - execute: { expected: undefined }, - query: { - expected: [{ testcol: "test" }, { testcol: "test1" }, { - testcol: "test2", - }, { testcol: "test3" }], - }, - queryOne: { expected: { testcol: "test" } }, - queryMany: { - expected: [{ testcol: "test" }, { testcol: "test1" }, { - testcol: "test2", - }, { testcol: "test3" }], - }, - queryArray: { expected: [["test"], ["test1"], ["test2"], ["test3"]] }, - queryOneArray: { expected: ["test"] }, - queryManyArray: { - expected: [["test"], ["test1"], ["test2"], ["test3"]], - }, - }, - }); - }); - }); -} - -async function testQueriable( +async function _testQueriable( { t, db, queries }: TestQueriableOptions, ): Promise { await t.step("testQueriable", async (t) => { - await t.step("has properties", () => { - assertEquals(typeof db["execute"], "function"); - assertEquals(typeof db["query"], "function"); - assertEquals(typeof db["queryOne"], "function"); - assertEquals(typeof db["queryMany"], "function"); - assertEquals(typeof db["queryArray"], "function"); - assertEquals(typeof db["queryOneArray"], "function"); - assertEquals(typeof db["queryManyArray"], "function"); - assertEquals(typeof db["sql"], "function"); - assertEquals(typeof db["sqlArray"], "function"); - }); + assertIsSqlQueriable(db); await t.step("should execute", async () => { await db.execute(queries.selectManyFromTable); @@ -522,43 +397,132 @@ async function testQueriable( }); } -async function testTransactionable( - { t, db, queries }: TestTransactionableOptions, +async function _testClientQueriable( + { t, db, queries }: TestClientQueriableOptions, ): Promise { - await t.step("testTransactionable", async (t) => { - await t.step("has properties", () => { - assertEquals(typeof db["beginTransaction"], "function"); - assertEquals(typeof db["transaction"], "function"); + await t.step("testClientQueriable", async (t) => { + assertIsSqlClientQueriable(db); + + await t.step("test prepared statements", async (t) => { + await t.step("prepare select1AsString", async (t) => { + const statement = db.prepare(queries.select1AsString); + await _testPreparedStatement({ + t, + db: statement, + cases: { + execute: { expected: undefined }, + query: { expected: [{ result: "1" }] }, + queryOne: { expected: { result: "1" } }, + queryMany: { expected: [{ result: "1" }] }, + queryArray: { expected: [["1"]] }, + queryOneArray: { expected: ["1"] }, + queryManyArray: { expected: [["1"]] }, + }, + }); + }); + await t.step("prepare select1Plus1AsNumber", async (t) => { + const statement = db.prepare(queries.select1Plus1AsNumber); + await _testPreparedStatement({ + t, + db: statement, + cases: { + execute: { expected: undefined }, + query: { expected: [{ result: 2 }] }, + queryOne: { expected: { result: 2 } }, + queryMany: { expected: [{ result: 2 }] }, + queryArray: { expected: [[2]] }, + queryOneArray: { expected: [2] }, + queryManyArray: { expected: [[2]] }, + }, + }); + }); + await t.step("prepare selectByMatchFromTable", async (t) => { + const statement = db.prepare(queries.selectOneFromTable); + await _testPreparedStatement({ + t, + db: statement, + cases: { + execute: { params: ["test"], expected: undefined }, + query: { params: ["test"], expected: [{ testcol: "test" }] }, + queryOne: { params: ["test"], expected: { testcol: "test" } }, + queryMany: { params: ["test"], expected: [{ testcol: "test" }] }, + queryArray: { params: ["test"], expected: [["test"]] }, + queryOneArray: { params: ["test"], expected: ["test"] }, + queryManyArray: { params: ["test"], expected: [["test"]] }, + }, + }); + }); + await t.step("prepare selectByMatchFromTable", async (t) => { + const statement = db.prepare(queries.selectByMatchFromTable); + await _testPreparedStatement({ + t, + db: statement, + cases: { + execute: { params: ["test"], expected: undefined }, + query: { params: ["test"], expected: [{ testcol: "test" }] }, + queryOne: { params: ["test"], expected: { testcol: "test" } }, + queryMany: { params: ["test"], expected: [{ testcol: "test" }] }, + queryArray: { params: ["test"], expected: [["test"]] }, + queryOneArray: { params: ["test"], expected: ["test"] }, + queryManyArray: { params: ["test"], expected: [["test"]] }, + }, + }); + }); + await t.step("prepare selectManyFromTable", async (t) => { + const statement = db.prepare(queries.selectManyFromTable); + await _testPreparedStatement({ + t, + db: statement, + cases: { + execute: { expected: undefined }, + query: { + expected: [{ testcol: "test" }, { testcol: "test1" }, { + testcol: "test2", + }, { testcol: "test3" }], + }, + queryOne: { expected: { testcol: "test" } }, + queryMany: { + expected: [{ testcol: "test" }, { testcol: "test1" }, { + testcol: "test2", + }, { testcol: "test3" }], + }, + queryArray: { + expected: [["test"], ["test1"], ["test2"], ["test3"]], + }, + queryOneArray: { expected: ["test"] }, + queryManyArray: { + expected: [["test"], ["test1"], ["test2"], ["test3"]], + }, + }, + }); + }); }); - await testQueriable({ t, db, queries }); + await t.step("test transactions", async (t) => { + await _testQueriable({ t, db, queries }); - await t.step("transaction wrapper", async (t) => { - await db.transaction(async (c) => { - await testTransaction({ t, db: c, queries }); + await t.step("transaction wrapper", async (t) => { + await db.transaction(async (c) => { + await _testTransaction({ t, db: c, queries }); + }); }); - }); - await t.step("get a transaction instance", async (t) => { - const res = await db.beginTransaction(); + await t.step("get a transaction instance", async (t) => { + const res = await db.beginTransaction(); - await testTransaction({ t, db: res, queries }); + await _testTransaction({ t, db: res, queries }); + }); }); }); } -async function testTransaction( +async function _testTransaction( { t, db, queries }: TestTransactionOptions, ): Promise { await t.step("testTransaction", async (t) => { - await t.step("has properties", () => { - assertEquals(typeof db["commitTransaction"], "function"); - assertEquals(typeof db["rollbackTransaction"], "function"); - assertEquals(typeof db["createSavepoint"], "function"); - assertEquals(typeof db["releaseSavepoint"], "function"); - }); + assertIsSqlTransaction(db); - await testQueriable({ t, db, queries }); + await _testQueriable({ t, db, queries }); await t.step( "createSavepoint, releaseSavepoint", @@ -570,17 +534,17 @@ async function testTransaction( }); } -export async function testSetupTable( +export async function _testSetupTable( { t, db, queries }: TestQueriableOptions, ): Promise { await t.step("drop table", async () => { const res = await db.execute(queries.dropTable); - assertEquals(res, 0); + assertFalse(res); }); await t.step("setup table", async () => { const res = await db.execute(queries.createTable); - assertEquals(res, 0); + assertFalse(res); }); await t.step("insert one", async () => { @@ -615,23 +579,11 @@ export async function testSetupTable( }); } -export async function connectionTest( +export async function _connectionTest( { t, connection }: ConnectionTestOptions, ): Promise { await t.step("Connection Test", async (t) => { - await t.step("is instance of base", () => { - assertInstanceOf(connection, SqlBase); - }); - - await t.step("has properties", () => { - assertEquals(typeof connection.connectionUrl, "string"); - assertEquals(typeof connection.connectionOptions, "object"); - assertEquals(typeof connection.connected, "boolean"); - assertEquals(connection.connected, false); - assertEquals(typeof connection.connect, "function"); - assertEquals(typeof connection.close, "function"); - assertEquals(typeof connection[Symbol.asyncDispose], "function"); - }); + assertIsSqlConnection(connection); await t.step("can connect and close", async () => { assertEquals(connection.connected, false); @@ -651,17 +603,6 @@ export async function connectionTest( }); } -export async function sqlxBaseStaticTest( - { t, Base }: SqlBaseStaticTestOptions, -): Promise { - await t.step("SqlBase Static Test", async (t) => { - await t.step("has properties", () => { - assertEquals(typeof Base, "function"); - assertEquals(Base.sqlxVersion, VERSION); - }); - }); -} - /** * connectionConstructorTest * @@ -681,16 +622,10 @@ export async function connectionConstructorTest( ConnectionConstructorTestOptions, ): Promise { await t.step("SQLx Connection Constructor Test", async (t) => { - await t.step("is constructor", async (t) => { - assertEquals(typeof Connection, "function"); - await sqlxBaseStaticTest({ t, Base: Connection }); - }); + assertEquals(typeof Connection, "function"); - await t.step("can construct", async (t) => { - const connection = new Connection(connectionUrl, connectionOptions); - - await connectionTest({ t, connection }); - }); + const connection = new Connection(connectionUrl, connectionOptions); + await _connectionTest({ t, connection }); await t.step("can connect with using and dispose", async () => { await using connection = new Connection(connectionUrl, connectionOptions); @@ -719,17 +654,18 @@ export async function clientTest( { t, Client, connectionUrl, connectionOptions, queries }: ClientTestOptions, ): Promise { await t.step("SQLx Client Tests", async (t) => { - await t.step("is constructor", async (t) => { + await t.step("is constructor", () => { assertEquals(typeof Client, "function"); - await sqlxBaseStaticTest({ t, Base: Client }); }); await t.step("can construct", async (t) => { const client = new Client(connectionUrl, connectionOptions); - await connectionTest({ t, connection: client.connection }); + assertIsSqlClient(client); + + await _connectionTest({ t, connection: client.connection }); - await testConnectAndCloseClient({ + await _testConnectAndCloseClient({ t, Client, connectionUrl, @@ -749,10 +685,9 @@ export async function clientTest( await db.connect(); - await testSetupTable({ t, db, queries }); + await _testSetupTable({ t, db, queries }); - await testPreparable({ t, db, queries }); - await testTransactionable({ t, db, queries }); + // await testClientQueriable({ t, db, queries }); await t.step("drop table", async () => { await db.execute(queries.dropTable); @@ -783,24 +718,16 @@ export async function clientPoolTest( maxSize: 3, lazyInitialization: true, }; - await t.step("is constructor", async (t) => { + await t.step("is constructor", () => { assertEquals(typeof Client, "function"); - await sqlxBaseStaticTest({ t, Base: Client }); }); - await t.step("can construct", async (t) => { + await t.step("can construct", () => { const client = new Client(connectionUrl, parsedConnectionOptions); - await t.step("has properties", () => { - assertEquals(typeof client.deferredStack, "object"); - assertEquals(typeof client.acquire, "function"); - assertEquals(typeof client.release, "function"); - assertEquals(typeof client.connect, "function"); - assertEquals(typeof client.close, "function"); - assertInstanceOf(client.deferredStack, DeferredStack); - }); + assertIsSqlClientPool(client); }); - await testConnectAndClosePoolClient({ + await _testConnectAndClosePoolClient({ t: t, Client, connectionUrl, @@ -890,13 +817,13 @@ export async function clientPoolTest( await db.connect(); const p = await db.acquire(); - await testSetupTable({ t, db: p, queries }); + await _testSetupTable({ t, db: p, queries }); await p.release(); await t.step("acquire and release with query", async (t) => { for (let i = 0; i < db.deferredStack.maxSize; i++) { const p = await db.acquire(); - await testTransactionable({ t, db: p, queries }); + await _testClientQueriable({ t, db: p, queries }); p.release(); } }); From 64505a35bab563d08daa1e95a70f217abc9cab10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Thu, 20 Jun 2024 15:48:28 +0200 Subject: [PATCH 07/29] Updated inheritance logic and test cases --- sql/README.md | 2 +- sql/_assets/inheritance_flowchart.jpg | Bin 230770 -> 250073 bytes sql/asserts.ts | 42 +- sql/client.ts | 38 +- sql/connection.ts | 25 + sql/core.ts | 214 +++--- sql/deno.json | 2 +- sql/events.ts | 4 +- sql/pool.ts | 58 +- sql/test.ts | 308 +++++---- sql/testing.ts | 911 +++++++------------------- 11 files changed, 612 insertions(+), 992 deletions(-) diff --git a/sql/README.md b/sql/README.md index c539b5b..c22914a 100644 --- a/sql/README.md +++ b/sql/README.md @@ -196,7 +196,7 @@ following classes for your database driver: It is also however advisable to create additional helper classes for easier inheritance. See [test.ts](./test.ts) for a minimum but functional example of -how to implement these interfaces. +how to implement these interfaces into intermediate classes. ### Inheritance graph diff --git a/sql/_assets/inheritance_flowchart.jpg b/sql/_assets/inheritance_flowchart.jpg index 5f7ef7fe90c9ce60891af58a46e621888885ff23..84f9ea72052d48b32239959500b59eb65576779b 100644 GIT binary patch literal 250073 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 NOg8*~#D*Kg{|2Nlr6&LY literal 230770 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