Skip to content
Open
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
94 changes: 90 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ on:
branches:
- master
jobs:
build-lint-test:
postgres:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version:
- 20
- 22
- 24
- 26
os:
- ubuntu-latest
postgres-version:
Expand Down Expand Up @@ -53,10 +52,97 @@ jobs:
run: npm run lint
- name: Test
run: npm test
- name: Test Integration (PostgreSQL)
run: npm run test:integration:pg
- name: Test Security
run: npm run test:security
# mysql2 supports modern caching_sha2_password auth, so it runs against the
# current MySQL releases.
mysql2:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version:
- 24
- 26
os:
- ubuntu-latest
mysql-version:
- 8
- 9
name: "Node ${{ matrix.node-version }} - mysql2 - MySQL ${{ matrix.mysql-version }}"
services:
mysql:
image: mysql:${{ matrix.mysql-version }}
env:
MYSQL_ROOT_PASSWORD: mysql
MYSQL_DATABASE: sqlmap
ports:
- 3306:3306
options: --health-cmd "mysqladmin ping -h 127.0.0.1 -uroot -pmysql --silent"
--health-interval 10s --health-timeout 5s --health-retries 10
env:
MYSQLHOST: 127.0.0.1
MYSQLUSER: root
MYSQLPASS: mysql
MYSQLDB: sqlmap
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- name: Install
run: npm install
- name: Test Integration (mysql2)
run: npm run test:integration:mysql2
# The legacy `mysql` driver only supports mysql_native_password, which is
# removed/disabled in MySQL 8.4+/9, so it is pinned to MySQL 8.0.
mysql:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version:
- 24
- 26
os:
- ubuntu-latest
name: "Node ${{ matrix.node-version }} - mysql - MySQL 8.0"
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: mysql
MYSQL_DATABASE: sqlmap
ports:
- 3306:3306
options: --health-cmd "mysqladmin ping -h 127.0.0.1 -uroot -pmysql --silent"
--health-interval 10s --health-timeout 5s --health-retries 10
env:
# The legacy driver reads the MYSQLLEGACY* namespace; point it at the
# 8.0 service on the default port.
MYSQLLEGACYHOST: 127.0.0.1
MYSQLLEGACYPORT: 3306
MYSQLLEGACYUSER: root
MYSQLLEGACYPASS: mysql
MYSQLLEGACYDB: sqlmap
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- name: Install
run: npm install
- name: Test Integration (mysql legacy)
run: npm run test:integration:mysql
automerge:
needs: build-lint-test
needs:
- postgres
- mysql2
- mysql
runs-on: ubuntu-latest
permissions:
pull-requests: write
Expand Down
37 changes: 32 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,13 +289,40 @@ const pascalOrCamelToSnake = str =>
This module can be tested and reported on in a variety of ways...

```sh
npm run test # runs tap based unit test suite.
npm run test:security # runs sqlmap security tests.
npm run test:typescript # runs type definition tests.
npm run coverage # generates a coverage report in docs dir.
npm run lint # lints via standardJS.
npm run test # runs the node:test unit test suite.
npm run test:integration # runs all integration tests against running PostgreSQL & MySQL.
npm run test:integration:pg # PostgreSQL only (pg driver).
npm run test:integration:mysql2 # MySQL only (mysql2 driver).
npm run test:integration:mysql # MySQL only (legacy mysql driver, requires MySQL 8.0).
npm run test:integration:docker # spins up DBs via docker compose, runs all integration tests, tears down.
npm run test:security # runs sqlmap security tests.
npm run test:typescript # runs type definition tests.
npm run coverage # generates a coverage report in docs dir.
npm run lint # lints via standardJS.
```

The integration suite executes queries built by every public API against real
PostgreSQL and MySQL instances (the latter via both the `mysql` and `mysql2`
drivers), verifying the generated SQL is valid and that interpolated values are
stored as literals. To run it locally:

```sh
npm run db:up # start postgres + mysql (9) + mysql8 (8.0) via docker compose
npm run test:integration # run the suite
npm run db:down # tear down
```

