Skip to content

Commit

Permalink
feat(sql): add sql standard interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
halvardssm committed May 30, 2024
1 parent e5458d8 commit 933e358
Show file tree
Hide file tree
Showing 13 changed files with 2,205 additions and 1 deletion.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"./collections",
"./crypto",
"./encoding",
"./http"
"./http",
"./sql"
],
"exclude": [
"./crypto/hash/_wasm/target"
Expand Down
230 changes: 230 additions & 0 deletions sql/README.md
Original file line number Diff line number Diff line change
@@ -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.
76 changes: 76 additions & 0 deletions sql/client.ts
Original file line number Diff line number Diff line change
@@ -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<ConnectionOptions> = 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<ConnectionOptions>,
SqlQueriable<
ConnectionOptions,
Connection,
ParameterType,
QueryOptions
>,
SqlPreparable<
ConnectionOptions,
Connection,
ParameterType,
QueryOptions,
Prepared
>,
SqlTransactionable<
ConnectionOptions,
Connection,
ParameterType,
QueryOptions,
TransactionOptions,
Transaction
>,
SqlEventable<EventTarget> {
}
58 changes: 58 additions & 0 deletions sql/connection.ts
Original file line number Diff line number Diff line change
@@ -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<void>;

/**
* Close the connection to the database
*/
close(): Promise<void>;
}
Loading

0 comments on commit 933e358

Please sign in to comment.