Repository: rickbergfalk/sqlpad
Pull request #563 (2020-03-01 23:38:54)

Refactor driver utils to DriverConnection class
Refactors drivers utility functions into separate functions and a `DriverConnection` class in preparation for persistent connection work. Persistent connections will require tracking state of whether a connection is connected, keeping it alive, etc, and I figured it'll be easier to manage with a proper class maintaining some state about itself than utility functions. 


Patch:
diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml
new file mode 100644
index 00000000..8bc1c8e0
--- /dev/null
+++ b/scripts/docker-compose.yml
@@ -0,0 +1,11 @@
+version: '3'
+services:
+  postgres:
+    image: postgres
+    ports:
+      - '5432:5432'
+    environment:
+      POSTGRES_USER: sqlpad
+      POSTGRES_PASSWORD: sqlpad
+    volumes:
+      - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
\ No newline at end of file
diff --git a/scripts/docker-entrypoint-initdb.d/pgdata.sh b/scripts/docker-entrypoint-initdb.d/pgdata.sh
new file mode 100644
index 00000000..056c637a
--- /dev/null
+++ b/scripts/docker-entrypoint-initdb.d/pgdata.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+set -e
+
+psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
+    CREATE TABLE test (id int, name text);
+    INSERT INTO test (id, name) VALUES (1, 'one'), (2, 'two');
+EOSQL
\ No newline at end of file
diff --git a/server/drivers/index.js b/server/drivers/index.js
index 83a90ddf..b8eb6125 100644
--- a/server/drivers/index.js
+++ b/server/drivers/index.js
@@ -1,317 +1,32 @@
-const uuid = require('uuid');
-const _ = require('lodash');
-const utils = require('./utils');
-const getMeta = require('../lib/getMeta');
 const appLog = require('../lib/appLog');
+const validate = require('./validate');
+
+const drivers = {
+  cassandra: require('./cassandra'),
+  crate: require('./crate'),
+  drill: require('./drill'),
+  hdb: require('./hdb'),
+  mock: require('./mock'),
+  mysql: require('./mysql'),
+  postgres: require('./postgres'),
+  presto: require('./presto'),
+  snowflake: require('./snowflake'),
+  sqlserver: require('./sqlserver'),
+  vertica: require('./vertica')
+};
 
