Skip to content

Commit

Permalink
feat: abstract away pg (#555)
Browse files Browse the repository at this point in the history
BREAKING CHANGE:

The primary goal of this refactor is to remove hard dependency on `pg`.

My intention is to move Slonik away from being a `pg` wrapper, to make it a universal client. The work in this PR abstracts all interactions with the database behind a pool and drivers (e.g. `createPgDriver`). This makes it very easy to implement drivers for `pg-native`, `postgres` and other drivers.

* Drops dependency on `pg.Pool`
  * Implements a more efficient internal pooling mechanism.
  * For context, test execution time dropped from ~50 seconds down to ~6 seconds.
* Renames `getPoolState()` to `state()`
* Removes `createMockPool`
  * It was at best not useful, and at worst misleading. Even Slonik itself didn't use it. There are better ways to mock/spy connections. Refer to `createPoolWithSpy` as one of the possible ways.
  * Refactors all tests to use database connection instead of mocking.
  • Loading branch information
gajus authored Mar 27, 2024
1 parent 8c53efb commit bba41af
Show file tree
Hide file tree
Showing 68 changed files with 1,984 additions and 2,359 deletions.
82 changes: 18 additions & 64 deletions .README/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Note: `pool.end()` does not terminate active connections/ transactions.

### Describing the current state of the connection pool

Use `pool.getPoolState()` to find out if pool is alive and how many connections are active and idle, and how many clients are waiting for a connection.
Use `pool.state()` to find out if pool is alive and how many connections are active and idle, and how many clients are waiting for a connection.

```ts
import {
Expand All @@ -113,44 +113,44 @@ import {
const pool = await createPool('postgres://');

const main = async () => {
pool.getPoolState();
pool.state();

// {
// activeConnectionCount: 0,
// activeConnections: 0,
// ended: false,
// idleConnectionCount: 0,
// waitingClientCount: 0,
// idleConnections: 0,
// waitingClients: 0,
// }

await pool.connect(() => {
pool.getPoolState();
pool.state();

// {
// activeConnectionCount: 1,
// activeConnections: 1,
// ended: false,
// idleConnectionCount: 0,
// waitingClientCount: 0,
// idleConnections: 0,
// waitingClients: 0,
// }
});

pool.getPoolState();
pool.state();

// {
// activeConnectionCount: 0,
// activeConnections: 0,
// ended: false,
// idleConnectionCount: 1,
// waitingClientCount: 0,
// idleConnections: 1,
// waitingClients: 0,
// }

await pool.end();

pool.getPoolState();
pool.state();

// {
// activeConnectionCount: 0,
// activeConnections: 0,
// ended: true,
// idleConnectionCount: 0,
// waitingClientCount: 0,
// idleConnections: 0,
// waitingClients: 0,
// }
};

Expand Down Expand Up @@ -299,50 +299,4 @@ result;

Connection is released back to the pool after the promise produced by the function supplied to `connect()` method is either resolved or rejected.

Read: [Protecting against unsafe connection handling](#protecting-against-unsafe-connection-handling)

### Mocking Slonik

Slonik provides a way to mock queries against the database.

* Use `createMockPool` to create a mock connection.
* Use `createMockQueryResult` to create a mock query result.

```ts
import {
createMockPool,
createMockQueryResult,
} from 'slonik';

type OverridesType =
query: (sql: string, values: PrimitiveValueExpression[],) => Promise<QueryResult<QueryResultRow>>,
};

createMockPool(overrides: OverridesType): DatabasePool;
createMockQueryResult(rows: QueryResultRow[]): QueryResult<QueryResultRow>;
```

Example:

```ts
import {
createMockPool,
createMockQueryResult,
} from 'slonik';

const pool = createMockPool({
query: async () => {
return createMockQueryResult([
{
foo: 'bar',
},
]);
},
});

