Skip to content

Commit 56509c3

Browse files
author
Bart Huijgen
committed
Initial commit
0 parents  commit 56509c3

13 files changed

+332
-0
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"deno.enable": true
3+
}

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2022
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./src/mod.ts";

readme.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Kysely Deno Postgres driver
2+
3+
## usage
4+
5+
```ts
6+
import { Kysely } from "https://cdn.jsdelivr.net/npm/[email protected]/dist/esm/index-nodeless.js";
7+
import { PostgresDialect } from "[not hosted yet]";
8+
9+
const db = new Kysely<{}>({
10+
dialect: new PostgresDialect({
11+
// ...
12+
}),
13+
});
14+
```
15+
16+
## Reference
17+
18+
Based on node driver for postgres https://github.com/koskimas/kysely/tree/master/src/dialect/postgres

src/deps/kysely.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "https://cdn.jsdelivr.net/npm/[email protected]/dist/esm/index-nodeless.js";

src/deps/postgres.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "https://deno.land/x/[email protected]/mod.ts";

src/mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { PostgresDialect } from "./postgres/postgres-dialect.ts";

src/postgres/postgres-adapter.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { DialectAdapterBase } from "../deps/kysely.ts";
2+
import type { Kysely } from "../deps/kysely.ts";
3+
4+
// Random id for our transaction lock.
5+
const LOCK_ID = "3853314791062309107";
6+
7+
export class PostgresAdapter extends DialectAdapterBase {
8+
get supportsTransactionalDdl(): boolean {
9+
return true;
10+
}
11+
12+
get supportsReturning(): boolean {
13+
return true;
14+
}
15+
16+
async acquireMigrationLock(db: Kysely<any>): Promise<void> {
17+
// Acquire a transaction level advisory lock.
18+
await db.raw(`select pg_advisory_xact_lock(${LOCK_ID})`).execute();
19+
}
20+
21+
async releaseMigrationLock(): Promise<void> {
22+
// Nothing to do here. `pg_advisory_xact_lock` is automatically released at the
23+
// end of the transaction and since `supportsTransactionalDdl` true, we know
24+
// the `db` instance passed to acquireMigrationLock is actually a transaction.
25+
}
26+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import type { ClientOptions } from "../deps/postgres.ts";
2+
3+
export type PostgresDialectConfig = ClientOptions;

src/postgres/postgres-dialect.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { PostgresDriver } from "./postgres-driver.ts";
2+
import { PostgresIntrospector } from "./postgres-introspector.ts";
3+
import { PostgresQueryCompiler } from "./postgres-query-compiler.ts";
4+
import { PostgresAdapter } from "./postgres-adapter.ts";
5+
import type { PostgresDialectConfig } from "./postgres-dialect-config.ts";
6+
import { Kysely } from "../deps/kysely.ts";
7+
import type {
8+
Driver,
9+
DialectAdapter,
10+
DatabaseIntrospector,
11+
Dialect,
12+
QueryCompiler,
13+
} from "../deps/kysely.ts";
14+
15+
export class PostgresDialect implements Dialect {
16+
readonly #config: PostgresDialectConfig;
17+
18+
constructor(config: PostgresDialectConfig) {
19+
this.#config = config;
20+
}
21+
22+
createDriver(): Driver {
23+
return new PostgresDriver(this.#config);
24+
}
25+
26+
createQueryCompiler(): QueryCompiler {
27+
return new PostgresQueryCompiler();
28+
}
29+
30+
createAdapter(): DialectAdapter {
31+
return new PostgresAdapter();
32+
}
33+
34+
createIntrospector(db: Kysely<any>): DatabaseIntrospector {
35+
return new PostgresIntrospector(db);
36+
}
37+
}

src/postgres/postgres-driver.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { Pool } from "../deps/postgres.ts";
2+
import type { PoolClient, ClientOptions } from "../deps/postgres.ts";
3+
import { CompiledQuery } from "../deps/kysely.ts";
4+
import type {
5+
DatabaseConnection,
6+
QueryResult,
7+
Driver,
8+
TransactionSettings,
9+
} from "../deps/kysely.ts";
10+
11+
const PRIVATE_RELEASE_METHOD = Symbol();
12+
13+
export class PostgresDriver implements Driver {
14+
readonly #config: ClientOptions;
15+
readonly #connections = new WeakMap<PoolClient, DatabaseConnection>();
16+
#pool: Pool | null = null;
17+
18+
constructor(config: ClientOptions) {
19+
this.#config = config;
20+
}
21+
22+
async init(): Promise<void> {
23+
// TODO: size is required unlike the node `pg` module.
24+
// Need to figure out what is a good value to use here
25+
this.#pool = new Pool(this.#config, 1);
26+
}
27+
28+
async acquireConnection(): Promise<DatabaseConnection> {
29+
const client = await this.#pool!.connect();
30+
let connection = this.#connections.get(client);
31+
32+
if (!connection) {
33+
connection = new PostgresConnection(client);
34+
this.#connections.set(client, connection);
35+
36+
// The driver must take care of calling `onCreateConnection` when a new
37+
// connection is created. The `pg` module doesn't provide an async hook
38+
// for the connection creation. We need to call the method explicitly.
39+
// if (this.#config.onCreateConnection) {
40+
// await this.#config.onCreateConnection(connection);
41+
// }
42+
}
43+
44+
return connection;
45+
}
46+
47+
async beginTransaction(
48+
connection: DatabaseConnection,
49+
settings: TransactionSettings
50+
): Promise<void> {
51+
if (settings.isolationLevel) {
52+
await connection.executeQuery(
53+
CompiledQuery.raw(
54+
`start transaction isolation level ${settings.isolationLevel}`
55+
)
56+
);
57+
} else {
58+
await connection.executeQuery(CompiledQuery.raw("begin"));
59+
}
60+
}
61+
62+
async commitTransaction(connection: DatabaseConnection): Promise<void> {
63+
await connection.executeQuery(CompiledQuery.raw("commit"));
64+
}
65+
66+
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
67+
await connection.executeQuery(CompiledQuery.raw("rollback"));
68+
}
69+
70+
// deno-lint-ignore require-await
71+
async releaseConnection(connection: DatabaseConnection): Promise<void> {
72+
const pgConnection = connection as PostgresConnection;
73+
pgConnection[PRIVATE_RELEASE_METHOD]();
74+
}
75+
76+
async destroy(): Promise<void> {
77+
if (this.#pool) {
78+
const pool = this.#pool;
79+
this.#pool = null;
80+
await pool.end();
81+
}
82+
}
83+
}
84+
85+
class PostgresConnection implements DatabaseConnection {
86+
#client: PoolClient;
87+
88+
constructor(client: PoolClient) {
89+
this.#client = client;
90+
}
91+
92+
async executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>> {
93+
const result = await this.#client.queryObject<O>(
94+
compiledQuery.sql,
95+
...compiledQuery.parameters
96+
);
97+
98+
if (result.command === "UPDATE" || result.command === "DELETE") {
99+
return {
100+
numUpdatedOrDeletedRows: BigInt(result.rowCount!),
101+
rows: result.rows ?? [],
102+
};
103+
}
104+
105+
return {
106+
rows: result.rows ?? [],
107+
};
108+
}
109+
110+
[PRIVATE_RELEASE_METHOD](): void {
111+
this.#client.release();
112+
}
113+
}

