Skip to content

Commit f60c176

Browse files
committed
Add ERR_ASO_KEY error for conflicting key-based FK directions across join clauses.
1 parent da44628 commit f60c176

6 files changed

Lines changed: 119 additions & 14 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package dummy
2+
3+
class Source
4+
{
5+
id: Long key;
6+
extraId: Long?;
7+
}
8+
9+
class Target
10+
{
11+
id: Long key;
12+
extraId: Long?;
13+
}
14+
15+
association MixedKeyJoin
16+
{
17+
mixedSource: Source[0..1];
18+
mixedTarget: Target[0..1] owned;
19+
20+
relationship this.id == Target.extraId && this.extraId == Target.id
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2026 Craig Motlin
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package cool.klass.model.converter.compiler.annotation.association;
18+
19+
import cool.klass.model.converter.compiler.annotation.AbstractKlassCompilerErrorTestCase;
20+
21+
public class CompoundJoinMixedKeyTest extends AbstractKlassCompilerErrorTestCase {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
════════════════════════════════════════ ERR_ASO_KEY ════════════════════════════════════════
2+
Error: Association 'MixedKeyJoin' has foreign keys on both sides: 'Target.extraId' and 'Source.extraId'.
3+
4+
At (CompoundJoinMixedKeyTest.klass:20:36)
5+
6+
 1║ package dummy
7+
15║ association MixedKeyJoin
8+
16║ {
9+
20║ relationship this.id == Target.extraId && this.extraId == Target.id
10+
 ║  ^^^^^^^ ^^^^^^^
11+
21║ }
12+

13+
Location: CompoundJoinMixedKeyTest.klass:20:36
14+
File: CompoundJoinMixedKeyTest.klass
15+
Line: 20
16+
Character: 36
17+
═════════════════════════════════════════════════════════════════════════════════════════════

klass-model-converters/klass-compiler/src/main/java/cool/klass/model/converter/compiler/state/criteria/OperatorAntlrCriteria.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import cool.klass.model.converter.compiler.state.value.literal.AbstractAntlrLiteralValue;
3939
import cool.klass.model.meta.domain.criteria.OperatorCriteriaImpl.OperatorCriteriaBuilder;
4040
import cool.klass.model.meta.grammar.KlassParser.CriteriaOperatorContext;
41+
import org.antlr.v4.runtime.ParserRuleContext;
4142
import org.eclipse.collections.api.list.ImmutableList;
4243
import org.eclipse.collections.api.list.ListIterable;
4344
import org.eclipse.collections.api.map.OrderedMap;
@@ -189,13 +190,16 @@ public void addForeignKeys() {
189190

190191
AntlrDataTypeProperty<?> foreignKeyProperty = isTargetEnd ? thisDataTypeProperty : typeDataTypeProperty;
191192
AntlrDataTypeProperty<?> keyProperty = isTargetEnd ? typeDataTypeProperty : thisDataTypeProperty;
192-
if (foreignKeyProperty.isKey() && !keyProperty.isKey()) {
193-
// This can happen for non-key but unique properties
194-
// TODO: Implement unique properties, and assert that the property is EITHER key or unique
195-
// throw new AssertionError(foreignKeyProperty);
196-
}
197-
198-
endWithForeignKeys.addForeignKeyPropertyMatchingProperty(foreignKeyProperty, keyProperty);
193+
// TODO: Implement unique properties, and assert that the matched key property is EITHER key or unique
194+
ParserRuleContext foreignKeyReferenceContext = isTargetEnd
195+
? thisMemberReferencePath.getElementContext().memberReference().identifier()
196+
: typeMemberReferencePath.getElementContext().memberReference().identifier();
197+
198+
endWithForeignKeys.addForeignKeyPropertyMatchingProperty(
199+
foreignKeyProperty,
200+
keyProperty,
201+
foreignKeyReferenceContext
202+
);
199203
}
200204

201205
@Override
@@ -236,12 +240,10 @@ private AntlrAssociationEnd getEndWithForeignKeys(
236240
// relationship this.queryCriteriaId == Criteria.id
237241
// }
238242

239-
// TODO: These two conditions don't make sense here, because we're only considering one pair of joined properties among several &&-clauses
240243
if (thisDataTypeProperty.isKey() && !typeDataTypeProperty.isKey()) {
241244
return sourceEnd;
242245
}
243246

244-
// TODO: These two conditions don't make sense here, because we're only considering one pair of joined properties among several &&-clauses
245247
if (typeDataTypeProperty.isKey() && !thisDataTypeProperty.isKey()) {
246248
return targetEnd;
247249
}

klass-model-converters/klass-compiler/src/main/java/cool/klass/model/converter/compiler/state/property/AntlrAssociationEnd.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import cool.klass.model.meta.domain.property.ModifierImpl.ModifierBuilder;
3737
import cool.klass.model.meta.grammar.KlassParser.AssociationEndContext;
3838
import cool.klass.model.meta.grammar.KlassParser.IdentifierContext;
39+
import org.antlr.v4.runtime.ParserRuleContext;
3940
import org.eclipse.collections.api.factory.Lists;
4041
import org.eclipse.collections.api.list.ImmutableList;
4142
import org.eclipse.collections.api.map.MutableOrderedMap;
@@ -96,6 +97,8 @@ public String toString() {
9697

9798
private final MutableOrderedMap<AntlrDataTypeProperty<?>, AntlrDataTypeProperty<?>> foreignKeys =
9899
OrderedMapAdapter.adapt(new LinkedHashMap<>());
100+
private final MutableOrderedMap<AntlrDataTypeProperty<?>, ParserRuleContext> foreignKeyReferenceContexts =
101+
OrderedMapAdapter.adapt(new LinkedHashMap<>());
99102

100103
public AntlrAssociationEnd(
101104
@Nonnull AssociationEndContext elementContext,
@@ -345,6 +348,10 @@ public boolean isVersioned() {
345348
return this.opposite.isVersion();
346349
}
347350

351+
public AntlrAssociationEnd getOpposite() {
352+
return this.opposite;
353+
}
354+
348355
public void setOpposite(@Nonnull AntlrAssociationEnd opposite) {
349356
this.opposite = Objects.requireNonNull(opposite);
350357
}
@@ -361,15 +368,29 @@ public AssociationEndContext getElementContext() {
361368
return (AssociationEndContext) super.getElementContext();
362369
}
363370

371+
public boolean hasForeignKeys() {
372+
return this.foreignKeys.notEmpty();
373+
}
374+
375+
public MutableOrderedMap<AntlrDataTypeProperty<?>, AntlrDataTypeProperty<?>> getForeignKeys() {
376+
return this.foreignKeys;
377+
}
378+
364379
public void addForeignKeyPropertyMatchingProperty(
365380
@Nonnull AntlrDataTypeProperty<?> foreignKeyProperty,
366-
@Nonnull AntlrDataTypeProperty<?> keyProperty
381+
@Nonnull AntlrDataTypeProperty<?> keyProperty,
382+
@Nonnull ParserRuleContext foreignKeyReferenceContext
367383
) {
368384
this.foreignKeys.put(foreignKeyProperty, keyProperty);
385+
this.foreignKeyReferenceContexts.put(foreignKeyProperty, foreignKeyReferenceContext);
369386
foreignKeyProperty.setKeyMatchingThisForeignKey(this, keyProperty);
370387
keyProperty.setForeignKeyMatchingThisKey(this, foreignKeyProperty);
371388
}
372389

390+
public ParserRuleContext getForeignKeyReferenceContext(@Nonnull AntlrDataTypeProperty<?> foreignKeyProperty) {
391+
return this.foreignKeyReferenceContexts.get(foreignKeyProperty);
392+
}
393+
373394
@Override
374395
protected IdentifierContext getTypeIdentifier() {
375396
return this.getElementContext().classReference().identifier();
@@ -382,8 +403,4 @@ public boolean isSourceEnd() {
382403
public boolean isTargetEnd() {
383404
return this == this.owningAssociation.getTargetEnd();
384405
}
385-
386-
public AntlrAssociationEnd getOpposite() {
387-
return this.opposite;
388-
}
389406
}

klass-model-converters/klass-compiler/src/main/java/cool/klass/model/converter/compiler/state/property/AntlrDataTypeProperty.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import cool.klass.model.converter.compiler.CompilationUnit;
2727
import cool.klass.model.converter.compiler.annotation.AnnotationSeverity;
2828
import cool.klass.model.converter.compiler.annotation.CompilerAnnotationHolder;
29+
import cool.klass.model.converter.compiler.state.AntlrAssociation;
2930
import cool.klass.model.converter.compiler.state.AntlrClassifier;
3031
import cool.klass.model.converter.compiler.state.AntlrElement;
3132
import cool.klass.model.converter.compiler.state.AntlrEnumeration;
@@ -452,6 +453,32 @@ private void reportInvalidForeignKeyProperties(
452453
throw new AssertionError(associationEnd);
453454
}
454455

456+
if (associationEnd.isSourceEnd() && associationEnd.getOpposite().hasForeignKeys()) {
457+
AntlrAssociationEnd oppositeEnd = associationEnd.getOpposite();
458+
AntlrDataTypeProperty<?> oppositeForeignKey = oppositeEnd.getForeignKeys().keysView().getFirst();
459+
String message = String.format(
460+
"Association '%s' has foreign keys on both sides: '%s.%s' and '%s.%s'.",
461+
associationEnd.getOwningAssociation().getName(),
462+
this.owningClassifier.getName(),
463+
this.getName(),
464+
oppositeForeignKey.getOwningClassifier().getName(),
465+
oppositeForeignKey.getName()
466+
);
467+
ParserRuleContext thisReferenceContext = associationEnd.getForeignKeyReferenceContext(this);
468+
ParserRuleContext oppositeReferenceContext = oppositeEnd.getForeignKeyReferenceContext(oppositeForeignKey);
469+
AntlrAssociation association = associationEnd.getOwningAssociation();
470+
compilerAnnotationHolder.add(
471+
"ERR_ASO_KEY",
472+
message,
473+
association,
474+
Lists.immutable
475+
.<IAntlrElement>with(association.getRelationship())
476+
.newWithAll(association.getSurroundingElements())
477+
.distinct(),
478+
Lists.immutable.with(thisReferenceContext, oppositeReferenceContext)
479+
);
480+
}
481+
455482
if (this.isOptional && associationEnd.isToOneRequired()) {
456483
String message = String.format(
457484
"Association end '%s.%s' has multiplicity [%s] so foreign key '%s.%s' ought to be required.",

0 commit comments

Comments
 (0)