From 71d9c5a3ee8067954995aed8513368533022b003 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 5 Feb 2025 09:55:56 -0700 Subject: [PATCH] fix(HasManyDeep): Account for compound keys in hasManyDeep relationships --- models/Relationships/HasManyDeep.cfc | 38 +++++++++++++------ server.json | 2 +- tests/resources/app/models/Country.cfc | 8 ++++ .../app/models/inLeague/PlayingField.cfc | 2 +- ..._11_102821_create_playing_fields_table.cfc | 7 +++- .../Relationships/HasManyThroughSpec.cfc | 13 +++++++ 6 files changed, 54 insertions(+), 16 deletions(-) diff --git a/models/Relationships/HasManyDeep.cfc b/models/Relationships/HasManyDeep.cfc index fdec80ed..5edfb7fe 100644 --- a/models/Relationships/HasManyDeep.cfc +++ b/models/Relationships/HasManyDeep.cfc @@ -159,28 +159,38 @@ component required any localKey ) { var joins = []; - if ( isArray( arguments.localKey ) ) { + + arguments.localKey = arrayWrap( arguments.localKey ); + arguments.foreignKey = arrayWrap( arguments.foreignKey ); + + // This handles polymorphic relationships + if ( arguments.localKey.len() > arguments.foreignKey.len() ) { arguments.builder.where( arguments.throughParent.qualifyColumn( arguments.localKey[ 1 ] ), arguments.predecessor.mappingName() ); - arguments.localKey = arguments.localKey[ 2 ]; + arguments.localKey = arraySlice( arguments.localKey, 2 ); } - if ( isArray( arguments.foreignKey ) ) { + // This handles polymorphic relationships + if ( arguments.foreignKey.len() > arguments.localKey.len() ) { arguments.builder.where( arguments.predecessor.qualifyColumn( arguments.foreignKey[ 1 ] ), arguments.throughParent.mappingName() ); - arguments.foreignKey = arguments.foreignKey[ 2 ]; + arguments.foreignKey = arraySlice( arguments.foreignKey, 2 ); } - joins.append( [ - arguments.throughParent.qualifyColumn( arguments.localKey ), - arguments.predecessor.qualifyColumn( arguments.foreignKey ) - ] ); + guardAgainstKeyLengthMismatch( arguments.foreignKey, arguments.localKey ); + + for ( var i = 1; i <= arguments.localKey.len(); i++ ) { + joins.append( [ + arguments.throughParent.qualifyColumn( arguments.localKey[ i ] ), + arguments.predecessor.qualifyColumn( arguments.foreignKey[ i ] ) + ] ); + } return joins; } @@ -242,15 +252,19 @@ component .split( "\s(?:[Aa][Ss]\s)?" ); var alias = segments[ 2 ] ?: ""; - var localKeys = []; + var qualifiedLocalKeys = []; for ( var i = 1; i <= variables.localKeys.len(); i++ ) { if ( i == 1 ) { - localKeys.append( variables.parent.qualifyColumn( variables.localKeys[ i ] ) ); + arrayWrap( variables.localKeys[ i ] ).each( function( localKey ) { + qualifiedLocalKeys.append( variables.parent.qualifyColumn( localKey ) ); + } ); } else { - localKeys.append( variables.throughParents[ i - 1 ].qualifyColumn( variables.localKeys[ i ] ) ); + arrayWrap( variables.localKeys[ i ] ).each( function( localKey ) { + qualifiedLocalKeys.append( variables.throughParents[ i - 1 ].qualifyColumn( localKey ) ); + } ); } } - return localKeys; + return qualifiedLocalKeys; } public boolean function addEagerConstraints( required array entities, required any baseEntity ) { diff --git a/server.json b/server.json index d980e6ed..913de7f6 100644 --- a/server.json +++ b/server.json @@ -1,6 +1,6 @@ { "app":{ - "cfengine":"adobe@2021" + "cfengine":"adobe@2023" }, "JVM":{ "javaVersion":"openjdk21" diff --git a/tests/resources/app/models/Country.cfc b/tests/resources/app/models/Country.cfc index b45fb21c..a902bd8e 100644 --- a/tests/resources/app/models/Country.cfc +++ b/tests/resources/app/models/Country.cfc @@ -204,6 +204,14 @@ component extends="quick.models.BaseEntity" accessors="true" { return hasManyThrough( [ "roles", "permissions" ] ); } + function playingFields() { + return hasMany( "PlayingField", "country_id" ); + } + + function games() { + return hasManyThrough( [ "playingFields", "games" ] ); + } + function keyType() { return variables._wirebox.getInstance( "UUIDKeyType@quick" ); } diff --git a/tests/resources/app/models/inLeague/PlayingField.cfc b/tests/resources/app/models/inLeague/PlayingField.cfc index 3a5d465a..5c89161d 100644 --- a/tests/resources/app/models/inLeague/PlayingField.cfc +++ b/tests/resources/app/models/inLeague/PlayingField.cfc @@ -10,7 +10,7 @@ component extends="quick.models.BaseEntity" accessors="true" { return variables._wirebox.getInstance( "NullKeyType@quick" ); } - function field() { + function games() { return hasMany( relationName = "Game", foreignKey = [ "fieldID", "clientID" ], diff --git a/tests/resources/database/migrations/2020_08_11_102821_create_playing_fields_table.cfc b/tests/resources/database/migrations/2020_08_11_102821_create_playing_fields_table.cfc index 73dae2fd..40c87f03 100755 --- a/tests/resources/database/migrations/2020_08_11_102821_create_playing_fields_table.cfc +++ b/tests/resources/database/migrations/2020_08_11_102821_create_playing_fields_table.cfc @@ -6,18 +6,21 @@ component { t.unsignedInteger( "clientID" ).nullable(); t.string( "fieldName" ); t.primaryKey( [ "fieldID", "clientID" ] ); + t.uuid( "country_id" ).nullable(); } ); qb.table( "playing_fields" ).insert( [ { "fieldID": 1, "clientID": 1, - "fieldName": "First Field" + "fieldName": "First Field", + "country_id": "02B84D66-0AA0-F7FB-1F71AFC954843861" // United States }, { "fieldID": 1, "clientID": 2, - "fieldName": "Second Field" + "fieldName": "Second Field", + "country_id": "02BA2DB0-EB1E-3F85-5F283AB5E45608C6" // Argentina } ] ); } diff --git a/tests/specs/integration/BaseEntity/Relationships/HasManyThroughSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/HasManyThroughSpec.cfc index 91f6af7f..26d2f213 100644 --- a/tests/specs/integration/BaseEntity/Relationships/HasManyThroughSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/HasManyThroughSpec.cfc @@ -118,6 +118,19 @@ component extends="tests.resources.ModuleIntegrationSpec" { expect( officemates[ 2 ].getId() ).toBe( 3 ); expect( officemates[ 3 ].getId() ).toBe( 4 ); } ); + + it( "can go through entities with multiple local and foreign keys", () => { + var argentina = getInstance( "Country" ).where( "name", "Argentina" ).firstOrFail(); + var argentinaGames = argentina.getGames(); + expect( argentinaGames ).toBeArray(); + expect( argentinaGames ).toHaveLength( 1 ); + expect( argentinaGames[ 1 ].getId() ).toBe( 1 ); + + var unitedStates = getInstance( "Country" ).where( "name", "United States" ).firstOrFail(); + var usGames = unitedStates.getGames(); + expect( usGames ).toBeArray(); + expect( usGames ).toBeEmpty(); + } ); } ); }