Skip to content

Guard rails to copy fields from parent entity using annotations. - #171

Open
kanhapatro37 wants to merge 15 commits into
santanusinha:masterfrom
kanhapatro37:txnl-guard-rails-poc-annotations
Open

Guard rails to copy fields from parent entity using annotations.#171
kanhapatro37 wants to merge 15 commits into
santanusinha:masterfrom
kanhapatro37:txnl-guard-rails-poc-annotations

Conversation

@kanhapatro37

@kanhapatro37 kanhapatro37 commented May 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a new SaveWithParent OpContext that carries the parent entity reference during LockedContext.save() operations. The existing Save OpContext has no access to the parent, so any observer in the chain that needs the parent (e.g., CopyFromParentObserver to copy annotated fields from parent to child before persistence) cannot do so today. This new OpContext makes the parent available to observers without modifying the existing Save contract.

Adds new annotations: @CopyFromParent / @ParentEntity to configure the source and destination fields to copy.

Also adds compile-time annotation processing to validate @CopyFromParent / @ParentEntity usage at build time, and a CopyFromParentUtils utility that:

  • Performs the actual field copying from parent to child at runtime using cached method handles.
  • Detect mismatches for annotated fields before copying.

Changes

New SaveWithParent<T, R, P> OpContext

  • A new OpContext that carries a typed parent reference (P) alongside the child entity being saved.
  • Added to OpType enum as COPY_FROM_PARENT_AND_SAVE.
  • Added visit(CopyFromParentAndSave) to OpContextVisitor interface. This is an additive change (not modifying existing Save<T,R>), so existing visitor implementations only need to add one new method.
  • BucketKeyPersistor implements the new visitor method to add bucket IDs to child entities.

LockedContext and DAO changes

  • LockedContext.save(), saveAll(), and save(entity, handler) now pass the parent reference to RelationalDao.save(context, entity, parent).
  • Overloaded save methods added to RelationalDao and MultiTenantRelationalDao that construct CopyFromParentAndSave instead of Save.

@CopyFromParent and @ParentEntity annotations

  • @ParentEntity(SomeParent.class) on a child entity class declares its parent type.
  • @CopyFromParent(field = "fieldName") on a child field marks it for automatic copying from the named parent field.

CopyFromParentObserver and CopyFromParentPersistor

  • New observer that can be registered by consumers.
  • This will intercept SaveWithParent Opcontext, extract the parent, and applies field copying/detects mismatches before persistence.
  • The Observer can be configured using: copyEnabled, mismatchDetectionEnabled, copyIfDefaultOnly and mismatchListenerSupplier parameters.

CopyFromParentUtils (MethodHandles-based)

  • Handles the logic for actual field copying and mismatch detection.
  • Uses cached MethodHandle getter/setter pairs for near-direct-access performance.
  • Handles inherited fields, null values, parent type validation.
  • Thread-safe via ConcurrentHashMap cache, populated once per child class per JVM lifetime.

Compile-time annotation processor (CopyFromParentProcessor)
Validates at compile time:

  • @CopyFromParent used without @ParentEntity on the enclosing class
  • @CopyFromParent(field = "x") where x doesn't exist on the parent (including superclass traversal)
  • Type mismatch between parent and child fields
  • Parent field marked @Transient (both javax.persistence and jakarta.persistence)

E2E testing:

  • Verified that mismatch detection works correctly when mismatchDetectionEnabled=true
  • Verified that the transaction fails with appropriate error when a non-default value is encountered and copyIfDefaultOnly=true.
  • Verified that the annotated fields are copied from parent to child only when copyEnabled=true

@kanhapatro37 kanhapatro37 changed the title POC changes for partitionID guards using annotations. Changes for guard rails using annotations. May 25, 2026
@sonarqubecloud

sonarqubecloud Bot commented Jun 3, 2026

Copy link
Copy Markdown

@sonarqubecloud

Copy link
Copy Markdown

@kanhapatro37 kanhapatro37 changed the title Changes for guard rails using annotations. Guard rails to copy fields from parent entity using annotations. Jun 17, 2026
@sonarqubecloud

Copy link
Copy Markdown


<T, R> P visit(Select<T, R> opContext);

default <T, R, U> P visit(SaveWithParent<T, R, U> opContext) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Existing visitors compile via this default, then every LockedContext.save* dispatch throws here. Preserve legacy visit(Save) behavior and add a compatibility test.

}

@Override
public <T, R, U> Void visit(SaveWithParent<T, R, U> opContext) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 This wrapper runs after copy wraps the saver, so addBucketId() executes first and reads a stale/default copied sharding key. Enforce copy → bucket calculation → save and test the full chain.


private void validateField(VariableElement childField) {
TypeElement childClass = (TypeElement) childField.getEnclosingElement();
ParentEntity parentAnn = childClass.getAnnotation(ParentEntity.class);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Runtime inherits @ParentEntity, but the processor checks only the direct class here. Walk the superclass hierarchy and add a subclass-field compile test.

// 3. Type mismatch check
TypeMirror parentFieldType = parentField.asType();
TypeMirror childFieldType = childField.asType();
if (!processingEnv.getTypeUtils().isAssignable(parentFieldType, childFieldType)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Validation accepts static source/target and final target fields, but runtime MethodHandles cannot invoke or set them safely. Reject these modifiers and test each case.


TransactionExecutionContext ctx = createContext(opContext);

IllegalStateException exception = assertThrows(IllegalStateException.class,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 This bypasses the bundle/transaction chain with saver(e -> e) and apply(null), so it does not verify persistence or rollback. Add a registered-observer DB integration test.

"}"));

Compilation compilation = Compiler.javac()
.withProcessors(new CopyFromParentProcessor())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Explicit .withProcessors(...) proves validation logic, not packaged service discovery. Add a downstream compile test without explicitly installing the processor.

}

// Copy from parent to child
m.childSetter.invoke(child, parentValue);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 This mutates each field immediately; a later default-only violation leaves earlier fields changed. Validate all fields first, then apply mutations in a second pass.

"io.appform.dropwizard.sharding.sharding.CopyFromParent",
"io.appform.dropwizard.sharding.sharding.ParentEntity"
})
@SupportedSourceVersion(SourceVersion.RELEASE_11)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This processor is auto-registered via META-INF/services, so downstream javac loads it even when the new annotations are unused. I reproduced an unrelated Java 17 compilation failing under -Werror because this emits the RELEASE_11 less than -source 17 warning. Please override getSupportedSourceVersion() to return SourceVersion.latestSupported() and add a packaged-JAR service-discovery test on Java 17 with -Werror.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants