Skip to content

Commit b44d720

Browse files
authored
Merge pull request #8 from observablehq/snowflake
Snowflake support
2 parents 61bfedd + 7484f4e commit b44d720

File tree

5 files changed

+783
-11
lines changed

5 files changed

+783
-11
lines changed

lib/commands.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export async function add(argv, reset = false) {
5555
// DB credentials
5656
if (!reset) {
5757
url = await question(
58-
"PostgreSQL or MySQL Database URL (including username and password): "
58+
"PostgreSQL, MySQL, or Snowflake Database URL (including username and password): "
5959
);
6060
}
6161

lib/server.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import serializeErrors from "./serialize-errors";
1010
import {notFound, unauthorized, exit} from "./errors";
1111
import mysql from "./mysql";
1212
import postgres from "./postgres";
13+
import snowflake from "./snowflake";
1314

1415
export function server(config, argv) {
1516
const development = process.env.NODE_ENV === "development";
@@ -25,7 +26,13 @@ export function server(config, argv) {
2526
} = config;
2627

2728
const handler =
28-
type === "mysql" ? mysql(url) : type === "postgres" ? postgres(url) : null;
29+
type === "mysql"
30+
? mysql(url)
31+
: type === "postgres"
32+
? postgres(url)
33+
: type === "snowflake"
34+
? snowflake(url)
35+
: null;
2936
if (!handler) {
3037
return exit(`Unknown database type: ${type}`);
3138
}

lib/snowflake.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import {json} from "micro";
2+
import {URL} from "url";
3+
import JSONStream from "JSONStream";
4+
import snowflake from "snowflake-sdk";
5+
6+
export default url => {
7+
url = new URL(url);
8+
const {host: account, username, password, pathname, searchParams} = new URL(
9+
url
10+
);
11+
const connection = snowflake.createConnection({
12+
account,
13+
username,
14+
password,
15+
database: pathname.slice(1),
16+
schema: searchParams.get("schema"),
17+
warehouse: searchParams.get("warehouse"),
18+
role: searchParams.get("role")
19+
});
20+
21+
return async function query(req, res) {
22+
const body = await json(req);
23+
const {sql, params} = body;
24+
25+
const client = await new Promise((resolve, reject) => {
26+
if (connection.isUp()) return resolve(connection);
27+
snowflake.configure({ocspFailOpen: false});
28+
connection.connect((err, conn) => {
29+
if (err) reject(err);
30+
else resolve(conn);
31+
});
32+
});
33+
34+
const statement = client.execute({sqlText: sql, binds: params});
35+
try {
36+
const stream = statement.streamRows();
37+
38+
await new Promise((resolve, reject) => {
39+
stream
40+
.once("end", resolve)
41+
.on("error", reject)
42+
.pipe(JSONStream.stringify(`{"data":[`, ",", "]"))
43+
.pipe(res, {end: false});
44+
});
45+
} catch (error) {
46+
if (!error.statusCode) error.statusCode = 400;
47+
throw error;
48+
}
49+
50+
const schema = {
51+
type: "array",
52+
items: {
53+
type: "object",
54+
properties: statement
55+
.getColumns()
56+
.reduce(
57+
(schema, column) => (
58+
(schema[column.getName()] = dataTypeSchema(column)), schema
59+
),
60+
{}
61+
)
62+
}
63+
};
64+
res.end(`,"schema":${JSON.stringify(schema)}}`);
65+
};
66+
};
67+
68+
// https://github.com/snowflakedb/snowflake-connector-nodejs/blob/master/lib/connection/result/data_types.js
69+
const array = ["null", "array"],
70+
boolean = ["null", "boolean"],
71+
integer = ["null", "integer"],
72+
number = ["null", "number"],
73+
object = ["null", "object"],
74+
string = ["null", "string"];
75+
function dataTypeSchema(column) {
76+
switch (column.getType()) {
77+
case "binary":
78+
return {type: object, buffer: true};
79+
case "boolean":
80+
return {type: boolean};
81+
case "fixed":
82+
case "real":
83+
return {type: column.getScale() ? number : integer};
84+
case "date":
85+
case "timestamp_ltz":
86+
case "timestamp_ntz":
87+
case "timestamp_tz":
88+
return {type: string, date: true};
89+
case "variant":
90+
case "object":
91+
return {type: object};
92+
case "array":
93+
return {type: array, items: {type: object}};
94+
case "time":
95+
case "text":
96+
default:
97+
return {type: string};
98+
}
99+
}

package.json

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
{
2+
"name": "@observablehq/database-proxy",
3+
"description": "A local proxy to connect private Observable notebooks to private databases",
4+
"version": "1.0.1",
25
"bin": {
36
"observable-database-proxy": "./bin/observable-database-proxy"
47
},
@@ -11,11 +14,9 @@
1114
"pg": "^7.11.0",
1215
"pg-query-stream": "^2.0.0",
1316
"serialize-error": "^4.1.0",
17+
"snowflake-sdk": "^1.5.0",
1418
"yargs": "^13.2.4"
1519
},
16-
"name": "@observablehq/database-proxy",
17-
"description": "A local proxy to connect private Observable notebooks to private databases",
18-
"version": "1.0.1",
1920
"devDependencies": {
2021
"nodemon": "^1.19.1"
2122
},
@@ -24,5 +25,9 @@
2425
"test": "echo \"Error: no test specified\" && exit 1"
2526
},
2627
"author": "Observable",
27-
"license": "ISC"
28+
"license": "ISC",
29+
"repository": {
30+
"type": "git",
31+
"url": "https://github.com/observablehq/database-proxy.git"
32+
}
2833
}

0 commit comments

Comments
 (0)