Skip to content

Commit

Permalink
feat(QueryBuilder): Generate SQL strings with bindings
Browse files Browse the repository at this point in the history
Instead of just generating SQL strings with placeholders
(`?`), generate a SQL string with the bindings inline.
This is useful for debugging. The bindings show as a
cfqueryparam struct. The generated SQL string
is not valid SQL so there is no worry about accidentally
executing it.
  • Loading branch information
elpete committed Dec 20, 2019
1 parent adce834 commit 2c84afb
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 17 deletions.
3 changes: 2 additions & 1 deletion box.json
Expand Up @@ -50,6 +50,7 @@
"yarn.lock"
],
"testbox":{
"runner":"http://localhost:7777/tests/runner.cfm"
"runner":"http://localhost:7777/tests/runner.cfm",
"verbose": false
}
}
49 changes: 33 additions & 16 deletions models/Query/QueryBuilder.cfc
Expand Up @@ -1737,7 +1737,7 @@ component displayname="QueryBuilder" accessors="true" {
* @return qb.models.Query.QueryBuilder
*/
public QueryBuilder function tap( required callback ) {
callback( duplicate( this ) );
callback( clone( this ) );
return this;
}

Expand Down Expand Up @@ -2343,19 +2343,19 @@ component displayname="QueryBuilder" accessors="true" {
public qb.models.Query.QueryBuilder function clone() {
var clonedQuery = newQuery();
clonedQuery.setDistinct( this.getDistinct() );
clonedQuery.setAggregate( this.getAggregate() );
clonedQuery.setColumns( this.getColumns() );
clonedQuery.setFrom( this.getFrom() );
clonedQuery.setJoins( this.getJoins() );
clonedQuery.setWheres( this.getWheres() );
clonedQuery.setGroups( this.getGroups() );
clonedQuery.setHavings( this.getHavings() );
clonedQuery.setUnions( this.getUnions() );
clonedQuery.setOrders( this.getOrders() );
clonedQuery.setCommonTables( this.getCommonTables() );
clonedQuery.setLimitValue( this.getLimitValue() );
clonedQuery.setOffsetValue( this.getOffsetValue() );
clonedQuery.setReturning( this.getReturning() );
clonedQuery.setAggregate( duplicate( this.getAggregate() ) );
clonedQuery.setColumns( duplicate( this.getColumns() ) );
clonedQuery.setFrom( duplicate( this.getFrom() ) );
clonedQuery.setJoins( duplicate( this.getJoins() ) );
clonedQuery.setWheres( duplicate( this.getWheres() ) );
clonedQuery.setGroups( duplicate( this.getGroups() ) );
clonedQuery.setHavings( duplicate( this.getHavings() ) );
clonedQuery.setUnions( duplicate( this.getUnions() ) );
clonedQuery.setOrders( duplicate( this.getOrders() ) );
clonedQuery.setCommonTables( duplicate( this.getCommonTables() ) );
clonedQuery.setLimitValue( duplicate( this.getLimitValue() ) );
clonedQuery.setOffsetValue( duplicate( this.getOffsetValue() ) );
clonedQuery.setReturning( duplicate( this.getReturning() ) );
clonedQuery.mergeBindings( this );
return clonedQuery;
}
Expand All @@ -2377,8 +2377,25 @@ component displayname="QueryBuilder" accessors="true" {
*
* @return string
*/
public string function toSQL() {
return grammar.compileSelect( this );
public string function toSQL( boolean showBindings = false ) {
var sql = grammar.compileSelect( this );

if ( ! showBindings ) {
return sql;
}

var bindings = getBindings();
var index = 1;
return replace( sql, "?", function( pattern, position, originalString ) {
var thisBinding = bindings[ index ];
var orderedBinding = structNew( "ordered" );
for ( var type in [ "value", "cfsqltype", "null" ] ) {
orderedBinding[ type ] = thisBinding[ type ];
}
var stringifiedBinding = serializeJSON( orderedBinding );
index++;
return stringifiedBinding;
}, "all" );
}

/**
Expand Down
48 changes: 48 additions & 0 deletions tests/specs/Query/Abstract/QueryDebuggingSpec.cfc
@@ -0,0 +1,48 @@
component extends="testbox.system.BaseSpec" {

function run() {
describe( "query debugging", function() {
it( "can output the configured sql with placeholders for the bindings", function() {
var query = getBuilder()
.from( "users" )
.join( "logins", function( j ) {
j.on( "users.id", "logins.user_id" )
.where( "logins.created_date", ">", "01 Jun 2019" );
} )
.whereIn( "users.type", [ "admin", "manager" ] )
.whereNotNull( "active" )
.orderBy( "logins.created_date", "desc" );

expect( query.toSQL() ).toBe(
'SELECT * FROM "users" INNER JOIN "logins" ON "users"."id" = "logins"."user_id" AND "logins"."created_date" > ? WHERE "users"."type" IN (?, ?) AND "active" IS NOT NULL ORDER BY "logins"."created_date" DESC'
);
} );

it( "can output the configured sql with the bindings substituted in", function() {
var query = getBuilder()
.from( "users" )
.join( "logins", function( j ) {
j.on( "users.id", "logins.user_id" )
.where( "logins.created_date", ">", "01 Jun 2019" );
} )
.whereIn( "users.type", [ "admin", "manager" ] )
.whereNotNull( "active" )
.orderBy( "logins.created_date", "desc" );

expect( query.toSQL( showBindings = true ) ).toBe(
'SELECT * FROM "users" INNER JOIN "logins" ON "users"."id" = "logins"."user_id" AND "logins"."created_date" > {"value":"01 Jun 2019","cfsqltype":"CF_SQL_TIMESTAMP","null":false} WHERE "users"."type" IN ({"value":"admin","cfsqltype":"CF_SQL_VARCHAR","null":false}, {"value":"manager","cfsqltype":"CF_SQL_VARCHAR","null":false}) AND "active" IS NOT NULL ORDER BY "logins"."created_date" DESC'
);
} );
} );
}

private function getBuilder() {
var grammar = getMockBox()
.createMock( "qb.models.Grammars.BaseGrammar" )
.init();
var builder = getMockBox().createMock( "qb.models.Query.QueryBuilder" )
.init( grammar );
return builder;
}

}

0 comments on commit 2c84afb

Please sign in to comment.