Skip to content

Snowflake support #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export async function add(argv, reset = false) {
// DB credentials
if (!reset) {
url = await question(
"PostgreSQL or MySQL Database URL (including username and password): "
"PostgreSQL, MySQL, or Snowflake Database URL (including username and password): "
);
}

Expand Down
9 changes: 8 additions & 1 deletion lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import serializeErrors from "./serialize-errors";
import {notFound, unauthorized, exit} from "./errors";
import mysql from "./mysql";
import postgres from "./postgres";
import snowflake from "./snowflake";

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

const handler =
type === "mysql" ? mysql(url) : type === "postgres" ? postgres(url) : null;
type === "mysql"
? mysql(url)
: type === "postgres"
? postgres(url)
: type === "snowflake"
? snowflake(url)
: null;
if (!handler) {
return exit(`Unknown database type: ${type}`);
}
Expand Down
99 changes: 99 additions & 0 deletions lib/snowflake.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {json} from "micro";
import {URL} from "url";
import JSONStream from "JSONStream";
import snowflake from "snowflake-sdk";

export default url => {
url = new URL(url);
const {host: account, username, password, pathname, searchParams} = new URL(
url
);
const connection = snowflake.createConnection({
account,
username,
password,
database: pathname.slice(1),
schema: searchParams.get("schema"),
warehouse: searchParams.get("warehouse"),
role: searchParams.get("role")
});

return async function query(req, res) {
const body = await json(req);
const {sql, params} = body;

const client = await new Promise((resolve, reject) => {
if (connection.isUp()) return resolve(connection);
snowflake.configure({ocspFailOpen: false});
connection.connect((err, conn) => {
if (err) reject(err);
else resolve(conn);
});
});

const statement = client.execute({sqlText: sql, binds: params});
try {
const stream = statement.streamRows();

await new Promise((resolve, reject) => {
stream
.once("end", resolve)
.on("error", reject)
.pipe(JSONStream.stringify(`{"data":[`, ",", "]"))
.pipe(res, {end: false});
});
} catch (error) {
if (!error.statusCode) error.statusCode = 400;
throw error;
}

const schema = {
type: "array",
items: {
type: "object",
properties: statement
.getColumns()
.reduce(
(schema, column) => (
(schema[column.getName()] = dataTypeSchema(column)), schema
),
{}
)
}
};
res.end(`,"schema":${JSON.stringify(schema)}}`);
};
};

// https://github.com/snowflakedb/snowflake-connector-nodejs/blob/master/lib/connection/result/data_types.js
const array = ["null", "array"],
boolean = ["null", "boolean"],
integer = ["null", "integer"],
number = ["null", "number"],
object = ["null", "object"],
string = ["null", "string"];
function dataTypeSchema(column) {
switch (column.getType()) {
case "binary":
return {type: object, buffer: true};
case "boolean":
return {type: boolean};
case "fixed":
case "real":
return {type: column.getScale() ? number : integer};
case "date":
case "timestamp_ltz":
case "timestamp_ntz":
case "timestamp_tz":
return {type: string, date: true};
case "variant":
case "object":
return {type: object};
case "array":
return {type: array, items: {type: object}};
case "time":
case "text":
default:
return {type: string};
}
}
13 changes: 9 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"name": "@observablehq/database-proxy",
"description": "A local proxy to connect private Observable notebooks to private databases",
"version": "1.0.1",
"bin": {
"observable-database-proxy": "./bin/observable-database-proxy"
},
Expand All @@ -11,11 +14,9 @@
"pg": "^7.11.0",
"pg-query-stream": "^2.0.0",
"serialize-error": "^4.1.0",
"snowflake-sdk": "^1.5.0",
"yargs": "^13.2.4"
},
"name": "@observablehq/database-proxy",
"description": "A local proxy to connect private Observable notebooks to private databases",
"version": "1.0.1",
"devDependencies": {
"nodemon": "^1.19.1"
},
Expand All @@ -24,5 +25,9 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Observable",
"license": "ISC"
"license": "ISC",
"repository": {
"type": "git",
"url": "https://github.com/observablehq/database-proxy.git"
}
}
Loading