Skip to content

Commit 168d407

Browse files
authored
feat(config): add returnFinalSqlQuery option
1 parent 70f10c7 commit 168d407

File tree

7 files changed

+227
-10
lines changed

7 files changed

+227
-10
lines changed

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,50 @@ exports.handler = async (event, context) => {
3939
}
4040
```
4141

42+
## Logging SQL Queries
43+
44+
You can enable logging of the final SQL query (with all parameter values substituted) by setting the `returnFinalSqlQuery` option to `true`:
45+
46+
```javascript
47+
// Require and initialize outside of your main handler
48+
const mysql = require('serverless-mysql')({
49+
config: {
50+
host : process.env.ENDPOINT,
51+
database : process.env.DATABASE,
52+
user : process.env.USERNAME,
53+
password : process.env.PASSWORD
54+
},
55+
returnFinalSqlQuery: true // Enable SQL query logging
56+
})
57+
58+
// Main handler function
59+
exports.handler = async (event, context) => {
60+
// Run your query with parameters
61+
const results = await mysql.query('SELECT * FROM users WHERE id = ?', [userId])
62+
63+
// Access the SQL query with substituted values
64+
console.log('Executed query:', results.sql)
65+
66+
// Run clean up function
67+
await mysql.end()
68+
69+
// Return the results
70+
return results
71+
}
72+
```
73+
74+
When `returnFinalSqlQuery` is enabled, the SQL query with substituted values is also attached to error objects when a query fails, making it easier to debug:
75+
76+
```javascript
77+
try {
78+
const results = await mysql.query('SELECT * FROM nonexistent_table')
79+
} catch (error) {
80+
// The error object will have the SQL property
81+
console.error('Failed query:', error.sql)
82+
console.error('Error message:', error.message)
83+
}
84+
```
85+
4286
## Installation
4387
```
4488
npm i serverless-mysql
@@ -169,6 +213,7 @@ Below is a table containing all of the possible configuration options for `serve
169213
| usedConnsFreq | `Integer` | The number of milliseconds to cache lookups of current connection usage. | `0` |
170214
| zombieMaxTimeout | `Integer` | The maximum number of seconds that a connection can stay idle before being recycled. | `900` |
171215
| zombieMinTimeout | `Integer` | The minimum number of *seconds* that a connection must be idle before the module will recycle it. | `3` |
216+
| returnFinalSqlQuery | `Boolean` | Flag indicating whether to attach the final SQL query (with substituted values) to the results. When enabled, the SQL query will be available as a non-enumerable `sql` property on array results or as a regular property on object results. | `false` |
172217

173218
### Connection Backoff
174219
If `manageConns` is not set to `false`, then this module will automatically kill idle connections or disconnect the current connection if the `connUtilization` limit is reached. Even with this aggressive strategy, it is possible that multiple functions will be competing for available connections. The `backoff` setting uses the strategy outlined [here](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) to use *Jitter* instead of *Exponential Backoff* when attempting connection retries.

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ services:
99
MYSQL_PASSWORD: testpassword
1010
# Allow connections from any host
1111
MYSQL_ROOT_HOST: "%"
12+
TZ: UTC
1213
ports:
1314
- "3306:3306"
1415
command: >

index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ declare namespace serverlessMysql {
8787
* Integer The minimum number of seconds that a connection must be idle before the module will recycle it. 3
8888
*/
8989
zombieMinTimeout?: number;
90+
/**
91+
* Boolean Flag indicating whether to attach the final SQL query with substituted values to the results. When enabled, the SQL query will be available as a non-enumerable `sql` property on array results or as a regular property on object results.
92+
* This also attaches the SQL query to error objects when a query fails, making it easier to debug. false
93+
*/
94+
returnFinalSqlQuery?: boolean;
9095
};
9196

9297
class Transaction {

index.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ module.exports = (params) => {
3636
// Init setting values
3737
let MYSQL, manageConns, cap, base, maxRetries, connUtilization, backoff,
3838
zombieMinTimeout, zombieMaxTimeout, maxConnsFreq, usedConnsFreq,
39-
onConnect, onConnectError, onRetry, onClose, onError, onKill, onKillError, PromiseLibrary
39+
onConnect, onConnectError, onRetry, onClose, onError, onKill, onKillError, PromiseLibrary, returnFinalSqlQuery
4040

4141
/********************************************************************/
4242
/** HELPER/CONVENIENCE FUNCTIONS **/
@@ -219,7 +219,11 @@ module.exports = (params) => {
219219
// If no args are passed in a transaction, ignore query
220220
if (this && this.rollback && args.length === 0) { return resolve([]) }
221221

222-
client.query(...args, async (err, results) => {
222+
const queryObj = client.query(...args, async (err, results) => {
223+
if (returnFinalSqlQuery && queryObj.sql && err) {
224+
err.sql = queryObj.sql
225+
}
226+
223227
if (err && err.code === 'PROTOCOL_SEQUENCE_TIMEOUT') {
224228
client.destroy() // destroy connection on timeout
225229
resetClient() // reset the client
@@ -239,6 +243,18 @@ module.exports = (params) => {
239243
}
240244
reject(err)
241245
}
246+
247+
if (returnFinalSqlQuery && queryObj.sql) {
248+
if (Array.isArray(results)) {
249+
Object.defineProperty(results, 'sql', {
250+
enumerable: false,
251+
value: queryObj.sql
252+
})
253+
} else if (results && typeof results === 'object') {
254+
results.sql = queryObj.sql
255+
}
256+
}
257+
242258
return resolve(results)
243259
})
244260
}
@@ -399,6 +415,7 @@ module.exports = (params) => {
399415
zombieMaxTimeout = Number.isInteger(cfg.zombieMaxTimeout) ? cfg.zombieMaxTimeout : 60 * 15 // default to 15 minutes
400416
maxConnsFreq = Number.isInteger(cfg.maxConnsFreq) ? cfg.maxConnsFreq : 15 * 1000 // default to 15 seconds
401417
usedConnsFreq = Number.isInteger(cfg.usedConnsFreq) ? cfg.usedConnsFreq : 0 // default to 0 ms
418+
returnFinalSqlQuery = cfg.returnFinalSqlQuery === true // default to false
402419

403420
// Event handlers
404421
onConnect = typeof cfg.onConnect === 'function' ? cfg.onConnect : () => { }

package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@
55
"main": "index.js",
66
"types": "index.d.ts",
77
"scripts": {
8-
"test:unit": "mocha --check-leaks --recursive test/unit/*.spec.js",
9-
"test:unit:debug": "NODE_DEBUG=mysql,net,stream mocha --check-leaks --recursive test/unit/*.spec.js",
10-
"test:integration": "mocha --check-leaks --recursive test/integration/*.spec.js",
11-
"test:integration:debug": "NODE_DEBUG=mysql,net,stream mocha --check-leaks --recursive test/integration/*.spec.js",
8+
"test:unit": "TZ=UTC mocha --check-leaks --recursive test/unit/*.spec.js",
9+
"test:unit:debug": "TZ=UTC NODE_DEBUG=mysql,net,stream mocha --check-leaks --recursive test/unit/*.spec.js",
10+
"test:integration": "TZ=UTC mocha --check-leaks --recursive test/integration/*.spec.js",
11+
"test:integration:debug": "TZ=UTC NODE_DEBUG=mysql,net,stream mocha --check-leaks --recursive test/integration/*.spec.js",
1212
"test:integration:docker": "./scripts/run-integration-tests.sh",
1313
"test:integration:docker:debug": "./scripts/run-integration-tests.sh debug",
1414
"test:docker": "./scripts/run-all-tests.sh",
1515
"test:docker:debug": "./scripts/run-all-tests.sh debug",
16-
"test": "mocha --check-leaks --recursive test/{unit,integration}/*.spec.js",
17-
"test:debug": "NODE_DEBUG=mysql,net,stream mocha --check-leaks --recursive test/{unit,integration}/*.spec.js",
18-
"test-cov": "nyc --reporter=html mocha --check-leaks --recursive test/{unit,integration}/*.spec.js",
19-
"test-cov:debug": "NODE_DEBUG=mysql,net,stream nyc --reporter=html mocha --check-leaks --recursive test/{unit,integration}/*.spec.js",
16+
"test": "TZ=UTC mocha --check-leaks --recursive test/{unit,integration}/*.spec.js",
17+
"test:debug": "TZ=UTC NODE_DEBUG=mysql,net,stream mocha --check-leaks --recursive test/{unit,integration}/*.spec.js",
18+
"test-cov": "TZ=UTC nyc --reporter=html mocha --check-leaks --recursive test/{unit,integration}/*.spec.js",
19+
"test-cov:debug": "TZ=UTC NODE_DEBUG=mysql,net,stream nyc --reporter=html mocha --check-leaks --recursive test/{unit,integration}/*.spec.js",
2020
"lint": "eslint .",
2121
"prepublishOnly": "npm run lint && npm run test:docker"
2222
},

scripts/run-integration-tests.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ echo "Running integration tests..."
9999
fi
100100

101101
# Add connection retry parameters to MySQL connection
102+
TZ=UTC \
102103
MYSQL_HOST=127.0.0.1 \
103104
MYSQL_PORT=3306 \
104105
MYSQL_DATABASE=serverless_mysql_test \
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
'use strict';
2+
3+
const { expect } = require('chai');
4+
const {
5+
createTestConnection,
6+
setupTestTable,
7+
cleanupTestTable,
8+
closeConnection
9+
} = require('./helpers/setup');
10+
11+
describe('Return Final SQL Query Integration Tests', function () {
12+
this.timeout(15000);
13+
14+
let db, dbWithoutLogging;
15+
const TEST_TABLE = 'final_sql_query_test_table';
16+
const TABLE_SCHEMA = `
17+
id INT AUTO_INCREMENT PRIMARY KEY,
18+
name VARCHAR(255) NOT NULL,
19+
active BOOLEAN DEFAULT true,
20+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
21+
`;
22+
23+
before(async function () {
24+
try {
25+
db = createTestConnection({ returnFinalSqlQuery: true });
26+
dbWithoutLogging = createTestConnection({ returnFinalSqlQuery: false });
27+
await setupTestTable(db, TEST_TABLE, TABLE_SCHEMA);
28+
} catch (err) {
29+
this.skip();
30+
}
31+
});
32+
33+
after(async function () {
34+
try {
35+
await cleanupTestTable(db, TEST_TABLE);
36+
37+
if (db) {
38+
await db.end();
39+
await closeConnection(db);
40+
}
41+
if (dbWithoutLogging) {
42+
await dbWithoutLogging.end();
43+
await closeConnection(dbWithoutLogging);
44+
}
45+
} catch (err) {
46+
}
47+
});
48+
49+
it('should include SQL with substituted parameters when returnFinalSqlQuery is enabled', async function () {
50+
const insertResult = await db.query(
51+
'INSERT INTO ?? (name, active) VALUES (?, ?)',
52+
[TEST_TABLE, 'Test User', true]
53+
);
54+
55+
expect(insertResult).to.have.property('sql');
56+
const expectedInsertSql = `INSERT INTO \`${TEST_TABLE}\` (name, active) VALUES ('Test User', true)`;
57+
expect(insertResult.sql).to.equal(expectedInsertSql);
58+
59+
const selectResult = await db.query(
60+
'SELECT * FROM ?? WHERE name = ? AND active = ?',
61+
[TEST_TABLE, 'Test User', true]
62+
);
63+
64+
expect(selectResult).to.have.property('sql');
65+
const expectedSelectSql = `SELECT * FROM \`${TEST_TABLE}\` WHERE name = 'Test User' AND active = true`;
66+
expect(selectResult.sql).to.equal(expectedSelectSql);
67+
});
68+
69+
it('should not include SQL when returnFinalSqlQuery is disabled', async function () {
70+
const insertResult = await dbWithoutLogging.query(
71+
'INSERT INTO ?? (name, active) VALUES (?, ?)',
72+
[TEST_TABLE, 'No Log User', true]
73+
);
74+
75+
expect(insertResult).to.not.have.property('sql');
76+
77+
const selectResult = await dbWithoutLogging.query(
78+
'SELECT * FROM ?? WHERE name = ? AND active = ?',
79+
[TEST_TABLE, 'No Log User', true]
80+
);
81+
82+
expect(selectResult).to.not.have.property('sql');
83+
});
84+
85+
it('should include SQL with complex parameter types correctly', async function () {
86+
const testDate = new Date('2020-01-01T00:00:00Z');
87+
88+
const insertResult = await db.query(
89+
'INSERT INTO ?? (name, active, created_at) VALUES (?, ?, ?)',
90+
[TEST_TABLE, 'Date User', true, testDate]
91+
);
92+
93+
expect(insertResult).to.have.property('sql');
94+
const expectedInsertSql = `INSERT INTO \`${TEST_TABLE}\` (name, active, created_at) VALUES ('Date User', true, '2020-01-01 00:00:00.000')`;
95+
expect(insertResult.sql).to.equal(expectedInsertSql);
96+
97+
const selectResult = await db.query(
98+
'SELECT * FROM ?? WHERE name = ?',
99+
[TEST_TABLE, 'Date User']
100+
);
101+
102+
expect(selectResult).to.have.property('sql');
103+
const expectedSelectSql = `SELECT * FROM \`${TEST_TABLE}\` WHERE name = 'Date User'`;
104+
expect(selectResult.sql).to.equal(expectedSelectSql);
105+
});
106+
107+
it('should include SQL with object parameters correctly', async function () {
108+
const userData = {
109+
name: 'Object User',
110+
active: false
111+
};
112+
113+
const insertResult = await db.query(
114+
'INSERT INTO ?? SET ?',
115+
[TEST_TABLE, userData]
116+
);
117+
118+
expect(insertResult).to.have.property('sql');
119+
const expectedInsertSql = `INSERT INTO \`${TEST_TABLE}\` SET \`name\` = 'Object User', \`active\` = false`;
120+
expect(insertResult.sql).to.equal(expectedInsertSql);
121+
122+
const selectResult = await db.query(
123+
'SELECT * FROM ?? WHERE name = ? AND active = ?',
124+
[TEST_TABLE, 'Object User', false]
125+
);
126+
127+
expect(selectResult).to.have.property('sql');
128+
const expectedSelectSql = `SELECT * FROM \`${TEST_TABLE}\` WHERE name = 'Object User' AND active = false`;
129+
expect(selectResult.sql).to.equal(expectedSelectSql);
130+
});
131+
132+
it('should include SQL in error objects when a query fails', async function () {
133+
try {
134+
await db.query(
135+
'SELECT * FROM ?? WHERE nonexistent_column = ?',
136+
[TEST_TABLE, 'Test Value']
137+
);
138+
139+
expect.fail('Query should have thrown an error');
140+
} catch (error) {
141+
expect(error).to.have.property('sql');
142+
const expectedSql = `SELECT * FROM \`${TEST_TABLE}\` WHERE nonexistent_column = 'Test Value'`;
143+
expect(error.sql).to.equal(expectedSql);
144+
expect(error).to.have.property('code');
145+
expect(error.code).to.match(/^ER_/);
146+
}
147+
});
148+
});

0 commit comments

Comments
 (0)