From 831b0b770c9390a4e80d1a4aa675f3218dba6564 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 22 Aug 2026 02:16:22 -0600 Subject: [PATCH 01/34] feat: return empty relationships for new entities --- models/BaseEntity.cfc | 14 ++++++++++++++ .../Relationships/RelationshipLoadingSpec.cfc | 19 +++++++++++++++++++ .../Relationships/WithDefaultSpec.cfc | 11 ++++++----- .../integration/GoodErrorMessagesSpec.cfc | 9 +-------- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 8900ec48..6ce55960 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -2732,6 +2732,20 @@ component accessors="true" { return retrieveRelationship( relationshipName ); } + if ( !isRelationshipLoaded( relationshipName ) && !isLoaded() ) { + var relationshipArguments = arguments.missingMethodArguments; + var unloadedRelationship = ignoreLoadedGuard( function() { + return invoke( + this, + relationshipName, + relationshipArguments + ); + } ); + unloadedRelationship.setRelationMethodName( relationshipName ); + unloadedRelationship.initRelation( [ this ], relationshipName ); + return retrieveRelationship( relationshipName ); + } + if ( !isRelationshipLoaded( relationshipName ) && variables._preventLazyLoading ) { variables._lazyLoadingViolationCallback( this, relationshipName ); } diff --git a/tests/specs/integration/BaseEntity/Relationships/RelationshipLoadingSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/RelationshipLoadingSpec.cfc index 91dcb06e..380654eb 100644 --- a/tests/specs/integration/BaseEntity/Relationships/RelationshipLoadingSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/RelationshipLoadingSpec.cfc @@ -18,6 +18,25 @@ component extends="tests.resources.ModuleIntegrationSpec" { variables.queries = []; } ); + it( "returns empty relationship values for new entities", function() { + var post = getInstance( "Post" ); + var user = getInstance( "User" ); + + expect( post.getAuthor() ).toBeNull(); + expect( user.getLatestPost() ).toBeNull(); + expect( user.getPosts() ).toBeArray().toBeEmpty(); + expect( post.getTags() ).toBeArray().toBeEmpty(); + expect( variables.queries ).toBeEmpty(); + } ); + + it( "includes empty relationships in mementos for new entities", function() { + var memento = getInstance( "User" ).getMemento( includes = [ "latestPost", "posts" ] ); + + expect( memento.latestPost ).toBe( "" ); + expect( memento.posts ).toBeArray().toBeEmpty(); + expect( variables.queries ).toBeEmpty(); + } ); + describe( "Eager Loading Spec", function() { it( "can load a relationship for an entity", function() { var elpete = getInstance( "User" ).where( "username", "elpete" ).firstOrFail(); diff --git a/tests/specs/integration/BaseEntity/Relationships/WithDefaultSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/WithDefaultSpec.cfc index 44fa70bf..95c634a9 100644 --- a/tests/specs/integration/BaseEntity/Relationships/WithDefaultSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/WithDefaultSpec.cfc @@ -2,12 +2,13 @@ component extends="tests.resources.ModuleIntegrationSpec" { function run() { describe( "WithDefault Spec", function() { - it( "will throw an exception when retrieving a relation on an unloaded entity", function() { - var post = getInstance( "Post" ); + it( "returns a configured default for a relation on an unloaded entity", function() { + var post = getInstance( "Post" ); + var author = post.getAuthorWithEmptyDefault(); - expect( function() { - post.getAuthor(); - } ).toThrow( message = "Retrieving an unloaded entity should throw an exception" ); + expect( author ).toBeInstanceOf( "User" ); + expect( author.isLoaded() ).toBeFalse( "A default model is not loaded" ); + expect( author.retrieveAttributesData() ).toBeEmpty(); } ); it( "can load a entity and return a default entity if there is no owning entity", function() { diff --git a/tests/specs/integration/GoodErrorMessagesSpec.cfc b/tests/specs/integration/GoodErrorMessagesSpec.cfc index b70a0adf..e9c0f69d 100644 --- a/tests/specs/integration/GoodErrorMessagesSpec.cfc +++ b/tests/specs/integration/GoodErrorMessagesSpec.cfc @@ -35,20 +35,13 @@ component extends="tests.resources.ModuleIntegrationSpec" { } ).toThrow( type = "QuickEntityDefaultedKey" ); } ); - it( "throws a helpful error message when trying to access relationships on unloaded entities", function() { + it( "throws a helpful error message when trying to query relationships on unloaded entities", function() { expect( function() { getInstance( "User" ).posts(); } ).toThrow( type = "QuickEntityNotLoaded", regex = "This instance is not loaded so it cannot access the \[posts\] relationship\. Either load the entity from the database using a query executor \(like \`first\`\) or base your query off of the \[Post\] entity directly and use the \`has\` or \`whereHas\` methods to constrain it based on data in \[User\]\." ); - - expect( function() { - getInstance( "User" ).getPosts(); - } ).toThrow( - type = "QuickEntityNotLoaded", - regex = "This instance is not loaded so it cannot access the \[posts\] relationship\. Either load the entity from the database using a query executor \(like \`first\`\) or base your query off of the \[Post\] entity directly and use the \`has\` or \`whereHas\` methods to constrain it based on data in \[User\]\." - ); } ); it( From 4a90fad403df31aa83ca9f787e26091011c6c917 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 22 Aug 2026 03:56:01 -0600 Subject: [PATCH 02/34] feat: improve relationship cache retrieval (#148) --- models/BaseEntity.cfc | 68 +++++++++++++++++-- models/Relationships/BaseRelationship.cfc | 17 +++++ .../Relationships/RelationshipLoadingSpec.cfc | 32 +++++++++ 3 files changed, 110 insertions(+), 7 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 6ce55960..88565f58 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1686,17 +1686,71 @@ component accessors="true" { } /** - * Retrieves the result of a loaded relationship. - * If there is no data, returns null instead. + * Retrieves the result of a loaded relationship. If the relationship has not + * been loaded, initializes and returns its relationship type default without + * executing a query. An explicit default value can be supplied instead. * - * @name The relationship name to retrieve. + * @name The relationship name to retrieve. + * @defaultValue An optional value to assign and return when the relationship + * has not been loaded. * * @return quick.models.BaseEntity | [quick.models.BaseEntity] */ - public any function retrieveRelationship( required string name ) { - return variables._relationshipsData.keyExists( arguments.name ) ? variables._relationshipsData[ arguments.name ] : javacast( - "null", - "" + public any function retrieveRelationship( required string name, any defaultValue ) { + if ( !hasRelationship( arguments.name ) ) { + throwRelationshipNotFound( arguments.name ); + } + if ( variables._relationshipsData.keyExists( arguments.name ) ) { + return variables._relationshipsData[ arguments.name ]; + } + if ( isRelationshipLoaded( arguments.name ) ) { + return javacast( "null", "" ); + } + + var relationship = resolveRelationship( arguments.name ); + if ( arguments.keyExists( "defaultValue" ) ) { + assignRelationship( arguments.name, arguments.defaultValue ); + return arguments.defaultValue; + } + + relationship.initRelation( [ this ], arguments.name ); + return variables._relationshipsData.keyExists( arguments.name ) + ? variables._relationshipsData[ arguments.name ] + : javacast( "null", "" ); + } + + /** + * Resolves and validates a relationship definition by name. + * + * @name The relationship method name to resolve. + * + * @throws RelationshipNotFound + * + * @return quick.models.Relationships.BaseRelationship + */ + private any function resolveRelationship( required string name ) { + var relationshipName = arguments.name; + var relationship = ignoreLoadedGuard( function() { + return invoke( this, relationshipName ); + } ); + if ( !isObject( relationship ) || !structKeyExists( relationship, "relationshipClass" ) ) { + throwRelationshipNotFound( arguments.name ); + } + relationship.setRelationMethodName( arguments.name ); + return relationship; + } + + /** + * Throws a consistent exception for an unknown relationship name. + * + * @name The unknown relationship name. + * + * @throws RelationshipNotFound + */ + private void function throwRelationshipNotFound( required string name ) { + throw( + type = "RelationshipNotFound", + message = "The [#arguments.name#] relationship was not found on the [#entityName()#] entity." ); } diff --git a/models/Relationships/BaseRelationship.cfc b/models/Relationships/BaseRelationship.cfc index ae51e560..96a59b99 100644 --- a/models/Relationships/BaseRelationship.cfc +++ b/models/Relationships/BaseRelationship.cfc @@ -116,6 +116,23 @@ component accessors="true" implements="IRelationship" { return this; } + /** + * Initializes a relationship to its unloaded default value. + * To-one relationships default to null. Collection relationships override + * this method to initialize an empty array. + * + * @entities The entities on which to initialize the relationship. + * @relation The relationship name to initialize. + * + * @return [quick.models.BaseEntity] + */ + public array function initRelation( required array entities, required string relation ) { + return arguments.entities.map( function( entity ) { + arguments.entity.assignRelationship( arguments.relation, javacast( "null", "" ) ); + return arguments.entity; + } ); + } + /** * Retrieves the entities for eager loading. * diff --git a/tests/specs/integration/BaseEntity/Relationships/RelationshipLoadingSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/RelationshipLoadingSpec.cfc index 380654eb..12e3691e 100644 --- a/tests/specs/integration/BaseEntity/Relationships/RelationshipLoadingSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/RelationshipLoadingSpec.cfc @@ -37,6 +37,38 @@ component extends="tests.resources.ModuleIntegrationSpec" { expect( variables.queries ).toBeEmpty(); } ); + it( "retrieves and caches the relationship type default without querying", function() { + var user = getInstance( "User" ); + var post = getInstance( "Post" ); + + expect( user.retrieveRelationship( "posts" ) ).toBeArray().toBeEmpty(); + expect( post.retrieveRelationship( "author" ) ).toBeNull(); + expect( post.retrieveRelationship( "authorWithEmptyDefault" ) ).toBeInstanceOf( "User" ); + expect( user.isRelationshipLoaded( "posts" ) ).toBeTrue(); + expect( post.isRelationshipLoaded( "author" ) ).toBeTrue(); + expect( variables.queries ).toBeEmpty(); + } ); + + it( "accepts a default relationship value", function() { + var user = getInstance( "User" ); + var seededPost = getInstance( "Post" ).fill( { "body" : "seeded" } ); + + var posts = user.retrieveRelationship( "posts", [ seededPost ] ); + + expect( posts ).toHaveLength( 1 ); + expect( posts[ 1 ].getBody() ).toBe( "seeded" ); + expect( user.isRelationshipLoaded( "posts" ) ).toBeTrue(); + expect( variables.queries ).toBeEmpty(); + } ); + + it( "throws when retrieving an unknown relationship", function() { + var post = getInstance( "Post" ); + + expect( function() { + post.retrieveRelationship( "missingRelationship" ); + } ).toThrow( "RelationshipNotFound" ); + } ); + describe( "Eager Loading Spec", function() { it( "can load a relationship for an entity", function() { var elpete = getInstance( "User" ).where( "username", "elpete" ).firstOrFail(); From d763b393c8108841a39762920f64c2ddc22d4ffa Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 13:45:20 -0600 Subject: [PATCH 03/34] refactor: avoid internal closures --- models/BaseEntity.cfc | 36 +++++++++++++++-------- models/Relationships/BaseRelationship.cfc | 8 ++--- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 88565f58..dc8e0bd3 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1729,10 +1729,7 @@ component accessors="true" { * @return quick.models.Relationships.BaseRelationship */ private any function resolveRelationship( required string name ) { - var relationshipName = arguments.name; - var relationship = ignoreLoadedGuard( function() { - return invoke( this, relationshipName ); - } ); + var relationship = invokeRelationshipIgnoringLoadedGuard( arguments.name ); if ( !isObject( relationship ) || !structKeyExists( relationship, "relationshipClass" ) ) { throwRelationshipNotFound( arguments.name ); } @@ -2787,14 +2784,10 @@ component accessors="true" { } if ( !isRelationshipLoaded( relationshipName ) && !isLoaded() ) { - var relationshipArguments = arguments.missingMethodArguments; - var unloadedRelationship = ignoreLoadedGuard( function() { - return invoke( - this, - relationshipName, - relationshipArguments - ); - } ); + var unloadedRelationship = invokeRelationshipIgnoringLoadedGuard( + relationshipName, + arguments.missingMethodArguments + ); unloadedRelationship.setRelationMethodName( relationshipName ); unloadedRelationship.initRelation( [ this ], relationshipName ); return retrieveRelationship( relationshipName ); @@ -2818,6 +2811,25 @@ component accessors="true" { return retrieveRelationship( relationshipName ); } + /** + * Invokes a relationship definition without applying loaded-entity guards. + */ + private any function invokeRelationshipIgnoringLoadedGuard( + required string relationshipName, + struct invokeArguments = {} + ) { + variables._ignoreNotLoadedGuard = true; + try { + return invoke( + this, + arguments.relationshipName, + arguments.invokeArguments + ); + } finally { + variables._ignoreNotLoadedGuard = false; + } + } + /** * Attempts to save a new relation to a relationship. * diff --git a/models/Relationships/BaseRelationship.cfc b/models/Relationships/BaseRelationship.cfc index 96a59b99..402ae1fe 100644 --- a/models/Relationships/BaseRelationship.cfc +++ b/models/Relationships/BaseRelationship.cfc @@ -127,10 +127,10 @@ component accessors="true" implements="IRelationship" { * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - return arguments.entities.map( function( entity ) { - arguments.entity.assignRelationship( arguments.relation, javacast( "null", "" ) ); - return arguments.entity; - } ); + for ( var entity in arguments.entities ) { + entity.assignRelationship( arguments.relation, javacast( "null", "" ) ); + } + return arguments.entities; } /** From a303ceb1757331a7f742754b6b2ae8710fa10734 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 14:11:12 -0600 Subject: [PATCH 04/34] fix: preserve assigned dynamic relationships --- models/BaseEntity.cfc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index dc8e0bd3..cbceba39 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1697,15 +1697,15 @@ component accessors="true" { * @return quick.models.BaseEntity | [quick.models.BaseEntity] */ public any function retrieveRelationship( required string name, any defaultValue ) { - if ( !hasRelationship( arguments.name ) ) { - throwRelationshipNotFound( arguments.name ); - } if ( variables._relationshipsData.keyExists( arguments.name ) ) { return variables._relationshipsData[ arguments.name ]; } if ( isRelationshipLoaded( arguments.name ) ) { return javacast( "null", "" ); } + if ( !hasRelationship( arguments.name ) ) { + throwRelationshipNotFound( arguments.name ); + } var relationship = resolveRelationship( arguments.name ); if ( arguments.keyExists( "defaultValue" ) ) { From 9b828c03bfea65a3ea1bf37f95bddb380aabc783 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 14:45:47 -0600 Subject: [PATCH 05/34] fix: preserve relationship factory scope --- models/BaseEntity.cfc | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index cbceba39..dc20904e 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1729,7 +1729,10 @@ component accessors="true" { * @return quick.models.Relationships.BaseRelationship */ private any function resolveRelationship( required string name ) { - var relationship = invokeRelationshipIgnoringLoadedGuard( arguments.name ); + var relationshipName = arguments.name; + var relationship = ignoreLoadedGuard( function() { + return invoke( this, relationshipName ); + } ); if ( !isObject( relationship ) || !structKeyExists( relationship, "relationshipClass" ) ) { throwRelationshipNotFound( arguments.name ); } @@ -2784,10 +2787,14 @@ component accessors="true" { } if ( !isRelationshipLoaded( relationshipName ) && !isLoaded() ) { - var unloadedRelationship = invokeRelationshipIgnoringLoadedGuard( - relationshipName, - arguments.missingMethodArguments - ); + var relationshipArguments = arguments.missingMethodArguments; + var unloadedRelationship = ignoreLoadedGuard( function() { + return invoke( + this, + relationshipName, + relationshipArguments + ); + } ); unloadedRelationship.setRelationMethodName( relationshipName ); unloadedRelationship.initRelation( [ this ], relationshipName ); return retrieveRelationship( relationshipName ); @@ -2811,25 +2818,6 @@ component accessors="true" { return retrieveRelationship( relationshipName ); } - /** - * Invokes a relationship definition without applying loaded-entity guards. - */ - private any function invokeRelationshipIgnoringLoadedGuard( - required string relationshipName, - struct invokeArguments = {} - ) { - variables._ignoreNotLoadedGuard = true; - try { - return invoke( - this, - arguments.relationshipName, - arguments.invokeArguments - ); - } finally { - variables._ignoreNotLoadedGuard = false; - } - } - /** * Attempts to save a new relation to a relationship. * From 9f8ebc8a145b83ea0e67cdf6a4835c2032c7a324 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 14:54:04 -0600 Subject: [PATCH 06/34] refactor: initialize relationships without closures --- models/Relationships/BelongsTo.cfc | 12 ++++++------ models/Relationships/BelongsToMany.cfc | 12 ++++++------ models/Relationships/BelongsToThrough.cfc | 14 +++++++------- models/Relationships/HasMany.cfc | 12 ++++++------ models/Relationships/HasManyDeep.cfc | 12 ++++++------ models/Relationships/HasManyThrough.cfc | 12 ++++++------ models/Relationships/HasOne.cfc | 14 +++++++------- models/Relationships/HasOneThrough.cfc | 14 +++++++------- models/Relationships/PolymorphicHasMany.cfc | 12 ++++++------ 9 files changed, 57 insertions(+), 57 deletions(-) diff --git a/models/Relationships/BelongsTo.cfc b/models/Relationships/BelongsTo.cfc index 955dc82f..b028456a 100644 --- a/models/Relationships/BelongsTo.cfc +++ b/models/Relationships/BelongsTo.cfc @@ -218,17 +218,17 @@ component * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - arguments.entities.each( function( entity ) { + for ( var entity in arguments.entities ) { var defaultEntity = newDefaultEntity(); - if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { - arguments.entity.assignRelationship( - relation, + if ( structKeyExists( entity, "isQuickEntity" ) ) { + entity.assignRelationship( + arguments.relation, isNull( defaultEntity ) ? javacast( "null", "" ) : defaultEntity ); } else { - arguments.entity[ relation ] = isNull( defaultEntity ) ? {} : defaultEntity.getMemento(); + entity[ arguments.relation ] = isNull( defaultEntity ) ? {} : defaultEntity.getMemento(); } - } ); + } return arguments.entities; } diff --git a/models/Relationships/BelongsToMany.cfc b/models/Relationships/BelongsToMany.cfc index 7bf587cc..55f3efd0 100644 --- a/models/Relationships/BelongsToMany.cfc +++ b/models/Relationships/BelongsToMany.cfc @@ -222,14 +222,14 @@ component * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - return arguments.entities.map( function( entity ) { - if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { - arguments.entity.assignRelationship( relation, [] ); + for ( var entity in arguments.entities ) { + if ( structKeyExists( entity, "isQuickEntity" ) ) { + entity.assignRelationship( arguments.relation, [] ); } else { - arguments.entity[ relation ] = []; + entity[ arguments.relation ] = []; } - return arguments.entity; - } ); + } + return arguments.entities; } /** diff --git a/models/Relationships/BelongsToThrough.cfc b/models/Relationships/BelongsToThrough.cfc index a9bb8d30..9400f69b 100644 --- a/models/Relationships/BelongsToThrough.cfc +++ b/models/Relationships/BelongsToThrough.cfc @@ -260,18 +260,18 @@ component extends="quick.models.Relationships.BaseRelationship" { * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - return arguments.entities.map( function( entity ) { + for ( var entity in arguments.entities ) { var defaultEntity = newDefaultEntity(); - if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { - arguments.entity.assignRelationship( - relation, + if ( structKeyExists( entity, "isQuickEntity" ) ) { + entity.assignRelationship( + arguments.relation, isNull( defaultEntity ) ? javacast( "null", "" ) : defaultEntity ); } else { - arguments.entity[ relation ] = isNull( defaultEntity ) ? {} : defaultEntity.getMemento(); + entity[ arguments.relation ] = isNull( defaultEntity ) ? {} : defaultEntity.getMemento(); } - return arguments.entity; - } ); + } + return arguments.entities; } /** diff --git a/models/Relationships/HasMany.cfc b/models/Relationships/HasMany.cfc index 687cbd84..6e98054a 100644 --- a/models/Relationships/HasMany.cfc +++ b/models/Relationships/HasMany.cfc @@ -41,14 +41,14 @@ component extends="quick.models.Relationships.HasOneOrMany" accessors="true" { * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - return arguments.entities.map( function( entity ) { - if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { - arguments.entity.assignRelationship( relation, [] ); + for ( var entity in arguments.entities ) { + if ( structKeyExists( entity, "isQuickEntity" ) ) { + entity.assignRelationship( arguments.relation, [] ); } else { - arguments.entity[ relation ] = []; + entity[ arguments.relation ] = []; } - return arguments.entity; - } ); + } + return arguments.entities; } /** diff --git a/models/Relationships/HasManyDeep.cfc b/models/Relationships/HasManyDeep.cfc index de3bdbf3..21d34507 100644 --- a/models/Relationships/HasManyDeep.cfc +++ b/models/Relationships/HasManyDeep.cfc @@ -340,14 +340,14 @@ component * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - return arguments.entities.map( function( entity ) { - if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { - arguments.entity.assignRelationship( relation, [] ); + for ( var entity in arguments.entities ) { + if ( structKeyExists( entity, "isQuickEntity" ) ) { + entity.assignRelationship( arguments.relation, [] ); } else { - arguments.entity[ relation ] = []; + entity[ arguments.relation ] = []; } - return arguments.entity; - } ); + } + return arguments.entities; } /** diff --git a/models/Relationships/HasManyThrough.cfc b/models/Relationships/HasManyThrough.cfc index c1b2990e..0e7fd9f2 100644 --- a/models/Relationships/HasManyThrough.cfc +++ b/models/Relationships/HasManyThrough.cfc @@ -37,14 +37,14 @@ component extends="quick.models.Relationships.HasOneOrManyThrough" { * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - return arguments.entities.map( function( entity ) { - if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { - arguments.entity.assignRelationship( relation, [] ); + for ( var entity in arguments.entities ) { + if ( structKeyExists( entity, "isQuickEntity" ) ) { + entity.assignRelationship( arguments.relation, [] ); } else { - arguments.entity[ relation ] = []; + entity[ arguments.relation ] = []; } - return arguments.entity; - } ); + } + return arguments.entities; } /** diff --git a/models/Relationships/HasOne.cfc b/models/Relationships/HasOne.cfc index 951c20cb..4b6570be 100644 --- a/models/Relationships/HasOne.cfc +++ b/models/Relationships/HasOne.cfc @@ -56,18 +56,18 @@ component extends="quick.models.Relationships.HasOneOrMany" { * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - return arguments.entities.map( function( entity ) { + for ( var entity in arguments.entities ) { var defaultEntity = newDefaultEntity(); - if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { - arguments.entity.assignRelationship( - relation, + if ( structKeyExists( entity, "isQuickEntity" ) ) { + entity.assignRelationship( + arguments.relation, isNull( defaultEntity ) ? javacast( "null", "" ) : defaultEntity ); } else { - arguments.entity[ relation ] = isNull( defaultEntity ) ? {} : defaultEntity.getMemento(); + entity[ arguments.relation ] = isNull( defaultEntity ) ? {} : defaultEntity.getMemento(); } - return arguments.entity; - } ); + } + return arguments.entities; } /** diff --git a/models/Relationships/HasOneThrough.cfc b/models/Relationships/HasOneThrough.cfc index 4ba9cd4f..9f0b11e1 100644 --- a/models/Relationships/HasOneThrough.cfc +++ b/models/Relationships/HasOneThrough.cfc @@ -52,18 +52,18 @@ component extends="quick.models.Relationships.HasOneOrManyThrough" { * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - return arguments.entities.map( function( entity ) { + for ( var entity in arguments.entities ) { var defaultEntity = newDefaultEntity(); - if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { - arguments.entity.assignRelationship( - relation, + if ( structKeyExists( entity, "isQuickEntity" ) ) { + entity.assignRelationship( + arguments.relation, isNull( defaultEntity ) ? javacast( "null", "" ) : defaultEntity ); } else { - arguments.entity[ relation ] = isNull( defaultEntity ) ? {} : defaultEntity.getMemento(); + entity[ arguments.relation ] = isNull( defaultEntity ) ? {} : defaultEntity.getMemento(); } - return arguments.entity; - } ); + } + return arguments.entities; } /** diff --git a/models/Relationships/PolymorphicHasMany.cfc b/models/Relationships/PolymorphicHasMany.cfc index 8732a395..3b23bd30 100644 --- a/models/Relationships/PolymorphicHasMany.cfc +++ b/models/Relationships/PolymorphicHasMany.cfc @@ -40,14 +40,14 @@ component extends="quick.models.Relationships.PolymorphicHasOneOrMany" accessors * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - return arguments.entities.map( function( entity ) { - if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { - arguments.entity.assignRelationship( relation, [] ); + for ( var entity in arguments.entities ) { + if ( structKeyExists( entity, "isQuickEntity" ) ) { + entity.assignRelationship( arguments.relation, [] ); } else { - arguments.entity[ relation ] = []; + entity[ arguments.relation ] = []; } - return arguments.entity; - } ); + } + return arguments.entities; } /** From fe48795419190b24d90fb4f6664bd9b505f76f55 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 15:00:22 -0600 Subject: [PATCH 07/34] Revert "refactor: initialize relationships without closures" This reverts commit 9f8ebc8a145b83ea0e67cdf6a4835c2032c7a324. --- models/Relationships/BelongsTo.cfc | 12 ++++++------ models/Relationships/BelongsToMany.cfc | 12 ++++++------ models/Relationships/BelongsToThrough.cfc | 14 +++++++------- models/Relationships/HasMany.cfc | 12 ++++++------ models/Relationships/HasManyDeep.cfc | 12 ++++++------ models/Relationships/HasManyThrough.cfc | 12 ++++++------ models/Relationships/HasOne.cfc | 14 +++++++------- models/Relationships/HasOneThrough.cfc | 14 +++++++------- models/Relationships/PolymorphicHasMany.cfc | 12 ++++++------ 9 files changed, 57 insertions(+), 57 deletions(-) diff --git a/models/Relationships/BelongsTo.cfc b/models/Relationships/BelongsTo.cfc index b028456a..955dc82f 100644 --- a/models/Relationships/BelongsTo.cfc +++ b/models/Relationships/BelongsTo.cfc @@ -218,17 +218,17 @@ component * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - for ( var entity in arguments.entities ) { + arguments.entities.each( function( entity ) { var defaultEntity = newDefaultEntity(); - if ( structKeyExists( entity, "isQuickEntity" ) ) { - entity.assignRelationship( - arguments.relation, + if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { + arguments.entity.assignRelationship( + relation, isNull( defaultEntity ) ? javacast( "null", "" ) : defaultEntity ); } else { - entity[ arguments.relation ] = isNull( defaultEntity ) ? {} : defaultEntity.getMemento(); + arguments.entity[ relation ] = isNull( defaultEntity ) ? {} : defaultEntity.getMemento(); } - } + } ); return arguments.entities; } diff --git a/models/Relationships/BelongsToMany.cfc b/models/Relationships/BelongsToMany.cfc index 55f3efd0..7bf587cc 100644 --- a/models/Relationships/BelongsToMany.cfc +++ b/models/Relationships/BelongsToMany.cfc @@ -222,14 +222,14 @@ component * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - for ( var entity in arguments.entities ) { - if ( structKeyExists( entity, "isQuickEntity" ) ) { - entity.assignRelationship( arguments.relation, [] ); + return arguments.entities.map( function( entity ) { + if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { + arguments.entity.assignRelationship( relation, [] ); } else { - entity[ arguments.relation ] = []; + arguments.entity[ relation ] = []; } - } - return arguments.entities; + return arguments.entity; + } ); } /** diff --git a/models/Relationships/BelongsToThrough.cfc b/models/Relationships/BelongsToThrough.cfc index 9400f69b..a9bb8d30 100644 --- a/models/Relationships/BelongsToThrough.cfc +++ b/models/Relationships/BelongsToThrough.cfc @@ -260,18 +260,18 @@ component extends="quick.models.Relationships.BaseRelationship" { * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - for ( var entity in arguments.entities ) { + return arguments.entities.map( function( entity ) { var defaultEntity = newDefaultEntity(); - if ( structKeyExists( entity, "isQuickEntity" ) ) { - entity.assignRelationship( - arguments.relation, + if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { + arguments.entity.assignRelationship( + relation, isNull( defaultEntity ) ? javacast( "null", "" ) : defaultEntity ); } else { - entity[ arguments.relation ] = isNull( defaultEntity ) ? {} : defaultEntity.getMemento(); + arguments.entity[ relation ] = isNull( defaultEntity ) ? {} : defaultEntity.getMemento(); } - } - return arguments.entities; + return arguments.entity; + } ); } /** diff --git a/models/Relationships/HasMany.cfc b/models/Relationships/HasMany.cfc index 6e98054a..687cbd84 100644 --- a/models/Relationships/HasMany.cfc +++ b/models/Relationships/HasMany.cfc @@ -41,14 +41,14 @@ component extends="quick.models.Relationships.HasOneOrMany" accessors="true" { * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - for ( var entity in arguments.entities ) { - if ( structKeyExists( entity, "isQuickEntity" ) ) { - entity.assignRelationship( arguments.relation, [] ); + return arguments.entities.map( function( entity ) { + if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { + arguments.entity.assignRelationship( relation, [] ); } else { - entity[ arguments.relation ] = []; + arguments.entity[ relation ] = []; } - } - return arguments.entities; + return arguments.entity; + } ); } /** diff --git a/models/Relationships/HasManyDeep.cfc b/models/Relationships/HasManyDeep.cfc index 21d34507..de3bdbf3 100644 --- a/models/Relationships/HasManyDeep.cfc +++ b/models/Relationships/HasManyDeep.cfc @@ -340,14 +340,14 @@ component * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - for ( var entity in arguments.entities ) { - if ( structKeyExists( entity, "isQuickEntity" ) ) { - entity.assignRelationship( arguments.relation, [] ); + return arguments.entities.map( function( entity ) { + if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { + arguments.entity.assignRelationship( relation, [] ); } else { - entity[ arguments.relation ] = []; + arguments.entity[ relation ] = []; } - } - return arguments.entities; + return arguments.entity; + } ); } /** diff --git a/models/Relationships/HasManyThrough.cfc b/models/Relationships/HasManyThrough.cfc index 0e7fd9f2..c1b2990e 100644 --- a/models/Relationships/HasManyThrough.cfc +++ b/models/Relationships/HasManyThrough.cfc @@ -37,14 +37,14 @@ component extends="quick.models.Relationships.HasOneOrManyThrough" { * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - for ( var entity in arguments.entities ) { - if ( structKeyExists( entity, "isQuickEntity" ) ) { - entity.assignRelationship( arguments.relation, [] ); + return arguments.entities.map( function( entity ) { + if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { + arguments.entity.assignRelationship( relation, [] ); } else { - entity[ arguments.relation ] = []; + arguments.entity[ relation ] = []; } - } - return arguments.entities; + return arguments.entity; + } ); } /** diff --git a/models/Relationships/HasOne.cfc b/models/Relationships/HasOne.cfc index 4b6570be..951c20cb 100644 --- a/models/Relationships/HasOne.cfc +++ b/models/Relationships/HasOne.cfc @@ -56,18 +56,18 @@ component extends="quick.models.Relationships.HasOneOrMany" { * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - for ( var entity in arguments.entities ) { + return arguments.entities.map( function( entity ) { var defaultEntity = newDefaultEntity(); - if ( structKeyExists( entity, "isQuickEntity" ) ) { - entity.assignRelationship( - arguments.relation, + if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { + arguments.entity.assignRelationship( + relation, isNull( defaultEntity ) ? javacast( "null", "" ) : defaultEntity ); } else { - entity[ arguments.relation ] = isNull( defaultEntity ) ? {} : defaultEntity.getMemento(); + arguments.entity[ relation ] = isNull( defaultEntity ) ? {} : defaultEntity.getMemento(); } - } - return arguments.entities; + return arguments.entity; + } ); } /** diff --git a/models/Relationships/HasOneThrough.cfc b/models/Relationships/HasOneThrough.cfc index 9f0b11e1..4ba9cd4f 100644 --- a/models/Relationships/HasOneThrough.cfc +++ b/models/Relationships/HasOneThrough.cfc @@ -52,18 +52,18 @@ component extends="quick.models.Relationships.HasOneOrManyThrough" { * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - for ( var entity in arguments.entities ) { + return arguments.entities.map( function( entity ) { var defaultEntity = newDefaultEntity(); - if ( structKeyExists( entity, "isQuickEntity" ) ) { - entity.assignRelationship( - arguments.relation, + if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { + arguments.entity.assignRelationship( + relation, isNull( defaultEntity ) ? javacast( "null", "" ) : defaultEntity ); } else { - entity[ arguments.relation ] = isNull( defaultEntity ) ? {} : defaultEntity.getMemento(); + arguments.entity[ relation ] = isNull( defaultEntity ) ? {} : defaultEntity.getMemento(); } - } - return arguments.entities; + return arguments.entity; + } ); } /** diff --git a/models/Relationships/PolymorphicHasMany.cfc b/models/Relationships/PolymorphicHasMany.cfc index 3b23bd30..8732a395 100644 --- a/models/Relationships/PolymorphicHasMany.cfc +++ b/models/Relationships/PolymorphicHasMany.cfc @@ -40,14 +40,14 @@ component extends="quick.models.Relationships.PolymorphicHasOneOrMany" accessors * @return [quick.models.BaseEntity] */ public array function initRelation( required array entities, required string relation ) { - for ( var entity in arguments.entities ) { - if ( structKeyExists( entity, "isQuickEntity" ) ) { - entity.assignRelationship( arguments.relation, [] ); + return arguments.entities.map( function( entity ) { + if ( structKeyExists( arguments.entity, "isQuickEntity" ) ) { + arguments.entity.assignRelationship( relation, [] ); } else { - entity[ arguments.relation ] = []; + arguments.entity[ relation ] = []; } - } - return arguments.entities; + return arguments.entity; + } ); } /** From b01c60398fa71859db1d0018df290840f72b6786 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 15:09:18 -0600 Subject: [PATCH 08/34] fix: preserve initialized relationship defaults across engines --- models/BaseEntity.cfc | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index dc20904e..917459b6 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1713,10 +1713,22 @@ component accessors="true" { return arguments.defaultValue; } - relationship.initRelation( [ this ], arguments.name ); - return variables._relationshipsData.keyExists( arguments.name ) - ? variables._relationshipsData[ arguments.name ] - : javacast( "null", "" ); + var initializedEntities = relationship.initRelation( [ this ], arguments.name ); + if ( variables._relationshipsData.keyExists( arguments.name ) ) { + return variables._relationshipsData[ arguments.name ]; + } + + if ( initializedEntities.len() > 0 && initializedEntities[ 1 ].isRelationshipLoaded( arguments.name ) ) { + var initializedValue = initializedEntities[ 1 ].retrieveRelationship( arguments.name ); + if ( isNull( initializedValue ) ) { + variables._relationshipsLoaded[ arguments.name ] = true; + return javacast( "null", "" ); + } + assignRelationship( arguments.name, initializedValue ); + return initializedValue; + } + + return javacast( "null", "" ); } /** From b07d5f357bfdafa1dca81d6963b86cb6d7c9407d Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 15:16:50 -0600 Subject: [PATCH 09/34] fix: initialize unloaded relationship defaults directly --- models/BaseEntity.cfc | 21 ++++++--------------- models/Relationships/BaseRelationship.cfc | 7 +++++++ models/Relationships/BelongsTo.cfc | 4 ++++ models/Relationships/BelongsToMany.cfc | 4 ++++ models/Relationships/BelongsToThrough.cfc | 4 ++++ models/Relationships/HasMany.cfc | 4 ++++ models/Relationships/HasManyDeep.cfc | 4 ++++ models/Relationships/HasManyThrough.cfc | 4 ++++ models/Relationships/HasOne.cfc | 4 ++++ models/Relationships/HasOneThrough.cfc | 4 ++++ models/Relationships/PolymorphicHasMany.cfc | 4 ++++ 11 files changed, 49 insertions(+), 15 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 917459b6..ff75d470 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1713,22 +1713,13 @@ component accessors="true" { return arguments.defaultValue; } - var initializedEntities = relationship.initRelation( [ this ], arguments.name ); - if ( variables._relationshipsData.keyExists( arguments.name ) ) { - return variables._relationshipsData[ arguments.name ]; - } - - if ( initializedEntities.len() > 0 && initializedEntities[ 1 ].isRelationshipLoaded( arguments.name ) ) { - var initializedValue = initializedEntities[ 1 ].retrieveRelationship( arguments.name ); - if ( isNull( initializedValue ) ) { - variables._relationshipsLoaded[ arguments.name ] = true; - return javacast( "null", "" ); - } - assignRelationship( arguments.name, initializedValue ); - return initializedValue; + var unloadedDefault = relationship.getUnloadedDefault(); + if ( isNull( unloadedDefault ) ) { + variables._relationshipsLoaded[ arguments.name ] = true; + return javacast( "null", "" ); } - - return javacast( "null", "" ); + assignRelationship( arguments.name, unloadedDefault ); + return unloadedDefault; } /** diff --git a/models/Relationships/BaseRelationship.cfc b/models/Relationships/BaseRelationship.cfc index 402ae1fe..761dc4c3 100644 --- a/models/Relationships/BaseRelationship.cfc +++ b/models/Relationships/BaseRelationship.cfc @@ -104,6 +104,13 @@ component accessors="true" implements="IRelationship" { ); } + /** + * Returns the unloaded default value for this relationship without querying. + */ + public any function getUnloadedDefault() { + return javacast( "null", "" ); + } + /** * Sets the relation method name for this relationship. * diff --git a/models/Relationships/BelongsTo.cfc b/models/Relationships/BelongsTo.cfc index 955dc82f..82e5b8e2 100644 --- a/models/Relationships/BelongsTo.cfc +++ b/models/Relationships/BelongsTo.cfc @@ -21,6 +21,10 @@ component accessors ="true" { + public any function getUnloadedDefault() { + return newDefaultEntity(); + } + /** * An alias for the parent entity. */ diff --git a/models/Relationships/BelongsToMany.cfc b/models/Relationships/BelongsToMany.cfc index 7bf587cc..57623a6c 100644 --- a/models/Relationships/BelongsToMany.cfc +++ b/models/Relationships/BelongsToMany.cfc @@ -21,6 +21,10 @@ component accessors ="true" { + public any function getUnloadedDefault() { + return []; + } + /** * The pivot table name between relationships. */ diff --git a/models/Relationships/BelongsToThrough.cfc b/models/Relationships/BelongsToThrough.cfc index a9bb8d30..f8330a33 100644 --- a/models/Relationships/BelongsToThrough.cfc +++ b/models/Relationships/BelongsToThrough.cfc @@ -16,6 +16,10 @@ */ component extends="quick.models.Relationships.BaseRelationship" { + public any function getUnloadedDefault() { + return newDefaultEntity(); + } + /** * An array of relationships between the parent entity and the related entity. */ diff --git a/models/Relationships/HasMany.cfc b/models/Relationships/HasMany.cfc index 687cbd84..b7e86127 100644 --- a/models/Relationships/HasMany.cfc +++ b/models/Relationships/HasMany.cfc @@ -16,6 +16,10 @@ */ component extends="quick.models.Relationships.HasOneOrMany" accessors="true" { + public any function getUnloadedDefault() { + return []; + } + /** * Returns the result of the relationship. * diff --git a/models/Relationships/HasManyDeep.cfc b/models/Relationships/HasManyDeep.cfc index de3bdbf3..fe522511 100644 --- a/models/Relationships/HasManyDeep.cfc +++ b/models/Relationships/HasManyDeep.cfc @@ -7,6 +7,10 @@ component accessors ="true" { + public any function getUnloadedDefault() { + return []; + } + /** * The through parent entities being traversed. */ diff --git a/models/Relationships/HasManyThrough.cfc b/models/Relationships/HasManyThrough.cfc index c1b2990e..ace605f2 100644 --- a/models/Relationships/HasManyThrough.cfc +++ b/models/Relationships/HasManyThrough.cfc @@ -17,6 +17,10 @@ */ component extends="quick.models.Relationships.HasOneOrManyThrough" { + public any function getUnloadedDefault() { + return []; + } + /** * Returns the result of the relationship. * diff --git a/models/Relationships/HasOne.cfc b/models/Relationships/HasOne.cfc index 951c20cb..3c04f26b 100644 --- a/models/Relationships/HasOne.cfc +++ b/models/Relationships/HasOne.cfc @@ -17,6 +17,10 @@ */ component extends="quick.models.Relationships.HasOneOrMany" { + public any function getUnloadedDefault() { + return newDefaultEntity(); + } + /** * Returns the result of the relationship. * diff --git a/models/Relationships/HasOneThrough.cfc b/models/Relationships/HasOneThrough.cfc index 4ba9cd4f..51f945ab 100644 --- a/models/Relationships/HasOneThrough.cfc +++ b/models/Relationships/HasOneThrough.cfc @@ -17,6 +17,10 @@ */ component extends="quick.models.Relationships.HasOneOrManyThrough" { + public any function getUnloadedDefault() { + return newDefaultEntity(); + } + /** * Returns the result of the relationship. * diff --git a/models/Relationships/PolymorphicHasMany.cfc b/models/Relationships/PolymorphicHasMany.cfc index 8732a395..7bbd5ece 100644 --- a/models/Relationships/PolymorphicHasMany.cfc +++ b/models/Relationships/PolymorphicHasMany.cfc @@ -20,6 +20,10 @@ */ component extends="quick.models.Relationships.PolymorphicHasOneOrMany" accessors="true" { + public any function getUnloadedDefault() { + return []; + } + /** * Returns the result of the relationship. * From 7e77785b0dc021a702402fdce92ae21d8ee472f5 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 15:23:21 -0600 Subject: [PATCH 10/34] fix: preserve Adobe relationship property declarations --- models/Relationships/BelongsTo.cfc | 8 ++++---- models/Relationships/BelongsToMany.cfc | 8 ++++---- models/Relationships/BelongsToThrough.cfc | 8 ++++---- models/Relationships/HasMany.cfc | 8 ++++---- models/Relationships/HasManyDeep.cfc | 8 ++++---- models/Relationships/HasManyThrough.cfc | 8 ++++---- models/Relationships/HasOne.cfc | 8 ++++---- models/Relationships/HasOneThrough.cfc | 8 ++++---- models/Relationships/PolymorphicHasMany.cfc | 8 ++++---- 9 files changed, 36 insertions(+), 36 deletions(-) diff --git a/models/Relationships/BelongsTo.cfc b/models/Relationships/BelongsTo.cfc index 82e5b8e2..d7ffef0e 100644 --- a/models/Relationships/BelongsTo.cfc +++ b/models/Relationships/BelongsTo.cfc @@ -21,10 +21,6 @@ component accessors ="true" { - public any function getUnloadedDefault() { - return newDefaultEntity(); - } - /** * An alias for the parent entity. */ @@ -504,4 +500,8 @@ component }; } + public any function getUnloadedDefault() { + return newDefaultEntity(); + } + } diff --git a/models/Relationships/BelongsToMany.cfc b/models/Relationships/BelongsToMany.cfc index 57623a6c..ae797f70 100644 --- a/models/Relationships/BelongsToMany.cfc +++ b/models/Relationships/BelongsToMany.cfc @@ -21,10 +21,6 @@ component accessors ="true" { - public any function getUnloadedDefault() { - return []; - } - /** * The pivot table name between relationships. */ @@ -1168,4 +1164,8 @@ component }; } + public any function getUnloadedDefault() { + return []; + } + } diff --git a/models/Relationships/BelongsToThrough.cfc b/models/Relationships/BelongsToThrough.cfc index f8330a33..c0f0e47e 100644 --- a/models/Relationships/BelongsToThrough.cfc +++ b/models/Relationships/BelongsToThrough.cfc @@ -16,10 +16,6 @@ */ component extends="quick.models.Relationships.BaseRelationship" { - public any function getUnloadedDefault() { - return newDefaultEntity(); - } - /** * An array of relationships between the parent entity and the related entity. */ @@ -367,4 +363,8 @@ component extends="quick.models.Relationships.BaseRelationship" { return variables.closestToParent.getForeignKeys(); } + public any function getUnloadedDefault() { + return newDefaultEntity(); + } + } diff --git a/models/Relationships/HasMany.cfc b/models/Relationships/HasMany.cfc index b7e86127..5a0b5cb4 100644 --- a/models/Relationships/HasMany.cfc +++ b/models/Relationships/HasMany.cfc @@ -16,10 +16,6 @@ */ component extends="quick.models.Relationships.HasOneOrMany" accessors="true" { - public any function getUnloadedDefault() { - return []; - } - /** * Returns the result of the relationship. * @@ -74,4 +70,8 @@ component extends="quick.models.Relationships.HasOneOrMany" accessors="true" { return matchMany( argumentCollection = arguments ); } + public any function getUnloadedDefault() { + return []; + } + } diff --git a/models/Relationships/HasManyDeep.cfc b/models/Relationships/HasManyDeep.cfc index fe522511..c1b28399 100644 --- a/models/Relationships/HasManyDeep.cfc +++ b/models/Relationships/HasManyDeep.cfc @@ -7,10 +7,6 @@ component accessors ="true" { - public any function getUnloadedDefault() { - return []; - } - /** * The through parent entities being traversed. */ @@ -448,4 +444,8 @@ component }; } + public any function getUnloadedDefault() { + return []; + } + } diff --git a/models/Relationships/HasManyThrough.cfc b/models/Relationships/HasManyThrough.cfc index ace605f2..4c2fc6ca 100644 --- a/models/Relationships/HasManyThrough.cfc +++ b/models/Relationships/HasManyThrough.cfc @@ -17,10 +17,6 @@ */ component extends="quick.models.Relationships.HasOneOrManyThrough" { - public any function getUnloadedDefault() { - return []; - } - /** * Returns the result of the relationship. * @@ -88,4 +84,8 @@ component extends="quick.models.Relationships.HasOneOrManyThrough" { return arguments.entities; } + public any function getUnloadedDefault() { + return []; + } + } diff --git a/models/Relationships/HasOne.cfc b/models/Relationships/HasOne.cfc index 3c04f26b..a4d5e78f 100644 --- a/models/Relationships/HasOne.cfc +++ b/models/Relationships/HasOne.cfc @@ -17,10 +17,6 @@ */ component extends="quick.models.Relationships.HasOneOrMany" { - public any function getUnloadedDefault() { - return newDefaultEntity(); - } - /** * Returns the result of the relationship. * @@ -117,4 +113,8 @@ component extends="quick.models.Relationships.HasOneOrMany" { } ); } + public any function getUnloadedDefault() { + return newDefaultEntity(); + } + } diff --git a/models/Relationships/HasOneThrough.cfc b/models/Relationships/HasOneThrough.cfc index 51f945ab..b128b993 100644 --- a/models/Relationships/HasOneThrough.cfc +++ b/models/Relationships/HasOneThrough.cfc @@ -17,10 +17,6 @@ */ component extends="quick.models.Relationships.HasOneOrManyThrough" { - public any function getUnloadedDefault() { - return newDefaultEntity(); - } - /** * Returns the result of the relationship. * @@ -113,4 +109,8 @@ component extends="quick.models.Relationships.HasOneOrManyThrough" { } ); } + public any function getUnloadedDefault() { + return newDefaultEntity(); + } + } diff --git a/models/Relationships/PolymorphicHasMany.cfc b/models/Relationships/PolymorphicHasMany.cfc index 7bbd5ece..0119103b 100644 --- a/models/Relationships/PolymorphicHasMany.cfc +++ b/models/Relationships/PolymorphicHasMany.cfc @@ -20,10 +20,6 @@ */ component extends="quick.models.Relationships.PolymorphicHasOneOrMany" accessors="true" { - public any function getUnloadedDefault() { - return []; - } - /** * Returns the result of the relationship. * @@ -124,4 +120,8 @@ component extends="quick.models.Relationships.PolymorphicHasOneOrMany" accessors } ); } + public any function getUnloadedDefault() { + return []; + } + } From e3220dde515cdbbf20ef7d9ae83923f56f823542 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 15:33:24 -0600 Subject: [PATCH 11/34] fix: use relationship cardinality for unloaded defaults --- models/BaseEntity.cfc | 2 +- models/Relationships/BaseRelationship.cfc | 10 ++-------- models/Relationships/BelongsTo.cfc | 4 ---- models/Relationships/BelongsToMany.cfc | 7 ++----- models/Relationships/BelongsToThrough.cfc | 4 ---- models/Relationships/HasMany.cfc | 6 ++---- models/Relationships/HasManyDeep.cfc | 7 ++----- models/Relationships/HasManyThrough.cfc | 6 ++---- models/Relationships/HasOne.cfc | 4 ---- models/Relationships/HasOneThrough.cfc | 4 ---- models/Relationships/PolymorphicHasMany.cfc | 6 ++---- 11 files changed, 13 insertions(+), 47 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index ff75d470..65a2a68b 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1713,7 +1713,7 @@ component accessors="true" { return arguments.defaultValue; } - var unloadedDefault = relationship.getUnloadedDefault(); + var unloadedDefault = relationship.relationshipCardinality == "many" ? [] : relationship.newDefaultEntity(); if ( isNull( unloadedDefault ) ) { variables._relationshipsLoaded[ arguments.name ] = true; return javacast( "null", "" ); diff --git a/models/Relationships/BaseRelationship.cfc b/models/Relationships/BaseRelationship.cfc index 761dc4c3..e448ca66 100644 --- a/models/Relationships/BaseRelationship.cfc +++ b/models/Relationships/BaseRelationship.cfc @@ -56,7 +56,8 @@ component accessors="true" implements="IRelationship" { /** * Used to check for the type of relationship more quickly than using isInstanceOf. */ - this.relationshipClass = "BaseRelationship"; + this.relationshipClass = "BaseRelationship"; + this.relationshipCardinality = "one"; /** * Creates a new relationship component to query and retrieve results. @@ -104,13 +105,6 @@ component accessors="true" implements="IRelationship" { ); } - /** - * Returns the unloaded default value for this relationship without querying. - */ - public any function getUnloadedDefault() { - return javacast( "null", "" ); - } - /** * Sets the relation method name for this relationship. * diff --git a/models/Relationships/BelongsTo.cfc b/models/Relationships/BelongsTo.cfc index d7ffef0e..955dc82f 100644 --- a/models/Relationships/BelongsTo.cfc +++ b/models/Relationships/BelongsTo.cfc @@ -500,8 +500,4 @@ component }; } - public any function getUnloadedDefault() { - return newDefaultEntity(); - } - } diff --git a/models/Relationships/BelongsToMany.cfc b/models/Relationships/BelongsToMany.cfc index ae797f70..bf455f3e 100644 --- a/models/Relationships/BelongsToMany.cfc +++ b/models/Relationships/BelongsToMany.cfc @@ -90,7 +90,8 @@ component /** * Used to check for the type of relationship more quickly than using isInstanceOf. */ - this.relationshipClass = "BelongsToMany"; + this.relationshipClass = "BelongsToMany"; + this.relationshipCardinality = "many"; /** * Creates a BelongsToMany relationship. @@ -1164,8 +1165,4 @@ component }; } - public any function getUnloadedDefault() { - return []; - } - } diff --git a/models/Relationships/BelongsToThrough.cfc b/models/Relationships/BelongsToThrough.cfc index c0f0e47e..a9bb8d30 100644 --- a/models/Relationships/BelongsToThrough.cfc +++ b/models/Relationships/BelongsToThrough.cfc @@ -363,8 +363,4 @@ component extends="quick.models.Relationships.BaseRelationship" { return variables.closestToParent.getForeignKeys(); } - public any function getUnloadedDefault() { - return newDefaultEntity(); - } - } diff --git a/models/Relationships/HasMany.cfc b/models/Relationships/HasMany.cfc index 5a0b5cb4..5ff272d2 100644 --- a/models/Relationships/HasMany.cfc +++ b/models/Relationships/HasMany.cfc @@ -16,6 +16,8 @@ */ component extends="quick.models.Relationships.HasOneOrMany" accessors="true" { + this.relationshipCardinality = "many"; + /** * Returns the result of the relationship. * @@ -70,8 +72,4 @@ component extends="quick.models.Relationships.HasOneOrMany" accessors="true" { return matchMany( argumentCollection = arguments ); } - public any function getUnloadedDefault() { - return []; - } - } diff --git a/models/Relationships/HasManyDeep.cfc b/models/Relationships/HasManyDeep.cfc index c1b28399..76a529c9 100644 --- a/models/Relationships/HasManyDeep.cfc +++ b/models/Relationships/HasManyDeep.cfc @@ -30,7 +30,8 @@ component /** * Used to check for the type of relationship more quickly than using isInstanceOf. */ - this.relationshipClass = "HasManyDeep"; + this.relationshipClass = "HasManyDeep"; + this.relationshipCardinality = "many"; /** * Creates a HasManyDeep relationship. @@ -444,8 +445,4 @@ component }; } - public any function getUnloadedDefault() { - return []; - } - } diff --git a/models/Relationships/HasManyThrough.cfc b/models/Relationships/HasManyThrough.cfc index 4c2fc6ca..55e74ab2 100644 --- a/models/Relationships/HasManyThrough.cfc +++ b/models/Relationships/HasManyThrough.cfc @@ -17,6 +17,8 @@ */ component extends="quick.models.Relationships.HasOneOrManyThrough" { + this.relationshipCardinality = "many"; + /** * Returns the result of the relationship. * @@ -84,8 +86,4 @@ component extends="quick.models.Relationships.HasOneOrManyThrough" { return arguments.entities; } - public any function getUnloadedDefault() { - return []; - } - } diff --git a/models/Relationships/HasOne.cfc b/models/Relationships/HasOne.cfc index a4d5e78f..951c20cb 100644 --- a/models/Relationships/HasOne.cfc +++ b/models/Relationships/HasOne.cfc @@ -113,8 +113,4 @@ component extends="quick.models.Relationships.HasOneOrMany" { } ); } - public any function getUnloadedDefault() { - return newDefaultEntity(); - } - } diff --git a/models/Relationships/HasOneThrough.cfc b/models/Relationships/HasOneThrough.cfc index b128b993..4ba9cd4f 100644 --- a/models/Relationships/HasOneThrough.cfc +++ b/models/Relationships/HasOneThrough.cfc @@ -109,8 +109,4 @@ component extends="quick.models.Relationships.HasOneOrManyThrough" { } ); } - public any function getUnloadedDefault() { - return newDefaultEntity(); - } - } diff --git a/models/Relationships/PolymorphicHasMany.cfc b/models/Relationships/PolymorphicHasMany.cfc index 0119103b..d282787e 100644 --- a/models/Relationships/PolymorphicHasMany.cfc +++ b/models/Relationships/PolymorphicHasMany.cfc @@ -20,6 +20,8 @@ */ component extends="quick.models.Relationships.PolymorphicHasOneOrMany" accessors="true" { + this.relationshipCardinality = "many"; + /** * Returns the result of the relationship. * @@ -120,8 +122,4 @@ component extends="quick.models.Relationships.PolymorphicHasOneOrMany" accessors } ); } - public any function getUnloadedDefault() { - return []; - } - } From d40f02d83c00450dbe23b12ab82c111cbb91ad6e Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 15:40:32 -0600 Subject: [PATCH 12/34] fix: identify collection relationship defaults by class --- models/BaseEntity.cfc | 11 ++++++++++- models/Relationships/BaseRelationship.cfc | 3 +-- models/Relationships/BelongsToMany.cfc | 3 +-- models/Relationships/HasMany.cfc | 2 +- models/Relationships/HasManyDeep.cfc | 3 +-- models/Relationships/HasManyThrough.cfc | 2 +- models/Relationships/PolymorphicHasMany.cfc | 2 +- 7 files changed, 16 insertions(+), 10 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 65a2a68b..0e1473f1 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1713,7 +1713,16 @@ component accessors="true" { return arguments.defaultValue; } - var unloadedDefault = relationship.relationshipCardinality == "many" ? [] : relationship.newDefaultEntity(); + var collectionRelationships = [ + "HasMany", + "HasManyThrough", + "HasManyDeep", + "BelongsToMany", + "PolymorphicHasMany" + ]; + var unloadedDefault = collectionRelationships.contains( relationship.relationshipClass ) + ? [] + : relationship.newDefaultEntity(); if ( isNull( unloadedDefault ) ) { variables._relationshipsLoaded[ arguments.name ] = true; return javacast( "null", "" ); diff --git a/models/Relationships/BaseRelationship.cfc b/models/Relationships/BaseRelationship.cfc index e448ca66..402ae1fe 100644 --- a/models/Relationships/BaseRelationship.cfc +++ b/models/Relationships/BaseRelationship.cfc @@ -56,8 +56,7 @@ component accessors="true" implements="IRelationship" { /** * Used to check for the type of relationship more quickly than using isInstanceOf. */ - this.relationshipClass = "BaseRelationship"; - this.relationshipCardinality = "one"; + this.relationshipClass = "BaseRelationship"; /** * Creates a new relationship component to query and retrieve results. diff --git a/models/Relationships/BelongsToMany.cfc b/models/Relationships/BelongsToMany.cfc index bf455f3e..7bf587cc 100644 --- a/models/Relationships/BelongsToMany.cfc +++ b/models/Relationships/BelongsToMany.cfc @@ -90,8 +90,7 @@ component /** * Used to check for the type of relationship more quickly than using isInstanceOf. */ - this.relationshipClass = "BelongsToMany"; - this.relationshipCardinality = "many"; + this.relationshipClass = "BelongsToMany"; /** * Creates a BelongsToMany relationship. diff --git a/models/Relationships/HasMany.cfc b/models/Relationships/HasMany.cfc index 5ff272d2..5a371b4c 100644 --- a/models/Relationships/HasMany.cfc +++ b/models/Relationships/HasMany.cfc @@ -16,7 +16,7 @@ */ component extends="quick.models.Relationships.HasOneOrMany" accessors="true" { - this.relationshipCardinality = "many"; + this.relationshipClass = "HasMany"; /** * Returns the result of the relationship. diff --git a/models/Relationships/HasManyDeep.cfc b/models/Relationships/HasManyDeep.cfc index 76a529c9..de3bdbf3 100644 --- a/models/Relationships/HasManyDeep.cfc +++ b/models/Relationships/HasManyDeep.cfc @@ -30,8 +30,7 @@ component /** * Used to check for the type of relationship more quickly than using isInstanceOf. */ - this.relationshipClass = "HasManyDeep"; - this.relationshipCardinality = "many"; + this.relationshipClass = "HasManyDeep"; /** * Creates a HasManyDeep relationship. diff --git a/models/Relationships/HasManyThrough.cfc b/models/Relationships/HasManyThrough.cfc index 55e74ab2..f67c3462 100644 --- a/models/Relationships/HasManyThrough.cfc +++ b/models/Relationships/HasManyThrough.cfc @@ -17,7 +17,7 @@ */ component extends="quick.models.Relationships.HasOneOrManyThrough" { - this.relationshipCardinality = "many"; + this.relationshipClass = "HasManyThrough"; /** * Returns the result of the relationship. diff --git a/models/Relationships/PolymorphicHasMany.cfc b/models/Relationships/PolymorphicHasMany.cfc index d282787e..32376d65 100644 --- a/models/Relationships/PolymorphicHasMany.cfc +++ b/models/Relationships/PolymorphicHasMany.cfc @@ -20,7 +20,7 @@ */ component extends="quick.models.Relationships.PolymorphicHasOneOrMany" accessors="true" { - this.relationshipCardinality = "many"; + this.relationshipClass = "PolymorphicHasMany"; /** * Returns the result of the relationship. From fe2dede4707584c50e9ffbdbd69b7e99fa558306 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 15:47:51 -0600 Subject: [PATCH 13/34] fix: identify collection defaults by concrete type --- models/BaseEntity.cfc | 14 ++++++-------- models/Relationships/HasMany.cfc | 2 -- models/Relationships/HasManyThrough.cfc | 2 -- models/Relationships/PolymorphicHasMany.cfc | 2 -- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 0e1473f1..c6be56b1 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1713,14 +1713,12 @@ component accessors="true" { return arguments.defaultValue; } - var collectionRelationships = [ - "HasMany", - "HasManyThrough", - "HasManyDeep", - "BelongsToMany", - "PolymorphicHasMany" - ]; - var unloadedDefault = collectionRelationships.contains( relationship.relationshipClass ) + var isCollectionRelationship = isInstanceOf( relationship, "quick.models.Relationships.HasMany" ) || + isInstanceOf( relationship, "quick.models.Relationships.HasManyThrough" ) || + isInstanceOf( relationship, "quick.models.Relationships.HasManyDeep" ) || + isInstanceOf( relationship, "quick.models.Relationships.BelongsToMany" ) || + isInstanceOf( relationship, "quick.models.Relationships.PolymorphicHasMany" ); + var unloadedDefault = isCollectionRelationship ? [] : relationship.newDefaultEntity(); if ( isNull( unloadedDefault ) ) { diff --git a/models/Relationships/HasMany.cfc b/models/Relationships/HasMany.cfc index 5a371b4c..687cbd84 100644 --- a/models/Relationships/HasMany.cfc +++ b/models/Relationships/HasMany.cfc @@ -16,8 +16,6 @@ */ component extends="quick.models.Relationships.HasOneOrMany" accessors="true" { - this.relationshipClass = "HasMany"; - /** * Returns the result of the relationship. * diff --git a/models/Relationships/HasManyThrough.cfc b/models/Relationships/HasManyThrough.cfc index f67c3462..c1b2990e 100644 --- a/models/Relationships/HasManyThrough.cfc +++ b/models/Relationships/HasManyThrough.cfc @@ -17,8 +17,6 @@ */ component extends="quick.models.Relationships.HasOneOrManyThrough" { - this.relationshipClass = "HasManyThrough"; - /** * Returns the result of the relationship. * diff --git a/models/Relationships/PolymorphicHasMany.cfc b/models/Relationships/PolymorphicHasMany.cfc index 32376d65..8732a395 100644 --- a/models/Relationships/PolymorphicHasMany.cfc +++ b/models/Relationships/PolymorphicHasMany.cfc @@ -20,8 +20,6 @@ */ component extends="quick.models.Relationships.PolymorphicHasOneOrMany" accessors="true" { - this.relationshipClass = "PolymorphicHasMany"; - /** * Returns the result of the relationship. * From 608256861475ebd824b4016dab00c5f8d639b294 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 15:55:41 -0600 Subject: [PATCH 14/34] fix: mark collection relationships at creation --- models/BaseEntity.cfc | 123 +++++++++++----------- models/Relationships/BaseRelationship.cfc | 13 ++- 2 files changed, 74 insertions(+), 62 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index c6be56b1..4b7825a1 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1713,12 +1713,7 @@ component accessors="true" { return arguments.defaultValue; } - var isCollectionRelationship = isInstanceOf( relationship, "quick.models.Relationships.HasMany" ) || - isInstanceOf( relationship, "quick.models.Relationships.HasManyThrough" ) || - isInstanceOf( relationship, "quick.models.Relationships.HasManyDeep" ) || - isInstanceOf( relationship, "quick.models.Relationships.BelongsToMany" ) || - isInstanceOf( relationship, "quick.models.Relationships.PolymorphicHasMany" ); - var unloadedDefault = isCollectionRelationship + var unloadedDefault = relationship.getCollectionRelationship() ? [] : relationship.newDefaultEntity(); if ( isNull( unloadedDefault ) ) { @@ -2009,18 +2004,20 @@ component accessors="true" { param arguments.localKey = keyNames(); arguments.localKey = arrayWrap( arguments.localKey ); - return variables._wirebox.getInstance( - name = "HasMany@quick", - initArguments = { - "related" : related, - "relationName" : arguments.relationName, - "relationMethodName" : arguments.relationMethodName, - "parent" : this, - "foreignKeys" : arguments.foreignKey, - "localKeys" : arguments.localKey, - "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) - } - ); + return variables._wirebox + .getInstance( + name = "HasMany@quick", + initArguments = { + "related" : related, + "relationName" : arguments.relationName, + "relationMethodName" : arguments.relationMethodName, + "parent" : this, + "foreignKeys" : arguments.foreignKey, + "localKeys" : arguments.localKey, + "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) + } + ) + .setCollectionRelationship( true ); } /** @@ -2097,21 +2094,23 @@ component accessors="true" { param arguments.relatedKey = related.keyNames(); arguments.relatedKey = arrayWrap( arguments.relatedKey ); - return variables._wirebox.getInstance( - name = "BelongsToMany@quick", - initArguments = { - "related" : related, - "relationName" : arguments.relationName, - "relationMethodName" : arguments.relationMethodName, - "parent" : this, - "table" : arguments.table, - "foreignPivotKeys" : arguments.foreignPivotKey, - "relatedPivotKeys" : arguments.relatedPivotKey, - "parentKeys" : arguments.parentKey, - "relatedKeys" : arguments.relatedKey, - "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) - } - ); + return variables._wirebox + .getInstance( + name = "BelongsToMany@quick", + initArguments = { + "related" : related, + "relationName" : arguments.relationName, + "relationMethodName" : arguments.relationMethodName, + "parent" : this, + "table" : arguments.table, + "foreignPivotKeys" : arguments.foreignPivotKey, + "relatedPivotKeys" : arguments.relatedPivotKey, + "parentKeys" : arguments.parentKey, + "relatedKeys" : arguments.relatedKey, + "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) + } + ) + .setCollectionRelationship( true ); } /** @@ -2411,19 +2410,21 @@ component accessors="true" { param arguments.localKey = keyNames(); arguments.localKey = arrayWrap( arguments.localKey ); - return variables._wirebox.getInstance( - name = "PolymorphicHasMany@quick", - initArguments = { - "related" : related, - "relationName" : arguments.relationName, - "relationMethodName" : arguments.relationMethodName, - "parent" : this, - "type" : arguments.type, - "ids" : arguments.id, - "localKeys" : arguments.localKey, - "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) - } - ); + return variables._wirebox + .getInstance( + name = "PolymorphicHasMany@quick", + initArguments = { + "related" : related, + "relationName" : arguments.relationName, + "relationMethodName" : arguments.relationMethodName, + "parent" : this, + "type" : arguments.type, + "ids" : arguments.id, + "localKeys" : arguments.localKey, + "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) + } + ) + .setCollectionRelationship( true ); } /** @@ -2561,20 +2562,22 @@ component accessors="true" { return throughEntity; } ); - return variables._wirebox.getInstance( - name = "HasManyDeep@quick", - initArguments = { - "related" : related, - "relationName" : related.getEntity().entityName(), - "relationMethodName" : arguments.relationMethodName, - "parent" : this, - "throughParents" : throughParents, - "foreignKeys" : arguments.foreignKeys, - "localKeys" : arguments.localKeys, - "nested" : arguments.nested, - "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) - } - ); + return variables._wirebox + .getInstance( + name = "HasManyDeep@quick", + initArguments = { + "related" : related, + "relationName" : related.getEntity().entityName(), + "relationMethodName" : arguments.relationMethodName, + "parent" : this, + "throughParents" : throughParents, + "foreignKeys" : arguments.foreignKeys, + "localKeys" : arguments.localKeys, + "nested" : arguments.nested, + "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) + } + ) + .setCollectionRelationship( true ); } private HasManyDeepBuilder function newHasManyDeepBuilder( string relationMethodName ) { diff --git a/models/Relationships/BaseRelationship.cfc b/models/Relationships/BaseRelationship.cfc index 402ae1fe..497d8c9c 100644 --- a/models/Relationships/BaseRelationship.cfc +++ b/models/Relationships/BaseRelationship.cfc @@ -53,6 +53,14 @@ component accessors="true" implements="IRelationship" { */ property name="parent"; + /** + * Whether this relationship returns a collection. + */ + property + name ="collectionRelationship" + type ="boolean" + default="false"; + /** * Used to check for the type of relationship more quickly than using isInstanceOf. */ @@ -77,8 +85,9 @@ component accessors="true" implements="IRelationship" { boolean withConstraints = true, QuickBuilder relationshipBuilder ) { - variables.returnDefaultEntity = false; - variables.defaultAttributes = {}; + variables.returnDefaultEntity = false; + variables.defaultAttributes = {}; + variables.collectionRelationship = false; variables.related = arguments.related; if ( !isNull( arguments.relationshipBuilder ) ) { From dbd4a1080283701d01b90123eae1e3447524761a Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 16:03:31 -0600 Subject: [PATCH 15/34] fix: initialize collection relationships in constructors --- models/BaseEntity.cfc | 120 +++++++++--------- models/Relationships/BaseRelationship.cfc | 5 +- models/Relationships/BelongsToMany.cfc | 14 +- models/Relationships/HasManyDeep.cfc | 18 +-- models/Relationships/HasOneOrMany.cfc | 14 +- .../Relationships/PolymorphicHasOneOrMany.cfc | 18 +-- 6 files changed, 97 insertions(+), 92 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 4b7825a1..2382a7e0 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -2004,20 +2004,19 @@ component accessors="true" { param arguments.localKey = keyNames(); arguments.localKey = arrayWrap( arguments.localKey ); - return variables._wirebox - .getInstance( - name = "HasMany@quick", - initArguments = { - "related" : related, - "relationName" : arguments.relationName, - "relationMethodName" : arguments.relationMethodName, - "parent" : this, - "foreignKeys" : arguments.foreignKey, - "localKeys" : arguments.localKey, - "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) - } - ) - .setCollectionRelationship( true ); + return variables._wirebox.getInstance( + name = "HasMany@quick", + initArguments = { + "related" : related, + "relationName" : arguments.relationName, + "relationMethodName" : arguments.relationMethodName, + "parent" : this, + "foreignKeys" : arguments.foreignKey, + "localKeys" : arguments.localKey, + "collectionRelationship" : true, + "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) + } + ); } /** @@ -2094,23 +2093,22 @@ component accessors="true" { param arguments.relatedKey = related.keyNames(); arguments.relatedKey = arrayWrap( arguments.relatedKey ); - return variables._wirebox - .getInstance( - name = "BelongsToMany@quick", - initArguments = { - "related" : related, - "relationName" : arguments.relationName, - "relationMethodName" : arguments.relationMethodName, - "parent" : this, - "table" : arguments.table, - "foreignPivotKeys" : arguments.foreignPivotKey, - "relatedPivotKeys" : arguments.relatedPivotKey, - "parentKeys" : arguments.parentKey, - "relatedKeys" : arguments.relatedKey, - "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) - } - ) - .setCollectionRelationship( true ); + return variables._wirebox.getInstance( + name = "BelongsToMany@quick", + initArguments = { + "related" : related, + "relationName" : arguments.relationName, + "relationMethodName" : arguments.relationMethodName, + "parent" : this, + "table" : arguments.table, + "foreignPivotKeys" : arguments.foreignPivotKey, + "relatedPivotKeys" : arguments.relatedPivotKey, + "parentKeys" : arguments.parentKey, + "relatedKeys" : arguments.relatedKey, + "collectionRelationship" : true, + "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) + } + ); } /** @@ -2410,21 +2408,20 @@ component accessors="true" { param arguments.localKey = keyNames(); arguments.localKey = arrayWrap( arguments.localKey ); - return variables._wirebox - .getInstance( - name = "PolymorphicHasMany@quick", - initArguments = { - "related" : related, - "relationName" : arguments.relationName, - "relationMethodName" : arguments.relationMethodName, - "parent" : this, - "type" : arguments.type, - "ids" : arguments.id, - "localKeys" : arguments.localKey, - "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) - } - ) - .setCollectionRelationship( true ); + return variables._wirebox.getInstance( + name = "PolymorphicHasMany@quick", + initArguments = { + "related" : related, + "relationName" : arguments.relationName, + "relationMethodName" : arguments.relationMethodName, + "parent" : this, + "type" : arguments.type, + "ids" : arguments.id, + "localKeys" : arguments.localKey, + "collectionRelationship" : true, + "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) + } + ); } /** @@ -2562,22 +2559,21 @@ component accessors="true" { return throughEntity; } ); - return variables._wirebox - .getInstance( - name = "HasManyDeep@quick", - initArguments = { - "related" : related, - "relationName" : related.getEntity().entityName(), - "relationMethodName" : arguments.relationMethodName, - "parent" : this, - "throughParents" : throughParents, - "foreignKeys" : arguments.foreignKeys, - "localKeys" : arguments.localKeys, - "nested" : arguments.nested, - "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) - } - ) - .setCollectionRelationship( true ); + return variables._wirebox.getInstance( + name = "HasManyDeep@quick", + initArguments = { + "related" : related, + "relationName" : related.getEntity().entityName(), + "relationMethodName" : arguments.relationMethodName, + "parent" : this, + "throughParents" : throughParents, + "foreignKeys" : arguments.foreignKeys, + "localKeys" : arguments.localKeys, + "nested" : arguments.nested, + "collectionRelationship" : true, + "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) + } + ); } private HasManyDeepBuilder function newHasManyDeepBuilder( string relationMethodName ) { diff --git a/models/Relationships/BaseRelationship.cfc b/models/Relationships/BaseRelationship.cfc index 497d8c9c..40a9d05d 100644 --- a/models/Relationships/BaseRelationship.cfc +++ b/models/Relationships/BaseRelationship.cfc @@ -82,12 +82,13 @@ component accessors="true" implements="IRelationship" { required string relationName, required string relationMethodName, required any parent, - boolean withConstraints = true, + boolean withConstraints = true, + boolean collectionRelationship = false, QuickBuilder relationshipBuilder ) { variables.returnDefaultEntity = false; variables.defaultAttributes = {}; - variables.collectionRelationship = false; + variables.collectionRelationship = arguments.collectionRelationship; variables.related = arguments.related; if ( !isNull( arguments.relationshipBuilder ) ) { diff --git a/models/Relationships/BelongsToMany.cfc b/models/Relationships/BelongsToMany.cfc index 7bf587cc..a6413bda 100644 --- a/models/Relationships/BelongsToMany.cfc +++ b/models/Relationships/BelongsToMany.cfc @@ -122,7 +122,8 @@ component required array relatedPivotKeys, required array parentKeys, required array relatedKeys, - boolean withConstraints = true + boolean withConstraints = true, + boolean collectionRelationship = false ) { variables.table = arguments.table; variables.parentKeys = arguments.parentKeys; @@ -139,11 +140,12 @@ component variables.pivotEntity = ""; super.init( - related = arguments.related, - relationName = arguments.relationName, - relationMethodName = arguments.relationMethodName, - parent = arguments.parent, - withConstraints = arguments.withConstraints + related = arguments.related, + relationName = arguments.relationName, + relationMethodName = arguments.relationMethodName, + parent = arguments.parent, + withConstraints = arguments.withConstraints, + collectionRelationship = arguments.collectionRelationship ); variables.relationshipBuilder.addEntityTransformer( function( entity ) { diff --git a/models/Relationships/HasManyDeep.cfc b/models/Relationships/HasManyDeep.cfc index de3bdbf3..f2f0e107 100644 --- a/models/Relationships/HasManyDeep.cfc +++ b/models/Relationships/HasManyDeep.cfc @@ -53,8 +53,9 @@ component required array throughParents, required array foreignKeys, required array localKeys, - boolean nested = false, - boolean withConstraints = true + boolean nested = false, + boolean withConstraints = true, + boolean collectionRelationship = false ) { variables.throughParents = arguments.throughParents; variables.localKeys = arguments.localKeys; @@ -63,12 +64,13 @@ component variables.nested = arguments.nested; var instance = super.init( - related = arguments.related.getEntity(), - relationName = arguments.relationName, - relationMethodName = arguments.relationMethodName, - parent = arguments.parent, - withConstraints = arguments.withConstraints, - relationshipBuilder = arguments.related + related = arguments.related.getEntity(), + relationName = arguments.relationName, + relationMethodName = arguments.relationMethodName, + parent = arguments.parent, + withConstraints = arguments.withConstraints, + collectionRelationship = arguments.collectionRelationship, + relationshipBuilder = arguments.related ); performJoin(); diff --git a/models/Relationships/HasOneOrMany.cfc b/models/Relationships/HasOneOrMany.cfc index f0119d07..3ae78b0b 100644 --- a/models/Relationships/HasOneOrMany.cfc +++ b/models/Relationships/HasOneOrMany.cfc @@ -44,17 +44,19 @@ component required any parent, required array foreignKeys, required array localKeys, - boolean withConstraints = true + boolean withConstraints = true, + boolean collectionRelationship = false ) { variables.localKeys = arguments.localKeys; variables.foreignKeys = arguments.foreignKeys; return super.init( - related = arguments.related, - relationName = arguments.relationName, - relationMethodName = arguments.relationMethodName, - parent = arguments.parent, - withConstraints = arguments.withConstraints + related = arguments.related, + relationName = arguments.relationName, + relationMethodName = arguments.relationMethodName, + parent = arguments.parent, + withConstraints = arguments.withConstraints, + collectionRelationship = arguments.collectionRelationship ); } diff --git a/models/Relationships/PolymorphicHasOneOrMany.cfc b/models/Relationships/PolymorphicHasOneOrMany.cfc index 7be7a195..c3001a15 100644 --- a/models/Relationships/PolymorphicHasOneOrMany.cfc +++ b/models/Relationships/PolymorphicHasOneOrMany.cfc @@ -52,19 +52,21 @@ component required string type, required array ids, required array localKeys, - boolean withConstraints = true + boolean withConstraints = true, + boolean collectionRelationship = false ) { variables.morphType = arguments.type; variables.morphMapping = arguments.parent.mappingName(); return super.init( - related = arguments.related, - relationName = arguments.relationName, - relationMethodName = arguments.relationMethodName, - parent = arguments.parent, - foreignKeys = arguments.ids, - localKeys = arguments.localKeys, - withConstraints = arguments.withConstraints + related = arguments.related, + relationName = arguments.relationName, + relationMethodName = arguments.relationMethodName, + parent = arguments.parent, + foreignKeys = arguments.ids, + localKeys = arguments.localKeys, + withConstraints = arguments.withConstraints, + collectionRelationship = arguments.collectionRelationship ); } From ef4578b2fbcd1696d4f3f421cf7f9022a08a9bd8 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 16:10:55 -0600 Subject: [PATCH 16/34] fix: register collection defaults on the entity --- models/BaseEntity.cfc | 104 +++++++++--------- models/Relationships/BaseRelationship.cfc | 16 +-- models/Relationships/BelongsToMany.cfc | 14 +-- models/Relationships/HasManyDeep.cfc | 18 ++- models/Relationships/HasOneOrMany.cfc | 14 +-- .../Relationships/PolymorphicHasOneOrMany.cfc | 18 ++- 6 files changed, 86 insertions(+), 98 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 2382a7e0..0e43f177 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -156,6 +156,11 @@ component accessors="true" { */ property name="_relationshipsLoaded" persistent="false"; + /** + * A map of relationship methods that return collections. + */ + property name="_collectionRelationships" persistent="false"; + /** * Discriminated chilrent property **/ @@ -262,6 +267,7 @@ component accessors="true" { param variables._data = {}; param variables._relationshipsData = {}; param variables._relationshipsLoaded = {}; + param variables._collectionRelationships = {}; param variables._with = []; variables._withoutRelationshipConstraints = createObject( "java", "java.util.HashSet" ).init(); variables._applyingGlobalScopes = false; @@ -1713,7 +1719,7 @@ component accessors="true" { return arguments.defaultValue; } - var unloadedDefault = relationship.getCollectionRelationship() + var unloadedDefault = variables._collectionRelationships.keyExists( arguments.name ) ? [] : relationship.newDefaultEntity(); if ( isNull( unloadedDefault ) ) { @@ -2000,21 +2006,21 @@ component accessors="true" { return entityName() & keyName; } ); } - arguments.foreignKey = arrayWrap( arguments.foreignKey ); - param arguments.localKey = keyNames(); - arguments.localKey = arrayWrap( arguments.localKey ); + arguments.foreignKey = arrayWrap( arguments.foreignKey ); + param arguments.localKey = keyNames(); + arguments.localKey = arrayWrap( arguments.localKey ); + variables._collectionRelationships[ arguments.relationMethodName ] = true; return variables._wirebox.getInstance( name = "HasMany@quick", initArguments = { - "related" : related, - "relationName" : arguments.relationName, - "relationMethodName" : arguments.relationMethodName, - "parent" : this, - "foreignKeys" : arguments.foreignKey, - "localKeys" : arguments.localKey, - "collectionRelationship" : true, - "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) + "related" : related, + "relationName" : arguments.relationName, + "relationMethodName" : arguments.relationMethodName, + "parent" : this, + "foreignKeys" : arguments.foreignKey, + "localKeys" : arguments.localKey, + "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); } @@ -2090,23 +2096,23 @@ component accessors="true" { param arguments.parentKey = keyNames(); arguments.parentKey = arrayWrap( arguments.parentKey ); - param arguments.relatedKey = related.keyNames(); - arguments.relatedKey = arrayWrap( arguments.relatedKey ); + param arguments.relatedKey = related.keyNames(); + arguments.relatedKey = arrayWrap( arguments.relatedKey ); + variables._collectionRelationships[ arguments.relationMethodName ] = true; return variables._wirebox.getInstance( name = "BelongsToMany@quick", initArguments = { - "related" : related, - "relationName" : arguments.relationName, - "relationMethodName" : arguments.relationMethodName, - "parent" : this, - "table" : arguments.table, - "foreignPivotKeys" : arguments.foreignPivotKey, - "relatedPivotKeys" : arguments.relatedPivotKey, - "parentKeys" : arguments.parentKey, - "relatedKeys" : arguments.relatedKey, - "collectionRelationship" : true, - "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) + "related" : related, + "relationName" : arguments.relationName, + "relationMethodName" : arguments.relationMethodName, + "parent" : this, + "table" : arguments.table, + "foreignPivotKeys" : arguments.foreignPivotKey, + "relatedPivotKeys" : arguments.relatedPivotKey, + "parentKeys" : arguments.parentKey, + "relatedKeys" : arguments.relatedKey, + "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); } @@ -2402,24 +2408,24 @@ component accessors="true" { var related = variables._wirebox.getInstance( arguments.relationName ); - param arguments.type = arguments.name & "_type"; - param arguments.id = arguments.name & "_id"; - arguments.id = arrayWrap( arguments.id ); - param arguments.localKey = keyNames(); - arguments.localKey = arrayWrap( arguments.localKey ); + param arguments.type = arguments.name & "_type"; + param arguments.id = arguments.name & "_id"; + arguments.id = arrayWrap( arguments.id ); + param arguments.localKey = keyNames(); + arguments.localKey = arrayWrap( arguments.localKey ); + variables._collectionRelationships[ arguments.relationMethodName ] = true; return variables._wirebox.getInstance( name = "PolymorphicHasMany@quick", initArguments = { - "related" : related, - "relationName" : arguments.relationName, - "relationMethodName" : arguments.relationMethodName, - "parent" : this, - "type" : arguments.type, - "ids" : arguments.id, - "localKeys" : arguments.localKey, - "collectionRelationship" : true, - "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) + "related" : related, + "relationName" : arguments.relationName, + "relationMethodName" : arguments.relationMethodName, + "parent" : this, + "type" : arguments.type, + "ids" : arguments.id, + "localKeys" : arguments.localKey, + "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); } @@ -2524,6 +2530,7 @@ component accessors="true" { if ( !structKeyExists( related, "isBuilder" ) ) { related = related.newQuery(); } + variables._collectionRelationships[ arguments.relationMethodName ] = true; guardAgainstNotLoaded( "This instance is not loaded so it cannot access the [#arguments.relationMethodName#] relationship. Either load the entity from the database using a query executor (like `first`) or base your query off of the [#related.getEntity().entityName()#] entity directly and use the `has` or `whereHas` methods to constrain it based on data in [#entityName()#]." @@ -2562,16 +2569,15 @@ component accessors="true" { return variables._wirebox.getInstance( name = "HasManyDeep@quick", initArguments = { - "related" : related, - "relationName" : related.getEntity().entityName(), - "relationMethodName" : arguments.relationMethodName, - "parent" : this, - "throughParents" : throughParents, - "foreignKeys" : arguments.foreignKeys, - "localKeys" : arguments.localKeys, - "nested" : arguments.nested, - "collectionRelationship" : true, - "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) + "related" : related, + "relationName" : related.getEntity().entityName(), + "relationMethodName" : arguments.relationMethodName, + "parent" : this, + "throughParents" : throughParents, + "foreignKeys" : arguments.foreignKeys, + "localKeys" : arguments.localKeys, + "nested" : arguments.nested, + "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); } diff --git a/models/Relationships/BaseRelationship.cfc b/models/Relationships/BaseRelationship.cfc index 40a9d05d..402ae1fe 100644 --- a/models/Relationships/BaseRelationship.cfc +++ b/models/Relationships/BaseRelationship.cfc @@ -53,14 +53,6 @@ component accessors="true" implements="IRelationship" { */ property name="parent"; - /** - * Whether this relationship returns a collection. - */ - property - name ="collectionRelationship" - type ="boolean" - default="false"; - /** * Used to check for the type of relationship more quickly than using isInstanceOf. */ @@ -82,13 +74,11 @@ component accessors="true" implements="IRelationship" { required string relationName, required string relationMethodName, required any parent, - boolean withConstraints = true, - boolean collectionRelationship = false, + boolean withConstraints = true, QuickBuilder relationshipBuilder ) { - variables.returnDefaultEntity = false; - variables.defaultAttributes = {}; - variables.collectionRelationship = arguments.collectionRelationship; + variables.returnDefaultEntity = false; + variables.defaultAttributes = {}; variables.related = arguments.related; if ( !isNull( arguments.relationshipBuilder ) ) { diff --git a/models/Relationships/BelongsToMany.cfc b/models/Relationships/BelongsToMany.cfc index a6413bda..7bf587cc 100644 --- a/models/Relationships/BelongsToMany.cfc +++ b/models/Relationships/BelongsToMany.cfc @@ -122,8 +122,7 @@ component required array relatedPivotKeys, required array parentKeys, required array relatedKeys, - boolean withConstraints = true, - boolean collectionRelationship = false + boolean withConstraints = true ) { variables.table = arguments.table; variables.parentKeys = arguments.parentKeys; @@ -140,12 +139,11 @@ component variables.pivotEntity = ""; super.init( - related = arguments.related, - relationName = arguments.relationName, - relationMethodName = arguments.relationMethodName, - parent = arguments.parent, - withConstraints = arguments.withConstraints, - collectionRelationship = arguments.collectionRelationship + related = arguments.related, + relationName = arguments.relationName, + relationMethodName = arguments.relationMethodName, + parent = arguments.parent, + withConstraints = arguments.withConstraints ); variables.relationshipBuilder.addEntityTransformer( function( entity ) { diff --git a/models/Relationships/HasManyDeep.cfc b/models/Relationships/HasManyDeep.cfc index f2f0e107..de3bdbf3 100644 --- a/models/Relationships/HasManyDeep.cfc +++ b/models/Relationships/HasManyDeep.cfc @@ -53,9 +53,8 @@ component required array throughParents, required array foreignKeys, required array localKeys, - boolean nested = false, - boolean withConstraints = true, - boolean collectionRelationship = false + boolean nested = false, + boolean withConstraints = true ) { variables.throughParents = arguments.throughParents; variables.localKeys = arguments.localKeys; @@ -64,13 +63,12 @@ component variables.nested = arguments.nested; var instance = super.init( - related = arguments.related.getEntity(), - relationName = arguments.relationName, - relationMethodName = arguments.relationMethodName, - parent = arguments.parent, - withConstraints = arguments.withConstraints, - collectionRelationship = arguments.collectionRelationship, - relationshipBuilder = arguments.related + related = arguments.related.getEntity(), + relationName = arguments.relationName, + relationMethodName = arguments.relationMethodName, + parent = arguments.parent, + withConstraints = arguments.withConstraints, + relationshipBuilder = arguments.related ); performJoin(); diff --git a/models/Relationships/HasOneOrMany.cfc b/models/Relationships/HasOneOrMany.cfc index 3ae78b0b..f0119d07 100644 --- a/models/Relationships/HasOneOrMany.cfc +++ b/models/Relationships/HasOneOrMany.cfc @@ -44,19 +44,17 @@ component required any parent, required array foreignKeys, required array localKeys, - boolean withConstraints = true, - boolean collectionRelationship = false + boolean withConstraints = true ) { variables.localKeys = arguments.localKeys; variables.foreignKeys = arguments.foreignKeys; return super.init( - related = arguments.related, - relationName = arguments.relationName, - relationMethodName = arguments.relationMethodName, - parent = arguments.parent, - withConstraints = arguments.withConstraints, - collectionRelationship = arguments.collectionRelationship + related = arguments.related, + relationName = arguments.relationName, + relationMethodName = arguments.relationMethodName, + parent = arguments.parent, + withConstraints = arguments.withConstraints ); } diff --git a/models/Relationships/PolymorphicHasOneOrMany.cfc b/models/Relationships/PolymorphicHasOneOrMany.cfc index c3001a15..7be7a195 100644 --- a/models/Relationships/PolymorphicHasOneOrMany.cfc +++ b/models/Relationships/PolymorphicHasOneOrMany.cfc @@ -52,21 +52,19 @@ component required string type, required array ids, required array localKeys, - boolean withConstraints = true, - boolean collectionRelationship = false + boolean withConstraints = true ) { variables.morphType = arguments.type; variables.morphMapping = arguments.parent.mappingName(); return super.init( - related = arguments.related, - relationName = arguments.relationName, - relationMethodName = arguments.relationMethodName, - parent = arguments.parent, - foreignKeys = arguments.ids, - localKeys = arguments.localKeys, - withConstraints = arguments.withConstraints, - collectionRelationship = arguments.collectionRelationship + related = arguments.related, + relationName = arguments.relationName, + relationMethodName = arguments.relationMethodName, + parent = arguments.parent, + foreignKeys = arguments.ids, + localKeys = arguments.localKeys, + withConstraints = arguments.withConstraints ); } From ffa12ccef88228fba8815fb79cb84a7c3b08638d Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 16:16:11 -0600 Subject: [PATCH 17/34] fix: register resolved collection names explicitly --- models/BaseEntity.cfc | 71 ++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 0e43f177..bd32e116 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -159,7 +159,8 @@ component accessors="true" { /** * A map of relationship methods that return collections. */ - property name="_collectionRelationships" persistent="false"; + property name="_collectionRelationships" persistent="false"; + property name="_resolvingCollectionRelationship" persistent="false"; /** * Discriminated chilrent property @@ -261,21 +262,22 @@ component accessors="true" { private any function assignDefaultProperties() { assignAttributesData( {} ); assignOriginalAttributes( {} ); - variables._globalScopeExclusions = []; - param variables._key = "id"; - param variables._meta = {}; - param variables._data = {}; - param variables._relationshipsData = {}; - param variables._relationshipsLoaded = {}; - param variables._collectionRelationships = {}; - param variables._with = []; - variables._withoutRelationshipConstraints = createObject( "java", "java.util.HashSet" ).init(); - variables._applyingGlobalScopes = false; - variables._globalScopesApplied = false; - variables._ignoreNotLoadedGuard = false; - variables._withoutFiringEvents = false; - variables._nullValueArgumentSentinel = createObject( "java", "java.lang.Object" ).init(); - param variables._preventLazyLoading = false; + variables._globalScopeExclusions = []; + param variables._key = "id"; + param variables._meta = {}; + param variables._data = {}; + param variables._relationshipsData = {}; + param variables._relationshipsLoaded = {}; + param variables._collectionRelationships = {}; + variables._resolvingCollectionRelationship = false; + param variables._with = []; + variables._withoutRelationshipConstraints = createObject( "java", "java.util.HashSet" ).init(); + variables._applyingGlobalScopes = false; + variables._globalScopesApplied = false; + variables._ignoreNotLoadedGuard = false; + variables._withoutFiringEvents = false; + variables._nullValueArgumentSentinel = createObject( "java", "java.lang.Object" ).init(); + param variables._preventLazyLoading = false; if ( !variables.keyExists( "_lazyLoadingViolationCallback" ) || isNull( variables._lazyLoadingViolationCallback ) ) { variables._lazyLoadingViolationCallback = ( entity, relationName ) => { throw( @@ -1740,10 +1742,15 @@ component accessors="true" { * @return quick.models.Relationships.BaseRelationship */ private any function resolveRelationship( required string name ) { - var relationshipName = arguments.name; - var relationship = ignoreLoadedGuard( function() { + var relationshipName = arguments.name; + variables._resolvingCollectionRelationship = false; + var relationship = ignoreLoadedGuard( function() { return invoke( this, relationshipName ); } ); + if ( variables._resolvingCollectionRelationship ) { + variables._collectionRelationships[ arguments.name ] = true; + } + variables._resolvingCollectionRelationship = false; if ( !isObject( relationship ) || !structKeyExists( relationship, "relationshipClass" ) ) { throwRelationshipNotFound( arguments.name ); } @@ -2006,10 +2013,10 @@ component accessors="true" { return entityName() & keyName; } ); } - arguments.foreignKey = arrayWrap( arguments.foreignKey ); - param arguments.localKey = keyNames(); - arguments.localKey = arrayWrap( arguments.localKey ); - variables._collectionRelationships[ arguments.relationMethodName ] = true; + arguments.foreignKey = arrayWrap( arguments.foreignKey ); + param arguments.localKey = keyNames(); + arguments.localKey = arrayWrap( arguments.localKey ); + variables._resolvingCollectionRelationship = true; return variables._wirebox.getInstance( name = "HasMany@quick", @@ -2096,9 +2103,9 @@ component accessors="true" { param arguments.parentKey = keyNames(); arguments.parentKey = arrayWrap( arguments.parentKey ); - param arguments.relatedKey = related.keyNames(); - arguments.relatedKey = arrayWrap( arguments.relatedKey ); - variables._collectionRelationships[ arguments.relationMethodName ] = true; + param arguments.relatedKey = related.keyNames(); + arguments.relatedKey = arrayWrap( arguments.relatedKey ); + variables._resolvingCollectionRelationship = true; return variables._wirebox.getInstance( name = "BelongsToMany@quick", @@ -2408,12 +2415,12 @@ component accessors="true" { var related = variables._wirebox.getInstance( arguments.relationName ); - param arguments.type = arguments.name & "_type"; - param arguments.id = arguments.name & "_id"; - arguments.id = arrayWrap( arguments.id ); - param arguments.localKey = keyNames(); - arguments.localKey = arrayWrap( arguments.localKey ); - variables._collectionRelationships[ arguments.relationMethodName ] = true; + param arguments.type = arguments.name & "_type"; + param arguments.id = arguments.name & "_id"; + arguments.id = arrayWrap( arguments.id ); + param arguments.localKey = keyNames(); + arguments.localKey = arrayWrap( arguments.localKey ); + variables._resolvingCollectionRelationship = true; return variables._wirebox.getInstance( name = "PolymorphicHasMany@quick", @@ -2530,7 +2537,7 @@ component accessors="true" { if ( !structKeyExists( related, "isBuilder" ) ) { related = related.newQuery(); } - variables._collectionRelationships[ arguments.relationMethodName ] = true; + variables._resolvingCollectionRelationship = true; guardAgainstNotLoaded( "This instance is not loaded so it cannot access the [#arguments.relationMethodName#] relationship. Either load the entity from the database using a query executor (like `first`) or base your query off of the [#related.getEntity().entityName()#] entity directly and use the `has` or `whereHas` methods to constrain it based on data in [#entityName()#]." From ac5fdb18b5abd43c682591e614ddaf7469e9f071 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 16:22:12 -0600 Subject: [PATCH 18/34] fix: return collection metadata across guard scope --- models/BaseEntity.cfc | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index bd32e116..b02b23d8 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1742,15 +1742,19 @@ component accessors="true" { * @return quick.models.Relationships.BaseRelationship */ private any function resolveRelationship( required string name ) { - var relationshipName = arguments.name; - variables._resolvingCollectionRelationship = false; - var relationship = ignoreLoadedGuard( function() { - return invoke( this, relationshipName ); + var relationshipName = arguments.name; + var resolved = ignoreLoadedGuard( function() { + variables._resolvingCollectionRelationship = false; + var resolvedRelationship = invoke( this, relationshipName ); + return { + "relationship" : resolvedRelationship, + "collection" : variables._resolvingCollectionRelationship + }; } ); - if ( variables._resolvingCollectionRelationship ) { + var relationship = resolved.relationship; + if ( resolved.collection ) { variables._collectionRelationships[ arguments.name ] = true; } - variables._resolvingCollectionRelationship = false; if ( !isObject( relationship ) || !structKeyExists( relationship, "relationshipClass" ) ) { throwRelationshipNotFound( arguments.name ); } From bedd6fe99bc3690737114c10f2f9dc7e645f5e10 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 16:28:34 -0600 Subject: [PATCH 19/34] fix: tag collection relationships on public scope --- models/BaseEntity.cfc | 95 ++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 56 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index b02b23d8..e4c525f6 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -156,12 +156,6 @@ component accessors="true" { */ property name="_relationshipsLoaded" persistent="false"; - /** - * A map of relationship methods that return collections. - */ - property name="_collectionRelationships" persistent="false"; - property name="_resolvingCollectionRelationship" persistent="false"; - /** * Discriminated chilrent property **/ @@ -262,22 +256,20 @@ component accessors="true" { private any function assignDefaultProperties() { assignAttributesData( {} ); assignOriginalAttributes( {} ); - variables._globalScopeExclusions = []; - param variables._key = "id"; - param variables._meta = {}; - param variables._data = {}; - param variables._relationshipsData = {}; - param variables._relationshipsLoaded = {}; - param variables._collectionRelationships = {}; - variables._resolvingCollectionRelationship = false; - param variables._with = []; - variables._withoutRelationshipConstraints = createObject( "java", "java.util.HashSet" ).init(); - variables._applyingGlobalScopes = false; - variables._globalScopesApplied = false; - variables._ignoreNotLoadedGuard = false; - variables._withoutFiringEvents = false; - variables._nullValueArgumentSentinel = createObject( "java", "java.lang.Object" ).init(); - param variables._preventLazyLoading = false; + variables._globalScopeExclusions = []; + param variables._key = "id"; + param variables._meta = {}; + param variables._data = {}; + param variables._relationshipsData = {}; + param variables._relationshipsLoaded = {}; + param variables._with = []; + variables._withoutRelationshipConstraints = createObject( "java", "java.util.HashSet" ).init(); + variables._applyingGlobalScopes = false; + variables._globalScopesApplied = false; + variables._ignoreNotLoadedGuard = false; + variables._withoutFiringEvents = false; + variables._nullValueArgumentSentinel = createObject( "java", "java.lang.Object" ).init(); + param variables._preventLazyLoading = false; if ( !variables.keyExists( "_lazyLoadingViolationCallback" ) || isNull( variables._lazyLoadingViolationCallback ) ) { variables._lazyLoadingViolationCallback = ( entity, relationName ) => { throw( @@ -1721,7 +1713,7 @@ component accessors="true" { return arguments.defaultValue; } - var unloadedDefault = variables._collectionRelationships.keyExists( arguments.name ) + var unloadedDefault = structKeyExists( relationship, "collectionRelationship" ) && relationship.collectionRelationship ? [] : relationship.newDefaultEntity(); if ( isNull( unloadedDefault ) ) { @@ -1743,18 +1735,9 @@ component accessors="true" { */ private any function resolveRelationship( required string name ) { var relationshipName = arguments.name; - var resolved = ignoreLoadedGuard( function() { - variables._resolvingCollectionRelationship = false; - var resolvedRelationship = invoke( this, relationshipName ); - return { - "relationship" : resolvedRelationship, - "collection" : variables._resolvingCollectionRelationship - }; + var relationship = ignoreLoadedGuard( function() { + return invoke( this, relationshipName ); } ); - var relationship = resolved.relationship; - if ( resolved.collection ) { - variables._collectionRelationships[ arguments.name ] = true; - } if ( !isObject( relationship ) || !structKeyExists( relationship, "relationshipClass" ) ) { throwRelationshipNotFound( arguments.name ); } @@ -2017,12 +2000,10 @@ component accessors="true" { return entityName() & keyName; } ); } - arguments.foreignKey = arrayWrap( arguments.foreignKey ); - param arguments.localKey = keyNames(); - arguments.localKey = arrayWrap( arguments.localKey ); - variables._resolvingCollectionRelationship = true; - - return variables._wirebox.getInstance( + arguments.foreignKey = arrayWrap( arguments.foreignKey ); + param arguments.localKey = keyNames(); + arguments.localKey = arrayWrap( arguments.localKey ); + var relationship = variables._wirebox.getInstance( name = "HasMany@quick", initArguments = { "related" : related, @@ -2034,6 +2015,8 @@ component accessors="true" { "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); + relationship.collectionRelationship = true; + return relationship; } /** @@ -2107,11 +2090,9 @@ component accessors="true" { param arguments.parentKey = keyNames(); arguments.parentKey = arrayWrap( arguments.parentKey ); - param arguments.relatedKey = related.keyNames(); - arguments.relatedKey = arrayWrap( arguments.relatedKey ); - variables._resolvingCollectionRelationship = true; - - return variables._wirebox.getInstance( + param arguments.relatedKey = related.keyNames(); + arguments.relatedKey = arrayWrap( arguments.relatedKey ); + var relationship = variables._wirebox.getInstance( name = "BelongsToMany@quick", initArguments = { "related" : related, @@ -2126,6 +2107,8 @@ component accessors="true" { "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); + relationship.collectionRelationship = true; + return relationship; } /** @@ -2419,14 +2402,12 @@ component accessors="true" { var related = variables._wirebox.getInstance( arguments.relationName ); - param arguments.type = arguments.name & "_type"; - param arguments.id = arguments.name & "_id"; - arguments.id = arrayWrap( arguments.id ); - param arguments.localKey = keyNames(); - arguments.localKey = arrayWrap( arguments.localKey ); - variables._resolvingCollectionRelationship = true; - - return variables._wirebox.getInstance( + param arguments.type = arguments.name & "_type"; + param arguments.id = arguments.name & "_id"; + arguments.id = arrayWrap( arguments.id ); + param arguments.localKey = keyNames(); + arguments.localKey = arrayWrap( arguments.localKey ); + var relationship = variables._wirebox.getInstance( name = "PolymorphicHasMany@quick", initArguments = { "related" : related, @@ -2439,6 +2420,8 @@ component accessors="true" { "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); + relationship.collectionRelationship = true; + return relationship; } /** @@ -2541,8 +2524,6 @@ component accessors="true" { if ( !structKeyExists( related, "isBuilder" ) ) { related = related.newQuery(); } - variables._resolvingCollectionRelationship = true; - guardAgainstNotLoaded( "This instance is not loaded so it cannot access the [#arguments.relationMethodName#] relationship. Either load the entity from the database using a query executor (like `first`) or base your query off of the [#related.getEntity().entityName()#] entity directly and use the `has` or `whereHas` methods to constrain it based on data in [#entityName()#]." ); @@ -2577,7 +2558,7 @@ component accessors="true" { return throughEntity; } ); - return variables._wirebox.getInstance( + var relationship = variables._wirebox.getInstance( name = "HasManyDeep@quick", initArguments = { "related" : related, @@ -2591,6 +2572,8 @@ component accessors="true" { "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); + relationship.collectionRelationship = true; + return relationship; } private HasManyDeepBuilder function newHasManyDeepBuilder( string relationMethodName ) { From 552924fd82d0f9550883986bf68939ba88a16deb Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 16:33:34 -0600 Subject: [PATCH 20/34] fix: use declared relationship class for defaults --- models/BaseEntity.cfc | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index e4c525f6..1c419a8e 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1713,7 +1713,13 @@ component accessors="true" { return arguments.defaultValue; } - var unloadedDefault = structKeyExists( relationship, "collectionRelationship" ) && relationship.collectionRelationship + var collectionRelationshipClasses = [ + "HasMany", + "BelongsToMany", + "PolymorphicHasMany", + "HasManyDeep" + ]; + var unloadedDefault = collectionRelationshipClasses.contains( relationship.relationshipClass ) ? [] : relationship.newDefaultEntity(); if ( isNull( unloadedDefault ) ) { @@ -2015,7 +2021,7 @@ component accessors="true" { "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); - relationship.collectionRelationship = true; + relationship.relationshipClass = "HasMany"; return relationship; } @@ -2107,7 +2113,6 @@ component accessors="true" { "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); - relationship.collectionRelationship = true; return relationship; } @@ -2420,7 +2425,7 @@ component accessors="true" { "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); - relationship.collectionRelationship = true; + relationship.relationshipClass = "PolymorphicHasMany"; return relationship; } @@ -2572,7 +2577,6 @@ component accessors="true" { "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); - relationship.collectionRelationship = true; return relationship; } From 9398b6aa192908444af1b74d8762ee409c0c0c6c Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 16:38:24 -0600 Subject: [PATCH 21/34] fix: expose collection relationship capability --- models/BaseEntity.cfc | 10 +--------- models/Relationships/BaseRelationship.cfc | 4 ++++ models/Relationships/BelongsToMany.cfc | 4 ++++ models/Relationships/HasMany.cfc | 4 ++++ models/Relationships/HasManyDeep.cfc | 4 ++++ models/Relationships/HasManyThrough.cfc | 4 ++++ models/Relationships/PolymorphicHasMany.cfc | 4 ++++ 7 files changed, 25 insertions(+), 9 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 1c419a8e..4ed51293 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1713,13 +1713,7 @@ component accessors="true" { return arguments.defaultValue; } - var collectionRelationshipClasses = [ - "HasMany", - "BelongsToMany", - "PolymorphicHasMany", - "HasManyDeep" - ]; - var unloadedDefault = collectionRelationshipClasses.contains( relationship.relationshipClass ) + var unloadedDefault = relationship.returnsCollection() ? [] : relationship.newDefaultEntity(); if ( isNull( unloadedDefault ) ) { @@ -2021,7 +2015,6 @@ component accessors="true" { "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); - relationship.relationshipClass = "HasMany"; return relationship; } @@ -2425,7 +2418,6 @@ component accessors="true" { "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); - relationship.relationshipClass = "PolymorphicHasMany"; return relationship; } diff --git a/models/Relationships/BaseRelationship.cfc b/models/Relationships/BaseRelationship.cfc index 402ae1fe..46379f48 100644 --- a/models/Relationships/BaseRelationship.cfc +++ b/models/Relationships/BaseRelationship.cfc @@ -104,6 +104,10 @@ component accessors="true" implements="IRelationship" { ); } + public boolean function returnsCollection() { + return false; + } + /** * Sets the relation method name for this relationship. * diff --git a/models/Relationships/BelongsToMany.cfc b/models/Relationships/BelongsToMany.cfc index 7bf587cc..f08e04d2 100644 --- a/models/Relationships/BelongsToMany.cfc +++ b/models/Relationships/BelongsToMany.cfc @@ -1164,4 +1164,8 @@ component }; } + public boolean function returnsCollection() { + return true; + } + } diff --git a/models/Relationships/HasMany.cfc b/models/Relationships/HasMany.cfc index 687cbd84..98b99d9d 100644 --- a/models/Relationships/HasMany.cfc +++ b/models/Relationships/HasMany.cfc @@ -70,4 +70,8 @@ component extends="quick.models.Relationships.HasOneOrMany" accessors="true" { return matchMany( argumentCollection = arguments ); } + public boolean function returnsCollection() { + return true; + } + } diff --git a/models/Relationships/HasManyDeep.cfc b/models/Relationships/HasManyDeep.cfc index de3bdbf3..9da1a751 100644 --- a/models/Relationships/HasManyDeep.cfc +++ b/models/Relationships/HasManyDeep.cfc @@ -444,4 +444,8 @@ component }; } + public boolean function returnsCollection() { + return true; + } + } diff --git a/models/Relationships/HasManyThrough.cfc b/models/Relationships/HasManyThrough.cfc index c1b2990e..7c88deba 100644 --- a/models/Relationships/HasManyThrough.cfc +++ b/models/Relationships/HasManyThrough.cfc @@ -84,4 +84,8 @@ component extends="quick.models.Relationships.HasOneOrManyThrough" { return arguments.entities; } + public boolean function returnsCollection() { + return true; + } + } diff --git a/models/Relationships/PolymorphicHasMany.cfc b/models/Relationships/PolymorphicHasMany.cfc index 8732a395..605eb128 100644 --- a/models/Relationships/PolymorphicHasMany.cfc +++ b/models/Relationships/PolymorphicHasMany.cfc @@ -120,4 +120,8 @@ component extends="quick.models.Relationships.PolymorphicHasOneOrMany" accessors } ); } + public boolean function returnsCollection() { + return true; + } + } From cfc99bebaca6e4fe6b17b3ddee4db26c3d9aa2d4 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 16:43:05 -0600 Subject: [PATCH 22/34] fix: preserve concrete relationship resolution --- models/BaseEntity.cfc | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 4ed51293..4ef77a8d 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1734,10 +1734,14 @@ component accessors="true" { * @return quick.models.Relationships.BaseRelationship */ private any function resolveRelationship( required string name ) { - var relationshipName = arguments.name; - var relationship = ignoreLoadedGuard( function() { - return invoke( this, relationshipName ); - } ); + var relationship = javacast( "null", "" ); + var wasIgnoringLoadedGuard = variables._ignoreNotLoadedGuard; + variables._ignoreNotLoadedGuard = true; + try { + relationship = invoke( this, arguments.name ); + } finally { + variables._ignoreNotLoadedGuard = wasIgnoringLoadedGuard; + } if ( !isObject( relationship ) || !structKeyExists( relationship, "relationshipClass" ) ) { throwRelationshipNotFound( arguments.name ); } From c3e4614e111858a521ed424f10a029a28389b072 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 16:49:04 -0600 Subject: [PATCH 23/34] fix: probe collection factories across guard scope --- models/BaseEntity.cfc | 39 ++++++++++++++++----- models/Relationships/BaseRelationship.cfc | 4 --- models/Relationships/BelongsToMany.cfc | 4 --- models/Relationships/HasMany.cfc | 4 --- models/Relationships/HasManyDeep.cfc | 4 --- models/Relationships/HasManyThrough.cfc | 4 --- models/Relationships/PolymorphicHasMany.cfc | 4 --- 7 files changed, 30 insertions(+), 33 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 4ef77a8d..f4eeb442 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -156,6 +156,11 @@ component accessors="true" { */ property name="_relationshipsLoaded" persistent="false"; + /** + * A map of relationship methods that return collections. + */ + property name="_collectionRelationships" persistent="false"; + /** * Discriminated chilrent property **/ @@ -262,6 +267,7 @@ component accessors="true" { param variables._data = {}; param variables._relationshipsData = {}; param variables._relationshipsLoaded = {}; + param variables._collectionRelationships = {}; param variables._with = []; variables._withoutRelationshipConstraints = createObject( "java", "java.util.HashSet" ).init(); variables._applyingGlobalScopes = false; @@ -1713,7 +1719,7 @@ component accessors="true" { return arguments.defaultValue; } - var unloadedDefault = relationship.returnsCollection() + var unloadedDefault = variables._collectionRelationships.keyExists( arguments.name ) ? [] : relationship.newDefaultEntity(); if ( isNull( unloadedDefault ) ) { @@ -1734,13 +1740,18 @@ component accessors="true" { * @return quick.models.Relationships.BaseRelationship */ private any function resolveRelationship( required string name ) { - var relationship = javacast( "null", "" ); - var wasIgnoringLoadedGuard = variables._ignoreNotLoadedGuard; - variables._ignoreNotLoadedGuard = true; + var relationshipName = arguments.name; + var collectionProbe = createObject( "java", "java.util.concurrent.atomic.AtomicBoolean" ).init( false ); + variables._relationshipCollectionProbe = collectionProbe; try { - relationship = invoke( this, arguments.name ); + var relationship = ignoreLoadedGuard( function() { + return invoke( this, relationshipName ); + } ); } finally { - variables._ignoreNotLoadedGuard = wasIgnoringLoadedGuard; + structDelete( variables, "_relationshipCollectionProbe" ); + } + if ( collectionProbe.get() ) { + variables._collectionRelationships[ arguments.name ] = true; } if ( !isObject( relationship ) || !structKeyExists( relationship, "relationshipClass" ) ) { throwRelationshipNotFound( arguments.name ); @@ -2007,7 +2018,8 @@ component accessors="true" { arguments.foreignKey = arrayWrap( arguments.foreignKey ); param arguments.localKey = keyNames(); arguments.localKey = arrayWrap( arguments.localKey ); - var relationship = variables._wirebox.getInstance( + markCollectionRelationship(); + var relationship = variables._wirebox.getInstance( name = "HasMany@quick", initArguments = { "related" : related, @@ -2095,7 +2107,8 @@ component accessors="true" { param arguments.relatedKey = related.keyNames(); arguments.relatedKey = arrayWrap( arguments.relatedKey ); - var relationship = variables._wirebox.getInstance( + markCollectionRelationship(); + var relationship = variables._wirebox.getInstance( name = "BelongsToMany@quick", initArguments = { "related" : related, @@ -2409,7 +2422,8 @@ component accessors="true" { arguments.id = arrayWrap( arguments.id ); param arguments.localKey = keyNames(); arguments.localKey = arrayWrap( arguments.localKey ); - var relationship = variables._wirebox.getInstance( + markCollectionRelationship(); + var relationship = variables._wirebox.getInstance( name = "PolymorphicHasMany@quick", initArguments = { "related" : related, @@ -2525,6 +2539,7 @@ component accessors="true" { if ( !structKeyExists( related, "isBuilder" ) ) { related = related.newQuery(); } + markCollectionRelationship(); guardAgainstNotLoaded( "This instance is not loaded so it cannot access the [#arguments.relationMethodName#] relationship. Either load the entity from the database using a query executor (like `first`) or base your query off of the [#related.getEntity().entityName()#] entity directly and use the `has` or `whereHas` methods to constrain it based on data in [#entityName()#]." ); @@ -3764,6 +3779,12 @@ component accessors="true" { * * @callback The callback to run without any loaded entity guarding. */ + private void function markCollectionRelationship() { + if ( variables.keyExists( "_relationshipCollectionProbe" ) ) { + variables._relationshipCollectionProbe.set( true ); + } + } + public any function ignoreLoadedGuard( required any callback ) { variables._ignoreNotLoadedGuard = true; try { diff --git a/models/Relationships/BaseRelationship.cfc b/models/Relationships/BaseRelationship.cfc index 46379f48..402ae1fe 100644 --- a/models/Relationships/BaseRelationship.cfc +++ b/models/Relationships/BaseRelationship.cfc @@ -104,10 +104,6 @@ component accessors="true" implements="IRelationship" { ); } - public boolean function returnsCollection() { - return false; - } - /** * Sets the relation method name for this relationship. * diff --git a/models/Relationships/BelongsToMany.cfc b/models/Relationships/BelongsToMany.cfc index f08e04d2..7bf587cc 100644 --- a/models/Relationships/BelongsToMany.cfc +++ b/models/Relationships/BelongsToMany.cfc @@ -1164,8 +1164,4 @@ component }; } - public boolean function returnsCollection() { - return true; - } - } diff --git a/models/Relationships/HasMany.cfc b/models/Relationships/HasMany.cfc index 98b99d9d..687cbd84 100644 --- a/models/Relationships/HasMany.cfc +++ b/models/Relationships/HasMany.cfc @@ -70,8 +70,4 @@ component extends="quick.models.Relationships.HasOneOrMany" accessors="true" { return matchMany( argumentCollection = arguments ); } - public boolean function returnsCollection() { - return true; - } - } diff --git a/models/Relationships/HasManyDeep.cfc b/models/Relationships/HasManyDeep.cfc index 9da1a751..de3bdbf3 100644 --- a/models/Relationships/HasManyDeep.cfc +++ b/models/Relationships/HasManyDeep.cfc @@ -444,8 +444,4 @@ component }; } - public boolean function returnsCollection() { - return true; - } - } diff --git a/models/Relationships/HasManyThrough.cfc b/models/Relationships/HasManyThrough.cfc index 7c88deba..c1b2990e 100644 --- a/models/Relationships/HasManyThrough.cfc +++ b/models/Relationships/HasManyThrough.cfc @@ -84,8 +84,4 @@ component extends="quick.models.Relationships.HasOneOrManyThrough" { return arguments.entities; } - public boolean function returnsCollection() { - return true; - } - } diff --git a/models/Relationships/PolymorphicHasMany.cfc b/models/Relationships/PolymorphicHasMany.cfc index 605eb128..8732a395 100644 --- a/models/Relationships/PolymorphicHasMany.cfc +++ b/models/Relationships/PolymorphicHasMany.cfc @@ -120,8 +120,4 @@ component extends="quick.models.Relationships.PolymorphicHasOneOrMany" accessors } ); } - public boolean function returnsCollection() { - return true; - } - } From 63322255b78483b79f9d31a26447db083e0c1a13 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 16:53:26 -0600 Subject: [PATCH 24/34] fix: persist the collection relationship probe --- models/BaseEntity.cfc | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index f4eeb442..096f0275 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -159,7 +159,8 @@ component accessors="true" { /** * A map of relationship methods that return collections. */ - property name="_collectionRelationships" persistent="false"; + property name="_collectionRelationships" persistent="false"; + property name="_relationshipCollectionProbe" persistent="false"; /** * Discriminated chilrent property @@ -261,13 +262,16 @@ component accessors="true" { private any function assignDefaultProperties() { assignAttributesData( {} ); assignOriginalAttributes( {} ); - variables._globalScopeExclusions = []; - param variables._key = "id"; - param variables._meta = {}; - param variables._data = {}; - param variables._relationshipsData = {}; - param variables._relationshipsLoaded = {}; - param variables._collectionRelationships = {}; + variables._globalScopeExclusions = []; + param variables._key = "id"; + param variables._meta = {}; + param variables._data = {}; + param variables._relationshipsData = {}; + param variables._relationshipsLoaded = {}; + param variables._collectionRelationships = {}; + variables._relationshipCollectionProbe = createObject( "java", "java.util.concurrent.atomic.AtomicBoolean" ).init( + false + ); param variables._with = []; variables._withoutRelationshipConstraints = createObject( "java", "java.util.HashSet" ).init(); variables._applyingGlobalScopes = false; @@ -1740,16 +1744,12 @@ component accessors="true" { * @return quick.models.Relationships.BaseRelationship */ private any function resolveRelationship( required string name ) { - var relationshipName = arguments.name; - var collectionProbe = createObject( "java", "java.util.concurrent.atomic.AtomicBoolean" ).init( false ); - variables._relationshipCollectionProbe = collectionProbe; - try { - var relationship = ignoreLoadedGuard( function() { - return invoke( this, relationshipName ); - } ); - } finally { - structDelete( variables, "_relationshipCollectionProbe" ); - } + var relationshipName = arguments.name; + var collectionProbe = variables._relationshipCollectionProbe; + collectionProbe.set( false ); + var relationship = ignoreLoadedGuard( function() { + return invoke( this, relationshipName ); + } ); if ( collectionProbe.get() ) { variables._collectionRelationships[ arguments.name ] = true; } From 504f520a0aaac0328a7989c2f34f0677b0b897dc Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 16:59:15 -0600 Subject: [PATCH 25/34] fix: share collection probes across relationship callbacks (#177) --- models/BaseEntity.cfc | 49 ++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 096f0275..8b305a36 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -159,8 +159,7 @@ component accessors="true" { /** * A map of relationship methods that return collections. */ - property name="_collectionRelationships" persistent="false"; - property name="_relationshipCollectionProbe" persistent="false"; + property name="_collectionRelationships" persistent="false"; /** * Discriminated chilrent property @@ -262,16 +261,13 @@ component accessors="true" { private any function assignDefaultProperties() { assignAttributesData( {} ); assignOriginalAttributes( {} ); - variables._globalScopeExclusions = []; - param variables._key = "id"; - param variables._meta = {}; - param variables._data = {}; - param variables._relationshipsData = {}; - param variables._relationshipsLoaded = {}; - param variables._collectionRelationships = {}; - variables._relationshipCollectionProbe = createObject( "java", "java.util.concurrent.atomic.AtomicBoolean" ).init( - false - ); + variables._globalScopeExclusions = []; + param variables._key = "id"; + param variables._meta = {}; + param variables._data = {}; + param variables._relationshipsData = {}; + param variables._relationshipsLoaded = {}; + param variables._collectionRelationships = {}; param variables._with = []; variables._withoutRelationshipConstraints = createObject( "java", "java.util.HashSet" ).init(); variables._applyingGlobalScopes = false; @@ -1745,11 +1741,25 @@ component accessors="true" { */ private any function resolveRelationship( required string name ) { var relationshipName = arguments.name; - var collectionProbe = variables._relationshipCollectionProbe; - collectionProbe.set( false ); - var relationship = ignoreLoadedGuard( function() { - return invoke( this, relationshipName ); - } ); + var probeKey = "__quickRelationshipCollectionProbe"; + var previousProbe = {}; + var hadPreviousProbe = request.keyExists( probeKey ); + if ( hadPreviousProbe ) { + previousProbe.value = request[ probeKey ]; + } + var collectionProbe = createObject( "java", "java.util.concurrent.atomic.AtomicBoolean" ).init( false ); + request[ probeKey ] = collectionProbe; + try { + var relationship = ignoreLoadedGuard( function() { + return invoke( this, relationshipName ); + } ); + } finally { + if ( hadPreviousProbe ) { + request[ probeKey ] = previousProbe.value; + } else { + structDelete( request, probeKey ); + } + } if ( collectionProbe.get() ) { variables._collectionRelationships[ arguments.name ] = true; } @@ -3780,8 +3790,9 @@ component accessors="true" { * @callback The callback to run without any loaded entity guarding. */ private void function markCollectionRelationship() { - if ( variables.keyExists( "_relationshipCollectionProbe" ) ) { - variables._relationshipCollectionProbe.set( true ); + var probeKey = "__quickRelationshipCollectionProbe"; + if ( request.keyExists( probeKey ) ) { + request[ probeKey ].set( true ); } } From ce495db63a83f6652412a7ce8999925845355b90 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 17:05:20 -0600 Subject: [PATCH 26/34] fix: initialize unloaded relationships through relationship API (#177) --- models/BaseEntity.cfc | 87 ++++++++++++------------------------------- 1 file changed, 23 insertions(+), 64 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 8b305a36..8d68c595 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -156,11 +156,6 @@ component accessors="true" { */ property name="_relationshipsLoaded" persistent="false"; - /** - * A map of relationship methods that return collections. - */ - property name="_collectionRelationships" persistent="false"; - /** * Discriminated chilrent property **/ @@ -267,7 +262,6 @@ component accessors="true" { param variables._data = {}; param variables._relationshipsData = {}; param variables._relationshipsLoaded = {}; - param variables._collectionRelationships = {}; param variables._with = []; variables._withoutRelationshipConstraints = createObject( "java", "java.util.HashSet" ).init(); variables._applyingGlobalScopes = false; @@ -1719,15 +1713,8 @@ component accessors="true" { return arguments.defaultValue; } - var unloadedDefault = variables._collectionRelationships.keyExists( arguments.name ) - ? [] - : relationship.newDefaultEntity(); - if ( isNull( unloadedDefault ) ) { - variables._relationshipsLoaded[ arguments.name ] = true; - return javacast( "null", "" ); - } - assignRelationship( arguments.name, unloadedDefault ); - return unloadedDefault; + relationship.initRelation( [ this ], arguments.name ); + return retrieveRelationship( arguments.name ); } /** @@ -1739,33 +1726,27 @@ component accessors="true" { * * @return quick.models.Relationships.BaseRelationship */ - private any function resolveRelationship( required string name ) { - var relationshipName = arguments.name; - var probeKey = "__quickRelationshipCollectionProbe"; - var previousProbe = {}; - var hadPreviousProbe = request.keyExists( probeKey ); - if ( hadPreviousProbe ) { - previousProbe.value = request[ probeKey ]; - } - var collectionProbe = createObject( "java", "java.util.concurrent.atomic.AtomicBoolean" ).init( false ); - request[ probeKey ] = collectionProbe; + private any function resolveRelationship( required string name, any relationshipArguments = {} ) { + var previousIgnoreLoadedGuard = variables._ignoreNotLoadedGuard; + variables._ignoreNotLoadedGuard = true; + var resolvedRelationshipContainer = {}; try { - var relationship = ignoreLoadedGuard( function() { - return invoke( this, relationshipName ); - } ); + resolvedRelationshipContainer.value = invoke( + this, + arguments.name, + arguments.relationshipArguments + ); } finally { - if ( hadPreviousProbe ) { - request[ probeKey ] = previousProbe.value; - } else { - structDelete( request, probeKey ); - } + variables._ignoreNotLoadedGuard = previousIgnoreLoadedGuard; } - if ( collectionProbe.get() ) { - variables._collectionRelationships[ arguments.name ] = true; - } - if ( !isObject( relationship ) || !structKeyExists( relationship, "relationshipClass" ) ) { + if ( + !resolvedRelationshipContainer.keyExists( "value" ) || + !isObject( resolvedRelationshipContainer.value ) || + !structKeyExists( resolvedRelationshipContainer.value, "relationshipClass" ) + ) { throwRelationshipNotFound( arguments.name ); } + var relationship = resolvedRelationshipContainer.value; relationship.setRelationMethodName( arguments.name ); return relationship; } @@ -2028,8 +2009,7 @@ component accessors="true" { arguments.foreignKey = arrayWrap( arguments.foreignKey ); param arguments.localKey = keyNames(); arguments.localKey = arrayWrap( arguments.localKey ); - markCollectionRelationship(); - var relationship = variables._wirebox.getInstance( + return variables._wirebox.getInstance( name = "HasMany@quick", initArguments = { "related" : related, @@ -2041,7 +2021,6 @@ component accessors="true" { "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); - return relationship; } /** @@ -2117,8 +2096,7 @@ component accessors="true" { param arguments.relatedKey = related.keyNames(); arguments.relatedKey = arrayWrap( arguments.relatedKey ); - markCollectionRelationship(); - var relationship = variables._wirebox.getInstance( + return variables._wirebox.getInstance( name = "BelongsToMany@quick", initArguments = { "related" : related, @@ -2133,7 +2111,6 @@ component accessors="true" { "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); - return relationship; } /** @@ -2432,8 +2409,7 @@ component accessors="true" { arguments.id = arrayWrap( arguments.id ); param arguments.localKey = keyNames(); arguments.localKey = arrayWrap( arguments.localKey ); - markCollectionRelationship(); - var relationship = variables._wirebox.getInstance( + return variables._wirebox.getInstance( name = "PolymorphicHasMany@quick", initArguments = { "related" : related, @@ -2446,7 +2422,6 @@ component accessors="true" { "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); - return relationship; } /** @@ -2549,7 +2524,6 @@ component accessors="true" { if ( !structKeyExists( related, "isBuilder" ) ) { related = related.newQuery(); } - markCollectionRelationship(); guardAgainstNotLoaded( "This instance is not loaded so it cannot access the [#arguments.relationMethodName#] relationship. Either load the entity from the database using a query executor (like `first`) or base your query off of the [#related.getEntity().entityName()#] entity directly and use the `has` or `whereHas` methods to constrain it based on data in [#entityName()#]." ); @@ -2584,7 +2558,7 @@ component accessors="true" { return throughEntity; } ); - var relationship = variables._wirebox.getInstance( + return variables._wirebox.getInstance( name = "HasManyDeep@quick", initArguments = { "related" : related, @@ -2598,7 +2572,6 @@ component accessors="true" { "withConstraints" : !shouldSkipRelationshipConstraints( arguments.relationMethodName ) } ); - return relationship; } private HasManyDeepBuilder function newHasManyDeepBuilder( string relationMethodName ) { @@ -2822,14 +2795,7 @@ component accessors="true" { if ( !isRelationshipLoaded( relationshipName ) && !isLoaded() ) { var relationshipArguments = arguments.missingMethodArguments; - var unloadedRelationship = ignoreLoadedGuard( function() { - return invoke( - this, - relationshipName, - relationshipArguments - ); - } ); - unloadedRelationship.setRelationMethodName( relationshipName ); + var unloadedRelationship = resolveRelationship( relationshipName, relationshipArguments ); unloadedRelationship.initRelation( [ this ], relationshipName ); return retrieveRelationship( relationshipName ); } @@ -3789,13 +3755,6 @@ component accessors="true" { * * @callback The callback to run without any loaded entity guarding. */ - private void function markCollectionRelationship() { - var probeKey = "__quickRelationshipCollectionProbe"; - if ( request.keyExists( probeKey ) ) { - request[ probeKey ].set( true ); - } - } - public any function ignoreLoadedGuard( required any callback ) { variables._ignoreNotLoadedGuard = true; try { From 35dee0806f4fcd90dd28d7a47906ebb29211f50f Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 17:10:07 -0600 Subject: [PATCH 27/34] fix: initialize defaults before relationship return boundaries (#177) --- models/BaseEntity.cfc | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 8d68c595..0974983d 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1707,26 +1707,24 @@ component accessors="true" { throwRelationshipNotFound( arguments.name ); } - var relationship = resolveRelationship( arguments.name ); if ( arguments.keyExists( "defaultValue" ) ) { assignRelationship( arguments.name, arguments.defaultValue ); return arguments.defaultValue; } - relationship.initRelation( [ this ], arguments.name ); + initializeUnloadedRelationship( arguments.name ); return retrieveRelationship( arguments.name ); } /** - * Resolves and validates a relationship definition by name. + * Resolves and initializes an unloaded relationship by name. * * @name The relationship method name to resolve. * * @throws RelationshipNotFound * - * @return quick.models.Relationships.BaseRelationship */ - private any function resolveRelationship( required string name, any relationshipArguments = {} ) { + private void function initializeUnloadedRelationship( required string name, any relationshipArguments = {} ) { var previousIgnoreLoadedGuard = variables._ignoreNotLoadedGuard; variables._ignoreNotLoadedGuard = true; var resolvedRelationshipContainer = {}; @@ -1748,7 +1746,7 @@ component accessors="true" { } var relationship = resolvedRelationshipContainer.value; relationship.setRelationMethodName( arguments.name ); - return relationship; + relationship.initRelation( [ this ], arguments.name ); } /** @@ -2795,8 +2793,7 @@ component accessors="true" { if ( !isRelationshipLoaded( relationshipName ) && !isLoaded() ) { var relationshipArguments = arguments.missingMethodArguments; - var unloadedRelationship = resolveRelationship( relationshipName, relationshipArguments ); - unloadedRelationship.initRelation( [ this ], relationshipName ); + initializeUnloadedRelationship( relationshipName, relationshipArguments ); return retrieveRelationship( relationshipName ); } From 36faa503c5841eff31829005bc9372f4f7fc550b Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 17:46:53 -0600 Subject: [PATCH 28/34] fix: pass explicit relationship arguments during initialization (#177) --- models/BaseEntity.cfc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 0974983d..cbd9a448 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1712,7 +1712,7 @@ component accessors="true" { return arguments.defaultValue; } - initializeUnloadedRelationship( arguments.name ); + initializeUnloadedRelationship( arguments.name, {} ); return retrieveRelationship( arguments.name ); } @@ -1724,7 +1724,7 @@ component accessors="true" { * @throws RelationshipNotFound * */ - private void function initializeUnloadedRelationship( required string name, any relationshipArguments = {} ) { + private void function initializeUnloadedRelationship( required string name, struct relationshipArguments = {} ) { var previousIgnoreLoadedGuard = variables._ignoreNotLoadedGuard; variables._ignoreNotLoadedGuard = true; var resolvedRelationshipContainer = {}; From e9370c74280db91e06cea56eac4fda593f73f172 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 17:54:17 -0600 Subject: [PATCH 29/34] fix: initialize new entity relations through public getters (#177) --- models/BaseEntity.cfc | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index cbd9a448..4dad123c 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1686,9 +1686,9 @@ component accessors="true" { } /** - * Retrieves the result of a loaded relationship. If the relationship has not - * been loaded, initializes and returns its relationship type default without - * executing a query. An explicit default value can be supplied instead. + * Retrieves the result of a loaded relationship. For a new entity, an unloaded + * relationship is initialized through its public getter without executing a + * query. An explicit default value can be supplied instead. * * @name The relationship name to retrieve. * @defaultValue An optional value to assign and return when the relationship @@ -1712,8 +1712,10 @@ component accessors="true" { return arguments.defaultValue; } - initializeUnloadedRelationship( arguments.name, {} ); - return retrieveRelationship( arguments.name ); + if ( !isLoaded() ) { + return invoke( this, "get#arguments.name#" ); + } + return javacast( "null", "" ); } /** From 25e4630fb82c0aac21a6091110fc77a936bef437 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 17:58:20 -0600 Subject: [PATCH 30/34] fix: dispatch unloaded relationship getters consistently (#177) --- models/BaseEntity.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 4dad123c..5c0639a1 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1713,7 +1713,7 @@ component accessors="true" { } if ( !isLoaded() ) { - return invoke( this, "get#arguments.name#" ); + return onMissingMethod( "get#arguments.name#", {} ); } return javacast( "null", "" ); } From 37133e5d32a54b60ad9834c8dc6fe1e8bc87a6a1 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 18:02:42 -0600 Subject: [PATCH 31/34] fix: read initialized relationship state directly (#177) --- models/BaseEntity.cfc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 5c0639a1..71d43e38 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1713,7 +1713,10 @@ component accessors="true" { } if ( !isLoaded() ) { - return onMissingMethod( "get#arguments.name#", {} ); + onMissingMethod( "get#arguments.name#", {} ); + return variables._relationshipsData.keyExists( arguments.name ) + ? variables._relationshipsData[ arguments.name ] + : javacast( "null", "" ); } return javacast( "null", "" ); } From 2a148d5df82df9d329f71c6bab1861d0e9e89804 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 18:07:21 -0600 Subject: [PATCH 32/34] fix: initialize retrieved relationships in caller scope (#177) --- models/BaseEntity.cfc | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 71d43e38..3666400c 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1713,7 +1713,23 @@ component accessors="true" { } if ( !isLoaded() ) { - onMissingMethod( "get#arguments.name#", {} ); + var previousIgnoreLoadedGuard = variables._ignoreNotLoadedGuard; + variables._ignoreNotLoadedGuard = true; + var resolvedRelationshipContainer = {}; + try { + resolvedRelationshipContainer.value = invoke( this, arguments.name, {} ); + } finally { + variables._ignoreNotLoadedGuard = previousIgnoreLoadedGuard; + } + if ( + !resolvedRelationshipContainer.keyExists( "value" ) || + !isObject( resolvedRelationshipContainer.value ) || + !structKeyExists( resolvedRelationshipContainer.value, "relationshipClass" ) + ) { + throwRelationshipNotFound( arguments.name ); + } + resolvedRelationshipContainer.value.setRelationMethodName( arguments.name ); + resolvedRelationshipContainer.value.initRelation( [ this ], arguments.name ); return variables._relationshipsData.keyExists( arguments.name ) ? variables._relationshipsData[ arguments.name ] : javacast( "null", "" ); From 0127361c27c0ad72a91aa5aab224bcf27a145e2c Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 18:11:50 -0600 Subject: [PATCH 33/34] fix: reuse the relationship getter initialization path (#177) --- models/BaseEntity.cfc | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 3666400c..25c0007f 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1713,23 +1713,7 @@ component accessors="true" { } if ( !isLoaded() ) { - var previousIgnoreLoadedGuard = variables._ignoreNotLoadedGuard; - variables._ignoreNotLoadedGuard = true; - var resolvedRelationshipContainer = {}; - try { - resolvedRelationshipContainer.value = invoke( this, arguments.name, {} ); - } finally { - variables._ignoreNotLoadedGuard = previousIgnoreLoadedGuard; - } - if ( - !resolvedRelationshipContainer.keyExists( "value" ) || - !isObject( resolvedRelationshipContainer.value ) || - !structKeyExists( resolvedRelationshipContainer.value, "relationshipClass" ) - ) { - throwRelationshipNotFound( arguments.name ); - } - resolvedRelationshipContainer.value.setRelationMethodName( arguments.name ); - resolvedRelationshipContainer.value.initRelation( [ this ], arguments.name ); + tryRelationshipGetter( "get#arguments.name#", {} ); return variables._relationshipsData.keyExists( arguments.name ) ? variables._relationshipsData[ arguments.name ] : javacast( "null", "" ); From 0f17fc8f6cfec9ed377c0d8796191a998979f5e1 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 18:36:55 -0600 Subject: [PATCH 34/34] fix: distinguish omitted relationship defaults from null (#177) --- models/BaseEntity.cfc | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 25c0007f..a7a1a2ce 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1696,7 +1696,10 @@ component accessors="true" { * * @return quick.models.BaseEntity | [quick.models.BaseEntity] */ - public any function retrieveRelationship( required string name, any defaultValue ) { + public any function retrieveRelationship( + required string name, + any defaultValue = variables._nullValueArgumentSentinel + ) { if ( variables._relationshipsData.keyExists( arguments.name ) ) { return variables._relationshipsData[ arguments.name ]; } @@ -1707,16 +1710,14 @@ component accessors="true" { throwRelationshipNotFound( arguments.name ); } - if ( arguments.keyExists( "defaultValue" ) ) { + if ( !variables._nullValueArgumentSentinel.equals( arguments.defaultValue ) ) { assignRelationship( arguments.name, arguments.defaultValue ); return arguments.defaultValue; } if ( !isLoaded() ) { - tryRelationshipGetter( "get#arguments.name#", {} ); - return variables._relationshipsData.keyExists( arguments.name ) - ? variables._relationshipsData[ arguments.name ] - : javacast( "null", "" ); + initializeUnloadedRelationship( arguments.name, {} ); + return retrieveRelationship( arguments.name ); } return javacast( "null", "" ); }