Skip to content

Commit

Permalink
feat(QueryUtils): Auto boolean casting
Browse files Browse the repository at this point in the history
Grammars will be able to influence the `cfsqltype` and value when passing
in a literal boolean value as a binding.  Postgres and SQLite have
boolean support, so they will keep the literal boolean value and use a
`cfsqltype` of `CF_SQL_OTHER`. SQL Server uses `CF_SQL_BIT`, Oracle users
`CF_SQL_NUMERIC`, and MySQL uses `CF_SQL_TINYINT` — all of these will
convert literal boolean values to either 1 or 0.  This behavior is skipped
when providing a custom `cfsqltype`. Custom grammars can implement the
`getBooleanSqlType` and `convertBooleanValue` methods to customize this
behavior.

BREAKING CHANGE: Previously, literal boolean values would be converted
and treated as `CF_SQL_VARCHAR`. Now they will be converted depending
on the grammar.  Additionally, attempting to change the grammar with
any bindings currently configured will throw an exception.  This is because
the bindings are converted via the grammar when added to the builder
and cannot be changed retroactively when setting a new grammar. Set
the grammar first before configuring the query to avoid this exception.
  • Loading branch information
elpete committed Nov 13, 2024
1 parent 1d1bd1e commit ed33bcf
Show file tree
Hide file tree
Showing 16 changed files with 501 additions and 64 deletions.
15 changes: 15 additions & 0 deletions models/Grammars/BaseGrammar.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -1509,6 +1509,21 @@ component displayname="Grammar" accessors="true" singleton {
return "TINYINT(1)";
}

public string function getBooleanSqlType() {
return "CF_SQL_TINYINT";
}

public any function convertBooleanValue( required any value ) {
return arguments.value ? 1 : 0;
}

function convertToBooleanType( any value ) {
return {
"value": isNull( value ) ? javacast( "null", "" ) : convertBooleanValue( value ),
"cfsqltype": getBooleanSqlType()
};
}

function typeChar( column ) {
return "CHAR(#column.getLength()#)";
}
Expand Down
4 changes: 4 additions & 0 deletions models/Grammars/OracleGrammar.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,10 @@ component extends="qb.models.Grammars.BaseGrammar" singleton {
return "NUMBER(1, 0)";
}

public string function getBooleanSqlType() {
return "CF_SQL_NUMERIC";
}

function typeDatetime( column ) {
return typeTimestamp( column );
}
Expand Down
8 changes: 8 additions & 0 deletions models/Grammars/PostgresGrammar.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,14 @@ component extends="qb.models.Grammars.BaseGrammar" singleton {
return "BOOLEAN";
}

public string function getBooleanSqlType() {
return "CF_SQL_OTHER";
}

public any function convertBooleanValue( required any value ) {
return !!arguments.value;
}

function typeDatetime( column ) {
return typeTimestamp( column );
}
Expand Down
8 changes: 8 additions & 0 deletions models/Grammars/SQLiteGrammar.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,14 @@ component extends="qb.models.Grammars.BaseGrammar" singleton {
return "BOOLEAN";
}

public string function getBooleanSqlType() {
return "CF_SQL_OTHER";
}

public any function convertBooleanValue( required any value ) {
return !!arguments.value;
}

function typeChar( column ) {
return "VARCHAR(#column.getLength()#)";
}
Expand Down
4 changes: 4 additions & 0 deletions models/Grammars/SqlServerGrammar.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,10 @@ component extends="qb.models.Grammars.BaseGrammar" singleton accessors="true" {
return "BIT";
}

public string function getBooleanSqlType() {
return "CF_SQL_BIT";
}