-const drivers = {};
-
-/**
- * Validate that the driver implementation has a function by name provided
- * @param {string} path
- * @param {object} driver
- * @param {string} functionName
- */
-function validateFunction(path, driver, functionName) {
-  if (typeof driver[functionName] !== 'function') {
-    appLog.error('%s missing .%s() implementation', path, functionName);
-    process.exit(1);
-  }
-}
-
-/**
- * Validate that the driver implementation has an array by name provided
- * @param {string} path
- * @param {object} driver
- * @param {string} arrayName
- */
-function validateArray(path, driver, arrayName) {
-  const arr = driver[arrayName];
-  if (!Array.isArray(arr)) {
-    appLog.error('%s missing %s array', path, arrayName);
-    process.exit(1);
-  }
-}
-
-/**
- * Iterates over connection object, replacing any template strings with values from user
- * This allows dynamic values inserted based on logged in user
- * This uses a mustache-like syntax, using double mustaches.
- * User variables can be referenced in connection strings using dot notation
- * Example: {{user.someKey}} and {{user.data.someKey}}
- * @param {object} connection
- * @param {object} user
- */
-function renderConnection(connection, user) {
-  const replaced = {};
-  Object.keys(connection).forEach(key => {
-    const value = connection[key];
-    if (typeof value === 'string') {
-      _.templateSettings.interpolate = /{{([\s\S]+?)}}/g;
-      const compiled = _.template(value);
-      replaced[key] = compiled({ user });
-    } else {
-      replaced[key] = value;
-    }
-  });
-  return replaced;
-}
-
-/**
- * Require driver implementation for provided path
- * and validate that it meets implementation spec as possible
- * @param {string} path
- */
-function requireValidate(path, optional = false) {
-  let driver;
-
-  try {
-    driver = require(path);
-  } catch (er) {
-    if (optional) {
-      appLog.info('optional driver %s not available', path);
-      return;
-    } else {
-      // rethrow
-      throw er;
-    }
-  }
-
-  if (!driver.id) {
-    appLog.error('%s must export a unique id', path);
-    process.exit(1);
-  }
-
-  if (!driver.name) {
-    appLog.error('%s must export a name', path);
-    process.exit(1);
-  }
-
-  if (drivers[driver.id]) {
-    appLog.error(`Driver with id ${driver.id} already loaded`);
-    appLog.error(`Ensure ${path} has a unique id exported`);
-    process.exit(1);
-  }
-
-  validateFunction(path, driver, 'getSchema');
-  validateFunction(path, driver, 'runQuery');
-  validateFunction(path, driver, 'testConnection');
-  validateArray(path, driver, 'fields');
-
-  driver.fieldsByKey = {};
-
-  driver.fields.forEach(field => {
-    driver.fieldsByKey[field.key] = field;
-  });
-
-  drivers[driver.id] = driver;
-}
-
-// Loads and validates drivers
-// Will populate drivers {} map
-requireValidate('./crate');
-requireValidate('./drill');
-requireValidate('./hdb');
-requireValidate('./mysql');
-requireValidate('./postgres');
-requireValidate('./presto');
-requireValidate('./sqlserver');
-requireValidate('./unixodbc', true);
-requireValidate('./vertica');
-requireValidate('./cassandra');
-requireValidate('./snowflake');
-requireValidate('./mock');
-
-/**
- * Run query using driver implementation of connection
- * @param {*} query
- * @param {*} connection
- * @param {object} [user] user may not be provided if chart links turned on
- * @returns {Promise}
- */
-async function runQuery(query, connection, user) {
-  const driver = drivers[connection.driver];
-
-  const finalResult = {
-    id: uuid.v4(),
-    cacheKey: null,
-    startTime: new Date(),
-    stopTime: null,
-    queryRunTime: null,
-    fields: [],
-    incomplete: false,
-    meta: {},
-    rows: []
-  };
-
-  const renderedConnection = renderConnection(connection, user);
-  const connectionName = renderedConnection.name;
-
-  appLog.debug(
-    {
-      originalConnection: connection,
-      renderedConnection,
-      user
-    },
-    'Rendered connection for user'
-  );
-
-  const queryContext = {
-    driver: connection.driver,
-    userId: user && user._id,
-    userEmail: user && user.email,
-    connectionId: connection._id,
-    connectionName,
-    query,
-    startTime: finalResult.startTime
-  };
-
-  appLog.info(queryContext, 'Running query');
-
-  let results;
-  try {
-    results = await driver.runQuery(query, renderedConnection);
-  } catch (error) {
-    // It is logged INFO because it isn't necessarily a server/application error
-    // It could just be a bad query
-    appLog.info(
-      { ...queryContext, error: error.toString() },
-      'Error running query'
-    );
-
-    // Rethrow the error
-    // The error here is something expected and should be shown to user
-    throw error;
-  }
-
-  let { rows, incomplete, suppressedResultSet } = results;
-
-  if (!Array.isArray(rows)) {
-    appLog.warn(
-      {
-        driver: connection.driver,
-        connectionId: connection._id,
-        connectionName,
-        query
-      },
-      'Expected rows to be an array but received %s.',
-      typeof rows
-    );
-    rows = [];
-  }
-
-  finalResult.incomplete = Boolean(incomplete);
-  finalResult.suppressedResultSet = Boolean(suppressedResultSet);
-  finalResult.rows = rows;
-  finalResult.stopTime = new Date();
-  finalResult.queryRunTime = finalResult.stopTime - finalResult.startTime;
-  finalResult.meta = getMeta(rows);
-  finalResult.fields = Object.keys(finalResult.meta);
-
-  appLog.info(
-    {
-      ...queryContext,
-      stopTime: finalResult.stopTime,
-      queryRunTime: finalResult.queryRunTime,
-      rowCount: rows.length,
-      incomplete: finalResult.incomplete,
-      suppressedResultSet: finalResult.suppressedResultSet
-    },
-    'Query finished'
-  );
-
-  return finalResult;
-}
-
-/**
- * Test connection passed in using the driver implementation
- * As long as promise resolves without error
- * it is considered a successful connection config
- * @param {object} connection
- * @param {object} user
- */
-function testConnection(connection, user) {
-  const driver = drivers[connection.driver];
-  const renderedConnection = renderConnection(connection, user);
-  return driver.testConnection(renderedConnection);
-}
-
-/**
- * Gets schema (sometimes called schemaInfo) for connection
- * This data is used by client to build schema tree in editor sidebar
- * @param {object} connection
- * @param {object} user
- * @returns {Promise}
- */
-function getSchema(connection, user) {
-  connection.maxRows = Number.MAX_SAFE_INTEGER;
-  const driver = drivers[connection.driver];
-  const renderedConnection = renderConnection(connection, user);
-  return driver.getSchema(renderedConnection);
-}
-
-/**
- * Gets array of driver objects
- * @returns {array} drivers
- */
-function getDrivers() {
-  return Object.keys(drivers).map(id => {
-    return {
-      id,
-      name: drivers[id].name,
-      fields: drivers[id].fields
-    };
-  });
+// unixodbc is an optional dependency due to it needing to be compiled
+// (and lacks prebuilt binaries like sqlite provides)
+try {
+  drivers.unixodbc = require('./unixodbc');
+} catch (error) {
+  appLog.info('ODBC driver not available');
 }
 
-/**
- * Validates connection object based on its driver
- * Unnecessary fields will be stripped out
- * @param {object} connection
- */
-function validateConnection(connection) {
-  const coreFields = ['_id', 'name', 'driver', 'createdDate', 'modifiedDate'];
-  if (!connection.name) {
-    throw new Error('connection.name required');
-  }
-  if (!connection.driver) {
-    throw new Error('connection.driver required');
-  }
-  const driver = drivers[connection.driver];
-  if (!driver) {
-    throw new Error(`driver implementation ${connection.driver} not found`);
-  }
-  const validFields = driver.fields.map(field => field.key).concat(coreFields);
-  const cleanedConnection = validFields.reduce(
-    (cleanedConnection, fieldKey) => {
-      if (connection.hasOwnProperty(fieldKey)) {
-        let value = connection[fieldKey];
-        const fieldDefinition =
-          drivers[connection.driver].fieldsByKey[fieldKey];
+// Validate each driver implementation to ensure it matches expectations
+Object.keys(drivers).forEach(id => {
+  const driver = drivers[id];
+  validate(id, driver);
+});
 
-        // field definition may not exist since
-        // this could be a core field like _id, name
-        if (fieldDefinition) {
-          if (fieldDefinition.formType === 'CHECKBOX') {
-            value = utils.ensureBoolean(value);
-          }
-        }
-
-        cleanedConnection[fieldKey] = value;
-      }
-      return cleanedConnection;
-    },
-    {}
-  );
-
-  return cleanedConnection;
-}
-
-module.exports = {
-  getDrivers,
-  getSchema,
-  renderConnection,
-  runQuery,
-  testConnection,
-  validateConnection
-};
+module.exports = drivers;
diff --git a/server/drivers/utils.js b/server/drivers/utils.js
index bfa00ee9..81b1681a 100644
--- a/server/drivers/utils.js
+++ b/server/drivers/utils.js
@@ -50,29 +50,6 @@ function formatSchemaQueryResults(queryResult) {
   return tree;
 }
 
-/**
- * Clean value to boolean
- * If value is not a boolean or can't be converted, an error is thrown
- * This is probably unnecessary but more a precaution
- * @param {any} value
- */
-function ensureBoolean(value) {
-  if (typeof value === 'boolean') {
-    return value;
-  }
-  if (typeof value === 'string' && value.toLowerCase() === 'true') {
-    return true;
-  } else if (typeof value === 'string' && value.toLowerCase() === 'false') {
-    return false;
-  } else if (value === 1) {
-    return true;
-  } else if (value === 0) {
-    return false;
-  }
-  throw new Error(`Unexpected value for boolean: ${value}`);
-}
-
 module.exports = {
-  ensureBoolean,
   formatSchemaQueryResults
 };
diff --git a/server/drivers/validate.js b/server/drivers/validate.js
new file mode 100644
index 00000000..8176842f
--- /dev/null
+++ b/server/drivers/validate.js
@@ -0,0 +1,48 @@
+/**
+ * Validate that the driver implementation has a function by name provided
+ * @param {object} driver
+ * @param {string} functionName
+ */
+function validateFunction(driver, functionName) {
+  if (typeof driver[functionName] !== 'function') {
+    throw new Error(`${driver.id} missing ${functionName} implementation`);
+  }
+}
+
+/**
+ * Validate that the driver implementation has an array by name provided
+ * @param {object} driver
+ * @param {string} arrayName
+ */
+function validateArray(driver, arrayName) {
+  const arr = driver[arrayName];
+  if (!Array.isArray(arr)) {
+    throw new Error(`${driver.id} missing ${arrayName} array`);
+  }
+}
+
+/**
+ * Validate driver implementation
+ * @param {string} id
+ * @param {object} driver
+ */
+function validate(id, driver) {
+  if (!driver.id) {
+    throw new Error(`${id} must export a unique id`);
+  }
+
+  if (id !== driver.id) {
+    throw new Error(`${id} does not match driver id ${driver.id}`);
+  }
+
+  if (!driver.name) {
+    throw new Error(`${id} must export a name`);
+  }
+
+  validateFunction(driver, 'getSchema');
+  validateFunction(driver, 'runQuery');
+  validateFunction(driver, 'testConnection');
+  validateArray(driver, 'fields');
+}
+
+module.exports = validate;
diff --git a/server/lib/config/index.js b/server/lib/config/index.js
index be060baf..1bc94b6d 100644
--- a/server/lib/config/index.js
+++ b/server/lib/config/index.js
@@ -1,6 +1,6 @@
 const _ = require('lodash');
 const appLog = require('../appLog');
-const drivers = require('../../drivers');
+const validateConnection = require('../validate-connection');
 const fromDefault = require('./fromDefault');
 const fromEnv = require('./fromEnv');
 const fromCli = require('./fromCli');
@@ -140,7 +140,7 @@ class Config {
       try {
         let connection = connectionsMap[id];
         connection._id = id;
-        connection = drivers.validateConnection(connection);
+        connection = validateConnection(connection);
         connection.editable = false;
         connectionsFromConfig.push(connection);
       } catch (error) {
diff --git a/server/lib/driver-connection.js b/server/lib/driver-connection.js
new file mode 100644
index 00000000..84759c04
--- /dev/null
+++ b/server/lib/driver-connection.js
@@ -0,0 +1,149 @@
+const uuid = require('uuid/v4');
+const drivers = require('../drivers');
+const renderConnection = require('./render-connection');
+const appLog = require('./appLog');
+const getMeta = require('./getMeta');
+
+/**
+ * Driver connection represents a driver and connection pairing, resulting in a connected database connection.
+ * In the future, it will optionally support a persisted connection,
+ * maintaining state about whether a connection is connected or not.
+ * These persisted connections will be reused across runQuery calls.
+ */
+class DriverConnection {
+  /**
+   * @param {object} connection
+   * @param {object} [user] - user to run query under. may not be provided if chart links turned on
+   */
+  constructor(connection, user) {
+    this.id = uuid();
+    this.connection = renderConnection(connection, user);
+    this.driver = drivers[connection.driver];
+    this.user = user;
+
+    appLog.debug(
+      {
+        originalConnection: connection,
+        renderedConnection: this.connection,
+        user
+      },
+      'Rendered connection for user'
+    );
+  }
+
+  /**
+   * Run query using driver implementation of connection
+   * @param {*} query
+   * @returns {Promise}
+   */
+  async runQuery(query) {
+    const connection = this.connection;
+    const driver = this.driver;
+    const user = this.user;
+
+    const finalResult = {
+      id: uuid.v4(),
+      cacheKey: null,
+      startTime: new Date(),
+      stopTime: null,
+      queryRunTime: null,
+      fields: [],
+      incomplete: false,
+      meta: {},
+      rows: []
+    };
+
+    const connectionName = connection.name;
+
+    const queryContext = {
+      driver: connection.driver,
+      userId: user && user._id,
+      userEmail: user && user.email,
+      connectionId: connection._id,
+      connectionName,
+      query,
+      startTime: finalResult.startTime
+    };
+
+    appLog.info(queryContext, 'Running query');
+
+    let results;
+    try {
+      results = await driver.runQuery(query, connection);
+    } catch (error) {
+      // It is logged INFO because it isn't necessarily a server/application error
+      // It could just be a bad query
+      appLog.info(
+        { ...queryContext, error: error.toString() },
+        'Error running query'
+      );
+
+      // Rethrow the error
+      // The error here is something expected and should be shown to user
+      throw error;
+    }
+
+    let { rows, incomplete, suppressedResultSet } = results;
+
+    if (!Array.isArray(rows)) {
+      appLog.warn(
+        {
+          driver: connection.driver,
+          connectionId: connection._id,
+          connectionName,
+          query
+        },
+        'Expected rows to be an array but received %s.',
+        typeof rows
+      );
+      rows = [];
+    }
+
+    finalResult.incomplete = Boolean(incomplete);
+    finalResult.suppressedResultSet = Boolean(suppressedResultSet);
+    finalResult.rows = rows;
+    finalResult.stopTime = new Date();
+    finalResult.queryRunTime = finalResult.stopTime - finalResult.startTime;
+    finalResult.meta = getMeta(rows);
+    finalResult.fields = Object.keys(finalResult.meta);
+
+    appLog.info(
+      {
+        ...queryContext,
+        stopTime: finalResult.stopTime,
+        queryRunTime: finalResult.queryRunTime,
+        rowCount: rows.length,
+        incomplete: finalResult.incomplete,
+        suppressedResultSet: finalResult.suppressedResultSet
+      },
+      'Query finished'
+    );
+
+    return finalResult;
+  }
+
+  /**
+   * Test connection passed in using the driver implementation
+   * As long as promise resolves without error
+   * it is considered a successful connection config
+   */
+  testConnection() {
+    return this.driver.testConnection(this.connection);
+  }
+
+  /**
+   * Gets schema (sometimes called schemaInfo) for connection
+   * This data is used by client to build schema tree in editor sidebar
+   * @returns {Promise}
+   */
+  getSchema() {
+    // Increase the max rows without modifiying original connection
+    const connectionMaxed = {
+      ...this.connection,
+      maxRows: Number.MAX_SAFE_INTEGER
+    };
+    return this.driver.getSchema(connectionMaxed);
+  }
+}
+
+module.exports = DriverConnection;
diff --git a/server/lib/render-connection.js b/server/lib/render-connection.js
new file mode 100644
index 00000000..ecbdc78f
--- /dev/null
+++ b/server/lib/render-connection.js
@@ -0,0 +1,27 @@
+const _ = require('lodash');
+
+/**
+ * Iterates over connection object, replacing any template strings with values from user
+ * This allows dynamic values inserted based on logged in user
+ * This uses a mustache-like syntax, using double mustaches.
+ * User variables can be referenced in connection strings using dot notation
+ * Example: {{user.someKey}} and {{user.data.someKey}}
+ * @param {object} connection
+ * @param {object} user
+ */
+function renderConnection(connection, user) {
+  const replaced = {};
+  Object.keys(connection).forEach(key => {
+    const value = connection[key];
+    if (typeof value === 'string') {
+      _.templateSettings.interpolate = /{{([\s\S]+?)}}/g;
+      const compiled = _.template(value);
+      replaced[key] = compiled({ user });
+    } else {
+      replaced[key] = value;
+    }
+  });
+  return replaced;
+}
+
+module.exports = renderConnection;
diff --git a/server/lib/validate-connection.js b/server/lib/validate-connection.js
new file mode 100644
index 00000000..97922161
--- /dev/null
+++ b/server/lib/validate-connection.js
@@ -0,0 +1,69 @@
+const drivers = require('../drivers');
+
+/**
+ * Clean value to boolean
+ * If value is not a boolean or can't be converted, an error is thrown
+ * This is probably unnecessary but more a precaution
+ * @param {any} value
+ */
+function ensureBoolean(value) {
+  if (typeof value === 'boolean') {
+    return value;
+  }
+  if (typeof value === 'string' && value.toLowerCase() === 'true') {
+    return true;
+  } else if (typeof value === 'string' && value.toLowerCase() === 'false') {
+    return false;
+  } else if (value === 1) {
+    return true;
+  } else if (value === 0) {
+    return false;
+  }
+  throw new Error(`Unexpected value for boolean: ${value}`);
+}
+
+/**
+ * Validates connection object based on its driver
+ * Unnecessary fields will be stripped out
+ * @param {object} connection
+ */
+function validateConnection(connection) {
+  const coreFields = ['_id', 'name', 'driver', 'createdDate', 'modifiedDate'];
+  if (!connection.name) {
+    throw new Error('connection.name required');
+  }
+  if (!connection.driver) {
+    throw new Error('connection.driver required');
+  }
+  const driver = drivers[connection.driver];
+  if (!driver) {
+    throw new Error(`driver implementation ${connection.driver} not found`);
+  }
+  const validFields = driver.fields.map(field => field.key).concat(coreFields);
+  const cleanedConnection = validFields.reduce(
+    (cleanedConnection, fieldKey) => {
+      if (connection.hasOwnProperty(fieldKey)) {
+        let value = connection[fieldKey];
+        const fieldDefinition = driver.fields.find(
+          field => field.key === fieldKey
+        );
+
+        // field definition may not exist since
+        // this could be a core field like _id, name
+        if (fieldDefinition) {
+          if (fieldDefinition.formType === 'CHECKBOX') {
+            value = ensureBoolean(value);
+          }
+        }
+
+        cleanedConnection[fieldKey] = value;
+      }
+      return cleanedConnection;
+    },
+    {}
+  );
+
+  return cleanedConnection;
+}
+
+module.exports = validateConnection;
diff --git a/server/models/connections.js b/server/models/connections.js
index 182fb2c4..1bbdbc9c 100644
--- a/server/models/connections.js
+++ b/server/models/connections.js
@@ -1,6 +1,6 @@
 const _ = require('lodash');
-const drivers = require('../drivers');
 const makeCipher = require('../lib/makeCipher');
+const validateConnection = require('../lib/validate-connection');
 
 class Connections {
   /**
@@ -73,7 +73,7 @@ class Connections {
     }
     connection.modifiedDate = new Date();
 
-    connection = drivers.validateConnection(connection);
+    connection = validateConnection(connection);
     const { _id } = connection;
 
     const existing = await this.findOneById(_id);
diff --git a/server/routes/drivers.js b/server/routes/drivers.js
index 6bf6f78f..6a4f8eb1 100644
--- a/server/routes/drivers.js
+++ b/server/routes/drivers.js
@@ -1,17 +1,25 @@
 const router = require('express').Router();
 const mustBeAuthenticated = require('../middleware/must-be-authenticated.js');
-const driversUtil = require('../drivers');
 const sendError = require('../lib/sendError');
+const drivers = require('../drivers');
 
 router.get('/api/drivers', mustBeAuthenticated, function(req, res) {
   const { config } = req;
   const debug = config.get('debug');
 
+  const driversArray = Object.keys(drivers).map(id => {
+    return {
+      id,
+      name: drivers[id].name,
+      fields: drivers[id].fields
+    };
+  });
+
   try {
     // If debug is turned on show all drivers, otherwise exclude mock driver
     // The mock driver is used for test cases, and is also useful for end-user debugging
     // TODO - deprecate debug for a config to enable mock
-    const drivers = driversUtil.getDrivers().filter(driver => {
+    const drivers = driversArray.filter(driver => {
       return debug || driver.id !== 'mock';
     });
 
diff --git a/server/routes/query-result.js b/server/routes/query-result.js
index 9f198c70..64a96d13 100644
--- a/server/routes/query-result.js
+++ b/server/routes/query-result.js
@@ -1,8 +1,8 @@
 const router = require('express').Router();
-const { runQuery } = require('../drivers/index');
 const mustHaveConnectionAccess = require('../middleware/must-have-connection-access.js');
 const mustHaveConnectionAccessOrChartLink = require('../middleware/must-have-connection-access-or-chart-link-noauth');
 const sendError = require('../lib/sendError');
+const DriverConnection = require('../lib/driver-connection');
 
 // This allows executing a query relying on the saved query text
 // Instead of relying on an open endpoint that executes arbitrary sql
@@ -72,7 +72,8 @@ async function getQueryResult(req, data) {
   }
   connection.maxRows = Number(req.config.get('queryResultMaxRows'));
 
-  const queryResult = await runQuery(queryText, connection, user);
+  const driverConnection = new DriverConnection(connection, user);
+  const queryResult = await driverConnection.runQuery(queryText);
   queryResult.cacheKey = cacheKey;
 
   if (req.config.get('queryHistoryRetentionTimeInDays') > 0) {
diff --git a/server/routes/schema-info.js b/server/routes/schema-info.js
index 0f746f18..3b89c67c 100644
--- a/server/routes/schema-info.js
+++ b/server/routes/schema-info.js
@@ -1,7 +1,7 @@
 const router = require('express').Router();
-const driver = require('../drivers');
 const mustHaveConnectionAccess = require('../middleware/must-have-connection-access.js');
 const sendError = require('../lib/sendError');
+const DriverConnection = require('../lib/driver-connection');
 
 router.get(
   '/api/schema-info/:connectionId',
@@ -24,7 +24,8 @@ router.get(
         return res.json({ schemaInfo });
       }
 
-      schemaInfo = await driver.getSchema(conn, user);
+      const driverConnection = new DriverConnection(conn, user);
+      schemaInfo = await driverConnection.getSchema();
       if (Object.keys(schemaInfo).length) {
         await models.schemaInfo.saveSchemaInfo(connectionId, schemaInfo);
       }


Test patch:
diff --git a/server/routes/test-connection.js b/server/routes/test-connection.js
index cf5e6cb7..06d07615 100644
--- a/server/routes/test-connection.js
+++ b/server/routes/test-connection.js
@@ -1,14 +1,15 @@
 const router = require('express').Router();
-const { testConnection } = require('../drivers/index');
 const mustBeAdmin = require('../middleware/must-be-admin.js');
 const sendError = require('../lib/sendError');
+const DriverConnection = require('../lib/driver-connection');
 
 /**
  * A non-error response is considered a success or valid connection config
  */
 router.post('/api/test-connection', mustBeAdmin, async function(req, res) {
   try {
-    await testConnection(req.body, req.user);
+    const driverConnection = new DriverConnection(req.body, req.user);
+    await driverConnection.testConnection();
     res.send({ success: true });
   } catch (error) {
     sendError(res, error);
diff --git a/server/test/api/drivers.js b/server/test/api/drivers.js
index 9b45a60c..c6146f08 100644
--- a/server/test/api/drivers.js
+++ b/server/test/api/drivers.js
@@ -12,9 +12,17 @@ describe('api/drivers', function() {
     const body = await utils.get('editor', '/api/drivers');
     const { drivers, error } = body;
     assert(!error, 'Expect no error');
+    const postgres = drivers.find(i => i.id === 'postgres');
+    assert(postgres, 'has postgres');
+    assert.strictEqual(postgres.name, 'Postgres');
+    assert(Array.isArray(postgres.fields));
     assert(
-      drivers.find(i => i.id === 'postgres'),
-      'has postgres'
+      postgres.fields.find(field => field.key === 'postgresSsl'),
+      'has postgres specific field'
+    );
+    assert(
+      !postgres.fields.find(field => field.key === 'sqlserverEncrypt'),
+      'Does not have a SQL Server field'
     );
   });
 });
diff --git a/server/test/drivers.js b/server/test/drivers.js
deleted file mode 100644
index 546d122b..00000000
--- a/server/test/drivers.js
+++ /dev/null
@@ -1,103 +0,0 @@
-const assert = require('assert');
-const drivers = require('../drivers');
-
-describe('drivers', function() {
-  it('loads and exposes api', function() {
-    // This test doesn't test much will expand later
-    assert(drivers);
-    assert(typeof drivers.getSchema === 'function');
-    assert(typeof drivers.runQuery === 'function');
-    assert(typeof drivers.testConnection === 'function');
-  });
-
-  it('getDrivers()', function() {
-    const driverItems = drivers.getDrivers();
-    assert(Array.isArray(driverItems), 'driverItems is array');
-    assert(driverItems.find(item => item.id === 'crate'));
-    assert(driverItems.find(item => item.id === 'hdb'));
-    assert(driverItems.find(item => item.id === 'mysql'));
-    assert(driverItems.find(item => item.id === 'postgres'));
-    assert(driverItems.find(item => item.id === 'presto'));
-    assert(driverItems.find(item => item.id === 'sqlserver'));
-    assert(driverItems.find(item => item.id === 'vertica'));
-
-    const postgres = driverItems.find(item => item.id === 'postgres');
-    assert.equal(postgres.id, 'postgres');
-    assert.equal(postgres.name, 'Postgres');
-    assert(Array.isArray(postgres.fields));
-    assert(
-      postgres.fields.find(field => field.key === 'postgresSsl'),
-      'has postgres specific field'
-    );
-    assert(
-      !postgres.fields.find(field => field.key === 'sqlserverEncrypt'),
-      'Does not have a SQL Server field'
-    );
-  });
-
-  it('validateConnection()', function() {
-    const validPostgres = drivers.validateConnection({
-      name: 'testname',
-      driver: 'postgres',
-      host: 'host',
-      port: 'port',
-      postgresSsl: true,
-      somethingStripped: 'shouldnotmakeit'
-    });
-    assert.equal(Object.keys(validPostgres).length, 5, 'only 5 keys valid');
-    assert.equal(validPostgres.name, 'testname');
-    assert.equal(validPostgres.driver, 'postgres');
-    assert.equal(validPostgres.host, 'host');
-    assert.equal(validPostgres.port, 'port');
-    assert.equal(validPostgres.postgresSsl, true);
-
-    assert.throws(() => {
-      drivers.validateConnection({ name: 'name' });
-    }, 'missing driver throws error');
-
-    assert.throws(() => {
-      drivers.validateConnection({ driver: 'postgres' });
-    }, 'missing name throws error');
-
-    assert.throws(() => {
-      drivers.validateConnection({ name: 'name', driver: 'not exist' });
-    }, 'missing driver imp throws error');
-
-    assert.throws(() => {
-      drivers.validateConnection({
-        name: 'name',
-        driver: 'postgres',
-        postgresSsl: 'notboolean'
-      });
-    }, 'boolean not convertable throws error');
-  });
-
-  it('renders connection with user', function() {
-    const secret = '123<>!@#$%^&*()-_+=';
-    const user = {
-      data: {
-        secret
-      }
-    };
-    const connection = {
-      connectionString: '{{user.data.secret}}',
-      singleStache: '{single}',
-      port: 6543
-    };
-    const rendered = drivers.renderConnection(connection, user);
-    assert.strictEqual(rendered.connectionString, secret);
-    assert.strictEqual(rendered.singleStache, '{single}', 'single left alone');
-    assert.strictEqual(rendered.port, 6543, 'number left alone');
-  });
-
-  it('renders connection without user', function() {
-    const connection = {
-      connectionString: 'test',
-      singleStache: '{single}',
-      port: 6543
-    };
-    const rendered = drivers.renderConnection(connection);
-    assert.strictEqual(rendered.connectionString, 'test');
-    assert.strictEqual(rendered.singleStache, '{single}');
-  });
-});
diff --git a/server/test/lib/driver-connection.js b/server/test/lib/driver-connection.js
new file mode 100644
index 00000000..e035103d
--- /dev/null
+++ b/server/test/lib/driver-connection.js
@@ -0,0 +1,20 @@
+const assert = require('assert');
+const DriverConnection = require('../../lib/driver-connection');
+
+describe('DriverConnection', function() {
+  it('loads and exposes api', async function() {
+    const driverConnection = new DriverConnection({
+      driver: 'postgres',
+      name: 'test'
+    });
+
+    assert(typeof driverConnection.getSchema === 'function');
+    assert(typeof driverConnection.runQuery === 'function');
+    assert(typeof driverConnection.testConnection === 'function');
+    // Notes for later. These methods will be added eventually
+    // assert(typeof driverConnection.connect === 'function');
+    // assert(typeof driverConnection.disconnect === 'function');
+    // assert(typeof driverConnection.keepAlive === 'function');
+    // assert(typeof driverConnection.getStatus === 'function');
+  });
+});
diff --git a/server/test/lib/render-connection.js b/server/test/lib/render-connection.js
new file mode 100644
index 00000000..245417ba
--- /dev/null
+++ b/server/test/lib/render-connection.js
@@ -0,0 +1,33 @@
+const assert = require('assert');
+const renderConnection = require('../../lib/render-connection');
+
+describe('lib/render-connection', function() {
+  it('renders connection with user', function() {
+    const secret = '123<>!@#$%^&*()-_+=';
+    const user = {
+      data: {
+        secret
+      }
+    };
+    const connection = {
+      connectionString: '{{user.data.secret}}',
+      singleStache: '{single}',
+      port: 6543
+    };
+    const rendered = renderConnection(connection, user);
+    assert.strictEqual(rendered.connectionString, secret);
+    assert.strictEqual(rendered.singleStache, '{single}', 'single left alone');
+    assert.strictEqual(rendered.port, 6543, 'number left alone');
+  });
+
+  it('renders connection without user', function() {
+    const connection = {
+      connectionString: 'test',
+      singleStache: '{single}',
+      port: 6543
+    };
+    const rendered = renderConnection(connection);
+    assert.strictEqual(rendered.connectionString, 'test');
+    assert.strictEqual(rendered.singleStache, '{single}');
+  });
+});
diff --git a/server/test/lib/validate-connection.js b/server/test/lib/validate-connection.js
new file mode 100644
index 00000000..c3e398d7
--- /dev/null
+++ b/server/test/lib/validate-connection.js
@@ -0,0 +1,41 @@
+const assert = require('assert');
+const validateConnection = require('../../lib/validate-connection');
+
+describe('drivers', function() {
+  it('validateConnection()', function() {
+    const validPostgres = validateConnection({
+      name: 'testname',
+      driver: 'postgres',
+      host: 'host',
+      port: 'port',
+      postgresSsl: true,
+      somethingStripped: 'shouldnotmakeit'
+    });
+    assert.equal(Object.keys(validPostgres).length, 5, 'only 5 keys valid');
+    assert.equal(validPostgres.name, 'testname');
+    assert.equal(validPostgres.driver, 'postgres');
+    assert.equal(validPostgres.host, 'host');
+    assert.equal(validPostgres.port, 'port');
+    assert.equal(validPostgres.postgresSsl, true);
+
+    assert.throws(() => {
+      validateConnection({ name: 'name' });
+    }, 'missing driver throws error');
+
+    assert.throws(() => {
+      validateConnection({ driver: 'postgres' });
+    }, 'missing name throws error');
+
+    assert.throws(() => {
+      validateConnection({ name: 'name', driver: 'not exist' });
+    }, 'missing driver imp throws error');
+
+    assert.throws(() => {
+      validateConnection({
+        name: 'name',
+        driver: 'postgres',
+        postgresSsl: 'notboolean'
+      });
+    }, 'boolean not convertable throws error');
+  });
+});