> **Note:** the legacy `mysql` driver only supports `mysql_native_password`,
> which is removed/disabled in MySQL 8.4+/9. It therefore runs against a
> dedicated MySQL 8.0 service (the `mysql8` container, exposed on port `3307`),
> while `mysql2` runs against the modern `mysql` container.

Connection settings default to the docker-compose services and can be overridden:

- PostgreSQL — `PGHOST/PGPORT/PGUSER/PGPASS/PGDB`
- MySQL (mysql2) — `MYSQLHOST/MYSQLPORT/MYSQLUSER/MYSQLPASS/MYSQLDB` (default port `3306`)
- MySQL (legacy mysql) — `MYSQLLEGACYHOST/MYSQLLEGACYPORT/MYSQLLEGACYUSER/MYSQLLEGACYPASS/MYSQLLEGACYDB` (default port `3307`)

## Benchmark

Find more about `@nearform/sql` speed [here](benchmark)
Expand Down
40 changes: 37 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,44 @@
version: '3.4'
services:
db:
image: postgres:9-alpine
postgres:
image: postgres:17-alpine
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: sqlmap
ports:
- 5432:5432
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 5s
timeout: 5s
retries: 10

# Modern MySQL for the mysql2 driver.
mysql:
image: mysql:9
environment:
MYSQL_ROOT_PASSWORD: mysql
MYSQL_DATABASE: sqlmap
ports:
- 3306:3306
healthcheck:
test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 -uroot -pmysql --silent']
interval: 5s
timeout: 5s
retries: 10

# MySQL 8.0 for the legacy `mysql` driver, which only supports
# mysql_native_password (removed/disabled in 8.4+/9). Exposed on 3307 so it
# can run alongside the modern instance above (see MYSQLLEGACY* env vars).
mysql8:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: mysql
MYSQL_DATABASE: sqlmap
ports:
- 3307:3306
healthcheck:
test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 -uroot -pmysql --silent']
interval: 5s
timeout: 5s
retries: 10
6 changes: 6 additions & 0 deletions integration/SQL.mysql.integration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict'

const { withMysql } = require('./helpers/db')
const runIntegrationFile = require('./helpers/runFile')

runIntegrationFile(withMysql)
6 changes: 6 additions & 0 deletions integration/SQL.mysql2.integration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict'

const { withMysql2 } = require('./helpers/db')
const runIntegrationFile = require('./helpers/runFile')

runIntegrationFile(withMysql2)
6 changes: 6 additions & 0 deletions integration/SQL.pg.integration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict'

const { withPg } = require('./helpers/db')
const runIntegrationFile = require('./helpers/runFile')

runIntegrationFile(withPg)
46 changes: 46 additions & 0 deletions integration/helpers/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use strict'

// PostgreSQL connection config.
// Mirrors the env-var convention already used by sqlmap/config.js so the same
// CI environment variables drive both suites.
const pg = {
user: process.env.PGUSER || 'postgres',
host: process.env.PGHOST || 'localhost',
database: process.env.PGDB || 'sqlmap',
password: process.env.PGPASS || 'postgres',
port: Number(process.env.PGPORT) || 5432
}

// MySQL connection config used by the `mysql2` driver. Targets a modern MySQL
// (8.x/9.x) — mysql2 speaks the default caching_sha2_password auth.
const mysql = {
host: process.env.MYSQLHOST || 'localhost',
port: Number(process.env.MYSQLPORT) || 3306,
user: process.env.MYSQLUSER || 'root',
password: process.env.MYSQLPASS || 'mysql',
database: process.env.MYSQLDB || 'sqlmap',
// utf8mb4 so 4-byte characters (e.g. emoji) round-trip.
charset: 'utf8mb4',
// A fresh caching_sha2_password account over a non-TLS connection needs the
// server's public key to complete auth; allow retrieving it. Fine for local/
// CI test infra (no untrusted network).
allowPublicKeyRetrieval: true
}

