From d1953357fad9b2f2130b280f18c22b65d591a66f Mon Sep 17 00:00:00 2001 From: "Karlin [bot]" Date: Tue, 26 May 2026 06:58:23 -0700 Subject: [PATCH] feat(moduleconfig): add shouldWrapValues setting (#309) --- ModuleConfig.cfc | 8 ++ models/Grammars/AutoDiscover.cfc | 35 +++++++-- tests/specs/Query/ShouldWrapValuesSpec.cfc | 91 ++++++++++++++++++++++ 3 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 tests/specs/Query/ShouldWrapValuesSpec.cfc diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index 91f40df1..c5a8748c 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -16,6 +16,7 @@ component { "validateQueryExecuteReturnType": false, "collectQueryLog": true, "convertEmptyStringsToNull": true, + "shouldWrapValues": true, "validateQueryParamStructKeys": true, "numericSQLType": "NUMERIC", "integerSQLType": "INTEGER", @@ -84,6 +85,13 @@ component { .map( alias = "SchemaBuilder@qb", force = true ) .to( "qb.models.Schema.SchemaBuilder" ) .initArg( name = "grammar", ref = settings.defaultGrammar ); + + // Apply shouldWrapValues setting to the configured grammar singleton. + // When defaultGrammar is AutoDiscover@qb, the setting is forwarded via + // onMissingMethod to whatever concrete grammar AutoDiscover resolves at runtime. + if ( structKeyExists( settings, "shouldWrapValues" ) ) { + wirebox.getInstance( settings.defaultGrammar ).setShouldWrapValues( settings.shouldWrapValues ); + } } } diff --git a/models/Grammars/AutoDiscover.cfc b/models/Grammars/AutoDiscover.cfc index 20914b70..90259524 100644 --- a/models/Grammars/AutoDiscover.cfc +++ b/models/Grammars/AutoDiscover.cfc @@ -2,32 +2,53 @@ component singleton { property name="wirebox" inject="wirebox"; property name="grammar"; + property name="shouldWrapValues"; function autoDiscoverGrammar() { cfdbinfo( type = "Version", name = "local.dbInfo" ); + var discoveredGrammar = ""; switch ( dbInfo.DATABASE_PRODUCTNAME ) { case "MySQL": case "MariaDB": - return wirebox.getInstance( "MySQLGrammar@qb" ); + discoveredGrammar = wirebox.getInstance( "MySQLGrammar@qb" ); + break; case "Derby": - return wirebox.getInstance( "DerbyGrammar@qb" ); + discoveredGrammar = wirebox.getInstance( "DerbyGrammar@qb" ); + break; case "PostgreSQL": - return wirebox.getInstance( "PostgresGrammar@qb" ); + discoveredGrammar = wirebox.getInstance( "PostgresGrammar@qb" ); + break; case "Microsoft SQL Server": - return wirebox.getInstance( "SQLServerGrammar@qb" ); + discoveredGrammar = wirebox.getInstance( "SQLServerGrammar@qb" ); + break; case "Oracle": - return wirebox.getInstance( "OracleGrammar@qb" ); + discoveredGrammar = wirebox.getInstance( "OracleGrammar@qb" ); + break; case "SQLite": - return wirebox.getInstance( "SQLiteGrammar@qb" ); + discoveredGrammar = wirebox.getInstance( "SQLiteGrammar@qb" ); + break; default: - return wirebox.getInstance( "BaseGrammar@qb" ); + discoveredGrammar = wirebox.getInstance( "BaseGrammar@qb" ); } + + return discoveredGrammar; + } + + public AutoDiscover function setShouldWrapValues( required boolean shouldWrapValues ) { + variables.shouldWrapValues = arguments.shouldWrapValues; + if ( !isNull( variables.grammar ) ) { + variables.grammar.setShouldWrapValues( arguments.shouldWrapValues ); + } + return this; } function onMissingMethod( missingMethodName, missingMethodArguments ) { if ( isNull( variables.grammar ) || !structKeyExists( variables, "grammar" ) ) { variables.grammar = autoDiscoverGrammar(); + if ( !isNull( variables.shouldWrapValues ) ) { + variables.grammar.setShouldWrapValues( variables.shouldWrapValues ); + } } return invoke( variables.grammar, missingMethodName, missingMethodArguments ); } diff --git a/tests/specs/Query/ShouldWrapValuesSpec.cfc b/tests/specs/Query/ShouldWrapValuesSpec.cfc new file mode 100644 index 00000000..eed37abe --- /dev/null +++ b/tests/specs/Query/ShouldWrapValuesSpec.cfc @@ -0,0 +1,91 @@ +component extends="testbox.system.BaseSpec" { + + function run() { + describe( "shouldWrapValues setting", function() { + it( "does not eagerly discover a grammar when configured", function() { + var grammar = getMockBox().createMock( "qb.models.Grammars.PostgresGrammar" ).init(); + var autoDiscover = getMockBox() + .createMock( "qb.models.Grammars.AutoDiscover" ) + .$( "autoDiscoverGrammar", grammar ); + + autoDiscover.setShouldWrapValues( false ); + + expect( autoDiscover.$count( "autoDiscoverGrammar" ) ).toBe( 0 ); + expect( autoDiscover.onMissingMethod( "wrapValue", { "value": "users" } ) ).toBe( "users" ); + expect( autoDiscover.$count( "autoDiscoverGrammar" ) ).toBe( 1 ); + expect( grammar.getShouldWrapValues() ).toBeFalse(); + } ); + + it( "defaults to true in BaseGrammar", function() { + var utils = getMockBox().createMock( "qb.models.Query.QueryUtils" ).init(); + var grammar = getMockBox().createMock( "qb.models.Grammars.PostgresGrammar" ).init( utils ); + + expect( grammar.getShouldWrapValues() ).toBeTrue( "shouldWrapValues should default to true" ); + } ); + + it( "wraps identifiers in double quotes when shouldWrapValues is true", function() { + var utils = getMockBox().createMock( "qb.models.Query.QueryUtils" ).init(); + var grammar = getMockBox().createMock( "qb.models.Grammars.PostgresGrammar" ).init( utils ); + grammar.setShouldWrapValues( true ); + + var builder = getMockBox().createMock( "qb.models.Query.QueryBuilder" ).init( grammar ); + + var sql = builder + .from( "users" ) + .select( "name" ) + .toSQL(); + + expect( sql ).toBe( "SELECT ""name"" FROM ""users""" ); + + sql = builder + .from( "users" ) + .select( "id" ) + .where( "email", "test@test.com" ) + .toSQL( withBindings = true ); + + expect( sql ).toBe( "SELECT ""id"" FROM ""users"" WHERE ""email"" = ?" ); + } ); + + it( "does not wrap identifiers when shouldWrapValues is false", function() { + var utils = getMockBox().createMock( "qb.models.Query.QueryUtils" ).init(); + var grammar = getMockBox().createMock( "qb.models.Grammars.PostgresGrammar" ).init( utils ); + grammar.setShouldWrapValues( false ); + + var builder = getMockBox().createMock( "qb.models.Query.QueryBuilder" ).init( grammar ); + + var sql = builder + .from( "users" ) + .select( "name" ) + .toSQL(); + + expect( sql ).toBe( "SELECT name FROM users" ); + + sql = builder + .from( "users" ) + .select( "id" ) + .where( "email", "test@test.com" ) + .toSQL(); + + expect( sql ).toBe( "SELECT id FROM users WHERE email = ?" ); + } ); + + it( "per-query withoutWrappingValues overrides grammar default", function() { + var utils = getMockBox().createMock( "qb.models.Query.QueryUtils" ).init(); + var grammar = getMockBox().createMock( "qb.models.Grammars.PostgresGrammar" ).init( utils ); + grammar.setShouldWrapValues( true ); + + var builder = getMockBox().createMock( "qb.models.Query.QueryBuilder" ).init( grammar ); + + // Grammar default is true, but per-query override to false + var sql = builder + .withoutWrappingValues() + .from( "users" ) + .select( "name" ) + .toSQL(); + + expect( sql ).toBe( "SELECT name FROM users" ); + } ); + } ); + } + +}