Skip to content

Commit 6ec5f9f

Browse files
committed
database connectors!
1 parent 834e5ee commit 6ec5f9f

20 files changed

+3242
-77
lines changed

package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
"types": "./dist/src/index.d.ts",
2828
"import": "./dist/src/index.js"
2929
},
30+
"./databases": {
31+
"types": "./dist/src/databases/index.d.ts",
32+
"import": "./dist/src/databases/index.js"
33+
},
3034
"./runtime": {
3135
"types": "./dist/src/runtime/index.d.ts",
3236
"import": "./dist/src/runtime/index.js"
@@ -65,9 +69,23 @@
6569
"eslint": "^9.29.0",
6670
"globals": "^16.2.0",
6771
"htl": "^0.3.1",
72+
"postgres": "^3.4.7",
73+
"snowflake-sdk": "^2.1.3",
6874
"tsx": "^4.20.3",
6975
"typescript": "^5.8.3",
7076
"typescript-eslint": "^8.35.0",
7177
"vitest": "^3.2.4"
78+
},
79+
"peerDependencies": {
80+
"postgres": "^3.4.7",
81+
"snowflake-sdk": "^2.1.3"
82+
},
83+
"peerDependenciesMeta": {
84+
"postgres": {
85+
"optional": true
86+
},
87+
"snowflake-sdk": {
88+
"optional": true
89+
}
7290
}
7391
}

src/databases/index.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type {ColumnSchema, QueryParam} from "../runtime/index.js";
2+
3+
export type DatabaseConfig = SnowflakeConfig | PostgresConfig;
4+
5+
export type SnowflakeConfig = {
6+
type: "snowflake";
7+
account: string;
8+
database?: string;
9+
role?: string;
10+
schema?: string;
11+
username?: string;
12+
warehouse?: string;
13+
password?: string;
14+
};
15+
16+
export type PostgresConfig = {
17+
type: "postgres";
18+
host?: string;
19+
port?: string | number;
20+
username?: string;
21+
password?: string;
22+
database?: string;
23+
ssl?: boolean;
24+
};
25+
26+
export type QueryTemplateFunction = (
27+
strings: string[],
28+
...params: QueryParam[]
29+
) => Promise<SerializableQueryResult>;
30+
31+
export type SerializableQueryResult = {
32+
rows: Record<string, unknown>[];
33+
schema: ColumnSchema[];
34+
duration: number;
35+
date: Date;
36+
};
37+
38+
export async function getDatabase(config: DatabaseConfig): Promise<QueryTemplateFunction> {
39+
switch (config.type) {
40+
case "snowflake":
41+
return (await import("./snowflake.js")).default(config);
42+
case "postgres":
43+
return (await import("./postgres.js")).default(config);
44+
default:
45+
throw new Error(`unsupported database type: ${config["type"]}`);
46+
}
47+
}

src/databases/options.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
function optional<T>(type: (value: unknown) => T): (value: unknown) => T | undefined {
2+
return (value) => (value == null ? undefined : type(value));
3+
}
4+
5+
export const optionalString = optional(String);
6+
export const optionalNumber = optional(Number);
7+
export const optionalBoolean = optional(Boolean);

src/databases/postgres.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type {Column, RowList} from "postgres";
2+
import Postgres from "postgres";
3+
import type {ColumnSchema} from "../runtime/index.js";
4+
import type {PostgresConfig, QueryTemplateFunction} from "./index.js";
5+
import {optionalBoolean, optionalNumber, optionalString} from "./options.js";
6+
7+
export default function postgres(options: PostgresConfig): QueryTemplateFunction {
8+
return async (strings, ...params) => {
9+
const sql = Postgres({
10+
host: optionalString(options.host),
11+
port: optionalNumber(options.port),
12+
username: optionalString(options.username),
13+
password: optionalString(options.password),
14+
database: optionalString(options.database),
15+
ssl: optionalBoolean(options.ssl) ? {rejectUnauthorized: false} : false
16+
});
17+
const date = new Date();
18+
let rows: RowList<Record<string, unknown>[]>;
19+
try {
20+
rows = await sql.unsafe(
21+
strings.reduce((p, c, i) => `${p}$${i}${c}`),
22+
params
23+
);
24+
} finally {
25+
await sql.end();
26+
}
27+
return {
28+
rows,
29+
schema: rows.columns.map(getColumnSchema),
30+
duration: Date.now() - +date,
31+
date
32+
};
33+
};
34+
}
35+
36+
function getColumnSchema(column: Column<string>): ColumnSchema {
37+
return {name: column.name, type: getColumnType(column.type)};
38+
}
39+
40+
// https://github.com/brianc/node-pg-types/blob/master/lib/textParsers.js#L166
41+
function getColumnType(oid: number): ColumnSchema["type"] {
42+
switch (oid) {
43+
case 20: // int8
44+
return "bigint";
45+
case 21: // int2
46+
case 23: // int4
47+
case 26: // oid
48+
return "integer";
49+
case 700: // float4/real
50+
case 701: // float8/double
51+
return "number";
52+
case 16: // bool
53+
return "boolean";
54+
case 1082: // date
55+
case 1114: // timestamp without timezone
56+
case 1184: // timestamp
57+
return "date";
58+
case 651: // cidr[]
59+
case 1000: // bool[]
60+
case 1001: // byte[]
61+
case 1002: // string[]
62+
case 1005: // int2[]
63+
case 1007: // int4[]
64+
case 1028: // oid[]
65+
case 1016: // int8[]
66+
case 1017: // point[]
67+
case 1021: // float4[]
68+
case 1022: // float8[]
69+
case 1231: // numeric[]
70+
case 1014: // char[]
71+
case 1015: // varchar[]
72+
case 1008: // string[]
73+
case 1009: // string[]
74+
case 1040: // macaddr[]
75+
case 1041: // inet[]
76+
case 1115: // timestamp without time zone[]
77+
case 1182: // date[]
78+
case 1185: // timestamp with time zone[]
79+
case 1187: // interval[]
80+
case 199: // json[]
81+
case 3807: // jsonb[]
82+
case 3907: // numrange[]
83+
case 2951: // uuid[]
84+
case 791: // money[]
85+
case 1183: // time[]
86+
case 1270: // timetz[]
87+
return "array";
88+
case 1186: // interval
89+
case 114: // json
90+
case 3802: // jsonb
91+
case 600: // point
92+
case 718: // circle
93+
return "object";
94+
case 17: // bytea
95+
return "buffer";
96+
case 18: // char
97+
case 1700: // numeric
98+
case 25: // text
99+
case 24: // regproc
100+
default:
101+
return "string";
102+
}
103+
}

