From 4582c44feb81376eda3bd262a8512b2fa078967b Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 15 Oct 2026 14:42:58 -0600 Subject: [PATCH 1/4] fix(refresh): hydrate projected rows with aliased primary keys --- models/BaseEntity.cfc | 7 ++++++- .../app/models/AliasedPrimaryKeyUser.cfc | 17 +++++++++++++++++ .../BaseEntity/RefreshSnapshotSpec.cfc | 14 ++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/resources/app/models/AliasedPrimaryKeyUser.cfc diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 7d231e00..c1c80398 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1527,7 +1527,12 @@ component accessors="true" { if ( !isStruct( freshData ) || structKeyExists( freshData, "isQuickEntity" ) ) { return freshData; } - var entity = newEntity().hydrate( freshData ); + // Snapshot rows use database column names, while hydrate() requires aliased keys. + // Use the same database-row hydration path as QuickBuilder.loadEntity(). + var entity = newEntity() + .assignAttributesData( freshData ) + .assignOriginalAttributes( freshData ) + .markLoaded(); return hasRefreshQuery ? entity.set_refreshQuery( variables._refreshQuery ) : entity; } diff --git a/tests/resources/app/models/AliasedPrimaryKeyUser.cfc b/tests/resources/app/models/AliasedPrimaryKeyUser.cfc new file mode 100644 index 00000000..cf231a94 --- /dev/null +++ b/tests/resources/app/models/AliasedPrimaryKeyUser.cfc @@ -0,0 +1,17 @@ +component + extends ="quick.models.BaseEntity" + table ="users" + key ="userID" + accessors="true" +{ + + property name="userID" column="id"; + property name="username"; + property name="firstName" column="first_name"; + property name="lastName" column="last_name"; + + function scopeWithFullName( qb ) { + qb.appendVirtualAttribute( "fullName" ).selectRaw( "CONCAT(first_name, ' ', last_name) AS fullName" ); + } + +} diff --git a/tests/specs/integration/BaseEntity/RefreshSnapshotSpec.cfc b/tests/specs/integration/BaseEntity/RefreshSnapshotSpec.cfc index 66e525f1..035a8448 100644 --- a/tests/specs/integration/BaseEntity/RefreshSnapshotSpec.cfc +++ b/tests/specs/integration/BaseEntity/RefreshSnapshotSpec.cfc @@ -2,6 +2,20 @@ component extends="tests.resources.ModuleIntegrationSpec" { function run() { describe( "Result-set refresh snapshots", function() { + it( "retrieves fresh projected entities with aliased primary keys", function() { + var user = getInstance( "AliasedPrimaryKeyUser" ).withFullName().findOrFail( 1 ); + queryExecute( "UPDATE `users` SET `first_name` = ? WHERE `id` = ?", [ "Aliased", 1 ] ); + + var fresh = user.fresh(); + expect( fresh.getUserID() ).toBe( 1 ); + expect( fresh.getFullName() ).toBe( "Aliased Peterson" ); + expect( fresh.isLoaded() ).toBeTrue(); + expect( fresh.isDirty() ).toBeFalse(); + expect( fresh.get_refreshQuery() ).toBeSameInstanceAs( user.get_refreshQuery() ); + expect( fresh.fresh().getUserID() ).toBe( 1 ); + expect( user.getFirstName() ).toBe( "Eric" ); + } ); + it( "shares an isolated projection snapshot across get results", function() { assertSharedSnapshot( "get" ); } ); From f0ef613e68d473dc9b3635f3761fbe0cf563c92c Mon Sep 17 00:00:00 2001 From: elpete <2583646+elpete@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:45:38 +0000 Subject: [PATCH 2/4] Apply cfformat changes --- dsl/QuickServiceDSL.cfc | 4 +--- resources/testing/FactoryManager.cfc | 4 ++-- tests/resources/factories/UserFactory.cfc | 4 ++-- .../integration/BaseEntity/ChildClassSpec.cfc | 6 ++---- tests/specs/integration/BaseEntity/ColumnsSpec.cfc | 2 +- tests/specs/integration/BaseEntity/GetSpec.cfc | 2 +- .../integration/BaseEntity/ReadOnlyEntitySpec.cfc | 6 +++--- .../BaseEntity/ReadOnlyPropertySpec.cfc | 8 ++++---- tests/specs/integration/BaseServiceSpec.cfc | 12 +++--------- tests/specs/integration/CBORMCompatEntitySpec.cfc | 8 ++++---- tests/specs/integration/FactorySpec.cfc | 14 +++++++------- 11 files changed, 30 insertions(+), 40 deletions(-) diff --git a/dsl/QuickServiceDSL.cfc b/dsl/QuickServiceDSL.cfc index f3675688..144e88f4 100644 --- a/dsl/QuickServiceDSL.cfc +++ b/dsl/QuickServiceDSL.cfc @@ -26,9 +26,7 @@ component { public any function process( required struct definition ) { return variables.injector.getInstance( name = "BaseService@quick", - initArguments = { - entity : variables.injector.getInstance( listRest( arguments.definition.dsl, ":" ) ) - } + initArguments = { entity : variables.injector.getInstance( listRest( arguments.definition.dsl, ":" ) ) } ); } diff --git a/resources/testing/FactoryManager.cfc b/resources/testing/FactoryManager.cfc index f52153bc..18f7a6aa 100644 --- a/resources/testing/FactoryManager.cfc +++ b/resources/testing/FactoryManager.cfc @@ -27,11 +27,11 @@ component { variables.factories[ arguments.name ] = variables.wirebox.getInstance( name = "#variables.factoryPath#.#arguments.name#Factory", initArguments = { - entityProvider : variables.wirebox.getInstance( + entityProvider : variables.wirebox.getInstance( dsl = "provider:#arguments.name#", targetObject = this ), - context : variables.context + context : variables.context } ); } diff --git a/tests/resources/factories/UserFactory.cfc b/tests/resources/factories/UserFactory.cfc index f1d37fda..91024b51 100644 --- a/tests/resources/factories/UserFactory.cfc +++ b/tests/resources/factories/UserFactory.cfc @@ -17,11 +17,11 @@ component extends="quick.resources.testing.Factory" { } any function administrator() { - return state( { type : "admin" } ); + return state( { type : "admin" } ); } any function wired() { - return state( { firstName : isObject( variables.wirebox ) ? "Injected" : "Missing" } ); + return state( { firstName : isObject( variables.wirebox ) ? "Injected" : "Missing" } ); } } diff --git a/tests/specs/integration/BaseEntity/ChildClassSpec.cfc b/tests/specs/integration/BaseEntity/ChildClassSpec.cfc index 64b77ed1..cbe11ac3 100644 --- a/tests/specs/integration/BaseEntity/ChildClassSpec.cfc +++ b/tests/specs/integration/BaseEntity/ChildClassSpec.cfc @@ -378,7 +378,7 @@ component extends="tests.resources.ModuleIntegrationSpec" { // comment id 4 = internal comment var internalComment = getInstance( "Comment" ) .findOrFail( 4 ) - .update( { reason : "Super private, ya know?" } ); + .update( { reason : "Super private, ya know?" } ); var uInternalComment = getInstance( "Comment" ).findOrFail( 4 ); @@ -386,9 +386,7 @@ component extends="tests.resources.ModuleIntegrationSpec" { expect( uInternalComment.getReason() ).toBe( "Super private, ya know?" ); // comment id 5 = picture comment - var pictureComment = getInstance( "Comment" ) - .findOrFail( 5 ) - .update( { filename : "Lenna.jpeg" } ); + var pictureComment = getInstance( "Comment" ).findOrFail( 5 ).update( { filename : "Lenna.jpeg" } ); var uPictureComment = getInstance( "Comment" ).findOrFail( 5 ); diff --git a/tests/specs/integration/BaseEntity/ColumnsSpec.cfc b/tests/specs/integration/BaseEntity/ColumnsSpec.cfc index 8a8fb1f0..fd1784da 100644 --- a/tests/specs/integration/BaseEntity/ColumnsSpec.cfc +++ b/tests/specs/integration/BaseEntity/ColumnsSpec.cfc @@ -100,7 +100,7 @@ component extends="tests.resources.ModuleIntegrationSpec" { it( "translates attributes to their column names", function() { expect( function() { - getInstance( "Link" ).create( { url : "https://example.com" } ); + getInstance( "Link" ).create( { url : "https://example.com" } ); } ).notToThrow(); } ); diff --git a/tests/specs/integration/BaseEntity/GetSpec.cfc b/tests/specs/integration/BaseEntity/GetSpec.cfc index cb133bcd..9f636357 100644 --- a/tests/specs/integration/BaseEntity/GetSpec.cfc +++ b/tests/specs/integration/BaseEntity/GetSpec.cfc @@ -22,7 +22,7 @@ component extends="tests.resources.ModuleIntegrationSpec" { it( "passes query options when finding an entity by primary key", function() { structDelete( request, "baseEntityGetSpecPreQBExecute" ); - var user = getInstance( "User" ).find( 1, { datasource : "quick" } ); + var user = getInstance( "User" ).find( 1, { datasource : "quick" } ); var executionsWithDatasource = request.baseEntityGetSpecPreQBExecute.filter( function( execution ) { return execution.options.keyExists( "datasource" ); } ); diff --git a/tests/specs/integration/BaseEntity/ReadOnlyEntitySpec.cfc b/tests/specs/integration/BaseEntity/ReadOnlyEntitySpec.cfc index 20b81268..dfce8b77 100644 --- a/tests/specs/integration/BaseEntity/ReadOnlyEntitySpec.cfc +++ b/tests/specs/integration/BaseEntity/ReadOnlyEntitySpec.cfc @@ -12,7 +12,7 @@ component extends="tests.resources.ModuleIntegrationSpec" { it( "prevents create from being performed on new instances", function() { expect( function() { - getInstance( "Referral" ).create( { type : "internal" } ); + getInstance( "Referral" ).create( { type : "internal" } ); } ).toThrow( type = "QuickReadOnlyException" ); } ); @@ -27,13 +27,13 @@ component extends="tests.resources.ModuleIntegrationSpec" { it( "prevents updates from being performed on existing instances", function() { var referral = getInstance( "Referral" ).findOrFail( 1 ); expect( function() { - referral.update( { type : "external" } ); + referral.update( { type : "external" } ); } ).toThrow( type = "QuickReadOnlyException" ); } ); it( "prevents mass updates from being performed on existing instances", function() { expect( function() { - getInstance( "Referral" ).updateAll( { type : "external" } ); + getInstance( "Referral" ).updateAll( { type : "external" } ); } ).toThrow( type = "QuickReadOnlyException" ); } ); diff --git a/tests/specs/integration/BaseEntity/ReadOnlyPropertySpec.cfc b/tests/specs/integration/BaseEntity/ReadOnlyPropertySpec.cfc index 23f68d17..8a22601d 100644 --- a/tests/specs/integration/BaseEntity/ReadOnlyPropertySpec.cfc +++ b/tests/specs/integration/BaseEntity/ReadOnlyPropertySpec.cfc @@ -31,7 +31,7 @@ component extends="tests.resources.ModuleIntegrationSpec" { it( "prevents create from setting read-only properties", function() { expect( function() { - getInstance( "Link" ).create( { createdDate : now() } ); + getInstance( "Link" ).create( { createdDate : now() } ); } ).toThrow( type = "QuickReadOnlyException" ); } ); @@ -45,20 +45,20 @@ component extends="tests.resources.ModuleIntegrationSpec" { it( "prevents fill from being called containing a read-only property", function() { var link = getInstance( "Link" ).findOrFail( 1 ); expect( function() { - link.fill( { createdDate : now() } ); + link.fill( { createdDate : now() } ); } ).toThrow( type = "QuickReadOnlyException" ); } ); it( "prevents updates from being performed on a read-only property", function() { var link = getInstance( "Link" ).findOrFail( 1 ); expect( function() { - link.update( { createdDate : now() } ); + link.update( { createdDate : now() } ); } ).toThrow( type = "QuickReadOnlyException" ); } ); it( "prevents mass updates from being performed on read-only properties", function() { expect( function() { - getInstance( "Link" ).updateAll( { createdDate : now() } ); + getInstance( "Link" ).updateAll( { createdDate : now() } ); } ).toThrow( type = "QuickReadOnlyException" ); } ); diff --git a/tests/specs/integration/BaseServiceSpec.cfc b/tests/specs/integration/BaseServiceSpec.cfc index fbb5f7c0..3625fb9f 100644 --- a/tests/specs/integration/BaseServiceSpec.cfc +++ b/tests/specs/integration/BaseServiceSpec.cfc @@ -17,18 +17,12 @@ component extends="tests.resources.ModuleIntegrationSpec" { describe( "instantiation", function() { it( "can be instantiated with an entity", function() { var user = getInstance( "User" ); - var service = getInstance( - name = "BaseService@quick", - initArguments = { entity : user } - ); + var service = getInstance( name = "BaseService@quick", initArguments = { entity : user } ); expect( service.entityName() ).toBe( "User" ); } ); it( "can be instantiated with a wirebox mapping", function() { - var service = getInstance( - name = "BaseService@quick", - initArguments = { entity : "User" } - ); + var service = getInstance( name = "BaseService@quick", initArguments = { entity : "User" } ); expect( service.entityName() ).toBe( "User" ); } ); @@ -68,7 +62,7 @@ component extends="tests.resources.ModuleIntegrationSpec" { var users = variables.service .whereNotNull( "created_date" ) - .get( options = { datasource : "quick" } ); + .get( options = { datasource : "quick" } ); expect( users ).toBeArray(); expect( request.baseServiceSpecPreQBExecute ).toHaveLength( 1 ); diff --git a/tests/specs/integration/CBORMCompatEntitySpec.cfc b/tests/specs/integration/CBORMCompatEntitySpec.cfc index c0b21d25..4fe74820 100644 --- a/tests/specs/integration/CBORMCompatEntitySpec.cfc +++ b/tests/specs/integration/CBORMCompatEntitySpec.cfc @@ -27,7 +27,7 @@ component extends="tests.resources.ModuleIntegrationSpec" { it( "list (with arguments)", function() { var users = user.list( - criteria = { lastName : "Doe" }, + criteria = { lastName : "Doe" }, sortOrder = "username", max = 2, offset = 1, @@ -101,7 +101,7 @@ component extends="tests.resources.ModuleIntegrationSpec" { } ); it( "findWhere", function() { - var john = user.findWhere( { firstName : "John" } ); + var john = user.findWhere( { firstName : "John" } ); expect( john.getId() ).toBe( 2 ); expect( john.getUsername() ).toBe( "johndoe" ); } ); @@ -178,14 +178,14 @@ component extends="tests.resources.ModuleIntegrationSpec" { } ); it( "new (with properties)", function() { - var newUser = user.new( { username : "new_username" } ); + var newUser = user.new( { username : "new_username" } ); expect( newUser.isLoaded() ).toBeFalse(); expect( newUser.getUsername() ).toBe( "new_username" ); } ); it( "populate", function() { var newUser = user.new(); - newUser.populate( { username : "new_username" } ); + newUser.populate( { username : "new_username" } ); expect( newUser.getUsername() ).toBe( "new_username" ); } ); diff --git a/tests/specs/integration/FactorySpec.cfc b/tests/specs/integration/FactorySpec.cfc index dd5a45dd..fd9c30d5 100644 --- a/tests/specs/integration/FactorySpec.cfc +++ b/tests/specs/integration/FactorySpec.cfc @@ -3,9 +3,9 @@ component extends="tests.resources.ModuleIntegrationSpec" { function run() { describe( "Quick model factories", function() { it( "makes unsaved entities from defaults and explicit overrides", function() { - var user = newFactoryManager( { suffix : "make" } ) + var user = newFactoryManager( { suffix : "make" } ) .factory( "User" ) - .make( { firstName : "Overridden" } ); + .make( { firstName : "Overridden" } ); expect( user ).toBeInstanceOf( "User" ); expect( user.isLoaded() ).toBeFalse(); @@ -16,17 +16,17 @@ component extends="tests.resources.ModuleIntegrationSpec" { } ); it( "combines counts, named states, sequences, and persisted Quick entities", function() { - var users = newFactoryManager( { suffix : "sequence" } ) + var users = newFactoryManager( { suffix : "sequence" } ) .factory( "User" ) .count( 3 ) .administrator() .state( function( attributes, context ) { - return { firstName : "State #context.index#" }; + return { firstName : "State #context.index#" }; } ) .sequence( [ - { lastName : "Sequence A" }, + { lastName : "Sequence A" }, function( attributes, context ) { - return { lastName : "Sequence #context.index#" }; + return { lastName : "Sequence #context.index#" }; } ] ) .create(); @@ -55,7 +55,7 @@ component extends="tests.resources.ModuleIntegrationSpec" { var created = []; var user = newFactoryManager() .factory( "User" ) - .state( { username : "factory-callback" } ) + .state( { username : "factory-callback" } ) .afterMaking( function( entity, attributes ) { arrayAppend( made, attributes.username ); } ) From a833c94f52c7b8b37e83e5eacf824d2d2f3dccdf Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 15 Oct 2026 14:46:55 -0600 Subject: [PATCH 3/4] test(refresh): configure the aliased fixture key explicitly --- tests/resources/app/models/AliasedPrimaryKeyUser.cfc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/resources/app/models/AliasedPrimaryKeyUser.cfc b/tests/resources/app/models/AliasedPrimaryKeyUser.cfc index cf231a94..17fece8a 100644 --- a/tests/resources/app/models/AliasedPrimaryKeyUser.cfc +++ b/tests/resources/app/models/AliasedPrimaryKeyUser.cfc @@ -1,7 +1,6 @@ component extends ="quick.models.BaseEntity" table ="users" - key ="userID" accessors="true" { @@ -10,6 +9,8 @@ component property name="firstName" column="first_name"; property name="lastName" column="last_name"; + variables._key = "userID"; + function scopeWithFullName( qb ) { qb.appendVirtualAttribute( "fullName" ).selectRaw( "CONCAT(first_name, ' ', last_name) AS fullName" ); } From 8384a4a4a883e4669db1403796912ed86a0e91a9 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 15 Oct 2026 14:52:01 -0600 Subject: [PATCH 4/4] test(refresh): check persisted attribute cleanliness --- tests/specs/integration/BaseEntity/RefreshSnapshotSpec.cfc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/specs/integration/BaseEntity/RefreshSnapshotSpec.cfc b/tests/specs/integration/BaseEntity/RefreshSnapshotSpec.cfc index 035a8448..64191e9b 100644 --- a/tests/specs/integration/BaseEntity/RefreshSnapshotSpec.cfc +++ b/tests/specs/integration/BaseEntity/RefreshSnapshotSpec.cfc @@ -10,7 +10,8 @@ component extends="tests.resources.ModuleIntegrationSpec" { expect( fresh.getUserID() ).toBe( 1 ); expect( fresh.getFullName() ).toBe( "Aliased Peterson" ); expect( fresh.isLoaded() ).toBeTrue(); - expect( fresh.isDirty() ).toBeFalse(); + expect( fresh.isDirty( "userID" ) ).toBeFalse(); + expect( fresh.isDirty( "firstName" ) ).toBeFalse(); expect( fresh.get_refreshQuery() ).toBeSameInstanceAs( user.get_refreshQuery() ); expect( fresh.fresh().getUserID() ).toBe( 1 ); expect( user.getFirstName() ).toBe( "Eric" );