├── .gitignore ├── index.js ├── package.json ├── LICENSE.md ├── lib └── index.js ├── README.md └── test └── e2e.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = require( "./lib/index" ); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sequelize-comment", 3 | "version": "1.0.1", 4 | "description": "Adds the ability to include a SQL-comment before generated SQL statements (pulls from options.comment).", 5 | "repository": { 6 | "type": "git", 7 | "url" : "https://github.com/bennadel/sequelize-comment.git" 8 | }, 9 | "main": "index.js", 10 | "scripts": { 11 | "test": "node test/e2e.js" 12 | }, 13 | "keywords": [ 14 | "sequelize", 15 | "comments", 16 | "debugging", 17 | "general", 18 | "log", 19 | "slow", 20 | "query", 21 | "log" 22 | ], 23 | "author": "Ben Nadel", 24 | "license": "ISC", 25 | "devDependencies": { 26 | "chalk": "^1.1.3", 27 | "mysql": "^2.13.0", 28 | "sequelize": "^3.30.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | # The MIT License (MIT) 3 | 4 | Copyright (c) 2013 [Ben Nadel][1] 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | [1]: http://www.bennadel.com -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // I am the key that gets applied to the Sequelize instance in order to make sure that 4 | // the same plugin doesn't get applied more than once. 5 | var trackingKey = "__comment_plugin__"; 6 | 7 | 8 | // --- 9 | // PUBLIC METHODS. 10 | // --- 11 | 12 | 13 | // I apply the Comment plug-in to the given sequelize instance. 14 | module.exports = function CommentPlugin( sequelizeDialect, settings ) { 15 | 16 | // Make sure we're not trying to apply the plug-in more than once to this instance. 17 | if ( hasPlugin( sequelizeDialect ) ) { 18 | 19 | return( sequelizeDialect ); 20 | 21 | } else { 22 | 23 | recordPlugin( sequelizeDialect ); 24 | 25 | } 26 | 27 | settings = ensureSettings( settings ); 28 | 29 | // Since the method signatures are not the same for all of the generator methods that 30 | // we're targeting, we have to explicitly define which argument offset includes the 31 | // options hash (that will contain our "comment" property). 32 | // -- 33 | // - selectQuery(tableName, options, model) 34 | // - insertQuery(table, valueHash, modelAttributes, options) 35 | // - updateQuery(tableName, attrValueHash, where, options, attributes) 36 | // - deleteQuery(tableName, where, options) 37 | // - bulkInsertQuery(tableName, attrValueHashes, options, rawAttributes) 38 | var methods = [ 39 | { 40 | name: "selectQuery", 41 | optionsArgument: 1 42 | }, 43 | { 44 | name: "insertQuery", 45 | optionsArgument: 3 46 | }, 47 | { 48 | name: "updateQuery", 49 | optionsArgument: 3 50 | }, 51 | { 52 | name: "deleteQuery", 53 | optionsArgument: 2 54 | }, 55 | { 56 | name: "bulkInsertQuery", 57 | optionsArgument: 2 58 | } 59 | ]; 60 | 61 | var queryGenerator = sequelizeDialect.getQueryInterface().QueryGenerator; 62 | 63 | // Proxy each query generator method. The proxy will invoke the underlying / original 64 | // method and then prefix the comment (if the option exists) before passing on the 65 | // resultant SQL fragment. 66 | methods.forEach( 67 | function iterator( method ) { 68 | 69 | var originalGeneratorMethod = queryGenerator[ method.name ]; 70 | 71 | queryGenerator[ method.name ] = function proxyMethod( /* arguments */ ) { 72 | 73 | var baseFragment = originalGeneratorMethod.apply( this, arguments ); 74 | var options = arguments[ method.optionsArgument ]; 75 | 76 | var fragment = ( options && options.comment ) 77 | ? prependComment( options.comment, getDelimiter( settings ), baseFragment ) 78 | : baseFragment 79 | ; 80 | 81 | return( fragment ); 82 | 83 | }; 84 | 85 | } 86 | ); 87 | 88 | return( sequelizeDialect ); 89 | 90 | }; 91 | 92 | 93 | // --- 94 | // PRIVATE METHODS. 95 | // --- 96 | 97 | 98 | // I ensure that the settings object exists and contains expected values. 99 | function ensureSettings( settings ) { 100 | 101 | settings = ( settings || {} ); 102 | 103 | if ( ! settings.hasOwnProperty( "newline" ) ) { 104 | 105 | settings.newline = true; 106 | 107 | } 108 | 109 | return( settings ); 110 | 111 | } 112 | 113 | 114 | // I return the delimiter that separates the comment from the SQL fragment. 115 | function getDelimiter( settings ) { 116 | 117 | return( ( settings.newline && "\n" ) || " " ); 118 | 119 | } 120 | 121 | 122 | // I determine if the given Sequelize instance already has the plug-in applied to it. 123 | function hasPlugin( sequelizeDialect ) { 124 | 125 | return( !! sequelizeDialect[ trackingKey ] ); 126 | 127 | } 128 | 129 | 130 | // I prepare and prepend the given comment to the given SQL fragment. 131 | function prependComment( comment, delimiter, fragment ) { 132 | 133 | var parts = [ 134 | "/* ", 135 | sanitizeComment( comment ), 136 | " */", 137 | delimiter, 138 | fragment 139 | ]; 140 | 141 | return( parts.join( "" ) ); 142 | 143 | } 144 | 145 | 146 | // I record the fact that the plug-in is being applied to the given Sequelize instance. 147 | function recordPlugin( sequelizeDialect ) { 148 | 149 | sequelizeDialect[ trackingKey ] = true; 150 | 151 | } 152 | 153 | 154 | // I sanitize the given comment value, ensuring that it won't break the syntax of the 155 | // SQL comment in which it will be contained. 156 | function sanitizeComment( comment ) { 157 | 158 | var sanitizedComment = String( comment ) 159 | .replace( /[\r\n]+/g, " " ) // Strip new lines. 160 | .replace( /\/\*|\*\\/g, " " ) // Strip comments. 161 | ; 162 | 163 | return( sanitizedComment ); 164 | 165 | } 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Sequelize Comment Plug-in 3 | 4 | by [Ben Nadel][1] (on [Google+][2]) 5 | 6 | **Version: 1.0.1** 7 | 8 | This is a Sequelize instance plug-in that will prepend a SQL comment to the generated SQL 9 | statements based on the `{options.comment}` property. These comments do not affect the 10 | execution of the SQL; but, they do provide critical debugging information for database 11 | administrators who can see these comments in the `general_log` and `slow_log` records 12 | (which will help people who are unfamiliar with the code quickly locate and debug 13 | problematic queries). 14 | 15 | The following query types are supported: 16 | 17 | * `SELECT` Query. 18 | * `INSERT` Query. 19 | * `UPDATE` Query. 20 | * `DELETE` Query. 21 | * Bulk Insert Query. 22 | 23 | The plug-in must be applied to an instance of Sequelize, not to the root library: 24 | 25 | ```js 26 | var commentPlugin = require( "sequelize-comment" ); 27 | var Sequelize = require( "sequelize" ); 28 | 29 | var sequelize = new Sequelize( /* configuration */ ); 30 | 31 | // Apply the plug-in to the Sequelize dialect instance. 32 | commentPlugin( sequelize ); 33 | ``` 34 | 35 | Once applied, you can then pass a `comment` option with your basic CRUD queries: 36 | 37 | ```js 38 | // Example comment usage: 39 | Model.findAll( 40 | { 41 | where: { 42 | typeID: 4 43 | }, 44 | comment: "DEBUG: Running a SELECT command." 45 | } 46 | ); 47 | 48 | // Example comment usage: 49 | Model.create( 50 | { 51 | id: 1, 52 | value: "Hello world" 53 | }, 54 | { 55 | comment: "DEBUG: Running an INSERT command." 56 | } 57 | ); 58 | 59 | // Example comment usage: 60 | Model.update( 61 | { 62 | value: "Good morning world" 63 | }, 64 | { 65 | where: { 66 | value: "Hello world" 67 | }, 68 | comment: "DEBUG: Running an UPDATE command." 69 | } 70 | ); 71 | ``` 72 | 73 | These Sequelize methods will generate SQL fragments that then include (starts with) 74 | comments that look like this: 75 | 76 | ```sql 77 | /* DEBUG: Running a SELECT command. */ SELECT ... 78 | /* DEBUG: Running an INSERT command. */ INSERT INTO ... 79 | /* DEBUG: Running an UPDATE command. */ UPDATE ... 80 | ``` 81 | 82 | Personally, I like to include the name of the calling component and method in the DEBUG 83 | comment. This way, the people who are debugging the problematic queries that show up in 84 | the slow logs will have some indication as to where the originating file is located: 85 | 86 | ```sql 87 | /* DEBUG: userRepository.getUser(). */ ... 88 | /* DEBUG: loginRepository.logFailedAuthentication(). */ ... 89 | /* DEBUG: activityGateway.generateActivityReport(). */ ... 90 | ``` 91 | 92 | This type of comment also allows `slow_log` queries and `general_log` queries to be more 93 | easily aggregated due to the concrete statement prefix. 94 | 95 | _**Read More**: [Putting DEBUG Comments In Your SQL Statements Makes Debugging Performance Problems Easier][3]_ 96 | 97 | By default, the delimiter between the comment and the actual SQL command is a newline 98 | `\n` character. However, you can change that to be a space ` ` if you apply the 99 | CommentPlugin() with additional settings: 100 | 101 | ```js 102 | var commentPlugin = require( "sequelize-comment" ); 103 | var Sequelize = require( "sequelize" ); 104 | 105 | var sequelize = new Sequelize( /* configuration */ ); 106 | 107 | // Disable the newline delimiter -- will use space instead. 108 | commentPlugin( sequelize, { newline: false } ); 109 | ``` 110 | 111 | ## Technical Approach 112 | 113 | This plug-in works by grabbing the underlying QueryGenerator reference (of your Sequelize 114 | dialect instance) and injecting methods that proxy the following SQL fragment generators: 115 | 116 | * `selectQuery()` 117 | * `insertQuery()` 118 | * `updateQuery()` 119 | * `deleteQuery()` 120 | * `bulkInsertQuery()` 121 | 122 | As such, this plug-in isn't tied to specific set of methods; but, rather, any method that 123 | uses any of the above fragment generators. 124 | 125 | _**Read More**: [Experiment: Putting DEBUG Comments In Your Sequelize-Generated Queries In Node.js][4]_ 126 | 127 | ## Tests 128 | 129 | You can run the tests using `npm run test`. The tests currently include an end-to-end 130 | test that requires a running database. The tests enable the `general_log`, run queries, 131 | and then inspect the `general_log` records in order to ensure that the `comment` value 132 | shows up in the expected log items. 133 | 134 | [1]: http://www.bennadel.com 135 | [2]: https://plus.google.com/108976367067760160494?rel=author 136 | [3]: https://www.bennadel.com/blog/3058-putting-debug-comments-in-your-sql-statements-makes-debugging-performance-problems-easier.htm 137 | [4]: https://www.bennadel.com/blog/3265-experiment-putting-debug-comments-in-your-sequelize-generated-queries-in-node-js.htm 138 | -------------------------------------------------------------------------------- /test/e2e.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Require the core node modules. 4 | var assert = require( "assert" ); 5 | var chalk = require( "chalk" ); 6 | var mysql = require( "mysql" ); 7 | var Sequelize = require( "sequelize" ); 8 | 9 | // Require the application modules. 10 | var commentPlugin = require( "../" ); 11 | 12 | // ----------------------------------------------------------------------------------- // 13 | // ----------------------------------------------------------------------------------- // 14 | 15 | console.log( chalk.red.bold( "CAUTION:" ), chalk.red( "These tests turn on and then turn off the GLOBAL general_log. This may leave your GLOBAL database logging in an unexecpted state." ) ); 16 | console.log( chalk.red( "> SET GLOBAL general_log = 'ON';" ) ); 17 | console.log( chalk.red( "> SET GLOBAL log_output = 'TABLE';" ) ); 18 | console.log( chalk.red( ">", chalk.italic( "... then... " ) ) ); 19 | console.log( chalk.red( "> SET GLOBAL general_log = 'OFF';" ) ); 20 | 21 | // ----------------------------------------------------------------------------------- // 22 | // ----------------------------------------------------------------------------------- // 23 | 24 | // Setup our Sequelize instance. 25 | var sequelize = new Sequelize( 26 | ( process.env.DATABASE || "testing" ), 27 | ( process.env.USERNAME || "root" ), 28 | ( process.env.PASSWORD || "" ), 29 | { 30 | host: "localhost", 31 | dialect: "mysql", 32 | dialectOptions: { 33 | multipleStatements: true 34 | } 35 | } 36 | ); 37 | 38 | // Apply the comment plugin (which we'll confirm in the general_log). 39 | commentPlugin( sequelize, { newline: false } ); 40 | 41 | // Define the ORM (Object-Relational Mapping) model that we'll use for testing the SQL 42 | // commands. 43 | var ValueModel = sequelize.define( 44 | "ValueModel", 45 | { 46 | id: { 47 | type: Sequelize.DataTypes.INTEGER(0).UNSIGNED, 48 | allowNull: false, 49 | primaryKey: true, 50 | autoIncrement: true 51 | }, 52 | value: { 53 | type: Sequelize.DataTypes.STRING, 54 | allowNull: false 55 | } 56 | }, 57 | { 58 | tableName: "__sequelize_comment_plugin_test__", 59 | createdAt: false, 60 | updatedAt: false 61 | } 62 | ); 63 | 64 | var queryGenerator = sequelize.getQueryInterface().QueryGenerator; 65 | var startedAt = new Date().toISOString(); 66 | 67 | Promise.resolve() 68 | .then( 69 | function dropTestTable() { 70 | 71 | var promise = queryGenerator.dropTableQuery( ValueModel.getTableName() ); 72 | 73 | return( promise ); 74 | 75 | } 76 | ) 77 | .then( 78 | function createTestTable() { 79 | 80 | var promise = sequelize.query( 81 | queryGenerator.createTableQuery( 82 | ValueModel.getTableName(), 83 | queryGenerator.attributesToSQL( ValueModel.rawAttributes ), 84 | {} 85 | ) 86 | ); 87 | 88 | return( promise ); 89 | 90 | } 91 | ) 92 | .then( 93 | function enableGeneralLog() { 94 | 95 | var promise = sequelize.query( "SET GLOBAL general_log = 'ON'; SET GLOBAL log_output = 'TABLE';" ); 96 | 97 | return( promise ); 98 | 99 | } 100 | ) 101 | .then( 102 | function runBulkCreate() { 103 | 104 | var promise = ValueModel.bulkCreate( 105 | [ 106 | { 107 | id: 1, 108 | value: "One" 109 | }, 110 | { 111 | id: 2, 112 | value: "Two" 113 | } 114 | ], 115 | { 116 | comment: "BULK CREATE" 117 | } 118 | ); 119 | 120 | return( promise ); 121 | 122 | } 123 | ) 124 | .then( 125 | function runInsert() { 126 | 127 | var promise = ValueModel.create( 128 | { 129 | id: 3, 130 | value: "Three" 131 | }, 132 | { 133 | comment: "INSERT" 134 | } 135 | ); 136 | 137 | return( promise ); 138 | 139 | } 140 | ) 141 | .then( 142 | function runUpdate() { 143 | 144 | var promise = ValueModel.update( 145 | { 146 | value: "Three (updated)" 147 | }, 148 | { 149 | where: { 150 | id: 3 151 | }, 152 | comment: "UPDATE" 153 | } 154 | ); 155 | 156 | return( promise ); 157 | 158 | } 159 | ) 160 | .then( 161 | function runDelete() { 162 | 163 | var promise = ValueModel.destroy({ 164 | where: { 165 | id: 3 166 | }, 167 | comment: "DELETE" 168 | }); 169 | 170 | return( promise ); 171 | 172 | } 173 | ) 174 | .then( 175 | function runSelect() { 176 | 177 | var promise = ValueModel.findAll({ 178 | comment: "SELECT" 179 | }); 180 | 181 | return( promise ); 182 | 183 | } 184 | ) 185 | .then( 186 | function assertSelectResults( results ) { 187 | 188 | assert.ok( results.length === 2 ); 189 | assert.ok( results[ 0 ].id === 1 ); 190 | assert.ok( results[ 0 ].value === "One" ); 191 | assert.ok( results[ 1 ].id === 2 ); 192 | assert.ok( results[ 1 ].value === "Two" ); 193 | 194 | } 195 | ) 196 | .then( 197 | function disableGeneralLog() { 198 | 199 | var promise = sequelize.query( "SET GLOBAL general_log = 'OFF';" ); 200 | 201 | return( promise ); 202 | 203 | } 204 | ) 205 | .then( 206 | function dropTestTable() { 207 | 208 | var promise = sequelize.query( queryGenerator.dropTableQuery( ValueModel.getTableName() ) ); 209 | 210 | return( promise ); 211 | 212 | } 213 | ) 214 | .then( 215 | function getGeneralLogResults() { 216 | 217 | var promise = sequelize.query( 218 | "SELECT * FROM mysql.general_log WHERE event_time >= ? AND argument LIKE ? ORDER BY event_time ASC", 219 | { 220 | replacements: [ 221 | startedAt, 222 | ( "%" + ValueModel.getTableName() + "%" ) 223 | ], 224 | raw: true 225 | } 226 | ); 227 | 228 | return( promise ); 229 | 230 | } 231 | ) 232 | .then( 233 | function assertGeneralLogResults( results ) { 234 | 235 | var records = results[ 0 ]; 236 | var offset = 0; 237 | 238 | console.log( "Analyzing general_log...." ); 239 | assert.ok( records[ offset++ ].argument.toString().indexOf( "/* BULK CREATE */" ) === 0 ); 240 | assert.ok( records[ offset++ ].argument.toString().indexOf( "/* INSERT */" ) === 0 ); 241 | assert.ok( records[ offset++ ].argument.toString().indexOf( "/* UPDATE */" ) === 0 ); 242 | assert.ok( records[ offset++ ].argument.toString().indexOf( "/* DELETE */" ) === 0 ); 243 | assert.ok( records[ offset++ ].argument.toString().indexOf( "/* SELECT */" ) === 0 ); 244 | 245 | console.log( chalk.green.bold( "TESTS PASS" ) ); 246 | 247 | } 248 | ) 249 | .catch( 250 | function handleTestFailure( error ) { 251 | 252 | console.log( chalk.red.bold( "TESTS FAIL" ) ); 253 | console.log( error ); 254 | 255 | } 256 | ) 257 | .then( 258 | function closeDatabaseConnections() { 259 | 260 | sequelize.close(); 261 | 262 | }, 263 | function closeDatabaseConnections( error ) { 264 | 265 | sequelize.close(); 266 | 267 | } 268 | ) 269 | ; 270 | --------------------------------------------------------------------------------