Skip to content

Commit

Permalink
Add support for MySQL 8 (#185)
Browse files Browse the repository at this point in the history
* πŸ”₯ Remove any-db and unused packages

* βž• Add mysql2

* 🩹 Switch db package to mysql2 to support mysql 8

* βž• Add postgres and types

* πŸ”₯ Remove any-db types

* ✨ Make database class  work with mysql 8

* 🎨 Let package figure out default port

* πŸ› Make it work with postgres

* ⚰️ Remove getDatabases

* ♻️ Refactor database module to use factory

* ♻️ Conform to existing design patterns and add tests

* πŸ”₯ Remove old database class

* 🀑 Mock mysql2 and pg in factory test

* 🏷️ Add void return type to end methods

* 🎨 Add new line to end of files
  • Loading branch information
lewissteele authored May 4, 2021
1 parent 1b413d9 commit e10904f
Show file tree
Hide file tree
Showing 12 changed files with 265 additions and 59 deletions.
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,12 @@ services:
MYSQL_ROOT_PASSWORD: password
ports:
- 3306:3306
postgres:
image: postgres
restart: always
environment:
POSTGRES_PASSWORD: password
ports:
- 5432:5432
volumes:
mysql: ~
9 changes: 3 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,20 @@
},
"dependencies": {
"@types/moo": "^0.5.3",
"@types/mysql": "^2.15.9",
"@types/nearley": "^2.11.1",
"@types/node": "^14.0.14",
"@types/pg": "^7.14.11",
"@types/sprintf-js": "^1.1.2",
"any-db": "^2.2.1",
"any-db-mysql": "^2.1.2",
"any-db-postgres": "^2.1.5",
"commander": "^5.1.0",
"moo": "^0.5.1",
"mysql": "^2.16.0",
"mysql2": "^2.2.5",
"nearley": "^2.19.3",
"param-case": "^3.0.3",
"pg": "^8.6.0",
"sprintf-js": "^1.1.2"
},
"description": "An intelligent SQL linter and checker",
"devDependencies": {
"@types/any-db": "^2.1.30",
"@types/jest": "^26.0.3",
"jest": "^26.1.0",
"pkg": "^4.3.7",
Expand Down
6 changes: 3 additions & 3 deletions src/checker/checkerRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as fs from "fs";
import * as path from "path";
import { CheckFactory } from "./checkFactory";
import { Query } from "../reader/query";
import { Database } from "../database";
import IDatabase from "../database/interface";
import { Printer } from "../printer";
import { categorise, tokenise } from "../lexer/lexer";
import { MySqlError } from "../barrel/checks";
Expand All @@ -17,7 +17,7 @@ class CheckerRunner {
prefix: string,
omittedErrors: string[],
driver: string,
database?: Database
database?: IDatabase
) {
const checks = fs
.readdirSync(`${__dirname}/checks/any`)
Expand Down Expand Up @@ -79,7 +79,7 @@ class CheckerRunner {
database &&
checker.appliesTo.includes(category)
) {
database.lintQuery(database.connection, content, (results: any) => {
database.lintQuery(content, (results: any) => {
const sqlChecker = new MySqlError(results);
printer.printCheck(sqlChecker, tokenised, prefix);
});
Expand Down
45 changes: 0 additions & 45 deletions src/database.ts

This file was deleted.

20 changes: 20 additions & 0 deletions src/database/databaseFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import IDatabase from './interface';
import MySqlDatabase from './mySqlDatabase';
import PostgresDatabase from './postgresDatabase';

export default function databaseFactory(
driver: string,
host: string,
user: string,
password: string,
port?: number
): IDatabase {
switch (driver) {
case 'mysql':
return new MySqlDatabase(host, user, password, port);
case 'postgres':
return new PostgresDatabase(host, user, password, port);
default:
throw new Error(`${driver} driver is unsupported`);
}
}
8 changes: 8 additions & 0 deletions src/database/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default interface IDatabase {
/**
* Runs an EXPLAIN on the query. If it doesn't run successfully, errors will come through,
* which is what we want.
*/
lintQuery(query: string, callback: any): void;
end(): void;
}
32 changes: 32 additions & 0 deletions src/database/mySqlDatabase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as mysql from 'mysql2';
import IDatabase from './interface';

export default class MySqlDatabase implements IDatabase {
private connection: mysql.Connection;

constructor (
host: string,
user: string,
password: string,
port?: number,
) {
this.connection = mysql.createConnection({
host,
user,
password,
port,
});
}

public lintQuery(query: string, callback: any): void {
this.connection.query(`EXPLAIN ${query}`, err => {
if (err) {
callback(err);
}
});
}

public end(): void {
this.connection.end();
}
}
35 changes: 35 additions & 0 deletions src/database/postgresDatabase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Pool } from 'pg';
import IDatabase from './interface';

export default class PostgresDatabase implements IDatabase {
private pool: Pool;

constructor(
host: string,
user: string,
password: string,
port?: number,
) {
this.pool = new Pool({
host,
user,
password,
port,
});
}

public lintQuery(query: string, callback: any): void {
this.pool.query(`EXPLAIN ${query}`, err => {
if (err) {
callback({
code: err.name,
sqlMessage: err.message,
});
}
});
}

public end(): void {
this.pool.end();
}
}
10 changes: 5 additions & 5 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as fs from "fs";
import * as process from "process";

import { CheckerRunner } from "./checker/checkerRunner";
import { Database } from "./database";
import databaseFactory from "./database/databaseFactory";
import { FormatterFactory } from "./formatter/formatterFactory";
import { Printer } from "./printer";
import { Query } from "./reader/query";
Expand Down Expand Up @@ -95,12 +95,12 @@ if (configuration === null) {
const driver = program.driver || configuration?.driver || "mysql";

if (program.host || configuration?.host) {
db = new Database(
driver,
db = databaseFactory(
driver,
program.host || configuration?.host || "localhost",
program.user || configuration?.user || "root", // bad practice but unfortunately common, make it easier for the user
program.password || configuration?.password,
program.port || configuration?.port || "3306"
program.port || configuration?.port || undefined // let mysql2 or pg figure out the default port
);
}

Expand All @@ -120,5 +120,5 @@ if (programFile) {
runner.run(queries, printer, prefix, omittedErrors, driver, db);

if (program.host || configuration?.host) {
db.connection.end();
db.end();
}
46 changes: 46 additions & 0 deletions test/unit/database/databaseFactory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import databaseFactory from "../../../src/database/databaseFactory";
import MySqlDatabase from "../../../src/database/mySqlDatabase";
import PostgresDatabase from "../../../src/database/postgresDatabase";

jest.mock("mysql2", () => {
const mock = {
createConnection: () => mock,
};
return mock;
});

jest.mock("pg", () => {
const mock = {
Pool: function () {
return mock;
},
};
return mock;
});

test.each([
["mysql", MySqlDatabase],
["postgres", PostgresDatabase],
])("it returns correct instance for driver", (driver, expected) => {
const database = databaseFactory(driver, "localhost", "user", "password", 3306);
expect(database).toBeInstanceOf(expected);
});

test("it throws an exception if driver is not supported", () => {
const t = () => databaseFactory("mongodb", "localhost", "user", "password", 3306);
expect(t).toThrow(Error);
});

test("it does not call callback if there is no error", () => {
const callback = jest.fn(() => true);

jest.mock("mysql2", () => {
const mock = {
createConnection: () => mock,
query: () => true,
};
return mock;
});

expect(callback.mock.calls.length).toBe(0);
});
52 changes: 52 additions & 0 deletions test/unit/database/mysqlDatabase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import MySqlDatabase from "../../../src/database/mySqlDatabase";

jest.mock("mysql2", () => {
const mock = {
createConnection: (config) => {
expect(config).toEqual({
host: "localhost",
user: "user",
password: "password",
port: 3306,
});
return mock;
},
query: (query, callback) => {
callback({
sqlMessage: "table does not exist",
});
},
end: () => true,
};
return mock;
});

test("it calls createConnection", () => {
const db = new MySqlDatabase("localhost", "user", "password", 3306);
});

test("it calls callback if there is an error", () => {
const db = new MySqlDatabase("localhost", "user", "password", 3306);
db.lintQuery("SELECT some_column FROM some_table WHERE id = 1", err => {
expect(err.sqlMessage).toEqual("table does not exist");
})
});

test("it calls end on connection", () => {
const db = new MySqlDatabase("localhost", "user", "password", 3306);
db.end();
});

test("it does not call callback if there is no error", () => {
const callback = jest.fn(() => true);

jest.mock("mysql2", () => {
const mock = {
createConnection: () => mock,
query: () => true,
};
return mock;
});

expect(callback.mock.calls.length).toBe(0);
});
54 changes: 54 additions & 0 deletions test/unit/database/postgresDatabase.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import PostgresDatabase from "../../../src/database/postgresDatabase";

jest.mock("pg", () => {
const mock = {
Pool: function (config) {
expect(config).toEqual({
host: "localhost",
user: "user",
password: "password",
port: 5432,
});
return mock;
},
query: (query, callback) => {
callback({
name: "name",
message: "table does not exist",
});
},
end: () => true,
};
return mock;
});

test("it calls createConnection", () => {
const db = new PostgresDatabase("localhost", "user", "password", 5432);
});

test("it calls callback if there is an error", () => {
const db = new PostgresDatabase("localhost", "user", "password", 5432);
db.lintQuery("SELECT some_column FROM some_table WHERE id = 1", err => {
expect(err.sqlMessage).toEqual("table does not exist");
expect(err.code).toEqual("name");
})
});

test("it calls end on connection", () => {
const db = new PostgresDatabase("localhost", "user", "password", 5432);
db.end();
});

test("it does not call callback if there is no error", () => {
const callback = jest.fn(() => true);

jest.mock("pg", () => {
const mock = {
createConnection: () => mock,
query: () => true,
};
return mock;
});

expect(callback.mock.calls.length).toBe(0);
});

0 comments on commit e10904f

Please sign in to comment.