// Separate config for the legacy `mysql` driver. It only supports
// mysql_native_password, which is removed/disabled in MySQL 8.4+/9, so it must
// point at a MySQL 8.0 server (own port locally; its own CI job). Its own
// env namespace lets it differ from the mysql2 target when both run together.
const mysqlLegacy = {
host: process.env.MYSQLLEGACYHOST || 'localhost',
port: Number(process.env.MYSQLLEGACYPORT) || 3307,
user: process.env.MYSQLLEGACYUSER || 'root',
password: process.env.MYSQLLEGACYPASS || 'mysql',
database: process.env.MYSQLLEGACYDB || 'sqlmap',
charset: 'utf8mb4',
// Used by the mysql2 connection that flips the account to native_password
// (see ensureNativePassword); the legacy `mysql` driver ignores it.
allowPublicKeyRetrieval: true
}

module.exports = { pg, mysql, mysqlLegacy }
114 changes: 114 additions & 0 deletions integration/helpers/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
'use strict'

const config = require('./config')

// Each adapter exposes the same shape so the shared feature suite is
// driver-agnostic:
// - dialect: 'pg' | 'mysql' (drives dialect-specific SQL in the suite)
// - query(stmt): runs a SqlStatement, returns the result rows
// - raw(text): runs a plain SQL string (used for DDL / cleanup)
// - end(): closes the connection

// PostgreSQL via `pg`. A SqlStatement is passed straight to client.query()
// because it exposes `.text` and `.values` getters — the documented usage.
async function withPg () {
const { Client } = require('pg')
const client = new Client(config.pg)
await client.connect()
return {
dialect: 'pg',
async query (stmt) {
const res = await client.query(stmt)
return res.rows
},
async raw (text) {
const res = await client.query(text)
return res.rows
},
async end () {
await client.end()
}
}
}

// MySQL via `mysql2/promise`. The driver reads `sql` and `values` off the
// options object, which map directly onto a SqlStatement's `.sql`/`.values`.
async function withMysql2 () {
const mysql = require('mysql2/promise')
const conn = await mysql.createConnection(config.mysql)
return {
dialect: 'mysql',
async query (stmt) {
const [rows] = await conn.query({ sql: stmt.sql, values: stmt.values })
return rows
},
async raw (text) {
const [rows] = await conn.query(text)
return rows
},
async end () {
await conn.end()
}
}
}

// The legacy `mysql` driver cannot speak MySQL's default `caching_sha2_password`
// auth — it only supports `mysql_native_password`. mysql2 can connect, so we use
// it to switch the account to native_password first. This requires a MySQL 8.0
// server (config.mysqlLegacy): native_password is removed/disabled in 8.4+/9, so
// the plugin wouldn't even be loaded there.
async function ensureNativePassword () {
const mysql = require('mysql2/promise')
const conn = await mysql.createConnection(config.mysqlLegacy)
try {
// We connect over TCP, so the account is `<user>@'%'`. `?` placeholders are
// escaped client-side (text protocol) into a valid `'user'@'%'` account.
await conn.query(
"ALTER USER ?@'%' IDENTIFIED WITH mysql_native_password BY ?",
[config.mysqlLegacy.user, config.mysqlLegacy.password]
)
} finally {
await conn.end()
}
}

// MySQL via the original callback-based `mysql` driver, promisified.
async function withMysql () {
await ensureNativePassword()
const mysql = require('mysql')
const conn = mysql.createConnection(config.mysqlLegacy)
await new Promise((resolve, reject) =>
conn.connect(err => (err ? reject(err) : resolve()))
)
const run = (sql, values) =>
new Promise((resolve, reject) =>
conn.query(sql, values, (err, rows) => (err ? reject(err) : resolve(rows)))
)
return {
dialect: 'mysql',
async query (stmt) {
return run(stmt.sql, stmt.values)
},
async raw (text) {
return run(text)
},
async end () {
await new Promise(resolve => conn.end(() => resolve()))
}
}
}

// Fresh, portable `users` table. Explicit (non-auto) integer ids keep the
// inserts identical across dialects.
async function createUsersTable (db) {
await db.raw('DROP TABLE IF EXISTS users')
await db.raw(`CREATE TABLE users (
id INTEGER PRIMARY KEY,
username VARCHAR(255) NOT NULL,
email VARCHAR(255),
password VARCHAR(255),
metadata VARCHAR(255)
)`)
}

module.exports = { withPg, withMysql, withMysql2, createUsersTable }
Loading
Loading