src/databases/snowflake.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import type {Column, Connection, ConnectionOptions, RowStatement} from "snowflake-sdk";
2+
import Snowflake from "snowflake-sdk";
3+
import type {ColumnSchema, QueryParam} from "../runtime/index.js";
4+
import {QueryTemplateFunction, SerializableQueryResult, SnowflakeConfig} from "./index.js";
5+
import {optionalString} from "./options.js";
6+
7+
Snowflake.configure({logLevel: "OFF"});
8+
9+
export default function snowflake(options: SnowflakeConfig): QueryTemplateFunction {
10+
return async (strings, ...params) => {
11+
const connection = await connect({
12+
account: String(options.account),
13+
username: optionalString(options.username),
14+
password: optionalString(options.password),
15+
database: optionalString(options.database),
16+
schema: optionalString(options.schema),
17+
warehouse: optionalString(options.warehouse),
18+
role: optionalString(options.role)
19+
});
20+
let result: SerializableQueryResult;
21+
try {
22+
result = await execute(connection, strings.join("?"), params);
23+
} finally {
24+
await destroy(connection);
25+
}
26+
return result;
27+
};
28+
}
29+
30+
async function connect(options: ConnectionOptions): Promise<Connection> {
31+
const connection = Snowflake.createConnection(options);
32+
await new Promise<void>((resolve, reject) => {
33+
connection.connect((error) => {
34+
if (error) return reject(error);
35+
resolve();
36+
});
37+
});
38+
return connection;
39+
}
40+
41+
async function destroy(connection: Connection): Promise<void> {
42+
await new Promise<void>((resolve, reject) => {
43+
connection.destroy((error) => {
44+
if (error) return reject(error);
45+
resolve();
46+
});
47+
});
48+
}
49+
50+
async function execute(
51+
connection: Connection,
52+
sql: string,
53+
params?: QueryParam[]
54+
): Promise<SerializableQueryResult> {
55+
return new Promise<SerializableQueryResult>((resolve, reject) => {
56+
const date = new Date();
57+
connection.execute({
58+
sqlText: sql,
59+
binds: params,
60+
complete(error, statement, rows) {
61+
if (error) return reject(error);
62+
resolve({
63+
rows: rows!,
64+
schema: getStatementSchema(statement),
65+
duration: Date.now() - +date,
66+
date
67+
});
68+
}
69+
});
70+
});
71+
}
72+
73+
function getStatementSchema(statement: RowStatement): ColumnSchema[] {
74+
return statement.getColumns().map(getColumnSchema);
75+
}
76+
77+
function getColumnSchema(column: Column): ColumnSchema {
78+
return {name: column.getName(), type: getColumnType(column), nullable: column.isNullable()};
79+
}
80+
81+
function getColumnType(column: Column): ColumnSchema["type"] {
82+
const type = column.getType();
83+
switch (type.toLowerCase()) {
84+
case "date":
85+
case "datetime":
86+
case "timestamp":
87+
case "timestamp_ltz":
88+
case "timestamp_ntz":
89+
case "timestamp_tz":
90+
return "date";
91+
case "time":
92+
case "text":
93+
return "string";
94+
case "fixed":
95+
return column.getScale() === 0 ? "integer" : "number";
96+
case "float":
97+
case "number":
98+
case "real":
99+
return "number";
100+
case "binary":
101+
return "buffer";
102+
case "array":
103+
return "array";
104+
case "boolean":
105+
return "boolean";
106+
case "object":
107+
case "variant":
108+
return "object";
109+
default:
110+
console.warn(`unknown type: ${type}`);
111+
return "other";
112+
}
113+
}

0 commit comments

Comments
 (0)