Skip to content
Closed
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
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,24 @@ jobs:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s
--health-retries 5
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:
PGPASS: postgres
PGUSER: postgres
PGDB: sqlmap
PGHOST: localhost
MYSQLHOST: 127.0.0.1
MYSQLUSER: root
MYSQLPASS: mysql
MYSQLDB: sqlmap
steps:
- name: Checkout source code
uses: actions/checkout@v6
Expand All @@ -53,6 +66,8 @@ jobs:
run: npm run lint
- name: Test
run: npm test
- name: Test Integration
run: npm run test:integration
- name: Test Security
run: npm run test:security
automerge:
Expand Down
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,13 +289,29 @@ 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 integration tests against running PostgreSQL & MySQL.
npm run test:integration:docker # spins up DBs via docker compose, runs 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 via docker compose (waits for healthy)
npm run test:integration # run the suite
npm run db:down # tear down
```

Connection settings default to the docker-compose services and can be overridden
with `PGHOST/PGPORT/PGUSER/PGPASS/PGDB` and `MYSQLHOST/MYSQLPORT/MYSQLUSER/MYSQLPASS/MYSQLDB`.

## Benchmark

Find more about `@nearform/sql` speed [here](benchmark)
Expand Down
24 changes: 21 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
version: '3.4'
version: '3.9'
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

mysql:
image: mysql:8.0
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
18 changes: 18 additions & 0 deletions integration/SQL.mysql.integration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict'

const { test, before, after } = require('node:test')
const { withMysql, createUsersTable } = require('./helpers/db')
const runFeatureSuite = require('./shared/featureSuite')

const ctx = { db: null }

before(async () => {
ctx.db = await withMysql()
await createUsersTable(ctx.db)
})

after(async () => {
if (ctx.db) await ctx.db.end()
})

runFeatureSuite(test, () => ctx.db)
18 changes: 18 additions & 0 deletions integration/SQL.mysql2.integration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict'

const { test, before, after } = require('node:test')
const { withMysql2, createUsersTable } = require('./helpers/db')
const runFeatureSuite = require('./shared/featureSuite')

const ctx = { db: null }

before(async () => {
ctx.db = await withMysql2()
await createUsersTable(ctx.db)
})

after(async () => {
if (ctx.db) await ctx.db.end()
})

runFeatureSuite(test, () => ctx.db)
18 changes: 18 additions & 0 deletions integration/SQL.pg.integration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict'

const { test, before, after } = require('node:test')
const { withPg, createUsersTable } = require('./helpers/db')
const runFeatureSuite = require('./shared/featureSuite')

const ctx = { db: null }

before(async () => {
ctx.db = await withPg()
await createUsersTable(ctx.db)
})

after(async () => {
if (ctx.db) await ctx.db.end()
})

runFeatureSuite(test, () => ctx.db)
26 changes: 26 additions & 0 deletions integration/helpers/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'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, shared by both the `mysql` and `mysql2` drivers.
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 on both the
// `mysql` driver (defaults to 3-byte utf8) and `mysql2`.
charset: 'utf8mb4'
}

module.exports = { pg, mysql }
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 8's default
// `caching_sha2_password` auth. mysql2 can, so we use it to switch the account
// to `mysql_native_password` first. This works identically locally and in CI
// without needing a container `command` override (unsupported by GitHub
// Actions service containers).
async function ensureNativePassword () {
const mysql = require('mysql2/promise')
const conn = await mysql.createConnection(config.mysql)
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.mysql.user, config.mysql.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.mysql)
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