await pool.connect(async (connection) => {
const results = await connection.query(sql.typeAlias('foo')`
SELECT ${'foo'} AS foo
`);
});
```
Read: [Protecting against unsafe connection handling](#protecting-against-unsafe-connection-handling).
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
}
},
{
"files": "*.test.ts",
"files": ["*.test.ts", "**/*.test/*"],
"extends": "canonical/ava",
"rules": {
"@typescript-eslint/no-explicit-any": 0,
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/feature.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ jobs:
test_only:
- utilities
- pg-integration
- postgres-integration
node_version:
- 20
max-parallel: 3
Expand Down
86 changes: 18 additions & 68 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ Note: Using this project does not require TypeScript. It is a regular ES6 module
* [API](#user-content-slonik-usage-api)
* [Default configuration](#user-content-slonik-usage-default-configuration)
* [Checking out a client from the connection pool](#user-content-slonik-usage-checking-out-a-client-from-the-connection-pool)
* [Mocking Slonik](#user-content-slonik-usage-mocking-slonik)
* [How are they different?](#user-content-slonik-how-are-they-different)
* [`pg` vs `slonik`](#user-content-slonik-how-are-they-different-pg-vs-slonik)
* [`pg-promise` vs `slonik`](#user-content-slonik-how-are-they-different-pg-promise-vs-slonik)
Expand Down Expand Up @@ -532,7 +531,7 @@ Note: `pool.end()` does not terminate active connections/ transactions.
<a name="slonik-usage-describing-the-current-state-of-the-connection-pool"></a>
### Describing the current state of the connection pool

Use `pool.getPoolState()` to find out if pool is alive and how many connections are active and idle, and how many clients are waiting for a connection.
Use `pool.state()` to find out if pool is alive and how many connections are active and idle, and how many clients are waiting for a connection.

```ts
import {
Expand All @@ -543,44 +542,44 @@ import {
const pool = await createPool('postgres://');

const main = async () => {
pool.getPoolState();
pool.state();

// {
// activeConnectionCount: 0,
// activeConnections: 0,
// ended: false,
// idleConnectionCount: 0,
// waitingClientCount: 0,
// idleConnections: 0,
// waitingClients: 0,
// }

await pool.connect(() => {
pool.getPoolState();
pool.state();

// {
// activeConnectionCount: 1,
// activeConnections: 1,
// ended: false,
// idleConnectionCount: 0,
// waitingClientCount: 0,
// idleConnections: 0,
// waitingClients: 0,
// }
});

pool.getPoolState();
pool.state();

// {
// activeConnectionCount: 0,
// activeConnections: 0,
// ended: false,
// idleConnectionCount: 1,
// waitingClientCount: 0,
// idleConnections: 1,
// waitingClients: 0,
// }

await pool.end();

pool.getPoolState();
pool.state();

// {
// activeConnectionCount: 0,
// activeConnections: 0,
// ended: true,
// idleConnectionCount: 0,
// waitingClientCount: 0,
// idleConnections: 0,
// waitingClients: 0,
// }
};

Expand Down Expand Up @@ -743,56 +742,7 @@ result;

Connection is released back to the pool after the promise produced by the function supplied to `connect()` method is either resolved or rejected.

Read: [Protecting against unsafe connection handling](#user-content-protecting-against-unsafe-connection-handling)

<a name="user-content-slonik-usage-mocking-slonik"></a>
<a name="slonik-usage-mocking-slonik"></a>
### Mocking Slonik

Slonik provides a way to mock queries against the database.

* Use `createMockPool` to create a mock connection.
* Use `createMockQueryResult` to create a mock query result.

```ts
import {
createMockPool,
createMockQueryResult,
} from 'slonik';

type OverridesType =
query: (sql: string, values: PrimitiveValueExpression[],) => Promise<QueryResult<QueryResultRow>>,
};

createMockPool(overrides: OverridesType): DatabasePool;
createMockQueryResult(rows: QueryResultRow[]): QueryResult<QueryResultRow>;
```

Example:

```ts
import {
createMockPool,
createMockQueryResult,
} from 'slonik';

const pool = createMockPool({
query: async () => {
return createMockQueryResult([
{
foo: 'bar',
},
]);
},
});

await pool.connect(async (connection) => {
const results = await connection.query(sql.typeAlias('foo')`
SELECT ${'foo'} AS foo
`);
});
```

Read: [Protecting against unsafe connection handling](#user-content-protecting-against-unsafe-connection-handling).

<a name="user-content-slonik-how-are-they-different"></a>
<a name="slonik-how-are-they-different"></a>
Expand Down
4 changes: 0 additions & 4 deletions ava.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ module.exports = () => {
files = ['src/integration.test/pg.test.ts'];
}

if (TEST_ONLY === 'postgres-integration') {
files = ['src/integration.test/postgres.test.ts'];
}

return {
extensions: ['ts'],
files,
Expand Down
47 changes: 7 additions & 40 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit bba41af

Please sign in to comment.