Skip to content

Data-connector and Database-proxy reconciliations #72

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

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
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
5 changes: 3 additions & 2 deletions .env.test
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
NODE_ENV=test
MSSQL_CREDENTIALS:Server=mssql,1433;Database=master;User Id=sa;Password=Pass@word;trustServerCertificate=true;
MSSQL_CREDENTIALS_READ_ONLY:Server=mssql,1433;Database=master;User Id=reader;Password=re@derP@ssw0rd;trustServerCertificate=true;
MYSQL_TEST_CREDENTIALS=mysql://root@mysql:3306/mysql?sslMode=DISABLED
MSSQL_TEST_CREDENTIALS=Server=mssql,1433;Database=master;User Id=sa;Password=Pass@word;trustServerCertificate=true;
POSTGRES_TEST_CREDENTIALS=postgres://postgres@postgres:5432/postgres?sslmode=disable
5 changes: 4 additions & 1 deletion .env.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export const MSSQL_CREDENTIALS = env("MSSQL_CREDENTIALS");
export const MSSQL_TEST_CREDENTIALS = env("MSSQL_TEST_CREDENTIALS");
export const MYSQL_TEST_CREDENTIALS = env("MYSQL_TEST_CREDENTIALS");
export const POSTGRES_TEST_CREDENTIALS = env("POSTGRES_TEST_CREDENTIALS");
export const SNOWFLAKE_TEST_CREDENTIALS = env("SNOWFLAKE_TEST_CREDENTIALS");
export const NODE_ENV = env("NODE_ENV");

function env(key, defaultValue) {
Expand Down
4 changes: 2 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2018
"ecmaVersion": 2022
},
"env": {
"node": true,
Expand All @@ -18,7 +18,7 @@
{
"files": ["*.test.js"],
"env": {
"jest": true
"mocha": true
}
}
],
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Test
on: push

jobs:
test:
runs-on: ubuntu-20.04
defaults:
run:
working-directory: .
env:
DOCKER_PACKAGE: ghcr.io/${{ github.repository }}/database-proxy_test

steps:
- uses: actions/checkout@v3
- name: Docker login
run: echo ${GITHUB_TOKEN} | docker login -u ${GITHUB_ACTOR} --password-stdin ghcr.io
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Republish
id: republish
continue-on-error: true
if: ${{ needs.Changes.outputs.connector == 'false' }}
run: |
../.github/retry docker pull ${DOCKER_PACKAGE}:${{ github.event.before }}
docker tag ${DOCKER_PACKAGE}:${{ github.event.before }} ${DOCKER_PACKAGE}:${GITHUB_SHA}
../.github/retry docker push ${DOCKER_PACKAGE}:${GITHUB_SHA}

- name: Build
if: ${{ steps.republish.outcome != 'success' }}
run: docker-compose build
- name: Lint
if: ${{ steps.republish.outcome != 'success' }}
run: docker-compose run lint
- name: Test
if: ${{ steps.republish.outcome != 'success' }}
run: docker-compose run test
env:
SNOWFLAKE_TEST_CREDENTIALS: ${{ secrets.SNOWFLAKE_TEST_CREDENTIALS }}
- name: Container logs
if: failure()
run: docker-compose logs --no-color --timestamps
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
node_modules
ssl/localhost.csr

*.secret
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
FROM node:18.12.1-alpine

RUN apk update && apk --no-cache add git

RUN mkdir /app
WORKDIR /app

Expand Down
4 changes: 2 additions & 2 deletions data/seed.mssql.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import mssql from "mssql";
import {MSSQL_CREDENTIALS} from "../.env.test.js";
import {MSSQL_TEST_CREDENTIALS} from "../.env.test.js";

const credentials = MSSQL_CREDENTIALS;
const credentials = MSSQL_TEST_CREDENTIALS;

