diff --git a/docs/2.connectors/mysql-pool.md b/docs/2.connectors/mysql-pool.md new file mode 100644 index 00000000..3489685e --- /dev/null +++ b/docs/2.connectors/mysql-pool.md @@ -0,0 +1,73 @@ +--- +icon: simple-icons:mysql +--- +# MySQL (Pool) + +> Connect DB0 to Mysql Database using mysql2 pool connection + +## Usage + +For this connector, you need to install [`mysql2`](https://www.npmjs.com/package/mysql2) dependency: + +:pm-install{name="mysql2"} + +Use `mysql2-pool` connector: + +```js +import { createDatabase } from "db0"; +import mysqlPool from "db0/connectors/mysql2-pool"; + +const db = createDatabase( + mysqlPool({ + /* options */ + }), +); +``` + +### Pool Query + +```js +const {rows} = db.sql`SELECT * FROM tbl`; +``` + +### Transactions + +For pool connections, do not use transactions with the **pool.query** method. + +:read-more{title="node-postgres transactions" to="https://pg.nodejs.cn/features/transactions"} + + + +To use transactions, get an instance first and dispose after finished. + +```js +const c = await db.getInstance(); +await c.sql`BEGIN`; +await c.sql`insert into test (name) values ('TEST1')`; +await c.sql`COMMIT`; +await c.sql`BEGIN`; +await c.sql`insert into test (name) values ('TEST2')`; +await c.sql`ROLLBACK`; +c.dispose(); +``` + +The pid can test by: + +```js +// Pool query: different pids +new Array(10).fill(1).forEach(async () => { + const {rows} = await db.sql`SELECT pg_backend_pid()`; + console.log(rows[0]); +}); + +// PoolConnection query: the same pid +const c = await db.getInstance(); +new Array(10).fill(1).forEach(async () => { + const {rows} = await c.sql`SELECT pg_backend_pid()`; + console.log(rows[0]); +}); +``` + +Options + +:read-more{to="https://github.com/sidorares/node-mysql2/blob/master/typings/mysql/lib/Connection.d.ts#L82-L329"} diff --git a/docs/2.connectors/postgresql-pool.md b/docs/2.connectors/postgresql-pool.md new file mode 100644 index 00000000..0e5bfbd1 --- /dev/null +++ b/docs/2.connectors/postgresql-pool.md @@ -0,0 +1,75 @@ +--- +icon: simple-icons:postgresql +--- +# PostgreSQL (Pool) + +> Connect DB0 to PostgreSQL + +:read-more{to="https://www.postgresql.org"} + +## Usage + +For this connector, you need to install [`pg`](https://www.npmjs.com/package/pg) dependency: + +:pm-install{name="pg @types/pg"} + +Use `postgresql-pool` connector: + +```js +import { createDatabase } from "db0"; +import postgresqlPool from "db0/connectors/postgresql-pool"; + +const db = createDatabase( + postgresqlPool({ + /* options */ + }), +); +``` + +### Pool Query + +```js +const {rows} = db.sql`SELECT * FROM tbl`; +``` + +### Transactions + +For pool connections, do not use transactions with the **pool.query** method. + +:read-more{title="node-postgres transactions" to="https://pg.nodejs.cn/features/transactions"} + + + +To use transactions, get an instance first and dispose after finished. + +```js +const c = await db.getInstance(); +await c.sql`BEGIN`; +await c.sql`insert into test (name) values ('TEST1')`; +await c.sql`COMMIT`; +await c.sql`BEGIN`; +await c.sql`insert into test (name) values ('TEST2')`; +await c.sql`ROLLBACK`; +c.dispose(); +``` + +The pid can test by: + +```js +// Pool query: different pids +new Array(10).fill(1).forEach(async () => { + const {rows} = await db.sql`SELECT pg_backend_pid()`; + console.log(rows[0]); +}); + +// PoolConnection query: the same pid +const c = await db.getInstance(); +new Array(10).fill(1).forEach(async () => { + const {rows} = await c.sql`SELECT pg_backend_pid()`; + console.log(rows[0]); +}); +``` + +## Options + +:read-more{title="node-postgres client options" to="https://node-postgres.com/apis/client#new-client"} diff --git a/src/_connectors.ts b/src/_connectors.ts index e0171c60..f442ac7b 100644 --- a/src/_connectors.ts +++ b/src/_connectors.ts @@ -10,10 +10,12 @@ import type { ConnectorOptions as LibSQLHttpOptions } from "db0/connectors/libsq import type { ConnectorOptions as LibSQLNodeOptions } from "db0/connectors/libsql/node"; import type { ConnectorOptions as LibSQLWebOptions } from "db0/connectors/libsql/web"; import type { ConnectorOptions as MySQL2Options } from "db0/connectors/mysql2"; +import type { ConnectorOptions as MySQL2PoolOptions } from "db0/connectors/mysql2-pool"; import type { ConnectorOptions as NodeSQLiteOptions } from "db0/connectors/node-sqlite"; import type { ConnectorOptions as PgliteOptions } from "db0/connectors/pglite"; import type { ConnectorOptions as PlanetscaleOptions } from "db0/connectors/planetscale"; import type { ConnectorOptions as PostgreSQLOptions } from "db0/connectors/postgresql"; +import type { ConnectorOptions as PostgrePoolSQLOptions } from "db0/connectors/postgresql-pool"; import type { ConnectorOptions as SQLite3Options } from "db0/connectors/sqlite3"; export type ConnectorName = "better-sqlite3" | "bun-sqlite" | "bun" | "cloudflare-d1" | "cloudflare-hyperdrive-mysql" | "cloudflare-hyperdrive-postgresql" | "libsql-core" | "libsql-http" | "libsql-node" | "libsql" | "libsql-web" | "mysql2" | "node-sqlite" | "sqlite" | "pglite" | "planetscale" | "postgresql" | "sqlite3"; @@ -33,12 +35,14 @@ export type ConnectorOptions = { "libsql": LibSQLNodeOptions; "libsql-web": LibSQLWebOptions; "mysql2": MySQL2Options; + "mysql2-pool": MySQL2PoolOptions; "node-sqlite": NodeSQLiteOptions; /** alias of node-sqlite */ "sqlite": NodeSQLiteOptions; "pglite": PgliteOptions; "planetscale": PlanetscaleOptions; "postgresql": PostgreSQLOptions; + "postgresql-pool": PostgrePoolSQLOptions; "sqlite3": SQLite3Options; }; @@ -57,11 +61,13 @@ export const connectors: Record = Object.freeze({ "libsql": "db0/connectors/libsql/node", "libsql-web": "db0/connectors/libsql/web", "mysql2": "db0/connectors/mysql2", + "mysql2-pool": "db0/connectors/mysql2-pool", "node-sqlite": "db0/connectors/node-sqlite", /** alias of node-sqlite */ "sqlite": "db0/connectors/node-sqlite", "pglite": "db0/connectors/pglite", "planetscale": "db0/connectors/planetscale", "postgresql": "db0/connectors/postgresql", + "postgresql-pool": "db0/connectors/postgresql-pool", "sqlite3": "db0/connectors/sqlite3", } as const); diff --git a/src/connectors/mysql2-pool.ts b/src/connectors/mysql2-pool.ts new file mode 100644 index 00000000..c21c808c --- /dev/null +++ b/src/connectors/mysql2-pool.ts @@ -0,0 +1,106 @@ +import mysql from "mysql2/promise"; +import { createDatabase } from "../database.ts"; +import type { Connector, Database, Primitive } from "db0"; +import { BoundableStatement } from "./_internal/statement.ts"; + +export type ConnectorOptions = mysql.PoolOptions; +type Pool = mysql.Pool & { + getClient?: () => Promise>>; +}; + +type InternalQuery = ( + sql: string, + params?: Primitive[], +) => Promise; + +export default function postgresqlPoolConnector( + opts: ConnectorOptions, +): Connector { + let _pool: Pool | undefined; + const getPool = () => { + if (!_pool) _pool = mysql.createPool(opts); + _pool.getClient = getClient; + return _pool; + }; + const getClient = async () => { + let _client: mysql.PoolConnection | undefined; + let _clientPromise: Promise | undefined; + const _getClient = async () => { + if (!_client) { + if (!_clientPromise) { + _clientPromise = getPool() + .getConnection() + .then((client) => { + _client = client; + _clientPromise = undefined; + return client; + }); + } + return _clientPromise; + } + return _client; + }; + const _query: InternalQuery = (sql, params) => + _getClient() + .then((c) => c.query(sql, params)) + .then((res) => res[0]); + return createDatabase({ + name: "postgresql-pool-client", + dialect: "postgresql", + getInstance: () => _getClient(), + exec: (sql) => _query(sql), + prepare: (sql) => new StatementWrapper(sql, _query), + dispose: async () => { + _client?.release?.(); + _client = undefined; + }, + }); + }; + + const query: InternalQuery = (sql, params) => + getClient() + .then((c) => c.getInstance()) + .then((c) => c.query(sql, params)) + .then((res) => res[0]); + + return { + name: "postgresql-pool", + dialect: "postgresql", + getInstance: () => getPool(), + exec: (sql) => query(sql), + prepare: (sql) => new StatementWrapper(sql, query), + dispose: async () => { + await _pool?.end?.(); + _pool = undefined; + }, + }; +} + +class StatementWrapper extends BoundableStatement { + #query: InternalQuery; + #sql: string; + + constructor(sql: string, query: InternalQuery) { + super(); + this.#sql = sql; + this.#query = query; + } + + async all(...params: Primitive[]) { + const res = (await this.#query(this.#sql, params)) as mysql.RowDataPacket[]; + return res; + } + + async run(...params: Primitive[]) { + const res = (await this.#query(this.#sql, params)) as mysql.RowDataPacket[]; + return { + success: true, + ...res, + }; + } + + async get(...params: Primitive[]) { + const res = (await this.#query(this.#sql, params)) as mysql.RowDataPacket[]; + return res[0]; + } +} diff --git a/src/connectors/postgresql-pool.ts b/src/connectors/postgresql-pool.ts new file mode 100644 index 00000000..f402ea60 --- /dev/null +++ b/src/connectors/postgresql-pool.ts @@ -0,0 +1,109 @@ +import pg from "pg"; +import { createDatabase } from "../database.ts"; +import type { Connector, Database, Primitive } from "db0"; +import { BoundableStatement } from "./_internal/statement.ts"; + +export type ConnectorOptions = pg.PoolConfig; +type Pool = pg.Pool & { + getClient?: () => Promise>>; +}; + +type InternalQuery = ( + sql: string, + params?: Primitive[], +) => Promise; + +export default function postgresqlPoolConnector( + opts: ConnectorOptions, +): Connector { + let _pool: Pool | undefined; + const getPool = () => { + if (!_pool) _pool = new pg.Pool(opts); + _pool.getClient = getClient; + return _pool; + }; + const getClient = async () => { + let _client: pg.PoolClient | undefined; + let _clientPromise: Promise | undefined; + const _getClient = async () => { + if (!_client) { + if (!_clientPromise) { + _clientPromise = getPool() + .connect() + .then((client) => { + _client = client; + _clientPromise = undefined; + return client; + }); + } + return _clientPromise; + } + return _client; + }; + const _query: InternalQuery = (sql, params) => + _getClient().then((c) => c.query(normalizeParams(sql), params)); + return createDatabase({ + name: "postgresql-pool-client", + dialect: "postgresql", + getInstance: () => _getClient(), + exec: (sql) => _query(sql), + prepare: (sql) => new StatementWrapper(sql, _query), + dispose: async () => { + _client?.release?.(); + _client = undefined; + }, + }); + }; + + const query: InternalQuery = async (sql, params) => { + const pool = getPool(); + return pool.query(normalizeParams(sql), params); + }; + + return { + name: "postgresql-pool", + dialect: "postgresql", + getInstance: () => getPool(), + exec: (sql) => query(sql), + prepare: (sql) => new StatementWrapper(sql, query), + dispose: async () => { + await _pool?.end?.(); + _pool = undefined; + }, + }; +} + +// https://www.postgresql.org/docs/9.3/sql-prepare.html +function normalizeParams(sql: string) { + let i = 0; + return sql.replace(/\?/g, () => `$${++i}`); +} + +class StatementWrapper extends BoundableStatement { + #query: InternalQuery; + #sql: string; + + constructor(sql: string, query: InternalQuery) { + super(); + this.#sql = sql; + this.#query = query; + } + + async all(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return res.rows; + } + + async run(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return { + success: true, + ...res, + }; + } + + async get(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return res.rows[0]; + } +}