function typeChar( column ) {
return "NCHAR(#column.getLength()#)";
}
Expand Down
51 changes: 33 additions & 18 deletions models/Query/QueryBuilder.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,7 @@ component displayname="QueryBuilder" accessors="true" {
if ( !arrayIsEmpty( arguments.bindings ) ) {
addBindings(
arguments.bindings.map( function( value ) {
return utils.extractBinding( value );
return utils.extractBinding( value, variables.grammar );
} ),
"from"
);
Expand All @@ -830,7 +830,7 @@ component displayname="QueryBuilder" accessors="true" {
if ( !arrayIsEmpty( arguments.bindings ) ) {
addBindings(
arguments.bindings.map( function( value ) {
return utils.extractBinding( value );
return utils.extractBinding( value, variables.grammar );
} ),
"from"
);
Expand Down Expand Up @@ -1706,7 +1706,7 @@ component displayname="QueryBuilder" accessors="true" {
);

if ( getUtils().isNotExpression( arguments.value ) ) {
addBindings( utils.extractBinding( arguments.value ), "where" );
addBindings( utils.extractBinding( arguments.value, variables.grammar ), "where" );
}

return this;
Expand Down Expand Up @@ -1812,7 +1812,7 @@ component displayname="QueryBuilder" accessors="true" {
var bindings = values
.filter( utils.isNotExpression )
.map( function( value ) {
return utils.extractBinding( value );
return utils.extractBinding( value, variables.grammar );
} );

addBindings( bindings, "where" );
Expand Down Expand Up @@ -1880,7 +1880,7 @@ component displayname="QueryBuilder" accessors="true" {
public QueryBuilder function whereRaw( required string sql, array whereBindings = [], string combinator = "and" ) {
addBindings(
whereBindings.map( function( binding ) {
return utils.extractBinding( binding );
return utils.extractBinding( binding, variables.grammar );
} ),
"where"
);
Expand Down Expand Up @@ -2102,8 +2102,8 @@ component displayname="QueryBuilder" accessors="true" {
callback( arguments.end );
}

addBindings( utils.extractBinding( arguments.start ), "where" );
addBindings( utils.extractBinding( arguments.end ), "where" );
addBindings( utils.extractBinding( arguments.start, variables.grammar ), "where" );
addBindings( utils.extractBinding( arguments.end, variables.grammar ), "where" );

if (
isStruct( arguments.start ) && !structKeyExists( arguments.start, "isBuilder" ) && arguments.start.keyExists(
Expand Down Expand Up @@ -2231,7 +2231,7 @@ component displayname="QueryBuilder" accessors="true" {
arguments.column
.getBindings()
.map( function( binding ) {
return utils.extractBinding( binding );
return utils.extractBinding( binding, variables.grammar );
} ),
"having"
);
Expand Down Expand Up @@ -2261,14 +2261,14 @@ component displayname="QueryBuilder" accessors="true" {
arguments.column
.getBindings()
.map( function( binding ) {
return utils.extractBinding( binding );
return utils.extractBinding( binding, variables.grammar );
} ),
"having"
);
}

if ( getUtils().isNotExpression( arguments.value ) ) {
addBindings( utils.extractBinding( arguments.value ), "having" );
addBindings( utils.extractBinding( arguments.value, variables.grammar ), "having" );
}

return this;
Expand Down Expand Up @@ -2399,7 +2399,7 @@ component displayname="QueryBuilder" accessors="true" {
column
.getBindings()
.map( function( value ) {
return variables.utils.extractBinding( arguments.value );
return variables.utils.extractBinding( arguments.value, variables.grammar );
} ),
"orderBy"
);
Expand Down Expand Up @@ -2524,7 +2524,7 @@ component displayname="QueryBuilder" accessors="true" {
if ( !arrayIsEmpty( arguments.bindings ) ) {
addBindings(
arguments.bindings.map( function( value ) {
return variables.utils.extractBinding( arguments.value );
return variables.utils.extractBinding( arguments.value, variables.grammar );
} ),
"orderBy"
);
Expand Down Expand Up @@ -2957,7 +2957,8 @@ component displayname="QueryBuilder" accessors="true" {
var newBindings = arguments.values.map( function( value ) {
return columns.map( function( column ) {
return getUtils().extractBinding(
value.keyExists( column.original ) ? value[ column.original ] : javacast( "null", "" )
value.keyExists( column.original ) ? value[ column.original ] : javacast( "null", "" ),
variables.grammar
);
} );
} );
Expand Down Expand Up @@ -3073,7 +3074,8 @@ component displayname="QueryBuilder" accessors="true" {
var newBindings = arguments.values.map( function( value ) {
return columns.map( function( column ) {
return getUtils().extractBinding(
value.keyExists( column.original ) ? value[ column.original ] : javacast( "null", "" )
value.keyExists( column.original ) ? value[ column.original ] : javacast( "null", "" ),
variables.grammar
);
} );
} );
Expand Down Expand Up @@ -3160,7 +3162,7 @@ component displayname="QueryBuilder" accessors="true" {
arguments.values[ column.original ] = value;
addBindings( value.getBindings(), "update" );
} else if ( !getUtils().isExpression( value ) ) {
addBindings( getUtils().extractBinding( value ), "update" );
addBindings( getUtils().extractBinding( value, variables.grammar ), "update" );
}
}

Expand Down Expand Up @@ -3295,7 +3297,8 @@ component displayname="QueryBuilder" accessors="true" {
newInsertBindings = arguments.values.map( function( value ) {
return columns.map( function( column ) {
return getUtils().extractBinding(
value.keyExists( column.original ) ? value[ column.original ] : javacast( "null", "" )
value.keyExists( column.original ) ? value[ column.original ] : javacast( "null", "" ),
variables.grammar
);
} );
} );
Expand Down Expand Up @@ -3635,7 +3638,7 @@ component displayname="QueryBuilder" accessors="true" {
*/
public boolean function existsOrFail( struct options = {}, any errorMessage ) {
if ( !this.exists( arguments.options ) ) {
param arguments.errorMessage = "No rows found with constraints [#variables.utils.serializeBindings( this.getBindings() )#]";
param arguments.errorMessage = "No rows found with constraints [#variables.utils.serializeBindings( this.getBindings(), variables.grammar )#]";
throw( type = "RecordNotFound", message = arguments.errorMessage );
}
return true;
Expand Down Expand Up @@ -3699,7 +3702,7 @@ component displayname="QueryBuilder" accessors="true" {
public any function firstOrFail( any errorMessage, struct options = {} ) {
var result = first( arguments.options );
if ( structIsEmpty( result ) ) {
param arguments.errorMessage = "No rows found with constraints [#variables.utils.serializeBindings( this.getBindings() )#]";
param arguments.errorMessage = "No rows found with constraints [#variables.utils.serializeBindings( this.getBindings(), variables.grammar )#]";
if ( isClosure( arguments.errorMessage ) || isCustomFunction( arguments.errorMessage ) ) {
arguments.errorMessage = arguments.errorMessage( this );
}
Expand Down Expand Up @@ -4416,4 +4419,16 @@ component displayname="QueryBuilder" accessors="true" {
return isSimpleValue( column ) ? variables.columnFormatter( column ) : column;
}

public QueryBuilder function setGrammar( required BaseGrammar grammar ) {
if ( !this.getBindings().isEmpty() ) {
throw(
type = "QBSetGrammarWithBindingsError",
message = "You cannot switch grammars after adding bindings. Please set the grammar before adding bindings.",
detail = "The easiest way to fix this error is to set the grammar before any other actions on the query builder."
);
}
variables.grammar = arguments.grammar;
return this;
}

}
38 changes: 32 additions & 6 deletions models/Query/QueryUtils.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ component singleton displayname="QueryUtils" accessors="true" {
*
* @return any
*/
public any function extractBinding( any value ) {
public any function extractBinding( any value, required BaseGrammar grammar ) {
if ( isNull( arguments.value ) ) {
return { "cfsqltype": "CF_SQL_VARCHAR", "value": "", "null": true };
}
Expand All @@ -104,7 +104,11 @@ component singleton displayname="QueryUtils" accessors="true" {
}

if ( !structKeyExists( binding, "cfsqltype" ) ) {
binding.cfsqltype = inferSqlType( binding.value );
if ( checkIsActuallyBoolean( binding.value ) ) {
structAppend( binding, arguments.grammar.convertToBooleanType( binding.value ), true );
} else {
binding.cfsqltype = inferSqlType( binding.value, arguments.grammar );
}
}

if ( binding.cfsqltype == "CF_SQL_TIMESTAMP" ) {
Expand Down Expand Up @@ -174,7 +178,7 @@ component singleton displayname="QueryUtils" accessors="true" {
*
* @return string
*/
public string function inferSqlType( any value ) {
public string function inferSqlType( any value, required BaseGrammar grammar ) {
if ( isNull( arguments.value ) ) {
return "CF_SQL_VARCHAR";
}
Expand All @@ -183,7 +187,7 @@ component singleton displayname="QueryUtils" accessors="true" {
return arraySame(
value,
function( val ) {
return inferSqlType( val );
return inferSqlType( val, grammar );
},
"CF_SQL_VARCHAR"
);
Expand All @@ -201,6 +205,10 @@ component singleton displayname="QueryUtils" accessors="true" {
return "CF_SQL_TIMESTAMP";
}

if ( checkIsActuallyBoolean( value ) ) {
return arguments.grammar.getBooleanSqlType();
}

return "CF_SQL_VARCHAR";
}

Expand Down Expand Up @@ -514,6 +522,24 @@ component singleton displayname="QueryUtils" accessors="true" {
}
}

/**
* Detects if value is a Boolean based on className
*
* @value The value
*
* @return boolean
*/
private boolean function checkIsActuallyBoolean( any value ) {
if ( isNull( arguments.value ) ) {
return false;
}

return arrayContainsNoCase(
[ "CFBoolean", "Boolean" ],
listLast( toString( getMetadata( arguments.value ) ), "." )
);
}


/** Utility functions to assist with preventing duplicate joins. Adapted from cflib.org **/
/**
Expand Down Expand Up @@ -615,10 +641,10 @@ component singleton displayname="QueryUtils" accessors="true" {
return true;
}

public string function serializeBindings( required array bindings ) {
public string function serializeBindings( required array bindings, required BaseGrammar grammar ) {
return serializeJSON(
arguments.bindings.map( function( binding ) {
var newBinding = extractBinding( duplicate( binding ) );
var newBinding = extractBinding( duplicate( binding ), grammar );
if ( isBinary( newBinding.value ) ) {
newBinding.value = toBase64( newBinding.value );
}
Expand Down
2 changes: 1 addition & 1 deletion server.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
}
},
"app":{
"cfengine":"boxlang@be"
"cfengine":"lucee@^5"
},
"directoryBrowsing":"true",
"JVM":{
Expand Down
Loading

0 comments on commit ed33bcf

Please sign in to comment.