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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,41 @@ public String getAlias(@Nonnull Class<? extends Data> table,
throw exceptionSupplier.get();
}

/**
* Returns the registered path for the specified table at the current nesting level, but only when the table is
* registered exactly once (after de-duplication on path).
*
* <p>This is used when columns originate from a sub-tree rooted at a different table than the query model root
* (for example, when {@code SELECT}ing a joined entity type from a repository whose root entity differs). In that
* case the column metamodel paths are relative to the column root, while the alias mapper has registered the
* paths relative to the query model root. The path returned here can be prepended to the column path to bridge
* the two coordinate systems.</p>
*
* @param table the table to look up.
* @return the single registered path for the table, or empty if the table is missing, ambiguous, or registered
* without a path.
*/
public Optional<String> findRegisteredPath(@Nonnull Class<? extends Data> table) {
var entries = aliasMap.get(table);
if (entries == null || entries.isEmpty()) {
return empty();
}
String selected = null;
for (var entry : entries) {
var entryPath = entry.path();
if (entryPath == null) {
continue;
}
if (selected == null) {
selected = entryPath;
} else if (!selected.equals(entryPath)) {
// Multiple distinct paths — caller cannot disambiguate.
return empty();
}
}
return Optional.ofNullable(selected);
}

public Optional<String> findAlias(@Nonnull Class<? extends Data> table,
@Nullable String path,
@Nonnull ResolveScope scope) throws SqlTemplateException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,18 @@ public ColumnExpression toColumnExpression(@Nonnull Column column) {
if (isRootTable && metamodel.path().isEmpty()) {
alias = table.alias();
} else {
alias = aliasMapper.findAlias(metamodel.tableType(), metamodel.path(), INNER).orElse(null);
String lookupPath = metamodel.path();
if (!isRootTable && metamodel.root() != model.type()) {
// The column's metamodel is rooted at a different table than the query model root
// (typically because the SELECT target differs from the FROM table). The alias mapper
// has paths relative to the query model root, so prepend the registered path of the
// column root before lookup.
String rootPrefix = aliasMapper.findRegisteredPath(metamodel.root()).orElse(null);
if (rootPrefix != null && !rootPrefix.isEmpty()) {
lookupPath = lookupPath.isEmpty() ? rootPrefix : rootPrefix + "." + lookupPath;
}
}
alias = aliasMapper.findAlias(metamodel.tableType(), lookupPath, INNER).orElse(null);
}
if (alias == null) {
alias = aliasMapper.findAlias(metamodel.tableType(), null, INNER).orElse(null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package st.orm.core;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.util.List;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import st.orm.core.model.Owner;
import st.orm.core.model.Tenant;
import st.orm.core.template.ORMTemplate;

/**
* Regression tests for SELECT-target alias resolution when the SELECT type differs from the
* repository's root entity type.
*
* <p>{@link Tenant} creates a diamond join graph: {@code tenant -> owner -> address.city} and
* {@code tenant -> city} both reach {@code City}. Selecting {@link Owner} from a Tenant
* repository expands Owner's nested column tree (which includes {@code address.city}) whose
* metamodels are rooted at Owner rather than at the repository root. Before the fix, alias
* resolution looked up paths rooted at Owner against the alias map that stores Tenant-rooted
* paths, fell through to a permissive null-path lookup, and failed with
* "Multiple aliases found for: City".</p>
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = IntegrationConfig.class)
@DataJpaTest(showSql = false)
public class SelectTargetAliasResolutionIntegrationTest {

@Autowired
private DataSource dataSource;

@Test
public void selectJoinedEntityWithDiamondPath() {
var tenants = ORMTemplate.of(dataSource).entity(Tenant.class);
// Owner sits one level deep; City is reachable via two paths (tenant.owner.address.city
// and tenant.city). Compilation must pick the alias on the Owner branch without falling
// back to a null-path lookup.
List<Owner> owners = assertDoesNotThrow(() -> tenants.select(Owner.class).getResultList());
assertNotNull(owners);
assertEquals(2, owners.size());
}

@Test
public void selectJoinedEntityResolvesNestedColumns() {
var tenants = ORMTemplate.of(dataSource).entity(Tenant.class);
List<Owner> owners = tenants.select(Owner.class).getResultList();
// Verify the nested city column on the Owner branch resolves correctly (rather than
// collapsing onto the tenant.city alias).
assertEquals(1, owners.get(0).address().city().id());
assertEquals(2, owners.get(1).address().city().id());
}
}
38 changes: 38 additions & 0 deletions storm-core/src/test/java/st/orm/core/model/Tenant.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package st.orm.core.model;

import jakarta.annotation.Nonnull;
import lombok.Builder;
import st.orm.Entity;
import st.orm.FK;
import st.orm.PK;

/**
* Test entity that creates a diamond join graph by reaching {@link City} through two independent
* paths: directly via {@link #city()} and indirectly via {@link Owner#address()}.{@code city()}.
*
* <p>Used to exercise alias path resolution when the {@code SELECT} target's expanded column tree
* references a table that is reachable through multiple paths from the {@code FROM} table.</p>
*/
@Builder(toBuilder = true)
public record Tenant(
@PK Integer id,
@Nonnull String name,
@Nonnull @FK Owner owner,
@Nonnull @FK City city
) implements Entity<Integer> {
}
8 changes: 8 additions & 0 deletions storm-core/src/test/resources/data.sql
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
drop table if exists tenant CASCADE;
drop table if exists city CASCADE;
drop table if exists owner CASCADE;
drop table if exists pet CASCADE;
Expand Down Expand Up @@ -25,6 +26,9 @@ alter table vet_specialty add constraint vet_specialty_specialty_fk foreign key
alter table vet_specialty add constraint vet_specialty_vet_fk foreign key (vet_id) references vet (id);
alter table visit add constraint visit_pet_fk foreign key (pet_id) references pet (id);
alter table visit add constraint visit_vet_specialty_fk foreign key (vet_id, specialty_id) references vet_specialty (vet_id, specialty_id);
create table tenant (id integer auto_increment, name varchar(255), owner_id integer not null, city_id integer not null, primary key (id));
alter table tenant add constraint tenant_owner_fk foreign key (owner_id) references owner (id);
alter table tenant add constraint tenant_city_fk foreign key (city_id) references city (id);
create view owner_view as select * from owner;
create view visit_view as select visit_date, description, pet_id, "timestamp" from visit;

Expand Down Expand Up @@ -206,3 +210,7 @@ INSERT INTO char_disc_animal (dtype, name, indoor) VALUES ('C', 'Whiskers', true
INSERT INTO char_disc_animal (dtype, name, indoor) VALUES ('C', 'Luna', false);
INSERT INTO char_disc_animal (dtype, name, weight) VALUES ('D', 'Rex', 30);
INSERT INTO char_disc_animal (dtype, name, weight) VALUES ('D', 'Max', 15);

-- Tenant creates a diamond join graph: tenant -> owner -> city and tenant -> city.
INSERT INTO tenant (name, owner_id, city_id) VALUES ('Alpha', 1, 1);
INSERT INTO tenant (name, owner_id, city_id) VALUES ('Beta', 2, 2);
Loading