diff --git a/box.json b/box.json index d8ec19e7..09c2ee58 100644 --- a/box.json +++ b/box.json @@ -50,6 +50,7 @@ "yarn.lock" ], "testbox":{ - "runner":"http://localhost:7777/tests/runner.cfm" + "runner":"http://localhost:7777/tests/runner.cfm", + "verbose": false } } diff --git a/models/Query/QueryBuilder.cfc b/models/Query/QueryBuilder.cfc index c6babda1..f7d07a08 100644 --- a/models/Query/QueryBuilder.cfc +++ b/models/Query/QueryBuilder.cfc @@ -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; } @@ -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; } @@ -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" ); } /** diff --git a/tests/specs/Query/Abstract/QueryDebuggingSpec.cfc b/tests/specs/Query/Abstract/QueryDebuggingSpec.cfc new file mode 100644 index 00000000..030007da --- /dev/null +++ b/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; + } + +}