const seed = async () => {
await mssql.connect(credentials);
Expand Down
16 changes: 14 additions & 2 deletions docker-compose.local.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
version: "3.7"

services:
test:
env_file:
- .env.secret

mssql:
image: mcr.microsoft.com/azure-sql-edge
expose:
- "1433"
ports:
- "1433:1433"

mysql:
ports:
- "3306:3306"

postgres:
ports:
- "5432:5432"
26 changes: 23 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
version: "3.7"

services:
lint:
build: .
command: eslint .

test:
build: .
depends_on:
- mssql
- mysql
- postgres
env_file:
- .env.test
environment:
- SNOWFLAKE_TEST_CREDENTIALS
networks:
- db_proxy_test
command: sh -c "set -o pipefail && wait-on -d 10000 -t 30000 tcp:mssql:1433 && node ./data/seed.mssql.js && TZ=UTC NODE_ENV=TEST node_modules/.bin/mocha"
command: sh -c "set -o pipefail && wait-on -d 15000 -t 30000 tcp:mysql:3306 tcp:mssql:1433 tcp:postgres:5432 && node ./data/seed.mssql.js && TZ=UTC NODE_ENV=TEST node_modules/.bin/mocha --exit"

mssql:
image: mcr.microsoft.com/mssql/server:2019-latest
Expand All @@ -20,8 +28,20 @@ services:
- MSSQL_SLEEP=7
volumes:
- ./data/AdventureWorks2019.bak:/var/opt/mssql/backup/test.bak
ports:
- "1433:1433"
networks:
- db_proxy_test

mysql:
image: mariadb:10.6.4
environment:
- MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=yes
networks:
- db_proxy_test

postgres:
image: postgres:13.8-alpine3.16
environment:
- POSTGRES_HOST_AUTH_METHOD=trust
networks:
- db_proxy_test

Expand Down
6 changes: 2 additions & 4 deletions lib/databricks.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,14 +253,14 @@ export async function queryStream(req, res, connection) {
res.write(`${JSON.stringify(responseSchema)}`);
res.write("\n");

await new Promise(async (resolve, reject) => {
await new Promise((resolve, reject) => {
const stream = new Readable.from(rows);

stream.once("data", () => {
clearInterval(keepAlive);
});

stream.on("close", (error) => {
stream.on("close", () => {
resolve();
stream.destroy();
});
Expand Down Expand Up @@ -345,8 +345,6 @@ export async function check(req, res, connection) {
});

return {ok: true};
} catch (e) {
throw e;
} finally {
if (connection) {
try {
Expand Down
11 changes: 9 additions & 2 deletions lib/mssql.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {Transform} from "stream";

import {failedCheck, badRequest, notImplemented} from "./errors.js";
import {validateQueryPayload} from "./validate.js";
import Pools from "./pools.js";

const TYPES = mssql.TYPES;
const READ_ONLY = new Set(["SELECT", "USAGE", "CONNECT"]);
Expand Down Expand Up @@ -124,8 +125,6 @@ export async function check(req, res, pool) {
return {ok: true};
}

export const ConnectionPool = mssql.ConnectionPool;

export default (credentials) => {
const pool = new mssql.ConnectionPool(credentials);

Expand All @@ -144,6 +143,14 @@ export default (credentials) => {
};
};

export const pools = new Pools((credentials) =>
Object.defineProperty(new mssql.ConnectionPool(credentials), "end", {
value() {
this.close();
},
})
);

// See https://github.com/tediousjs/node-mssql/blob/66587d97c9ce21bffba8ca360c72a540f2bc47a6/lib/datatypes.js#L6
const boolean = ["null", "boolean"],
integer = ["null", "integer"],
Expand Down
17 changes: 16 additions & 1 deletion lib/mysql.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import JSONStream from "JSONStream";
import {json} from "micro";
import mysql, {createConnection} from "mysql2";
import mysql, {createConnection, createPool} from "mysql2";
import {failedCheck} from "./errors.js";
import {notFound} from "./errors.js";
import Pools from "./pools.js";

const {Types, ConnectionConfig} = mysql;

export const pools = new Pools(({host, port, database, user, password, ssl}) =>
createPool({
host,
port,
database,
user,
password,
ssl: ssl === "required" ? {} : false,
connectTimeout: 25e3,
connectionLimit: 30,
decimalNumbers: true,
})
);

export async function query(req, res, pool) {
const {sql, params} = await json(req);
const keepAlive = setInterval(() => res.write("\n"), 25e3);
Expand Down
24 changes: 20 additions & 4 deletions lib/oracle.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Transform} from "stream";

import {badRequest, failedCheck} from "./errors.js";
import {validateQueryPayload} from "./validate.js";
import Pools from "./pools.js";

const READ_ONLY = new Set(["SELECT", "USAGE", "CONNECT"]);
export class OracleSingleton {
Expand Down Expand Up @@ -204,8 +205,6 @@ export async function check(req, res, pool) {
);

return {ok: true};
} catch (e) {
throw e;
} finally {
if (connection) {
try {
Expand All @@ -217,13 +216,30 @@ export async function check(req, res, pool) {
}
}

export const pools = new Pools(async (credentials) => {
const oracledb = await OracleSingleton.getInstance();
credentials.connectionString = decodeURI(credentials.connectionString);
const pool = await oracledb.createPool(credentials);

Object.defineProperty(pool, "end", {
value() {
// We must ensure there is no query still running before we close the pool.
if (this._connectionsOut === 0) {
this.close();
}
},
});

return pool;
});

export default async ({url, username, password}) => {
OracleSingleton.initialize();
// We do not want to import the oracledb library until we are sure that the user is looking to use Oracle.
// Installing the oracledb library is a pain, so we want to avoid it if possible.
const config = {
username: username,
password: password,
username,
password,
connectionString: decodeURI(url),
};

Expand Down
55 changes: 55 additions & 0 deletions lib/pools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import LRU from "lru-cache";
import * as Sentry from "@sentry/node";

const maxAge = 1000 * 60 * 10; // 10m

export default class Pools {
constructor(createPool) {
this.createPool = createPool;
this.cache = new LRU({
max: 100,
maxAge,
updateAgeOnGet: true,
dispose(_key, pool) {
pool.end();
},
});

let loop;
(loop = () => {
this.cache.prune();
this.timeout = setTimeout(loop, maxAge / 2);
})();
}

async get(credentials) {
const key = JSON.stringify(credentials);
if (this.cache.has(key)) return this.cache.get(key);
const pool = await this.createPool(credentials);

pool.on("error", (error) => {
// We need to attach a handler otherwise the process could exit, but we
// just don't care about these errors because the client will get cleaned
// up already. For debugging purposes, we'll add a Sentry breadcrumb if
// something else errors more loudly.
Sentry.addBreadcrumb({
message: error.message,
category: "pool",
level: "error",
data: error,
});
});

this.cache.set(key, pool);
return pool;
}

del(credentials) {
this.cache.del(JSON.stringify(credentials));
}

end() {
if (this.timeout) clearTimeout(this.timeout);
for (const pool of this.cache.values()) pool.end();
}
}
Loading