src/postgres/postgres-introspector.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {
2+
Kysely,
3+
MIGRATION_TABLE,
4+
MIGRATION_LOCK_TABLE,
5+
} from "../deps/kysely.ts";
6+
import type {
7+
ColumnDataType,
8+
DatabaseIntrospector,
9+
DatabaseMetadata,
10+
DatabaseMetadataOptions,
11+
TableMetadata,
12+
} from "../deps/kysely.ts";
13+
14+
const freeze = Object.freeze;
15+
16+
export class PostgresIntrospector implements DatabaseIntrospector {
17+
readonly #db: Kysely<any>;
18+
19+
constructor(db: Kysely<any>) {
20+
this.#db = db;
21+
}
22+
23+
async getMetadata(
24+
options: DatabaseMetadataOptions = { withInternalKyselyTables: false }
25+
): Promise<DatabaseMetadata> {
26+
let query = this.#db
27+
.selectFrom("pg_catalog.pg_attribute as a")
28+
.innerJoin("pg_catalog.pg_class as c", "a.attrelid", "c.oid")
29+
.innerJoin("pg_catalog.pg_tables as t", "t.tablename", "c.relname")
30+
.innerJoin("pg_catalog.pg_type as typ", "a.atttypid", "typ.oid")
31+
.select([
32+
"a.attname as column",
33+
"a.attnotnull as not_null",
34+
"t.tablename as table",
35+
"t.schemaname as schema",
36+
"typ.typname as type",
37+
])
38+
.where("t.schemaname", "!~", "^pg_")
39+
.where("t.schemaname", "!=", "information_schema")
40+
.where("a.attnum", ">=", 0) // No system columns
41+
.where("a.attisdropped", "!=", true)
42+
.castTo<RawColumnMetadata>();
43+
44+
if (!options.withInternalKyselyTables) {
45+
query = query
46+
.where("t.tablename", "!=", MIGRATION_TABLE)
47+
.where("t.tablename", "!=", MIGRATION_LOCK_TABLE);
48+
}
49+
50+
const rawColumns = await query.execute();
51+
52+
return {
53+
tables: this.#parseTableMetadata(rawColumns),
54+
};
55+
}
56+
57+
#parseTableMetadata(columns: RawColumnMetadata[]): TableMetadata[] {
58+
return columns.reduce<TableMetadata[]>((tables, it) => {
59+
let table = tables.find(
60+
(tbl) => tbl.name === it.table && tbl.schema === it.schema
61+
);
62+
63+
if (!table) {
64+
table = freeze({
65+
name: it.table,
66+
schema: it.schema,
67+
columns: [],
68+
});
69+
70+
tables.push(table);
71+
}
72+
73+
table.columns.push(
74+
freeze({
75+
name: it.column,
76+
dataType: it.type,
77+
isNullable: !it.not_null,
78+
})
79+
);
80+
81+
return tables;
82+
}, []);
83+
}
84+
}
85+
86+
interface RawColumnMetadata {
87+
column: string;
88+
table: string;
89+
schema: string;
90+
not_null: boolean;
91+
type: ColumnDataType;
92+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { DefaultQueryCompiler } from "../deps/kysely.ts";
2+
3+
export class PostgresQueryCompiler extends DefaultQueryCompiler {
4+
protected getCurrentParameterPlaceholder(): string {
5+
return "$" + this.numParameters;
6+
}
7+
8+
protected override getLeftIdentifierWrapper(): string {
9+
return '"';
10+
}
11+
12+
protected override getRightIdentifierWrapper(): string {
13+
return '"';
14+
}
15+
}

0 commit comments

Comments
 (0)