Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion ModuleConfig.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ component {
settings = {
"defaultGrammar": "AutoDiscover@qb",
"defaultReturnFormat": "array",
"defaultReturnFormatOptions": {},
"preventDuplicateJoins": false,
"validateOperatorsAndCombinators": true,
"validateQueryExecuteReturnType": false,
"collectQueryLog": true,
"convertEmptyStringsToNull": true,
"validateQueryParamStructKeys": true,
Expand All @@ -29,7 +31,8 @@ component {
},
"shouldMaxRowsOverrideToAll": function( maxRows ) {
return maxRows <= 0;
}
},
"returnFormatters": {}
};

interceptorSettings = { "customInterceptionPoints": "preQBExecute,postQBExecute" };
Expand All @@ -55,15 +58,29 @@ component {
.initArg( name = "integerSQLType", value = settings.integerSQLType )
.initArg( name = "decimalSQLType", value = settings.decimalSQLType );

binder
.map( alias = "StructFormatter@qb", force = true )
.to( "qb.models.Query.Formatters.StructFormatter" )
.initArg( name = "utils", ref = "QueryUtils@qb" );

binder
.map( alias = "ReturnFormatterRegistry@qb", force = true )
.to( "qb.models.Query.ReturnFormatterRegistry" )
.initArg( name = "utils", ref = "QueryUtils@qb" )
.initArg( name = "returnFormatters", value = settings.returnFormatters );

binder
.map( alias = "QueryBuilder@qb", force = true )
.to( "qb.models.Query.QueryBuilder" )
.initArg( name = "grammar", ref = settings.defaultGrammar )
.initArg( name = "utils", ref = "QueryUtils@qb" )
.initArg( name = "returnFormatterRegistry", ref = "ReturnFormatterRegistry@qb" )
.initArg( name = "preventDuplicateJoins", value = settings.preventDuplicateJoins )
.initArg( name = "validateOperatorsAndCombinators", value = settings.validateOperatorsAndCombinators )
.initArg( name = "validateQueryExecuteReturnType", value = settings.validateQueryExecuteReturnType )
.initArg( name = "collectQueryLog", value = settings.collectQueryLog )
.initArg( name = "returnFormat", value = settings.defaultReturnFormat )
.initArg( name = "defaultReturnFormatOptions", value = settings.defaultReturnFormatOptions )
.initArg( name = "defaultOptions", value = settings.defaultOptions )
.initArg( name = "sqlCommenter", ref = "ColdBoxSQLCommenter@qb" )
.initArg( name = "shouldMaxRowsOverrideToAll", value = settings.shouldMaxRowsOverrideToAll );
Expand Down
14 changes: 14 additions & 0 deletions models/Query/Formatters/StructFormatter.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
component accessors="true" {

property name="utils";

public StructFormatter function init( any utils = new qb.models.Query.QueryUtils() ) {
variables.utils = arguments.utils;
return this;
}

public function toFormatter( struct options = {} ) {
return new qb.models.Query.Formatters.StructReturnFormatter( variables.utils, arguments.options );
}

}
27 changes: 27 additions & 0 deletions models/Query/Formatters/StructReturnFormatter.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
component accessors="true" {

property name="utils";
property name="options";

public StructReturnFormatter function init( any utils = new qb.models.Query.QueryUtils(), struct options = {} ) {
variables.utils = arguments.utils;
variables.options = arguments.options;
return this;
}

public struct function format( required any q ) {
if (
!variables.options.keyExists( "columnKey" ) || isNull( variables.options.columnKey ) || !len(
variables.options.columnKey
)
) {
throw(
type = "MissingColumnKey",
message = "A columnKey option is required for the [struct] return formatter."
);
}

return variables.utils.queryToStructOfStructs( arguments.q, variables.options.columnKey );
}

}
122 changes: 100 additions & 22 deletions models/Query/QueryBuilder.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ component displayname="QueryBuilder" accessors="true" {
*/
property name="returnFormat";

/**
* Registry used to resolve named return formats.
*/
property name="returnFormatterRegistry";

/**
* preventDuplicateJoins
* If true, QB will introspect all existing JoinClauses for a match before creating a new join clause.
Expand All @@ -38,6 +43,13 @@ component displayname="QueryBuilder" accessors="true" {
*/
property name="validateOperatorsAndCombinators";

/**
* If true, QB throws when queryExecute returntype options are passed.
* If false, QB strips those options so return formatters always receive a query.
* @default false
*/
property name="validateQueryExecuteReturnType";

/**
* paginationCollector
* A component or struct with a `generateWithResults` method.
Expand Down Expand Up @@ -289,11 +301,17 @@ component displayname="QueryBuilder" accessors="true" {
* Default: qb.models.Query.QueryUtils
* @returnFormat The closure (or string format shortcut) that modifies the query
* and is eventually returned to the caller. Default: 'array'
* @defaultReturnFormatOptions Options passed to the default return formatter.
* Default: {}
* @returnFormatterRegistry Registry used to resolve named return formatters.
* @preventDuplicateJoins Whether QB should ignore a .join() statement that matches an existing join
* Default: false
* @validateOperatorsAndCombinators
* Whether QB validates operators/combinators before storing clauses.
* Default: true
* @validateQueryExecuteReturnType
* Whether QB throws when queryExecute returntype options are passed.
* Default: false
* @paginationCollector The closure that processes the pagination result.
* Default: cbpaginator.models.Pagination
* @columnFormatter The closure that modifies each column before being
Expand All @@ -315,8 +333,11 @@ component displayname="QueryBuilder" accessors="true" {
grammar = new qb.models.Grammars.BaseGrammar(),
utils = new qb.models.Query.QueryUtils(),
returnFormat = "array",
struct defaultReturnFormatOptions = {},
returnFormatterRegistry,
preventDuplicateJoins = false,
validateOperatorsAndCombinators = true,
validateQueryExecuteReturnType = false,
paginationCollector = new cbpaginator.models.Pagination(),
columnFormatter,
parentQuery,
Expand All @@ -330,6 +351,11 @@ component displayname="QueryBuilder" accessors="true" {

setPreventDuplicateJoins( arguments.preventDuplicateJoins );
setValidateOperatorsAndCombinators( arguments.validateOperatorsAndCombinators );
setValidateQueryExecuteReturnType( arguments.validateQueryExecuteReturnType );
if ( isNull( arguments.returnFormatterRegistry ) ) {
arguments.returnFormatterRegistry = new qb.models.Query.ReturnFormatterRegistry( arguments.utils );
}
setReturnFormatterRegistry( arguments.returnFormatterRegistry );
if ( isNull( arguments.columnFormatter ) ) {
arguments.columnFormatter = function( column ) {
return column;
Expand All @@ -341,7 +367,7 @@ component displayname="QueryBuilder" accessors="true" {
setParentQuery( arguments.parentQuery );
}
param variables.defaultOptions = {};
setReturnFormat( arguments.returnFormat );
setReturnFormat( arguments.returnFormat, arguments.defaultReturnFormatOptions );
mergeDefaultOptions( arguments.defaultOptions );
setSqlCommenter( arguments.sqlCommenter );
setCollectQueryLog( arguments.collectQueryLog );
Expand Down Expand Up @@ -3543,6 +3569,24 @@ component displayname="QueryBuilder" accessors="true" {
} );
} );

if ( isStruct( arguments.update ) ) {
var updates = arguments.update;
updateArray.each( function( column ) {
if (
isNull( updates[ column.original ] ) ||
getUtils().isNotExpression( updates[ column.original ] )
) {
addBindings(
getUtils().extractBinding(
isNull( updates[ column.original ] ) ? javacast( "null", "" ) : updates[ column.original ],
variables.grammar
),
"where"
);
}
} );
}

if ( isClosure( arguments.deleteUnmatched ) || isCustomFunction( arguments.deleteUnmatched ) ) {
var deleteRestrictions = newQuery().setColumnFormatter( ( column ) => {
if ( listLen( column, "." ) > 1 ) {
Expand All @@ -3554,6 +3598,10 @@ component displayname="QueryBuilder" accessors="true" {
arguments.deleteUnmatched = deleteRestrictions;
}

if ( getUtils().isBuilder( arguments.deleteUnmatched ) ) {
addBindingsFromBuilder( arguments.deleteUnmatched );
}

columns.each( ( c ) => {
c.formatted = mapToColumnType( c.formatted );
} );
Expand Down Expand Up @@ -4289,18 +4337,35 @@ component displayname="QueryBuilder" accessors="true" {
}

if ( isQuery( q ) ) {
return returnFormat( q );
return applyReturnFormat( q );
}

if ( isArray( q ) ) {
return returnFormat( q );
return applyReturnFormat( q );
}

if ( !q.keyExists( "result" ) || !q.keyExists( "query" ) ) {
return returnFormat( q );
return applyReturnFormat( q );
}

return { result: q.result, query: returnFormat( q.query ) };
return { result: q.result, query: applyReturnFormat( q.query ) };
}

private any function applyReturnFormat( required any q ) {
var formatter = getReturnFormat();

if ( isClosure( formatter ) || isCustomFunction( formatter ) ) {
return formatter( arguments.q );
}

if ( structKeyExists( formatter, "format" ) ) {
return formatter.format( arguments.q );
}

throw(
type = "InvalidFormat",
message = "The configured return formatter must be a closure or a component with a format method."
);
}

/**
Expand All @@ -4315,6 +4380,7 @@ component displayname="QueryBuilder" accessors="true" {
*/
private any function runQuery( required string sql, struct options = {}, string returnObject = "query" ) {
structAppend( arguments.options, getDefaultOptions(), false );
normalizeQueryExecuteReturnTypeOptions( arguments.options );
var bindings = getBindings( except = getAggregate().isEmpty() ? [] : [ "select" ] );

var result = grammar.runQuery(
Expand Down Expand Up @@ -4367,10 +4433,12 @@ component displayname="QueryBuilder" accessors="true" {
grammar = getGrammar(),
utils = getUtils(),
returnFormat = getReturnFormat(),
returnFormatterRegistry = getReturnFormatterRegistry(),
paginationCollector = isNull( variables.paginationCollector ) ? javacast( "null", "" ) : variables.paginationCollector,
columnFormatter = isNull( getColumnFormatter() ) ? javacast( "null", "" ) : getColumnFormatter(),
parentQuery = isNull( getParentQuery() ) ? javacast( "null", "" ) : getParentQuery(),
defaultOptions = getDefaultOptions()
defaultOptions = getDefaultOptions(),
validateQueryExecuteReturnType = getValidateQueryExecuteReturnType()
);
}

Expand Down Expand Up @@ -4490,27 +4558,19 @@ component displayname="QueryBuilder" accessors="true" {
* Alternative, the return format can be a closure. The closure is passed the query as the only argument. The result of the closure is returned as the result of the query.
*
* @format "query", "array", or a closure.
* @options Options passed to named return formatter factories.
*
* @return qb.models.Query.QueryBuilder
*/
public QueryBuilder function setReturnFormat( required any format ) {
public QueryBuilder function setReturnFormat( required any format, struct options = {} ) {
structDelete( variables.defaultOptions, "returntype" );
if ( isClosure( arguments.format ) || isCustomFunction( arguments.format ) ) {
variables.returnFormat = format;
} else if ( arguments.format == "array" ) {
variables.returnFormat = function( q ) {
return getUtils().queryToArrayOfStructs( q );
};
} else if ( arguments.format == "query" ) {
variables.returnFormat = function( q ) {
return q;
};
} else if ( arguments.format == "none" ) {
variables.returnFormat = function( q ) {
return q;
};
} else {
throw( type = "InvalidFormat", message = "The format passed to Builder is invalid." );
variables.returnFormat = getReturnFormatterRegistry().getReturnFormatter(
arguments.format,
arguments.options
);
}

return this;
Expand All @@ -4536,17 +4596,35 @@ component displayname="QueryBuilder" accessors="true" {
*
* @returnFormat "query", "array", or a closure.
* @callback The code to execute with the given return format.
* @options Options passed to named return formatter factories.
*
* @return any
*/
public any function withReturnFormat( required any returnFormat, required any callback ) {
public any function withReturnFormat( required any returnFormat, required any callback, struct options = {} ) {
var originalReturnFormat = getReturnFormat();
setReturnFormat( arguments.returnFormat );
setReturnFormat( arguments.returnFormat, arguments.options );
var result = callback();
setReturnFormat( originalReturnFormat );
return result;
}

private void function normalizeQueryExecuteReturnTypeOptions( required struct options ) {
if ( !arguments.options.keyExists( "returntype" ) ) {
return;
}

if ( getValidateQueryExecuteReturnType() ) {
throw(
type = "InvalidQueryExecuteOption",
message = "The queryExecute returntype option cannot be used with qb return formatters."
);
}

structDelete( arguments.options, "returntype" );
structDelete( arguments.options, "columnkey" );
structDelete( arguments.options, "columnKey" );
}

/**
* Runs the code inside the callback with the given columns selected and then sets the columns back to its original value.
*
Expand Down
26 changes: 26 additions & 0 deletions models/Query/QueryUtils.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,32 @@ component singleton displayname="QueryUtils" accessors="true" {
return results;
}

/**
* Converts a query object to a struct of structs keyed by the provided column.
*
* @q The query to convert.
* @columnKey The query column to use as the key for the returned struct.
*
* @return struct
*/
public struct function queryToStructOfStructs( required any q, required string columnKey ) {
var rows = queryToArrayOfStructs( arguments.q );
var results = {};

for ( var row in rows ) {
if ( !row.keyExists( arguments.columnKey ) ) {
throw(
type = "MissingColumnKey",
message = "The columnKey [#arguments.columnKey#] was not found in the query results."
);
}

results[ row[ arguments.columnKey ] ] = row;
}

return results;
}

/**
* Remove a list of columns from a specified query.
*
Expand Down
Loading
Loading