You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tomee.apache.org by rz...@apache.org on 2026/03/24 11:39:22 UTC

(tomee) branch main updated: TOMEE-4594 - Jakarta Data 1.0 (#2550)

This is an automated email from the ASF dual-hosted git repository.

rzo1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomee.git


The following commit(s) were added to refs/heads/main by this push:
     new 0791e1069f  TOMEE-4594 - Jakarta Data 1.0 (#2550)
0791e1069f is described below

commit 0791e1069f1ddff491baff88a670c131f751b1d5
Author: Richard Zowalla <13...@users.noreply.github.com>
AuthorDate: Tue Mar 24 12:39:17 2026 +0100

     TOMEE-4594 - Jakarta Data 1.0 (#2550)
    
    * First Jakarta Data impl (JPA only). Passes Data 1.0 TCK
    
    * Cleaup of Code
    
    * Cleaup of Code
    
    * Jakarta Data: add Hibernate as third TCK JPA provider, address review comments
    
      - Add tomee-hibernate container in arquillian.xml that swaps OpenJPA for
        Hibernate via additionalLibs (remove:openjpa + mvn: Hibernate deps)
      - Add third Surefire execution (tomee-hibernate) with jpa.provider=hibernate
      - DataTCKArchiveProcessor selects provider based on jpa.provider system property
      - Enable resource filtering for arquillian.xml to resolve version properties
      - Skip OpenJPA mergeWithVersionCheck workaround for Hibernate/EclipseLink:
        detect OpenJPA at runtime via isOpenJpa(), use standard em.merge() otherwise
      - All three providers pass 98/98 TCK tests
    
    * Jakarta Data: add query caching
    
     - jpqlCache (ConcurrentHashMap<Method, String>) stores JPQL strings with named parameter placeholders (:p0,
      :p1, etc.) — no bound values
      - Pre-computed at constructor time: findAllJpql, deleteAllJpql, countAllJpql — static per repository
      - Cached on first invocation: @Query JDQL→JPQL expansion, @Find+@By JPQL, @Delete+@By JPQL, method-name
      find/count/delete/exists JPQL
      - Per-call: em.createQuery(cachedJpql) → bind parameters → execute
      - Dynamic Sort/Order params appended to the cached JPQL at execution time via appendDynamicSort
---
 .../arquillian-tomee-data-tests/pom.xml            |  91 ++
 .../openejb/arquillian/tests/data/DataItem.java    |  66 ++
 .../arquillian/tests/data/DataItemRepository.java  |  38 +
 .../tests/data/JakartaDataNoGeneratedImplTest.java | 135 +++
 .../data/JakartaDataWithGeneratedImplTest.java     | 109 +++
 .../openejb/arquillian/tests/data/SimpleItem.java  |  55 ++
 .../tests/data/SimpleItemRepository.java           |  33 +
 .../src/test/resources/arquillian.xml              |  34 +
 arquillian/arquillian-tomee-tests/pom.xml          |   1 +
 boms/tomee-microprofile/pom.xml                    |  15 +
 boms/tomee-plume/pom.xml                           |  15 +
 boms/tomee-plus/pom.xml                            |  15 +
 boms/tomee-webprofile/pom.xml                      |  15 +
 .../openejb-jakarta-data}/pom.xml                  |  71 +-
 .../data/extension/DataRepositoryExtension.java    | 113 +++
 .../openejb/data/extension/RepositoryBean.java     | 118 +++
 .../data/handler/RepositoryInvocationHandler.java  | 946 +++++++++++++++++++++
 .../apache/openejb/data/meta/MethodMetadata.java   |  43 +
 .../openejb/data/meta/RepositoryMetadata.java      |  89 ++
 .../openejb/data/proxy/EntityManagerLookup.java    | 191 +++++
 .../openejb/data/query/AnnotatedQueryExecutor.java | 206 +++++
 .../openejb/data/query/CriteriaQueryBuilder.java   | 240 ++++++
 .../openejb/data/query/FindAnnotationExecutor.java | 118 +++
 .../openejb/data/query/MethodNameParser.java       | 244 ++++++
 .../jakarta.enterprise.inject.spi.Extension        |   1 +
 .../openejb/data/test/AnnotatedQueryTest.java      | 124 +++
 .../apache/openejb/data/test/BasicCrudTest.java    | 395 +++++++++
 .../openejb/data/test/BeanValidationTest.java      | 193 +++++
 .../test/DataRepositoryExtensionDisabledTest.java  |  76 ++
 .../data/test/DataRepositoryExtensionTest.java     |  80 ++
 .../data/test/DataRepositoryExtensionUnitTest.java |  37 +
 .../test/EntityManagerLookupAmbiguousTest.java     | 104 +++
 .../data/test/EntityManagerLookupCDINamedTest.java | 120 +++
 .../test/EntityManagerLookupCDIProducerTest.java   | 124 +++
 .../data/test/EntityManagerLookupCDITest.java      |  97 +++
 .../data/test/EntityManagerLookupJNDITest.java     | 149 ++++
 .../EntityManagerLookupNamedDataStoreTest.java     | 136 +++
 .../apache/openejb/data/test/ExtendedCrudTest.java | 522 ++++++++++++
 .../apache/openejb/data/test/FinderMethodTest.java | 158 ++++
 .../openejb/data/test/MethodMetadataTest.java      |  51 ++
 .../data/test/MethodNameParserExtendedTest.java    | 306 +++++++
 .../openejb/data/test/MethodNameParserTest.java    | 136 +++
 .../apache/openejb/data/test/PaginationTest.java   |  97 +++
 .../openejb/data/test/QueryOperatorTest.java       | 275 ++++++
 .../openejb/data/test/RepositoryBeanTest.java      | 125 +++
 .../openejb/data/test/RepositoryMetadataTest.java  | 118 +++
 .../apache/openejb/data/test/SortAndLimitTest.java | 306 +++++++
 .../openejb/data/test/TCKLikePersistenceTest.java  | 337 ++++++++
 .../test/cdi/EntityManagerFactoryProducer.java     |  38 +
 .../cdi/NamedEntityManagerFactoryProducer.java     |  41 +
 .../apache/openejb/data/test/entity/Person.java    |  77 ++
 .../apache/openejb/data/test/entity/Product.java   |  85 ++
 .../org/apache/openejb/data/test/entity/Task.java  |  88 ++
 .../data/test/repo/NamedCDIPersonRepository.java   |  29 +
 .../test/repo/NamedDataStorePersonRepository.java  |  31 +
 .../data/test/repo/PersonCustomRepository.java     |  66 ++
 .../data/test/repo/PersonExtendedRepository.java   |  89 ++
 .../openejb/data/test/repo/PersonRepository.java   |  33 +
 .../openejb/data/test/repo/ProductCatalog.java     |  82 ++
 .../openejb/data/test/repo/TaskRepository.java     |  79 ++
 .../data/test/repo/ValidatedPersonRepository.java  |  38 +
 container/pom.xml                                  |   1 +
 examples/jakarta-data-repository/README.adoc       | 235 +++++
 examples/jakarta-data-repository/pom.xml           | 126 +++
 .../src/main/java/org/superbiz/data/Movie.java     |  75 ++
 .../java/org/superbiz/data/MovieRepository.java    |  61 ++
 .../main/java/org/superbiz/data/MovieService.java  |  64 ++
 .../src/main/resources/META-INF/persistence.xml    |  31 +
 .../org/superbiz/data/MovieRepositoryTest.java     | 213 +++++
 examples/jpa-and-jakarta-data/README.adoc          | 218 +++++
 examples/jpa-and-jakarta-data/pom.xml              | 126 +++
 .../src/main/java/org/superbiz/combined/Genre.java |  26 +
 .../src/main/java/org/superbiz/combined/Movie.java |  94 ++
 .../main/java/org/superbiz/combined/MovieDao.java  |  91 ++
 .../org/superbiz/combined/MovieRepository.java     |  49 ++
 .../java/org/superbiz/combined/MovieService.java   | 106 +++
 .../src/main/resources/META-INF/persistence.xml    |  31 +
 .../superbiz/combined/JpaAndJakartaDataTest.java   | 313 +++++++
 examples/pom.xml                                   |   2 +
 tck/data-signature-test/pom.xml                    | 172 ++++
 .../src/test/resources/arquillian.xml              |  39 +
 tck/data-standalone/pom.xml                        | 234 +++++
 .../tomee/tck/data/DataTCKArchiveProcessor.java    | 151 ++++
 .../apache/tomee/tck/data/DataTCKExtension.java    |  32 +
 ...org.jboss.arquillian.core.spi.LoadableExtension |   1 +
 .../src/test/resources/arquillian.xml              |  81 ++
 tck/pom.xml                                        |   2 +
 tomee/tomee-common/pom.xml                         |   5 +
 88 files changed, 10393 insertions(+), 30 deletions(-)

diff --git a/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/pom.xml b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/pom.xml
new file mode 100644
index 0000000000..ae28720e1a
--- /dev/null
+++ b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/pom.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <artifactId>arquillian-tomee-tests</artifactId>
+    <groupId>org.apache.tomee</groupId>
+    <version>11.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>arquillian-tomee-data-tests</artifactId>
+  <name>TomEE :: Arquillian Adaptors Parent :: Tests :: Jakarta Data</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>openejb-jakarta-data</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <!-- Hibernate annotation processor — generates Repository_ impl classes at compile time -->
+    <dependency>
+      <groupId>org.hibernate.orm</groupId>
+      <artifactId>hibernate-processor</artifactId>
+      <version>${version.hibernate.orm}</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <annotationProcessorPaths>
+            <path>
+              <groupId>org.hibernate.orm</groupId>
+              <artifactId>hibernate-processor</artifactId>
+              <version>${version.hibernate.orm}</version>
+            </path>
+          </annotationProcessorPaths>
+        </configuration>
+      </plugin>
+      <!--
+        Remove Hibernate-generated JPA static metamodel classes (*_.class) from
+        test-classes after compilation. These conflict with OpenJPA's runtime metamodel
+        in embedded test mode. The Jakarta Data metamodel (_*) and repository impl
+        (DataItemRepository_) are kept.
+      -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-antrun-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>remove-jpa-metamodel</id>
+            <phase>test-compile</phase>
+            <goals>
+              <goal>run</goal>
+            </goals>
+            <configuration>
+              <target>
+                <delete>
+                  <fileset dir="${project.build.testOutputDirectory}">
+                    <include name="**/SimpleItem_.class" />
+                  </fileset>
+                </delete>
+              </target>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/DataItem.java b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/DataItem.java
new file mode 100644
index 0000000000..9f2c068769
--- /dev/null
+++ b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/DataItem.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.arquillian.tests.data;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+
+@Entity
+public class DataItem {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    private String name;
+
+    private int value;
+
+    public DataItem() {
+    }
+
+    public DataItem(final String name, final int value) {
+        this.name = name;
+        this.value = value;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(final Long id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    public void setValue(final int value) {
+        this.value = value;
+    }
+}
diff --git a/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/DataItemRepository.java b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/DataItemRepository.java
new file mode 100644
index 0000000000..73d53da1bd
--- /dev/null
+++ b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/DataItemRepository.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.arquillian.tests.data;
+
+import jakarta.data.repository.CrudRepository;
+import jakarta.data.repository.Find;
+import jakarta.data.repository.Repository;
+
+import java.util.List;
+
+/**
+ * Jakarta Data repository for DataItem.
+ *
+ * When Hibernate's annotation processor is on the compile path, it will generate
+ * a {@code _DataItemRepository} class. If that generated class is included in the
+ * deployment, TomEE's Jakarta Data extension should detect it and skip proxy creation.
+ * If it's NOT included, TomEE's extension should create the proxy.
+ */
+@Repository
+public interface DataItemRepository extends CrudRepository<DataItem, Long> {
+
+    @Find
+    List<DataItem> findByName(String name);
+}
diff --git a/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/JakartaDataNoGeneratedImplTest.java b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/JakartaDataNoGeneratedImplTest.java
new file mode 100644
index 0000000000..bd0604739c
--- /dev/null
+++ b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/JakartaDataNoGeneratedImplTest.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.arquillian.tests.data;
+
+import jakarta.annotation.Resource;
+import jakarta.inject.Inject;
+import jakarta.transaction.UserTransaction;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit.Arquillian;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Verifies TomEE's Jakarta Data extension creates a working proxy for a @Repository
+ * when NO Hibernate-generated implementation class exists.
+ *
+ * Uses {@link SimpleItemRepository} which the Hibernate annotation processor cannot
+ * fully implement (method-name query convention without @Find), so no
+ * SimpleItemRepository_ is generated. TomEE's extension handles it.
+ */
+@RunWith(Arquillian.class)
+public class JakartaDataNoGeneratedImplTest {
+
+    private static final String PERSISTENCE_XML =
+        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+        "<persistence xmlns=\"https://jakarta.ee/xml/ns/persistence\" version=\"3.2\">\n" +
+        "    <persistence-unit name=\"data-pu\" transaction-type=\"JTA\">\n" +
+        "        <jta-data-source>java:comp/DefaultDataSource</jta-data-source>\n" +
+        "        <class>org.apache.openejb.arquillian.tests.data.SimpleItem</class>\n" +
+        "        <exclude-unlisted-classes>true</exclude-unlisted-classes>\n" +
+        "        <properties>\n" +
+        "            <property name=\"openjpa.jdbc.SynchronizeMappings\"\n" +
+        "                      value=\"buildSchema(ForeignKeys=true)\"/>\n" +
+        "        </properties>\n" +
+        "    </persistence-unit>\n" +
+        "</persistence>\n";
+
+    private static final String BEANS_XML =
+        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+        "<beans xmlns=\"https://jakarta.ee/xml/ns/jakartaee\" version=\"4.0\"\n" +
+        "       bean-discovery-mode=\"all\">\n" +
+        "</beans>\n";
+
+    @Deployment
+    public static WebArchive createDeployment() {
+        return ShrinkWrap.create(WebArchive.class, "data-no-generated-impl.war")
+            .addClass(SimpleItem.class)
+            .addClass(SimpleItemRepository.class)
+            .addAsResource(new StringAsset(PERSISTENCE_XML), "META-INF/persistence.xml")
+            .addAsWebInfResource(new StringAsset(BEANS_XML), "beans.xml");
+    }
+
+    @Inject
+    private SimpleItemRepository repository;
+
+    @Resource
+    private UserTransaction utx;
+
+    @Test
+    public void repositoryIsInjected() {
+        assertNotNull("Repository should be injected via TomEE's Jakarta Data extension", repository);
+    }
+
+    @Test
+    public void insertAndFindById() throws Exception {
+        utx.begin();
+        try {
+            final SimpleItem item = new SimpleItem("alpha");
+            repository.insert(item);
+            assertNotNull("Item should have an ID after insert", item.getId());
+
+            final Optional<SimpleItem> found = repository.findById(item.getId());
+            assertTrue("Should find item by ID", found.isPresent());
+            assertEquals("alpha", found.get().getLabel());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void findByLabel() throws Exception {
+        utx.begin();
+        try {
+            repository.insert(new SimpleItem("beta"));
+            repository.insert(new SimpleItem("gamma"));
+            repository.insert(new SimpleItem("beta"));
+
+            final List<SimpleItem> betas = repository.findByLabel("beta");
+            assertEquals("Should find 2 items with label 'beta'", 2, betas.size());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void deleteById() throws Exception {
+        utx.begin();
+        try {
+            final SimpleItem item = new SimpleItem("delta");
+            repository.insert(item);
+            final Long id = item.getId();
+            assertNotNull(id);
+
+            repository.deleteById(id);
+
+            assertTrue("Deleted item should not be found", repository.findById(id).isEmpty());
+        } finally {
+            utx.rollback();
+        }
+    }
+}
diff --git a/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/JakartaDataWithGeneratedImplTest.java b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/JakartaDataWithGeneratedImplTest.java
new file mode 100644
index 0000000000..fa514194e8
--- /dev/null
+++ b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/JakartaDataWithGeneratedImplTest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.arquillian.tests.data;
+
+import jakarta.enterprise.inject.spi.BeanManager;
+import jakarta.inject.Inject;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit.Arquillian;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Verifies TomEE's Jakarta Data extension skips a @Repository when a
+ * Hibernate-generated implementation class (DataItemRepository_) is present
+ * in the deployment.
+ *
+ * The Hibernate annotation processor generates DataItemRepository_ at compile
+ * time in this module. This test includes it in the WAR deployment so that
+ * the CDI extension detects it and does NOT register its own proxy bean.
+ *
+ * Since the actual Hibernate runtime isn't available in embedded mode with
+ * OpenJPA, the generated impl can't function. The test verifies the extension's
+ * detection logic — that it correctly backs off when a generated impl exists.
+ */
+@RunWith(Arquillian.class)
+public class JakartaDataWithGeneratedImplTest {
+
+    private static final String PERSISTENCE_XML =
+        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+        "<persistence xmlns=\"https://jakarta.ee/xml/ns/persistence\" version=\"3.2\">\n" +
+        "    <persistence-unit name=\"data-pu\" transaction-type=\"JTA\">\n" +
+        "        <jta-data-source>java:comp/DefaultDataSource</jta-data-source>\n" +
+        "        <class>org.apache.openejb.arquillian.tests.data.DataItem</class>\n" +
+        "        <exclude-unlisted-classes>true</exclude-unlisted-classes>\n" +
+        "        <properties>\n" +
+        "            <property name=\"openjpa.jdbc.SynchronizeMappings\"\n" +
+        "                      value=\"buildSchema(ForeignKeys=true)\"/>\n" +
+        "        </properties>\n" +
+        "    </persistence-unit>\n" +
+        "</persistence>\n";
+
+    private static final String BEANS_XML =
+        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+        "<beans xmlns=\"https://jakarta.ee/xml/ns/jakartaee\" version=\"4.0\"\n" +
+        "       bean-discovery-mode=\"all\">\n" +
+        "</beans>\n";
+
+    @Deployment
+    public static WebArchive createDeployment() {
+        final WebArchive war = ShrinkWrap.create(WebArchive.class, "data-with-generated-impl.war")
+            .addClass(DataItem.class)
+            .addClass(DataItemRepository.class)
+            .addAsResource(new StringAsset(PERSISTENCE_XML), "META-INF/persistence.xml")
+            .addAsWebInfResource(new StringAsset(BEANS_XML), "beans.xml");
+
+        // Include the Hibernate-generated implementation and JPA metamodel.
+        // This causes TomEE's extension to detect it and skip proxy creation.
+        try {
+            war.addClass(Class.forName("org.apache.openejb.arquillian.tests.data.DataItemRepository_"));
+            war.addClass(Class.forName("org.apache.openejb.arquillian.tests.data.DataItem_"));
+        } catch (final ClassNotFoundException e) {
+            throw new AssertionError(
+                "Hibernate annotation processor did not generate DataItemRepository_. " +
+                "Check that hibernate-processor is configured in annotationProcessorPaths.", e);
+        }
+
+        return war;
+    }
+
+    @Inject
+    private BeanManager beanManager;
+
+    @Test
+    public void extensionSkipsRepositoryWithGeneratedImpl() {
+        assertNotNull(beanManager);
+
+        // TomEE's extension should have detected DataItemRepository_ and skipped registration.
+        // The Hibernate-generated DataItemRepository_ is a @Dependent CDI bean itself, so it
+        // may still be resolvable. The key is that our extension did NOT create a proxy.
+        // We verify by checking that the bean (if resolvable) is the Hibernate-generated
+        // class, not our proxy.
+        final var beans = beanManager.getBeans(DataItemRepository.class);
+        for (final var bean : beans) {
+            assertFalse(
+                "TomEE's extension should not have registered a proxy when Hibernate-generated impl is present",
+                bean.getBeanClass().getName().contains("openejb.data.extension.RepositoryBean"));
+        }
+    }
+}
diff --git a/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/SimpleItem.java b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/SimpleItem.java
new file mode 100644
index 0000000000..d9d30d41b3
--- /dev/null
+++ b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/SimpleItem.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.arquillian.tests.data;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+
+@Entity
+public class SimpleItem {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    private String label;
+
+    public SimpleItem() {
+    }
+
+    public SimpleItem(final String label) {
+        this.label = label;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(final Long id) {
+        this.id = id;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public void setLabel(final String label) {
+        this.label = label;
+    }
+}
diff --git a/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/SimpleItemRepository.java b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/SimpleItemRepository.java
new file mode 100644
index 0000000000..ee069000de
--- /dev/null
+++ b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/java/org/apache/openejb/arquillian/tests/data/SimpleItemRepository.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.arquillian.tests.data;
+
+import jakarta.data.repository.CrudRepository;
+import jakarta.data.repository.Repository;
+
+import java.util.List;
+
+/**
+ * A repository that Hibernate's annotation processor cannot fully process
+ * (uses method-name query convention without @Find), so no
+ * SimpleItemRepository_ will be generated. TomEE's extension handles it.
+ */
+@Repository
+public interface SimpleItemRepository extends CrudRepository<SimpleItem, Long> {
+
+    List<SimpleItem> findByLabel(String label);
+}
diff --git a/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/resources/arquillian.xml b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/resources/arquillian.xml
new file mode 100644
index 0000000000..ad9c884fa6
--- /dev/null
+++ b/arquillian/arquillian-tomee-tests/arquillian-tomee-data-tests/src/test/resources/arquillian.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You 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.
+-->
+<arquillian xmlns="http://jboss.org/schema/arquillian"
+            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+            xsi:schemaLocation="http://jboss.org/schema/arquillian http://jboss.org/schema/arquillian/arquillian_1_0.xsd">
+  <container qualifier="tomee-embedded" default="true">
+    <configuration>
+      <property name="httpPort">-1</property>
+      <property name="stopPort">-1</property>
+      <property name="dir">target/tomee-embedded</property>
+      <property name="appWorkingDir">target/arquillian-embedded-working-dir</property>
+      <property name="portRange">30001-33000</property>
+      <property name="properties">
+        openejb.environment.default=true
+        tomee.jpa.cdi=false
+      </property>
+    </configuration>
+  </container>
+</arquillian>
diff --git a/arquillian/arquillian-tomee-tests/pom.xml b/arquillian/arquillian-tomee-tests/pom.xml
index 2264e3317d..e6f5cce3d4 100644
--- a/arquillian/arquillian-tomee-tests/pom.xml
+++ b/arquillian/arquillian-tomee-tests/pom.xml
@@ -30,6 +30,7 @@
     <module>arquillian-tomee-jaxws-tests</module>
     <module>arquillian-tomee-jms-tests</module>
     <module>arquillian-tomee-config-tests</module>
+    <module>arquillian-tomee-data-tests</module>
   </modules>
 
   <dependencies>
diff --git a/boms/tomee-microprofile/pom.xml b/boms/tomee-microprofile/pom.xml
index e01854295a..dd27ad228e 100644
--- a/boms/tomee-microprofile/pom.xml
+++ b/boms/tomee-microprofile/pom.xml
@@ -756,6 +756,11 @@
         <artifactId>openejb-http</artifactId>
         <version>${project.version}</version>
       </dependency>
+      <dependency>
+        <groupId>org.apache.tomee</groupId>
+        <artifactId>openejb-jakarta-data</artifactId>
+        <version>${project.version}</version>
+      </dependency>
       <dependency>
         <groupId>org.apache.tomee</groupId>
         <artifactId>openejb-javaagent</artifactId>
@@ -2575,6 +2580,16 @@
         </exclusion>
       </exclusions>
     </dependency>
+    <dependency>
+      <groupId>org.apache.tomee</groupId>
+      <artifactId>openejb-jakarta-data</artifactId>
+      <exclusions>
+        <exclusion>
+          <artifactId>*</artifactId>
+          <groupId>*</groupId>
+        </exclusion>
+      </exclusions>
+    </dependency>
     <dependency>
       <groupId>org.apache.tomee</groupId>
       <artifactId>openejb-javaagent</artifactId>
diff --git a/boms/tomee-plume/pom.xml b/boms/tomee-plume/pom.xml
index e99fb5591d..fd5d3e059d 100644
--- a/boms/tomee-plume/pom.xml
+++ b/boms/tomee-plume/pom.xml
@@ -796,6 +796,11 @@
         <artifactId>openejb-http</artifactId>
         <version>${project.version}</version>
       </dependency>
+      <dependency>
+        <groupId>org.apache.tomee</groupId>
+        <artifactId>openejb-jakarta-data</artifactId>
+        <version>${project.version}</version>
+      </dependency>
       <dependency>
         <groupId>org.apache.tomee</groupId>
         <artifactId>openejb-javaagent</artifactId>
@@ -2705,6 +2710,16 @@
         </exclusion>
       </exclusions>
     </dependency>
+    <dependency>
+      <groupId>org.apache.tomee</groupId>
+      <artifactId>openejb-jakarta-data</artifactId>
+      <exclusions>
+        <exclusion>
+          <artifactId>*</artifactId>
+          <groupId>*</groupId>
+        </exclusion>
+      </exclusions>
+    </dependency>
     <dependency>
       <groupId>org.apache.tomee</groupId>
       <artifactId>openejb-javaagent</artifactId>
diff --git a/boms/tomee-plus/pom.xml b/boms/tomee-plus/pom.xml
index f356ef975f..49a46c60b8 100644
--- a/boms/tomee-plus/pom.xml
+++ b/boms/tomee-plus/pom.xml
@@ -801,6 +801,11 @@
         <artifactId>openejb-http</artifactId>
         <version>${project.version}</version>
       </dependency>
+      <dependency>
+        <groupId>org.apache.tomee</groupId>
+        <artifactId>openejb-jakarta-data</artifactId>
+        <version>${project.version}</version>
+      </dependency>
       <dependency>
         <groupId>org.apache.tomee</groupId>
         <artifactId>openejb-javaagent</artifactId>
@@ -2715,6 +2720,16 @@
         </exclusion>
       </exclusions>
     </dependency>
+    <dependency>
+      <groupId>org.apache.tomee</groupId>
+      <artifactId>openejb-jakarta-data</artifactId>
+      <exclusions>
+        <exclusion>
+          <artifactId>*</artifactId>
+          <groupId>*</groupId>
+        </exclusion>
+      </exclusions>
+    </dependency>
     <dependency>
       <groupId>org.apache.tomee</groupId>
       <artifactId>openejb-javaagent</artifactId>
diff --git a/boms/tomee-webprofile/pom.xml b/boms/tomee-webprofile/pom.xml
index 28812fdb9e..74268f8456 100644
--- a/boms/tomee-webprofile/pom.xml
+++ b/boms/tomee-webprofile/pom.xml
@@ -496,6 +496,11 @@
         <artifactId>openejb-http</artifactId>
         <version>${project.version}</version>
       </dependency>
+      <dependency>
+        <groupId>org.apache.tomee</groupId>
+        <artifactId>openejb-jakarta-data</artifactId>
+        <version>${project.version}</version>
+      </dependency>
       <dependency>
         <groupId>org.apache.tomee</groupId>
         <artifactId>openejb-javaagent</artifactId>
@@ -1675,6 +1680,16 @@
         </exclusion>
       </exclusions>
     </dependency>
+    <dependency>
+      <groupId>org.apache.tomee</groupId>
+      <artifactId>openejb-jakarta-data</artifactId>
+      <exclusions>
+        <exclusion>
+          <artifactId>*</artifactId>
+          <groupId>*</groupId>
+        </exclusion>
+      </exclusions>
+    </dependency>
     <dependency>
       <groupId>org.apache.tomee</groupId>
       <artifactId>openejb-javaagent</artifactId>
diff --git a/tomee/tomee-common/pom.xml b/container/openejb-jakarta-data/pom.xml
similarity index 60%
copy from tomee/tomee-common/pom.xml
copy to container/openejb-jakarta-data/pom.xml
index b23d6cb300..93e8332c0b 100644
--- a/tomee/tomee-common/pom.xml
+++ b/container/openejb-jakarta-data/pom.xml
@@ -15,64 +15,75 @@
     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.
--->
-
-<!-- $Rev: 600338 $ $Date: 2007-12-02 09:08:04 -0800 (Sun, 02 Dec 2007) $ -->
 
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <parent>
-    <artifactId>tomee</artifactId>
+    <artifactId>container</artifactId>
     <groupId>org.apache.tomee</groupId>
     <version>11.0.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
-  <artifactId>tomee-common</artifactId>
-  <packaging>jar</packaging>
-  <name>TomEE :: TomEE :: Common</name>
-  <description>This module contains the classes that will be added to the common class loader</description>
+
+  <artifactId>openejb-jakarta-data</artifactId>
+  <name>TomEE :: Container :: Jakarta Data</name>
+
   <properties>
-    <tomee.build.name>${project.groupId}.tomee.common</tomee.build.name>
+    <tomee.build.name>${project.groupId}.container.jakarta.data</tomee.build.name>
   </properties>
+
   <dependencies>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>jakartaee-api</artifactId>
+      <scope>provided</scope>
+    </dependency>
     <dependency>
       <groupId>org.apache.tomee</groupId>
       <artifactId>openejb-core</artifactId>
       <version>${project.version}</version>
+      <scope>provided</scope>
     </dependency>
     <dependency>
-      <groupId>org.apache.tomee</groupId>
-      <artifactId>openejb-jpa-integration</artifactId>
-      <version>${project.version}</version>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+      <scope>provided</scope>
     </dependency>
+
+    <!-- Test -->
     <dependency>
-      <groupId>${project.groupId}</groupId>
-      <artifactId>openejb-loader</artifactId>
-      <version>${project.version}</version>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>org.tomitribe</groupId>
-      <artifactId>swizzle</artifactId>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-api</artifactId>
+      <version>${junit.jupiter.version}</version>
+      <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>org.apache.xbean</groupId>
-      <artifactId>xbean-naming</artifactId>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-engine</artifactId>
+      <version>${junit.jupiter.version}</version>
+      <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>${project.groupId}</groupId>
-      <artifactId>openejb-client</artifactId>
-      <version>${project.version}</version>
+      <groupId>org.junit.vintage</groupId>
+      <artifactId>junit-vintage-engine</artifactId>
+      <version>${junit.jupiter.version}</version>
+      <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>junit</groupId>
-      <artifactId>junit</artifactId>
+      <groupId>org.apache.tomee</groupId>
+      <artifactId>openejb-junit</artifactId>
+      <version>${project.version}</version>
       <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>org.apache.tomcat</groupId>
-      <artifactId>tomcat-catalina</artifactId>
-      <version>${tomcat.version}</version>
-      <scope>provided</scope>
+      <groupId>org.hsqldb</groupId>
+      <artifactId>hsqldb</artifactId>
+      <scope>test</scope>
     </dependency>
   </dependencies>
 </project>
-
diff --git a/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/extension/DataRepositoryExtension.java b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/extension/DataRepositoryExtension.java
new file mode 100644
index 0000000000..ae7de40b65
--- /dev/null
+++ b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/extension/DataRepositoryExtension.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.extension;
+
+import jakarta.data.repository.Repository;
+import jakarta.enterprise.event.Observes;
+import jakarta.enterprise.inject.spi.AfterBeanDiscovery;
+import jakarta.enterprise.inject.spi.BeforeBeanDiscovery;
+import jakarta.enterprise.inject.spi.Extension;
+import jakarta.enterprise.inject.spi.ProcessAnnotatedType;
+import jakarta.enterprise.inject.spi.WithAnnotations;
+
+import org.apache.openejb.data.meta.RepositoryMetadata;
+import org.apache.openejb.loader.SystemInstance;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+
+public class DataRepositoryExtension implements Extension {
+
+    private static final Logger LOGGER = Logger.getLogger(DataRepositoryExtension.class.getName());
+
+    private final List<Class<?>> repositoryInterfaces = new ArrayList<>();
+    private boolean active = true;
+
+    public void beforeBeanDiscovery(@Observes final BeforeBeanDiscovery event) {
+        active = Boolean.parseBoolean(
+                SystemInstance.get().getOptions().get("tomee.jakarta-data.active", "true"));
+        if (!active) {
+            LOGGER.info("Jakarta Data extension disabled via tomee.jakarta-data.active=false");
+        }
+    }
+
+    public <T> void detectRepositories(@Observes @WithAnnotations(Repository.class) final ProcessAnnotatedType<T> event) {
+        if (!active) {
+            return;
+        }
+        final Class<T> javaClass = event.getAnnotatedType().getJavaClass();
+        if (!javaClass.isInterface()) {
+            return;
+        }
+
+        final Repository repoAnnotation = javaClass.getAnnotation(Repository.class);
+        if (repoAnnotation == null) {
+            return;
+        }
+
+        // Skip repositories with a non-empty provider that isn't ours
+        final String provider = repoAnnotation.provider();
+        if (provider != null && !provider.isEmpty()) {
+            LOGGER.fine("Skipping Jakarta Data repository with provider '" + provider + "': " + javaClass.getName());
+            return;
+        }
+
+        // Skip repositories whose entity type is not a JPA @Entity
+        final Class<?> entityClass = RepositoryMetadata.resolveEntityClass(javaClass);
+        if (entityClass != null && entityClass != Object.class
+            && !entityClass.isAnnotationPresent(jakarta.persistence.Entity.class)) {
+            LOGGER.fine("Skipping Jakarta Data repository for non-JPA entity " + entityClass.getName() + ": " + javaClass.getName());
+            return;
+        }
+
+        // Hibernate's annotation processor generates implementation classes with an underscore
+        // prefix (e.g., _MyRepository) at compile time. If such a class exists, Hibernate
+        // handles this repository — we should not register our proxy.
+        if (hasHibernateGeneratedImpl(javaClass)) {
+            LOGGER.fine("Skipping Jakarta Data repository with Hibernate-generated implementation: " + javaClass.getName());
+            return;
+        }
+
+        LOGGER.info("Discovered Jakarta Data repository: " + javaClass.getName());
+        repositoryInterfaces.add(javaClass);
+        event.veto();
+    }
+
+    private static boolean hasHibernateGeneratedImpl(final Class<?> repoInterface) {
+        // Hibernate's annotation processor generates implementation classes with a
+        // trailing underscore (e.g., DataItemRepository_) in the same package.
+        final String generatedName = repoInterface.getName() + "_";
+        try {
+            Thread.currentThread().getContextClassLoader().loadClass(generatedName);
+            return true;
+        } catch (final ClassNotFoundException | NoClassDefFoundError e) {
+            return false;
+        }
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    public void registerRepositoryBeans(@Observes final AfterBeanDiscovery event) {
+        if (!active) {
+            return;
+        }
+        for (final Class<?> repoInterface : repositoryInterfaces) {
+            LOGGER.info("Registering Jakarta Data repository bean: " + repoInterface.getName());
+            event.addBean(new RepositoryBean(repoInterface));
+        }
+    }
+}
diff --git a/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/extension/RepositoryBean.java b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/extension/RepositoryBean.java
new file mode 100644
index 0000000000..b109d282a5
--- /dev/null
+++ b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/extension/RepositoryBean.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.extension;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.context.spi.CreationalContext;
+import jakarta.enterprise.inject.Any;
+import jakarta.enterprise.inject.Default;
+import jakarta.enterprise.inject.spi.Bean;
+import jakarta.enterprise.inject.spi.InjectionPoint;
+import jakarta.enterprise.inject.spi.PassivationCapable;
+import org.apache.openejb.data.handler.RepositoryInvocationHandler;
+import org.apache.openejb.data.meta.RepositoryMetadata;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Proxy;
+import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public class RepositoryBean<T> implements Bean<T>, PassivationCapable {
+
+    private final Class<T> repositoryInterface;
+    private final RepositoryMetadata metadata;
+    private final Set<Type> types;
+    private final Set<Annotation> qualifiers;
+
+    public RepositoryBean(final Class<T> repositoryInterface) {
+        this.repositoryInterface = repositoryInterface;
+        this.metadata = new RepositoryMetadata(repositoryInterface);
+
+        final Set<Type> t = new HashSet<>();
+        t.add(repositoryInterface);
+        t.add(Object.class);
+        this.types = Collections.unmodifiableSet(t);
+
+        final Set<Annotation> q = new HashSet<>();
+        q.add(Default.Literal.INSTANCE);
+        q.add(Any.Literal.INSTANCE);
+        this.qualifiers = Collections.unmodifiableSet(q);
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public T create(final CreationalContext<T> creationalContext) {
+        final RepositoryInvocationHandler handler = new RepositoryInvocationHandler(metadata);
+        return (T) Proxy.newProxyInstance(
+            repositoryInterface.getClassLoader(),
+            new Class<?>[]{repositoryInterface},
+            handler
+        );
+    }
+
+    @Override
+    public void destroy(final T instance, final CreationalContext<T> creationalContext) {
+        creationalContext.release();
+    }
+
+    @Override
+    public Set<Type> getTypes() {
+        return types;
+    }
+
+    @Override
+    public Set<Annotation> getQualifiers() {
+        return qualifiers;
+    }
+
+    @Override
+    public Class<? extends Annotation> getScope() {
+        return ApplicationScoped.class;
+    }
+
+    @Override
+    public String getName() {
+        return null;
+    }
+
+    @Override
+    public Set<Class<? extends Annotation>> getStereotypes() {
+        return Collections.emptySet();
+    }
+
+    @Override
+    public boolean isAlternative() {
+        return false;
+    }
+
+    @Override
+    public Class<?> getBeanClass() {
+        return repositoryInterface;
+    }
+
+    @Override
+    public Set<InjectionPoint> getInjectionPoints() {
+        return Collections.emptySet();
+    }
+
+    @Override
+    public String getId() {
+        return "openejb-jakarta-data-" + repositoryInterface.getName();
+    }
+}
diff --git a/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/handler/RepositoryInvocationHandler.java b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/handler/RepositoryInvocationHandler.java
new file mode 100644
index 0000000000..9d8b797e4c
--- /dev/null
+++ b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/handler/RepositoryInvocationHandler.java
@@ -0,0 +1,946 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.handler;
+
+import jakarta.data.page.Page;
+import jakarta.data.page.PageRequest;
+import jakarta.data.page.impl.PageRecord;
+import jakarta.data.repository.BasicRepository;
+import jakarta.data.repository.CrudRepository;
+import jakarta.data.repository.Delete;
+import jakarta.data.repository.Find;
+import jakarta.data.repository.Insert;
+import jakarta.data.repository.Query;
+import jakarta.data.repository.Save;
+import jakarta.data.repository.Update;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.TypedQuery;
+import jakarta.transaction.Status;
+import jakarta.transaction.UserTransaction;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import jakarta.validation.executable.ExecutableValidator;
+import org.apache.openejb.bval.ValidatorUtil;
+import org.apache.openejb.data.meta.MethodMetadata;
+import org.apache.openejb.data.meta.RepositoryMetadata;
+import org.apache.openejb.data.proxy.EntityManagerLookup;
+import org.apache.openejb.data.query.AnnotatedQueryExecutor;
+import org.apache.openejb.data.query.CriteriaQueryBuilder;
+import org.apache.openejb.data.query.FindAnnotationExecutor;
+import org.apache.openejb.data.query.MethodNameParser;
+import org.apache.openejb.loader.SystemInstance;
+import org.apache.openejb.spi.ContainerSystem;
+
+import javax.naming.NamingException;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Stream;
+
+public class RepositoryInvocationHandler implements InvocationHandler {
+
+    private static final Logger LOGGER = Logger.getLogger(RepositoryInvocationHandler.class.getName());
+
+    private final RepositoryMetadata metadata;
+    private final ConcurrentHashMap<Method, MethodMetadata> methodCache = new ConcurrentHashMap<>();
+    private final ConcurrentHashMap<Method, String> jpqlCache = new ConcurrentHashMap<>();
+    private final String findAllJpql;
+    private final String deleteAllJpql;
+    private final String countAllJpql;
+
+    public RepositoryInvocationHandler(final RepositoryMetadata metadata) {
+        this.metadata = metadata;
+        final String entityName = metadata.getEntityClass().getSimpleName();
+        this.findAllJpql = "SELECT e FROM " + entityName + " e";
+        this.deleteAllJpql = "DELETE FROM " + entityName;
+        this.countAllJpql = "SELECT COUNT(e) FROM " + entityName + " e";
+    }
+
+    @Override
+    public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
+        // Object methods
+        if (method.getDeclaringClass().equals(Object.class)) {
+            return switch (method.getName()) {
+                case "toString" -> "Jakarta Data Repository Proxy [" + metadata.getRepositoryInterface().getName() + "]";
+                case "hashCode" -> System.identityHashCode(proxy);
+                case "equals" -> proxy == args[0];
+                default -> method.invoke(this, args);
+            };
+        }
+
+        // Default methods
+        if (method.isDefault()) {
+            return InvocationHandler.invokeDefault(proxy, method, args);
+        }
+
+        try {
+            // Bean Validation: validate parameters
+            validateParameters(proxy, method, args);
+
+            final EntityManager em = EntityManagerLookup.lookup(metadata.getDataStore());
+            final Object result = executeInTransaction(em, method, args);
+
+            // Bean Validation: validate return value
+            validateReturnValue(proxy, method, result);
+
+            return result;
+        } catch (final jakarta.data.exceptions.DataException de) {
+            // Already a Jakarta Data exception, pass through
+            throw de;
+        } catch (final ConstraintViolationException cve) {
+            // Bean Validation exception, pass through
+            throw cve;
+        } catch (final Exception e) {
+            throw mapException(e);
+        }
+    }
+
+    private void validateParameters(final Object proxy, final Method method, final Object[] args) {
+        if (args == null || args.length == 0) {
+            return;
+        }
+        try {
+            final ExecutableValidator execValidator = getExecutableValidator();
+            if (execValidator == null) {
+                return;
+            }
+            final Set<ConstraintViolation<Object>> violations =
+                execValidator.validateParameters(proxy, method, args);
+            if (!violations.isEmpty()) {
+                throw new ConstraintViolationException(violations);
+            }
+        } catch (final ConstraintViolationException e) {
+            throw e;
+        } catch (final Exception e) {
+            LOGGER.log(Level.FINE, "Bean Validation not available for parameter validation", e);
+        }
+    }
+
+    private void validateReturnValue(final Object proxy, final Method method, final Object result) {
+        if (method.getReturnType() == void.class) {
+            return;
+        }
+        try {
+            final ExecutableValidator execValidator = getExecutableValidator();
+            if (execValidator == null) {
+                return;
+            }
+            final Set<ConstraintViolation<Object>> violations =
+                execValidator.validateReturnValue(proxy, method, result);
+            if (!violations.isEmpty()) {
+                throw new ConstraintViolationException(violations);
+            }
+        } catch (final ConstraintViolationException e) {
+            throw e;
+        } catch (final Exception e) {
+            LOGGER.log(Level.FINE, "Bean Validation not available for return value validation", e);
+        }
+    }
+
+    private ExecutableValidator getExecutableValidator() {
+        try {
+            return ValidatorUtil.validator().forExecutables();
+        } catch (final Exception e) {
+            return null;
+        }
+    }
+
+    private Object executeInTransaction(final EntityManager em, final Method method, final Object[] args) throws Exception {
+        final UserTransaction ut = lookupUserTransaction();
+        if (ut == null) {
+            return doInvoke(em, method, args);
+        }
+
+        final boolean startedTx = ut.getStatus() != Status.STATUS_ACTIVE;
+        if (startedTx) {
+            ut.begin();
+        }
+        try {
+            final Object result = doInvoke(em, method, args);
+            if (startedTx) {
+                ut.commit();
+            }
+            return result;
+        } catch (final Exception e) {
+            if (startedTx) {
+                try {
+                    ut.rollback();
+                } catch (final Exception rollbackEx) {
+                    LOGGER.log(Level.WARNING, "Rollback failed", rollbackEx);
+                }
+            }
+            throw mapException(e);
+        }
+    }
+
+    private UserTransaction lookupUserTransaction() {
+        try {
+            final ContainerSystem containerSystem = SystemInstance.get().getComponent(ContainerSystem.class);
+            if (containerSystem != null) {
+                return (UserTransaction) containerSystem.getJNDIContext().lookup("comp/UserTransaction");
+            }
+        } catch (final NamingException e) {
+            LOGGER.log(Level.FINE, "UserTransaction not available via JNDI", e);
+        }
+        return null;
+    }
+
+    private Object doInvoke(final EntityManager em, final Method method, final Object[] args) {
+        // Check built-in method names from BasicRepository/CrudRepository FIRST
+        // These have well-known semantics and their inherited annotations (like @Find on findById)
+        // may not work correctly with the generic annotation handlers.
+        final String methodName = method.getName();
+        switch (methodName) {
+            case "save": return handleSave(em, args);
+            case "saveAll": return handleSaveAll(em, args);
+            case "insert", "insertAll": return handleInsert(em, args);
+            case "update", "updateAll": return handleUpdate(em, args);
+            case "findById": return handleFindById(em, method, args);
+            case "findAll": return handleFindAll(em, method, args);
+            case "delete": handleDelete(em, args); return null;
+            case "deleteById": {
+                // Check if this is a custom @Delete + @By method (not the built-in CrudRepository.deleteById)
+                if (method.isAnnotationPresent(Delete.class) && isCustomDeleteById(method)) {
+                    return handleDeleteAnnotation(em, method, args);
+                }
+                handleDeleteById(em, args);
+                return null;
+            }
+            case "deleteAll": handleDeleteAll(em, args); return null;
+            case "count", "countAll": return handleCount(em);
+            case "existsById": return handleExistsById(em, args);
+            default: break;
+        }
+
+        // Check annotations for custom methods
+        if (method.isAnnotationPresent(Query.class)) {
+            final String jpql = jpqlCache.computeIfAbsent(method,
+                m -> AnnotatedQueryExecutor.buildJpql(m, metadata.getEntityClass(), em));
+            return AnnotatedQueryExecutor.execute(em, method, args, jpql);
+        }
+
+        if (method.isAnnotationPresent(Insert.class)) {
+            return handleInsert(em, args);
+        }
+
+        if (method.isAnnotationPresent(Update.class)) {
+            return handleUpdate(em, args);
+        }
+
+        if (method.isAnnotationPresent(Delete.class)) {
+            return handleDeleteAnnotation(em, method, args);
+        }
+
+        if (method.isAnnotationPresent(Save.class)) {
+            return handleSave(em, args);
+        }
+
+        if (method.isAnnotationPresent(Find.class)) {
+            final String jpql = jpqlCache.computeIfAbsent(method,
+                m -> FindAnnotationExecutor.buildJpql(m, metadata.getEntityClass()));
+            return FindAnnotationExecutor.execute(em, method, args, metadata.getEntityClass(), jpql);
+        }
+
+        return handleCustomMethod(em, method, args);
+    }
+
+    // -- Built-in CRUD operations --
+
+    @SuppressWarnings("unchecked")
+    private <T> T handleSave(final EntityManager em, final Object[] args) {
+        if (args == null || args.length != 1) {
+            throw new IllegalArgumentException("save requires exactly one argument");
+        }
+        final Object result = applyToEntities(args[0], e -> em.merge(e));
+        em.flush();
+        return (T) result;
+    }
+
+    @SuppressWarnings("unchecked")
+    private <T> List<T> handleSaveAll(final EntityManager em, final Object[] args) {
+        final List<T> result = new ArrayList<>();
+        for (final Object entity : asIterable(args[0])) {
+            result.add((T) em.merge(entity));
+        }
+        em.flush();
+        return result;
+    }
+
+    @SuppressWarnings("unchecked")
+    private <T> T handleInsert(final EntityManager em, final Object[] args) {
+        if (args == null || args.length != 1) {
+            throw new IllegalArgumentException("insert requires exactly one argument");
+        }
+        return (T) applyToEntities(args[0], e -> {
+            clearVersionField(em, e);
+            em.persist(e);
+            em.flush();
+            return e;
+        });
+    }
+
+    private void clearVersionField(final EntityManager em, final Object entity) {
+        try {
+            final jakarta.persistence.metamodel.EntityType<?> entityType =
+                em.getMetamodel().entity(entity.getClass());
+            if (entityType.hasVersionAttribute()) {
+                for (final jakarta.persistence.metamodel.SingularAttribute<?, ?> attr : entityType.getSingularAttributes()) {
+                    if (attr.isVersion()) {
+                        final java.lang.reflect.Field field = entity.getClass().getDeclaredField(attr.getName());
+                        field.setAccessible(true);
+                        if (field.getType() == long.class) {
+                            field.setLong(entity, 0L);
+                        } else if (field.getType() == int.class) {
+                            field.setInt(entity, 0);
+                        } else if (field.getType() == Long.class || field.getType() == Integer.class) {
+                            field.set(entity, null);
+                        }
+                        break;
+                    }
+                }
+            }
+        } catch (final Exception e) {
+            LOGGER.log(Level.FINE, "Could not clear version field", e);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private <T> T handleUpdate(final EntityManager em, final Object[] args) {
+        if (args == null || args.length != 1) {
+            throw new IllegalArgumentException("update requires exactly one argument");
+        }
+        return (T) applyToEntities(args[0], e -> mergeWithVersionCheck(em, e));
+    }
+
+    /**
+     * Performs an update with version checking. The strategy depends on the JPA provider:
+     * <ul>
+     *   <li>Hibernate/EclipseLink: standard {@code em.merge()} handles @Version correctly</li>
+     *   <li>OpenJPA: requires a workaround — reflection-based field copying with
+     *       OPTIMISTIC_FORCE_INCREMENT because OpenJPA restricts setting @Version fields</li>
+     * </ul>
+     */
+    private Object mergeWithVersionCheck(final EntityManager em, final Object entity) {
+        final Object id = getEntityId(em, entity);
+        if (id == null) {
+            return em.merge(entity);
+        }
+
+        final Object existing = em.find(entity.getClass(), id);
+        if (existing == null) {
+            throw new jakarta.data.exceptions.OptimisticLockingFailureException(
+                "Entity not found for update: " + entity);
+        }
+
+        // Check version before merge
+        checkVersionMatch(em, entity, existing);
+
+        // Providers other than OpenJPA handle @Version correctly via em.merge()
+        if (!isOpenJpa(em)) {
+            return em.merge(entity);
+        }
+
+        // OpenJPA workaround: copy fields via reflection, force version increment
+        try {
+            final jakarta.persistence.metamodel.EntityType<?> entityType =
+                em.getMetamodel().entity(entity.getClass());
+
+            for (final jakarta.persistence.metamodel.Attribute<?, ?> attr : entityType.getAttributes()) {
+                if (attr instanceof jakarta.persistence.metamodel.SingularAttribute<?, ?> sa) {
+                    if (sa.isId() || sa.isVersion()) {
+                        continue;
+                    }
+                }
+                final java.lang.reflect.Field field = findField(entity.getClass(), attr.getName());
+                field.setAccessible(true);
+                final java.lang.reflect.Field existingField = findField(existing.getClass(), attr.getName());
+                existingField.setAccessible(true);
+                existingField.set(existing, field.get(entity));
+            }
+            em.lock(existing, jakarta.persistence.LockModeType.OPTIMISTIC_FORCE_INCREMENT);
+            em.flush();
+            em.refresh(existing);
+            // Copy updated fields (including version) back to the input entity
+            for (final jakarta.persistence.metamodel.Attribute<?, ?> attr : entityType.getAttributes()) {
+                try {
+                    final java.lang.reflect.Field f = findField(entity.getClass(), attr.getName());
+                    f.setAccessible(true);
+                    final java.lang.reflect.Field ef = findField(existing.getClass(), attr.getName());
+                    ef.setAccessible(true);
+                    f.set(entity, ef.get(existing));
+                } catch (final NoSuchFieldException ignored) {
+                    // skip
+                }
+            }
+            return entity;
+        } catch (final jakarta.data.exceptions.OptimisticLockingFailureException e) {
+            throw e;
+        } catch (final Exception e) {
+            return em.merge(entity);
+        }
+    }
+
+    private static boolean isOpenJpa(final EntityManager em) {
+        return em.getClass().getName().startsWith("org.apache.openjpa.")
+            || em.getDelegate().getClass().getName().startsWith("org.apache.openjpa.");
+    }
+
+    private Object handleFindById(final EntityManager em, final Method method, final Object[] args) {
+        final Object id = args[0];
+        final Object entity = em.find(metadata.getEntityClass(), id);
+        if (Optional.class.isAssignableFrom(method.getReturnType())) {
+            return Optional.ofNullable(entity);
+        }
+        return entity;
+    }
+
+    private Object handleFindAll(final EntityManager em, final Method method, final Object[] args) {
+        final TypedQuery<Object> query = em.createQuery(findAllJpql, Object.class);
+        CriteriaQueryBuilder.applyPagination(query, args);
+
+        final List<Object> results = query.getResultList();
+        final Class<?> returnType = method.getReturnType();
+
+        if (Page.class.isAssignableFrom(returnType)) {
+            return buildPage(em, metadata.getEntityClass(), results, args, null);
+        }
+        if (Stream.class.isAssignableFrom(returnType)) {
+            return results.stream();
+        }
+        return results;
+    }
+
+    private void handleDelete(final EntityManager em, final Object[] args) {
+        if (args == null || args.length != 1) {
+            throw new IllegalArgumentException("delete requires exactly one argument");
+        }
+        final Object entity = args[0];
+        if (entity instanceof Iterable<?> iterable) {
+            for (final Object e : iterable) {
+                removeEntity(em, e);
+            }
+        } else {
+            removeEntity(em, entity);
+        }
+    }
+
+    private void handleDeleteById(final EntityManager em, final Object[] args) {
+        final Object id = args[0];
+        final Object entity = em.find(metadata.getEntityClass(), id);
+        if (entity != null) {
+            em.remove(entity);
+        }
+    }
+
+    private void handleDeleteAll(final EntityManager em, final Object[] args) {
+        if (args != null && args.length == 1) {
+            for (final Object entity : asIterable(args[0])) {
+                removeEntity(em, entity);
+            }
+        } else {
+            em.createQuery(deleteAllJpql).executeUpdate();
+        }
+    }
+
+    private Object handleDeleteAnnotation(final EntityManager em, final Method method, final Object[] args) {
+        if (args == null || args.length < 1) {
+            em.createQuery(deleteAllJpql).executeUpdate();
+            return null;
+        }
+
+        // Check if parameters have @By annotations (field-based delete)
+        final java.lang.reflect.Parameter[] params = method.getParameters();
+        boolean hasByAnnotation = false;
+        for (final java.lang.reflect.Parameter p : params) {
+            if (p.isAnnotationPresent(jakarta.data.repository.By.class)) {
+                hasByAnnotation = true;
+                break;
+            }
+        }
+
+        if (hasByAnnotation) {
+            final String jpql = jpqlCache.computeIfAbsent(method, m -> {
+                final java.lang.reflect.Parameter[] mp = m.getParameters();
+                final StringBuilder sb = new StringBuilder("DELETE FROM ")
+                    .append(metadata.getEntityClass().getSimpleName()).append(" e WHERE ");
+                final List<String> conds = new ArrayList<>();
+                for (int j = 0; j < mp.length; j++) {
+                    final jakarta.data.repository.By by = mp[j].getAnnotation(jakarta.data.repository.By.class);
+                    if (by != null) {
+                        conds.add("e." + by.value() + " = :p" + j);
+                    }
+                }
+                sb.append(String.join(" AND ", conds));
+                return sb.toString();
+            });
+
+            final jakarta.persistence.Query query = em.createQuery(jpql);
+            for (int i = 0; i < params.length; i++) {
+                if (params[i].isAnnotationPresent(jakarta.data.repository.By.class)) {
+                    query.setParameter("p" + i, args[i]);
+                }
+            }
+            query.executeUpdate();
+            return null;
+        }
+
+        // Entity-based delete
+        final Object entity = args[0];
+        if (entity instanceof Iterable<?> iterable) {
+            for (final Object e : iterable) {
+                removeEntity(em, e);
+            }
+        } else if (entity.getClass().isArray()) {
+            for (final Object e : (Object[]) entity) {
+                removeEntity(em, e);
+            }
+        } else {
+            removeEntity(em, entity);
+        }
+        return null;
+    }
+
+    private long handleCount(final EntityManager em) {
+        return em.createQuery(countAllJpql, Long.class).getSingleResult();
+    }
+
+    private boolean handleExistsById(final EntityManager em, final Object[] args) {
+        return em.find(metadata.getEntityClass(), args[0]) != null;
+    }
+
+    // -- Custom method handling --
+
+    @SuppressWarnings("unchecked")
+    private Object handleCustomMethod(final EntityManager em, final Method method, final Object[] args) {
+        final MethodMetadata meta = methodCache.computeIfAbsent(method, this::analyzeMethod);
+
+        if (meta.getStrategy() == MethodMetadata.Strategy.METHOD_NAME && meta.getParsedQuery() != null) {
+            final MethodNameParser.ParsedQuery parsed = meta.getParsedQuery();
+
+            return switch (parsed.action()) {
+                case FIND -> {
+                    final String jpql = jpqlCache.computeIfAbsent(method,
+                        m -> CriteriaQueryBuilder.buildFindJpql(metadata.getEntityClass(), parsed));
+                    yield executeFindQuery(em, method, jpql, parsed, args);
+                }
+                case DELETE -> {
+                    final String jpql = jpqlCache.computeIfAbsent(method,
+                        m -> CriteriaQueryBuilder.buildDeleteJpql(metadata.getEntityClass(), parsed));
+                    final long deleted = CriteriaQueryBuilder.executeDelete(em, jpql, parsed, args);
+                    final Class<?> rt = method.getReturnType();
+                    if (rt == int.class || rt == Integer.class) {
+                        yield (int) deleted;
+                    }
+                    yield deleted;
+                }
+                case COUNT -> {
+                    final String jpql = jpqlCache.computeIfAbsent(method,
+                        m -> CriteriaQueryBuilder.buildCountJpql(metadata.getEntityClass(), parsed));
+                    yield CriteriaQueryBuilder.executeCount(em, jpql, parsed, args).getSingleResult();
+                }
+                case EXISTS -> {
+                    final String jpql = jpqlCache.computeIfAbsent(method,
+                        m -> CriteriaQueryBuilder.buildCountJpql(metadata.getEntityClass(), parsed));
+                    yield CriteriaQueryBuilder.executeCount(em, jpql, parsed, args).getSingleResult() > 0;
+                }
+            };
+        }
+
+        throw new UnsupportedOperationException("Cannot resolve method: " + method.getName()
+            + " on repository " + metadata.getRepositoryInterface().getName());
+    }
+
+    @SuppressWarnings("unchecked")
+    private Object executeFindQuery(final EntityManager em, final Method method,
+                                    final String jpql,
+                                    final MethodNameParser.ParsedQuery parsed, final Object[] args) {
+        final TypedQuery<?> query = CriteriaQueryBuilder.executeFind(em, metadata.getEntityClass(), jpql, parsed, args);
+        final Class<?> returnType = method.getReturnType();
+
+        if (Page.class.isAssignableFrom(returnType)) {
+            return buildPage(em, metadata.getEntityClass(), new ArrayList<>(query.getResultList()), args, parsed);
+        }
+
+        if (List.class.isAssignableFrom(returnType) || Collection.class.isAssignableFrom(returnType)) {
+            return query.getResultList();
+        }
+
+        if (Stream.class.isAssignableFrom(returnType)) {
+            return query.getResultList().stream();
+        }
+
+        if (Optional.class.isAssignableFrom(returnType)) {
+            final List<?> results = query.getResultList();
+            return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
+        }
+
+        if (returnType.isArray()) {
+            return query.getResultList().toArray();
+        }
+
+        // Single result
+        final List<?> results = query.getResultList();
+        return results.isEmpty() ? null : results.get(0);
+    }
+
+    private MethodMetadata analyzeMethod(final Method method) {
+        // Try method name parsing
+        final MethodNameParser.ParsedQuery parsed = MethodNameParser.parse(method.getName());
+        if (parsed != null) {
+            return new MethodMetadata(MethodMetadata.Strategy.METHOD_NAME, parsed);
+        }
+        return new MethodMetadata(MethodMetadata.Strategy.BUILTIN, null);
+    }
+
+    // -- Helper methods --
+
+    private void removeEntity(final EntityManager em, final Object entity) {
+        if (em.contains(entity)) {
+            em.remove(entity);
+            return;
+        }
+        // Check if entity exists by looking up its ID
+        final Object id = getEntityId(em, entity);
+        if (id != null) {
+            final Object existing = em.find(entity.getClass(), id);
+            if (existing == null) {
+                throw new jakarta.data.exceptions.OptimisticLockingFailureException(
+                    "Entity not found for deletion: " + entity);
+            }
+            // Check version if entity has @Version
+            checkVersionMatch(em, entity, existing);
+            em.remove(existing);
+        } else {
+            try {
+                final Object merged = em.merge(entity);
+                em.remove(merged);
+            } catch (final Exception e) {
+                throw new jakarta.data.exceptions.OptimisticLockingFailureException(
+                    "Entity not found for deletion: " + entity, e);
+            }
+        }
+    }
+
+    private static java.lang.reflect.Field findField(final Class<?> clazz, final String name) throws NoSuchFieldException {
+        Class<?> current = clazz;
+        while (current != null) {
+            try {
+                return current.getDeclaredField(name);
+            } catch (final NoSuchFieldException e) {
+                current = current.getSuperclass();
+            }
+        }
+        throw new NoSuchFieldException(name);
+    }
+
+    /**
+     * Checks if a deleteById method is a custom one declared on the repository (with @By annotations
+     * pointing to real fields) vs the inherited CrudRepository.deleteById (which may have synthetic @By).
+     */
+    private boolean isCustomDeleteById(final Method method) {
+        // If the method is declared on a built-in repository interface (not user-defined), it's not custom
+        final Class<?> declaringClass = method.getDeclaringClass();
+        if (declaringClass.equals(CrudRepository.class) || declaringClass.equals(BasicRepository.class)) {
+            return false;
+        }
+        // Check if @By annotations reference real entity fields (not synthetic like "id(this)")
+        for (final java.lang.reflect.Parameter p : method.getParameters()) {
+            final jakarta.data.repository.By by = p.getAnnotation(jakarta.data.repository.By.class);
+            if (by != null && by.value().contains("(")) {
+                return false; // Synthetic @By like "id(this)"
+            }
+        }
+        return true;
+    }
+
+    private void checkVersionMatch(final EntityManager em, final Object input, final Object existing) {
+        try {
+            final jakarta.persistence.metamodel.EntityType<?> entityType =
+                em.getMetamodel().entity(input.getClass());
+            if (entityType.hasVersionAttribute()) {
+                for (final jakarta.persistence.metamodel.SingularAttribute<?, ?> attr : entityType.getSingularAttributes()) {
+                    if (attr.isVersion()) {
+                        final java.lang.reflect.Field field = input.getClass().getDeclaredField(attr.getName());
+                        field.setAccessible(true);
+                        final Object inputVersion = field.get(input);
+                        final Object existingVersion = field.get(existing);
+                        if (inputVersion != null && existingVersion != null && !inputVersion.equals(existingVersion)) {
+                            throw new jakarta.data.exceptions.OptimisticLockingFailureException(
+                                "Version mismatch: expected " + existingVersion + " but got " + inputVersion);
+                        }
+                        break;
+                    }
+                }
+            }
+        } catch (final jakarta.data.exceptions.OptimisticLockingFailureException e) {
+            throw e;
+        } catch (final Exception e) {
+            LOGGER.log(Level.FINE, "Could not check version for deletion", e);
+        }
+    }
+
+    private Object getEntityId(final EntityManager em, final Object entity) {
+        try {
+            final jakarta.persistence.metamodel.EntityType<?> entityType =
+                em.getMetamodel().entity(entity.getClass());
+            for (final jakarta.persistence.metamodel.SingularAttribute<?, ?> attr : entityType.getSingularAttributes()) {
+                if (attr.isId()) {
+                    final java.lang.reflect.Field field = entity.getClass().getDeclaredField(attr.getName());
+                    field.setAccessible(true);
+                    return field.get(entity);
+                }
+            }
+        } catch (final Exception e) {
+            LOGGER.log(Level.FINE, "Could not get entity ID", e);
+        }
+        return null;
+    }
+
+    /**
+     * Maps JPA and provider-specific exceptions to Jakarta Data exceptions as required by the spec.
+     * Provider-specific exceptions (EclipseLink, OpenJPA, etc.) are not embedded as causes
+     * to avoid serialization issues in remote containers (e.g., Arquillian).
+     */
+    private static Exception mapException(final Exception e) {
+        // First, check for standard JPA exceptions
+        final Throwable jpaCause = findJpaCause(e);
+        if (jpaCause != null) {
+            final String message = buildExceptionMessage(jpaCause);
+
+            if (jpaCause instanceof jakarta.persistence.EntityExistsException) {
+                return new jakarta.data.exceptions.EntityExistsException(message);
+            }
+            if (jpaCause instanceof jakarta.persistence.OptimisticLockException) {
+                return new jakarta.data.exceptions.OptimisticLockingFailureException(message);
+            }
+            if (jpaCause instanceof jakarta.persistence.EntityNotFoundException) {
+                return new jakarta.data.exceptions.EmptyResultException(message);
+            }
+            if (jpaCause instanceof jakarta.persistence.NonUniqueResultException) {
+                return new jakarta.data.exceptions.NonUniqueResultException(message);
+            }
+            if (jpaCause instanceof jakarta.persistence.NoResultException) {
+                return new jakarta.data.exceptions.EmptyResultException(message);
+            }
+            if (jpaCause instanceof jakarta.persistence.PersistenceException) {
+                // SQL constraint violation within a PersistenceException indicates
+                // an insert-duplicate / unique-key conflict → EntityExistsException per spec
+                if (hasSqlConstraintViolation(jpaCause)) {
+                    return new jakarta.data.exceptions.EntityExistsException(message);
+                }
+                return new jakarta.data.exceptions.DataException(message);
+            }
+        }
+
+        // Second, check for SQL constraint violations anywhere in the chain
+        // (provider-specific exceptions like EclipseLink DatabaseException may not
+        //  extend PersistenceException but still wrap SQLIntegrityConstraintViolationException)
+        if (hasSqlConstraintViolation(e)) {
+            return new jakarta.data.exceptions.EntityExistsException(buildExceptionMessage(e));
+        }
+
+        // Third, wrap any remaining provider-specific exceptions to prevent serialization issues
+        if (isProviderSpecificException(e)) {
+            return new jakarta.data.exceptions.DataException(buildExceptionMessage(e));
+        }
+
+        return e;
+    }
+
+    /**
+     * Checks if the exception cause chain contains a SQL constraint violation
+     * (indicating a duplicate key / entity exists scenario).
+     */
+    private static boolean hasSqlConstraintViolation(final Throwable t) {
+        Throwable current = t;
+        while (current != null) {
+            if (current instanceof java.sql.SQLIntegrityConstraintViolationException) {
+                return true;
+            }
+            // Also check class name for providers that use custom exception hierarchies
+            final String className = current.getClass().getName();
+            if (className.contains("ConstraintViolation") || className.contains("IntegrityConstraint")) {
+                return true;
+            }
+            // Check exception message for constraint violation keywords (covers providers
+            // like EclipseLink where the internal exception may not be in the getCause() chain)
+            final String message = current.getMessage();
+            if (message != null) {
+                final String lowerMsg = message.toLowerCase(java.util.Locale.ROOT);
+                if (lowerMsg.contains("unique constraint") || lowerMsg.contains("integrity constraint")
+                    || lowerMsg.contains("duplicate key") || lowerMsg.contains("duplicate entry")
+                    || lowerMsg.contains("primary key violation")) {
+                    return true;
+                }
+            }
+            // EclipseLink stores internal exceptions in a field not returned by getCause().
+            // Use reflection to access getInternalException() if available.
+            final Throwable internal = getInternalException(current);
+            if (internal != null && internal != current.getCause()) {
+                if (internal instanceof java.sql.SQLIntegrityConstraintViolationException) {
+                    return true;
+                }
+                final String internalName = internal.getClass().getName();
+                if (internalName.contains("ConstraintViolation") || internalName.contains("IntegrityConstraint")) {
+                    return true;
+                }
+            }
+            current = current.getCause();
+        }
+        return false;
+    }
+
+    /**
+     * Attempts to call getInternalException() on the throwable via reflection.
+     * This handles EclipseLink's EclipseLinkException which stores internal exceptions
+     * separately from the standard getCause() chain.
+     */
+    private static Throwable getInternalException(final Throwable t) {
+        try {
+            final java.lang.reflect.Method method = t.getClass().getMethod("getInternalException");
+            final Object result = method.invoke(t);
+            if (result instanceof Throwable) {
+                return (Throwable) result;
+            }
+        } catch (final Exception ignored) {
+            // Method doesn't exist or can't be called - that's fine
+        }
+        return null;
+    }
+
+    /**
+     * Checks if the exception originates from a JPA provider's internal package
+     * (e.g., EclipseLink, OpenJPA) and might not be serializable on a remote client.
+     */
+    private static boolean isProviderSpecificException(final Throwable t) {
+        Throwable current = t;
+        while (current != null) {
+            final String className = current.getClass().getName();
+            if (className.startsWith("org.eclipse.persistence.")
+                || className.startsWith("org.apache.openjpa.")
+                || className.startsWith("org.hibernate.")) {
+                return true;
+            }
+            current = current.getCause();
+        }
+        return false;
+    }
+
+    /**
+     * Builds a descriptive exception message that includes the root cause details
+     * without embedding provider-specific exception objects.
+     */
+    private static String buildExceptionMessage(final Throwable cause) {
+        final StringBuilder sb = new StringBuilder();
+        if (cause.getMessage() != null) {
+            sb.append(cause.getMessage());
+        }
+        Throwable root = cause.getCause();
+        while (root != null) {
+            if (root.getMessage() != null && !root.getMessage().isEmpty()) {
+                sb.append(" [Caused by: ").append(root.getMessage()).append("]");
+                break;
+            }
+            root = root.getCause();
+        }
+        return sb.toString();
+    }
+
+    private static Throwable findJpaCause(final Throwable t) {
+        Throwable current = t;
+        while (current != null) {
+            if (current instanceof jakarta.persistence.PersistenceException) {
+                return current;
+            }
+            current = current.getCause();
+        }
+        return null;
+    }
+
+    @FunctionalInterface
+    private interface EntityOperation {
+        Object apply(Object entity);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Object applyToEntities(final Object arg, final EntityOperation op) {
+        if (arg instanceof Iterable<?>) {
+            final List<Object> result = new ArrayList<>();
+            for (final Object entity : (Iterable<?>) arg) {
+                result.add(op.apply(entity));
+            }
+            return result;
+        }
+        if (arg.getClass().isArray()) {
+            final Object[] array = (Object[]) arg;
+            final Object[] result = java.util.Arrays.copyOf(array, array.length);
+            for (int i = 0; i < array.length; i++) {
+                result[i] = op.apply(array[i]);
+            }
+            return result;
+        }
+        return op.apply(arg);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Iterable<Object> asIterable(final Object arg) {
+        if (arg instanceof Iterable<?> it) {
+            return (Iterable<Object>) it;
+        }
+        if (arg.getClass().isArray()) {
+            return List.of((Object[]) arg);
+        }
+        return List.of(arg);
+    }
+
+    @SuppressWarnings("unchecked")
+    private Page<Object> buildPage(final EntityManager em, final Class<?> entityClass,
+                                   final List<Object> content, final Object[] args,
+                                   final MethodNameParser.ParsedQuery parsed) {
+        PageRequest pageRequest = null;
+        if (args != null) {
+            for (final Object arg : args) {
+                if (arg instanceof PageRequest pr) {
+                    pageRequest = pr;
+                    break;
+                }
+            }
+        }
+
+        if (pageRequest == null) {
+            pageRequest = PageRequest.ofPage(1);
+        }
+
+        // Count total - use the same conditions as the finder query if available
+        final long total;
+        if (parsed != null && !parsed.conditions().isEmpty()) {
+            final String countJpql = CriteriaQueryBuilder.buildCountJpql(entityClass, parsed);
+            total = CriteriaQueryBuilder.executeCount(em, countJpql, parsed, args).getSingleResult();
+        } else {
+            total = em.createQuery(countAllJpql, Long.class).getSingleResult();
+        }
+
+        return new PageRecord<>(pageRequest, content, total);
+    }
+}
diff --git a/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/meta/MethodMetadata.java b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/meta/MethodMetadata.java
new file mode 100644
index 0000000000..0af419e547
--- /dev/null
+++ b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/meta/MethodMetadata.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.meta;
+
+import org.apache.openejb.data.query.MethodNameParser;
+
+public class MethodMetadata {
+
+    public enum Strategy {
+        BUILTIN,
+        METHOD_NAME
+    }
+
+    private final Strategy strategy;
+    private final MethodNameParser.ParsedQuery parsedQuery;
+
+    public MethodMetadata(final Strategy strategy, final MethodNameParser.ParsedQuery parsedQuery) {
+        this.strategy = strategy;
+        this.parsedQuery = parsedQuery;
+    }
+
+    public Strategy getStrategy() {
+        return strategy;
+    }
+
+    public MethodNameParser.ParsedQuery getParsedQuery() {
+        return parsedQuery;
+    }
+}
diff --git a/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/meta/RepositoryMetadata.java b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/meta/RepositoryMetadata.java
new file mode 100644
index 0000000000..63b7068692
--- /dev/null
+++ b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/meta/RepositoryMetadata.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.meta;
+
+import jakarta.data.repository.Repository;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+
+public class RepositoryMetadata {
+
+    private final Class<?> repositoryInterface;
+    private final Class<?> entityClass;
+    private final String dataStore;
+
+    public RepositoryMetadata(final Class<?> repositoryInterface) {
+        this.repositoryInterface = repositoryInterface;
+
+        final Repository repoAnnotation = repositoryInterface.getAnnotation(Repository.class);
+        this.dataStore = repoAnnotation != null ? repoAnnotation.dataStore() : "";
+
+        this.entityClass = resolveEntityClass(repositoryInterface);
+    }
+
+    public static Class<?> resolveEntityClass(final Class<?> repoInterface) {
+        for (final Type type : repoInterface.getGenericInterfaces()) {
+            if (type instanceof ParameterizedType pt) {
+                final Type rawType = pt.getRawType();
+                if (rawType instanceof Class<?> rawClass && isDataRepository(rawClass)) {
+                    final Type[] args = pt.getActualTypeArguments();
+                    if (args.length >= 1 && args[0] instanceof Class<?>) {
+                        return (Class<?>) args[0];
+                    }
+                }
+                // recurse into parent interfaces
+                if (rawType instanceof Class<?> rawClass) {
+                    final Class<?> result = resolveEntityClass(rawClass);
+                    if (result != Object.class) {
+                        return result;
+                    }
+                }
+            } else if (type instanceof Class<?> parentInterface) {
+                final Class<?> result = resolveEntityClass(parentInterface);
+                if (result != Object.class) {
+                    return result;
+                }
+            }
+        }
+        return Object.class;
+    }
+
+    private static boolean isDataRepository(final Class<?> clazz) {
+        if (clazz.getName().startsWith("jakarta.data.repository.")) {
+            return true;
+        }
+        for (final Class<?> iface : clazz.getInterfaces()) {
+            if (isDataRepository(iface)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public Class<?> getRepositoryInterface() {
+        return repositoryInterface;
+    }
+
+    public Class<?> getEntityClass() {
+        return entityClass;
+    }
+
+    public String getDataStore() {
+        return dataStore;
+    }
+}
diff --git a/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/proxy/EntityManagerLookup.java b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/proxy/EntityManagerLookup.java
new file mode 100644
index 0000000000..6b3289eb7e
--- /dev/null
+++ b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/proxy/EntityManagerLookup.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.proxy;
+
+import jakarta.enterprise.inject.spi.Bean;
+import jakarta.enterprise.inject.spi.BeanManager;
+import jakarta.enterprise.inject.spi.CDI;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.EntityManagerFactory;
+
+import org.apache.openejb.loader.SystemInstance;
+import org.apache.openejb.persistence.JtaEntityManager;
+import org.apache.openejb.persistence.JtaEntityManagerRegistry;
+import org.apache.openejb.spi.ContainerSystem;
+
+import javax.naming.Context;
+import javax.naming.NamingException;
+import java.util.HashMap;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public final class EntityManagerLookup {
+
+    private static final Logger LOGGER = Logger.getLogger(EntityManagerLookup.class.getName());
+
+    private EntityManagerLookup() {
+    }
+
+    public static EntityManager lookup(final String dataStore) {
+        // First, try to resolve via CDI (e.g., @Produces EntityManagerFactory or @PersistenceUnit)
+        final EntityManagerFactory cdiEmf = lookupViaCDI(dataStore);
+        if (cdiEmf != null) {
+            return wrapWithJta(cdiEmf, dataStore != null && !dataStore.isEmpty() ? dataStore : "default");
+        }
+
+        // Fall back to JNDI lookup
+        return lookupViaJNDI(dataStore);
+    }
+
+    private static EntityManagerFactory lookupViaCDI(final String dataStore) {
+        try {
+            final BeanManager bm = CDI.current().getBeanManager();
+            final Set<Bean<?>> beans = bm.getBeans(EntityManagerFactory.class);
+            if (beans.isEmpty()) {
+                return null;
+            }
+
+            // If a specific dataStore is requested, try to find a matching bean by name
+            if (dataStore != null && !dataStore.isEmpty()) {
+                for (final Bean<?> bean : beans) {
+                    if (dataStore.equals(bean.getName())) {
+                        return (EntityManagerFactory) bm.getReference(bean, EntityManagerFactory.class, bm.createCreationalContext(bean));
+                    }
+                }
+            }
+
+            // If there's exactly one EMF bean, use it
+            if (beans.size() == 1) {
+                final Bean<?> bean = bm.resolve(beans);
+                return (EntityManagerFactory) bm.getReference(bean, EntityManagerFactory.class, bm.createCreationalContext(bean));
+            }
+
+            return null;
+        } catch (final Exception e) {
+            LOGGER.log(Level.FINE, "CDI lookup for EntityManagerFactory not available, falling back to JNDI", e);
+            return null;
+        }
+    }
+
+    private static EntityManager lookupViaJNDI(final String dataStore) {
+        final ContainerSystem containerSystem = SystemInstance.get().getComponent(ContainerSystem.class);
+        if (containerSystem == null) {
+            throw new IllegalStateException("ContainerSystem not available");
+        }
+
+        final Context jndiContext = containerSystem.getJNDIContext();
+        final String jndiName = resolveJndiName(dataStore, jndiContext);
+
+        try {
+            final Object obj = jndiContext.lookup(jndiName);
+            if (obj instanceof EntityManager em) {
+                return em;
+            }
+            if (obj instanceof EntityManagerFactory emf) {
+                return wrapWithJta(emf, extractUnitName(jndiName));
+            }
+            throw new IllegalStateException("JNDI lookup for '" + jndiName + "' returned unexpected type: " + obj.getClass());
+        } catch (final NamingException e) {
+            throw new IllegalStateException("Failed to lookup EntityManagerFactory for dataStore='" + dataStore + "'", e);
+        }
+    }
+
+    private static EntityManager wrapWithJta(final EntityManagerFactory emf, final String unitName) {
+        final JtaEntityManagerRegistry registry =
+                SystemInstance.get().getComponent(JtaEntityManagerRegistry.class);
+        if (registry != null) {
+            return new JtaEntityManager(registry, emf, new HashMap<>(), unitName, "SYNCHRONIZED");
+        }
+        return emf.createEntityManager();
+    }
+
+    private static String extractUnitName(final String jndiName) {
+        final int lastSlash = jndiName.lastIndexOf('/');
+        return lastSlash >= 0 ? jndiName.substring(lastSlash + 1) : jndiName;
+    }
+
+    private static String resolveJndiName(final String dataStore, final Context jndiContext) {
+        if (dataStore != null && !dataStore.isEmpty()) {
+            // Try direct JNDI paths first
+            final String[] candidates = {
+                "openejb/PersistenceUnit/" + dataStore,
+                "java:openejb/PersistenceUnit/" + dataStore
+            };
+            for (final String candidate : candidates) {
+                try {
+                    jndiContext.lookup(candidate);
+                    return candidate;
+                } catch (final NamingException ignored) {
+                    // try next
+                }
+            }
+
+            // Search by PU name within enumerated entries (handles app-scoped names)
+            final String matchedName = findPersistenceUnitByName(dataStore, jndiContext);
+            if (matchedName != null) {
+                return matchedName;
+            }
+
+            return "openejb/PersistenceUnit/" + dataStore;
+        }
+
+        // Default: try to find any available persistence unit
+        try {
+            final Context puContext = (Context) jndiContext.lookup("openejb/PersistenceUnit");
+            final javax.naming.NamingEnumeration<javax.naming.NameClassPair> list = puContext.list("");
+            String found = null;
+            while (list.hasMore()) {
+                if (found != null) {
+                    throw new IllegalStateException(
+                        "Multiple persistence units found. Use @Repository(dataStore=\"...\") to specify which one to use.");
+                }
+                found = list.next().getName();
+            }
+            if (found != null) {
+                return "openejb/PersistenceUnit/" + found;
+            }
+        } catch (final NamingException ignored) {
+            // fall through
+        }
+
+        throw new IllegalStateException("No persistence unit found. Define a persistence unit or use @Repository(dataStore=\"...\")");
+    }
+
+    /**
+     * Searches the JNDI persistence unit context for a PU whose name ends with the given dataStore name.
+     * This handles cases where PUs are registered under app-scoped names (e.g., "AppName/unitName").
+     */
+    private static String findPersistenceUnitByName(final String dataStore, final Context jndiContext) {
+        try {
+            final Context puContext = (Context) jndiContext.lookup("openejb/PersistenceUnit");
+            final javax.naming.NamingEnumeration<javax.naming.NameClassPair> list = puContext.list("");
+            while (list.hasMore()) {
+                final String name = list.next().getName();
+                // Match exact name, app-scoped name ending with /dataStore,
+                // or name starting with dataStore (OpenEJB may append suffixes like " 0")
+                if (name.equals(dataStore) || name.endsWith("/" + dataStore)
+                    || name.startsWith(dataStore + " ") || name.startsWith(dataStore + "/")) {
+                    return "openejb/PersistenceUnit/" + name;
+                }
+            }
+        } catch (final NamingException ignored) {
+            // not available
+        }
+        return null;
+    }
+}
diff --git a/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/query/AnnotatedQueryExecutor.java b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/query/AnnotatedQueryExecutor.java
new file mode 100644
index 0000000000..74c3642ed5
--- /dev/null
+++ b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/query/AnnotatedQueryExecutor.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.query;
+
+import jakarta.data.repository.Param;
+import jakarta.data.repository.Query;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.metamodel.EntityType;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+public final class AnnotatedQueryExecutor {
+
+    private AnnotatedQueryExecutor() {
+    }
+
+    /**
+     * Builds the JPQL string for a @Query-annotated method. Cacheable — depends only on
+     * the annotation value and entity class. The first call requires an EntityManager
+     * for JDQL field reference qualification; subsequent calls use the cached result.
+     */
+    public static String buildJpql(final Method method, final Class<?> entityClass, final EntityManager em) {
+        final Query queryAnnotation = method.getAnnotation(Query.class);
+        return expandJdqlToJpql(queryAnnotation.value(), entityClass, em);
+    }
+
+    /**
+     * Executes a @Query-annotated method using the given (cached) JPQL string.
+     */
+    public static Object execute(final EntityManager em, final Method method, final Object[] args,
+                                  final String jpql) {
+        final jakarta.persistence.Query query = em.createQuery(jpql);
+
+        // Bind parameters
+        if (args != null) {
+            final Parameter[] parameters = method.getParameters();
+            int ordinal = 1;
+            for (int i = 0; i < parameters.length; i++) {
+                if (CriteriaQueryBuilder.isSpecialParameter(parameters[i].getType())) {
+                    continue;
+                }
+                final Param param = parameters[i].getAnnotation(Param.class);
+                if (param != null) {
+                    query.setParameter(param.value(), args[i]);
+                } else {
+                    query.setParameter(ordinal++, args[i]);
+                }
+            }
+        }
+
+        CriteriaQueryBuilder.applyPagination(query, args);
+
+        return adaptResult(method, query);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Object adaptResult(final Method method, final jakarta.persistence.Query query) {
+        final Class<?> returnType = method.getReturnType();
+
+        if (returnType.equals(void.class) || returnType.equals(Void.class)) {
+            query.executeUpdate();
+            return null;
+        }
+
+        if (returnType.equals(long.class) || returnType.equals(Long.class)
+            || returnType.equals(int.class) || returnType.equals(Integer.class)) {
+            return query.getSingleResult();
+        }
+
+        if (returnType.equals(boolean.class) || returnType.equals(Boolean.class)) {
+            return !query.getResultList().isEmpty();
+        }
+
+        if (List.class.isAssignableFrom(returnType) || Collection.class.isAssignableFrom(returnType)) {
+            return query.getResultList();
+        }
+
+        if (Stream.class.isAssignableFrom(returnType)) {
+            return query.getResultList().stream();
+        }
+
+        if (Optional.class.isAssignableFrom(returnType)) {
+            final List<?> results = query.getResultList();
+            return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
+        }
+
+        if (returnType.isArray()) {
+            final List<?> results = query.getResultList();
+            return results.toArray();
+        }
+
+        // Single result
+        final List<?> results = query.getResultList();
+        return results.isEmpty() ? null : results.get(0);
+    }
+
+    /**
+     * Expands JDQL shorthand to full JPQL.
+     * JDQL allows queries starting with FROM, WHERE, or ORDER BY without SELECT.
+     * JDQL also allows unqualified field references which need the entity alias prefix.
+     */
+    static String expandJdqlToJpql(final String jdql, final Class<?> entityClass, final EntityManager em) {
+        final String trimmed = jdql.stripLeading();
+        final String upper = trimmed.toUpperCase();
+        final String entityName = entityClass.getSimpleName();
+        boolean expanded = false;
+        String result;
+
+        if (upper.startsWith("WHERE ") || upper.startsWith("WHERE(")) {
+            result = "SELECT e FROM " + entityName + " e " + trimmed;
+            expanded = true;
+        } else if (upper.startsWith("FROM ")) {
+            final String afterFrom = trimmed.substring(5).stripLeading();
+            final int spaceIdx = afterFrom.indexOf(' ');
+            if (spaceIdx > 0) {
+                final String fromEntity = afterFrom.substring(0, spaceIdx);
+                final String rest = afterFrom.substring(spaceIdx).stripLeading();
+                if (rest.toUpperCase().startsWith("WHERE ")
+                    || rest.toUpperCase().startsWith("ORDER ")
+                    || rest.toUpperCase().startsWith("GROUP ")
+                    || rest.toUpperCase().startsWith("HAVING ")) {
+                    result = "SELECT e FROM " + fromEntity + " e " + rest;
+                    expanded = true;
+                } else {
+                    result = "SELECT e " + trimmed;
+                    expanded = false;
+                }
+            } else {
+                result = "SELECT e " + trimmed;
+                expanded = false;
+            }
+        } else if (upper.startsWith("ORDER BY ")) {
+            result = "SELECT e FROM " + entityName + " e " + trimmed;
+            expanded = true;
+        } else {
+            result = jdql;
+        }
+
+        // For JDQL-expanded queries, qualify unqualified field references with the entity alias "e."
+        if (expanded && em != null) {
+            result = qualifyFieldReferences(result, entityClass, em);
+        }
+
+        return result;
+    }
+
+    /**
+     * Qualifies unqualified field references in JPQL with the entity alias "e.".
+     * Uses JPA metamodel to discover entity attribute names.
+     */
+    private static String qualifyFieldReferences(final String jpql, final Class<?> entityClass, final EntityManager em) {
+        final Set<String> fieldNames = new HashSet<>();
+        try {
+            final EntityType<?> entityType = em.getMetamodel().entity(entityClass);
+            entityType.getAttributes().forEach(attr -> fieldNames.add(attr.getName()));
+        } catch (final Exception e) {
+            return jpql;
+        }
+
+        if (fieldNames.isEmpty()) {
+            return jpql;
+        }
+
+        String result = jpql;
+        for (final String field : fieldNames) {
+            final Pattern pattern = Pattern.compile("(?<![.\\w])" + Pattern.quote(field) + "(?![\\w])");
+            final Matcher matcher = pattern.matcher(result);
+            final StringBuilder sb = new StringBuilder();
+            while (matcher.find()) {
+                final int start = matcher.start();
+                if (start > 0 && result.charAt(start - 1) == '.') {
+                    matcher.appendReplacement(sb, Matcher.quoteReplacement(field));
+                } else {
+                    matcher.appendReplacement(sb, "e." + Matcher.quoteReplacement(field));
+                }
+            }
+            matcher.appendTail(sb);
+            result = sb.toString();
+        }
+
+        return result;
+    }
+}
diff --git a/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/query/CriteriaQueryBuilder.java b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/query/CriteriaQueryBuilder.java
new file mode 100644
index 0000000000..85b911f803
--- /dev/null
+++ b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/query/CriteriaQueryBuilder.java
@@ -0,0 +1,240 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.query;
+
+import jakarta.data.Limit;
+import jakarta.data.Sort;
+import jakarta.data.page.PageRequest;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.TypedQuery;
+
+import java.util.List;
+
+public final class CriteriaQueryBuilder {
+
+    private CriteriaQueryBuilder() {
+    }
+
+    // -- JPQL building (cacheable, no args dependency) --
+
+    public static String buildFindJpql(final Class<?> entityClass,
+                                       final MethodNameParser.ParsedQuery parsed) {
+        final StringBuilder jpql = new StringBuilder("SELECT e FROM ")
+            .append(entityClass.getSimpleName()).append(" e");
+
+        appendWhereClause(jpql, parsed.conditions());
+        appendOrderByClause(jpql, parsed.orderClauses());
+
+        return jpql.toString();
+    }
+
+    public static String buildCountJpql(final Class<?> entityClass,
+                                        final MethodNameParser.ParsedQuery parsed) {
+        final StringBuilder jpql = new StringBuilder("SELECT COUNT(e) FROM ")
+            .append(entityClass.getSimpleName()).append(" e");
+
+        appendWhereClause(jpql, parsed.conditions());
+
+        return jpql.toString();
+    }
+
+    public static String buildDeleteJpql(final Class<?> entityClass,
+                                         final MethodNameParser.ParsedQuery parsed) {
+        final StringBuilder jpql = new StringBuilder("DELETE FROM ")
+            .append(entityClass.getSimpleName()).append(" e");
+
+        appendWhereClause(jpql, parsed.conditions());
+
+        return jpql.toString();
+    }
+
+    // -- Query execution (per-call, binds args) --
+
+    @SuppressWarnings("unchecked")
+    public static <T> TypedQuery<T> executeFind(final EntityManager em, final Class<T> entityClass,
+                                                 final String jpql,
+                                                 final MethodNameParser.ParsedQuery parsed,
+                                                 final Object[] args) {
+        final String effectiveJpql = appendDynamicSort(jpql, args);
+        final TypedQuery<T> query = em.createQuery(effectiveJpql, entityClass);
+        if (!bindConditionParameters(query, parsed.conditions(), args)) {
+            // Empty IN collection — return a query that yields no results
+            query.setMaxResults(0);
+            return query;
+        }
+        applyPagination(query, args);
+        return query;
+    }
+
+    public static TypedQuery<Long> executeCount(final EntityManager em,
+                                                 final String jpql,
+                                                 final MethodNameParser.ParsedQuery parsed,
+                                                 final Object[] args) {
+        final TypedQuery<Long> query = em.createQuery(jpql, Long.class);
+        if (!bindConditionParameters(query, parsed.conditions(), args)) {
+            return em.createQuery("SELECT 0L", Long.class);
+        }
+        return query;
+    }
+
+    public static long executeDelete(final EntityManager em,
+                                     final String jpql,
+                                     final MethodNameParser.ParsedQuery parsed,
+                                     final Object[] args) {
+        final jakarta.persistence.Query query = em.createQuery(jpql);
+        if (!bindConditionParameters(query, parsed.conditions(), args)) {
+            return 0L;
+        }
+        return (long) query.executeUpdate();
+    }
+
+    // -- Shared helpers --
+
+    private static void appendWhereClause(final StringBuilder jpql,
+                                           final List<MethodNameParser.Condition> conditions) {
+        if (conditions.isEmpty()) {
+            return;
+        }
+        jpql.append(" WHERE ");
+        for (int i = 0; i < conditions.size(); i++) {
+            final MethodNameParser.Condition c = conditions.get(i);
+            if (i > 0) {
+                jpql.append(c.connector() == MethodNameParser.Connector.OR ? " OR " : " AND ");
+            }
+            final String paramName = "p" + i;
+            switch (c.operator()) {
+                case EQUAL -> jpql.append("e.").append(c.property()).append(" = :").append(paramName);
+                case GREATER_THAN -> jpql.append("e.").append(c.property()).append(" > :").append(paramName);
+                case GREATER_THAN_EQUAL -> jpql.append("e.").append(c.property()).append(" >= :").append(paramName);
+                case LESS_THAN -> jpql.append("e.").append(c.property()).append(" < :").append(paramName);
+                case LESS_THAN_EQUAL -> jpql.append("e.").append(c.property()).append(" <= :").append(paramName);
+                case LIKE, CONTAINS, STARTS_WITH, ENDS_WITH ->
+                    jpql.append("e.").append(c.property()).append(" LIKE :").append(paramName);
+                case NOT -> jpql.append("e.").append(c.property()).append(" <> :").append(paramName);
+                case NULL -> jpql.append("e.").append(c.property()).append(" IS NULL");
+                case NOT_NULL -> jpql.append("e.").append(c.property()).append(" IS NOT NULL");
+                case IN -> jpql.append("e.").append(c.property()).append(" IN :").append(paramName);
+                case BETWEEN -> jpql.append("e.").append(c.property()).append(" BETWEEN :").append(paramName)
+                    .append("a AND :").append(paramName).append("b");
+                case TRUE -> jpql.append("e.").append(c.property()).append(" = TRUE");
+                case FALSE -> jpql.append("e.").append(c.property()).append(" = FALSE");
+                default -> jpql.append("e.").append(c.property()).append(" = :").append(paramName);
+            }
+        }
+    }
+
+    private static void appendOrderByClause(final StringBuilder jpql,
+                                             final List<MethodNameParser.OrderClause> orderClauses) {
+        if (orderClauses.isEmpty()) {
+            return;
+        }
+        jpql.append(" ORDER BY ");
+        for (int i = 0; i < orderClauses.size(); i++) {
+            if (i > 0) {
+                jpql.append(", ");
+            }
+            final MethodNameParser.OrderClause clause = orderClauses.get(i);
+            jpql.append("e.").append(clause.property()).append(clause.ascending() ? " ASC" : " DESC");
+        }
+    }
+
+    /**
+     * Binds args to named parameters. Returns false if an IN parameter has an empty
+     * collection (the query should return empty results without execution).
+     */
+    private static boolean bindConditionParameters(final jakarta.persistence.Query query,
+                                                    final List<MethodNameParser.Condition> conditions,
+                                                    final Object[] args) {
+        int paramIdx = 0;
+        for (int i = 0; i < conditions.size(); i++) {
+            final MethodNameParser.Condition c = conditions.get(i);
+            final int paramCount = c.operator().parameterCount();
+            if (paramCount == 1) {
+                Object value = args[paramIdx++];
+                if (c.operator() == MethodNameParser.Operator.IN
+                    && value instanceof java.util.Collection<?> col && col.isEmpty()) {
+                    return false; // empty IN — query cannot match anything
+                }
+                if (c.operator() == MethodNameParser.Operator.CONTAINS) {
+                    value = "%" + value + "%";
+                } else if (c.operator() == MethodNameParser.Operator.STARTS_WITH) {
+                    value = value + "%";
+                } else if (c.operator() == MethodNameParser.Operator.ENDS_WITH) {
+                    value = "%" + value;
+                }
+                query.setParameter("p" + i, value);
+            } else if (paramCount == 2) {
+                query.setParameter("p" + i + "a", args[paramIdx++]);
+                query.setParameter("p" + i + "b", args[paramIdx++]);
+            }
+        }
+        return true;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static String appendDynamicSort(final String jpql, final Object[] args) {
+        if (args == null) {
+            return jpql;
+        }
+        final List<String> sortClauses = new java.util.ArrayList<>();
+        for (final Object arg : args) {
+            if (arg instanceof Sort<?> sort) {
+                sortClauses.add("e." + sort.property() + (sort.isAscending() ? " ASC" : " DESC"));
+            } else if (arg instanceof jakarta.data.Order<?> order) {
+                for (final Object s : order.sorts()) {
+                    final Sort<?> sort = (Sort<?>) s;
+                    sortClauses.add("e." + sort.property() + (sort.isAscending() ? " ASC" : " DESC"));
+                }
+            }
+        }
+        if (sortClauses.isEmpty()) {
+            return jpql;
+        }
+        // Dynamic sort overrides any static ORDER BY in the cached JPQL
+        final String upperJpql = jpql.toUpperCase();
+        final String baseJpql;
+        final int orderByIdx = upperJpql.lastIndexOf("ORDER BY");
+        if (orderByIdx > 0) {
+            baseJpql = jpql.substring(0, orderByIdx).stripTrailing();
+        } else {
+            baseJpql = jpql;
+        }
+        return baseJpql + " ORDER BY " + String.join(", ", sortClauses);
+    }
+
+    static boolean isSpecialParameter(final Class<?> type) {
+        return jakarta.data.Limit.class.isAssignableFrom(type)
+            || jakarta.data.Sort.class.isAssignableFrom(type)
+            || jakarta.data.Order.class.isAssignableFrom(type)
+            || jakarta.data.page.PageRequest.class.isAssignableFrom(type);
+    }
+
+    public static void applyPagination(final jakarta.persistence.Query query, final Object[] args) {
+        if (args == null) {
+            return;
+        }
+        for (final Object arg : args) {
+            if (arg instanceof Limit limit) {
+                query.setFirstResult((int) (limit.startAt() - 1));
+                query.setMaxResults(limit.maxResults());
+            } else if (arg instanceof PageRequest pageRequest) {
+                query.setFirstResult((int) ((pageRequest.page() - 1) * pageRequest.size()));
+                query.setMaxResults(pageRequest.size());
+            }
+        }
+    }
+}
diff --git a/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/query/FindAnnotationExecutor.java b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/query/FindAnnotationExecutor.java
new file mode 100644
index 0000000000..c41fa64d99
--- /dev/null
+++ b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/query/FindAnnotationExecutor.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.query;
+
+import jakarta.data.repository.By;
+import jakarta.data.repository.OrderBy;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.TypedQuery;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+public final class FindAnnotationExecutor {
+
+    private FindAnnotationExecutor() {
+    }
+
+    /**
+     * Builds the JPQL string for a @Find-annotated method. Cacheable — depends only on method metadata.
+     */
+    public static <T> String buildJpql(final Method method, final Class<T> entityClass) {
+        final StringBuilder jpql = new StringBuilder("SELECT e FROM ")
+            .append(entityClass.getSimpleName()).append(" e");
+
+        final Parameter[] parameters = method.getParameters();
+        final List<String> conditions = new ArrayList<>();
+
+        for (int i = 0; i < parameters.length; i++) {
+            if (CriteriaQueryBuilder.isSpecialParameter(parameters[i].getType())) {
+                continue;
+            }
+            final By by = parameters[i].getAnnotation(By.class);
+            final String property = by != null ? by.value() : parameters[i].getName();
+            conditions.add("e." + property + " = :p" + i);
+        }
+
+        if (!conditions.isEmpty()) {
+            jpql.append(" WHERE ").append(String.join(" AND ", conditions));
+        }
+
+        // Handle @OrderBy annotation(s) on the method
+        final OrderBy[] orderBys = method.getAnnotationsByType(OrderBy.class);
+        if (orderBys != null && orderBys.length > 0) {
+            jpql.append(" ORDER BY ");
+            for (int i = 0; i < orderBys.length; i++) {
+                if (i > 0) {
+                    jpql.append(", ");
+                }
+                jpql.append("e.").append(orderBys[i].value())
+                    .append(orderBys[i].descending() ? " DESC" : " ASC");
+            }
+        }
+
+        return jpql.toString();
+    }
+
+    /**
+     * Executes a @Find-annotated method using the given (cached) JPQL string.
+     */
+    public static <T> Object execute(final EntityManager em, final Method method, final Object[] args,
+                                     final Class<T> entityClass, final String jpql) {
+        final TypedQuery<T> query = em.createQuery(jpql, entityClass);
+
+        // Bind parameters
+        final Parameter[] parameters = method.getParameters();
+        for (int i = 0; i < parameters.length; i++) {
+            if (CriteriaQueryBuilder.isSpecialParameter(parameters[i].getType())) {
+                continue;
+            }
+            query.setParameter("p" + i, args[i]);
+        }
+
+        CriteriaQueryBuilder.applyPagination(query, args);
+
+        return adaptResult(method, query);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> Object adaptResult(final Method method, final TypedQuery<T> query) {
+        final Class<?> returnType = method.getReturnType();
+
+        if (List.class.isAssignableFrom(returnType) || Collection.class.isAssignableFrom(returnType)) {
+            return query.getResultList();
+        }
+
+        if (Stream.class.isAssignableFrom(returnType)) {
+            return query.getResultList().stream();
+        }
+
+        if (Optional.class.isAssignableFrom(returnType)) {
+            final List<T> results = query.getResultList();
+            return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
+        }
+
+        // Single result
+        final List<T> results = query.getResultList();
+        return results.isEmpty() ? null : results.get(0);
+    }
+}
diff --git a/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/query/MethodNameParser.java b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/query/MethodNameParser.java
new file mode 100644
index 0000000000..1626fa1e72
--- /dev/null
+++ b/container/openejb-jakarta-data/src/main/java/org/apache/openejb/data/query/MethodNameParser.java
@@ -0,0 +1,244 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.query;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public final class MethodNameParser {
+
+    private static final String FIND_BY = "findBy";
+    private static final String DELETE_BY = "deleteBy";
+    private static final String COUNT_BY = "countBy";
+    private static final String EXISTS_BY = "existsBy";
+    private static final String ORDER_BY = "OrderBy";
+
+    // Derived from Operator enum, sorted longest-first to match greedily
+    private static final String[] OPERATOR_SUFFIXES;
+    static {
+        OPERATOR_SUFFIXES = Arrays.stream(Operator.values())
+            .filter(op -> op != Operator.EQUAL)
+            .map(Operator::suffix)
+            .sorted((a, b) -> Integer.compare(b.length(), a.length()))
+            .toArray(String[]::new);
+    }
+
+    private MethodNameParser() {
+    }
+
+    public static ParsedQuery parse(final String methodName) {
+        final Action action;
+        String toParse;
+
+        if (methodName.startsWith(FIND_BY)) {
+            action = Action.FIND;
+            toParse = methodName.substring(FIND_BY.length());
+        } else if (methodName.startsWith(DELETE_BY)) {
+            action = Action.DELETE;
+            toParse = methodName.substring(DELETE_BY.length());
+        } else if (methodName.startsWith(COUNT_BY)) {
+            action = Action.COUNT;
+            toParse = methodName.substring(COUNT_BY.length());
+        } else if (methodName.startsWith(EXISTS_BY)) {
+            action = Action.EXISTS;
+            toParse = methodName.substring(EXISTS_BY.length());
+        } else {
+            return null;
+        }
+
+        // Extract OrderBy suffix
+        final List<OrderClause> orderClauses = new ArrayList<>();
+        final int orderByIdx = toParse.indexOf(ORDER_BY);
+        if (orderByIdx >= 0) {
+            final String orderPart = toParse.substring(orderByIdx + ORDER_BY.length());
+            toParse = toParse.substring(0, orderByIdx);
+            parseOrderBy(orderPart, orderClauses);
+        }
+
+        // Split by And/Or
+        final List<Condition> conditions = new ArrayList<>();
+        if (!toParse.isEmpty()) {
+            parseConditions(toParse, conditions);
+        }
+
+        return new ParsedQuery(action, conditions, orderClauses);
+    }
+
+    private static void parseConditions(final String input, final List<Condition> conditions) {
+        // Split by "And" and "Or" while tracking the connector
+        final List<String> parts = new ArrayList<>();
+        final List<Connector> connectors = new ArrayList<>();
+
+        String remaining = input;
+        while (!remaining.isEmpty()) {
+            final int andIdx = findConnector(remaining, "And");
+            final int orIdx = findConnector(remaining, "Or");
+
+            if (andIdx < 0 && orIdx < 0) {
+                parts.add(remaining);
+                break;
+            }
+
+            if (andIdx >= 0 && (orIdx < 0 || andIdx < orIdx)) {
+                parts.add(remaining.substring(0, andIdx));
+                connectors.add(Connector.AND);
+                remaining = remaining.substring(andIdx + 3);
+            } else {
+                parts.add(remaining.substring(0, orIdx));
+                connectors.add(Connector.OR);
+                remaining = remaining.substring(orIdx + 2);
+            }
+        }
+
+        for (int i = 0; i < parts.size(); i++) {
+            final String part = parts.get(i);
+            final Connector connector = i < connectors.size() ? connectors.get(i) : Connector.AND;
+            conditions.add(parseConditionPart(part, i == 0 ? Connector.AND : connectors.get(i - 1)));
+        }
+    }
+
+    private static int findConnector(final String input, final String connector) {
+        // Find "And" or "Or" that separates property conditions
+        // Must not be at position 0 and must be followed by uppercase letter
+        int idx = input.indexOf(connector);
+        while (idx > 0) {
+            if (idx + connector.length() < input.length()
+                && Character.isUpperCase(input.charAt(idx + connector.length()))) {
+                return idx;
+            }
+            idx = input.indexOf(connector, idx + 1);
+        }
+        return -1;
+    }
+
+    private static Condition parseConditionPart(final String part, final Connector connector) {
+        for (final String op : OPERATOR_SUFFIXES) {
+            if (part.endsWith(op)) {
+                final String property = StringUtils.uncapitalize(part.substring(0, part.length() - op.length()));
+                return new Condition(property, Operator.fromSuffix(op), connector);
+            }
+        }
+        // Default: equality
+        return new Condition(StringUtils.uncapitalize(part), Operator.EQUAL, connector);
+    }
+
+    private static void parseOrderBy(final String orderPart, final List<OrderClause> orderClauses) {
+        // OrderByNameAscAgeDesc -> [name ASC, age DESC]
+        String remaining = orderPart;
+        while (!remaining.isEmpty()) {
+            // Try to find Asc or Desc in the middle first (followed by uppercase = another clause)
+            final int ascIdx = findDirectionSuffix(remaining, "Asc");
+            final int descIdx = findDirectionSuffix(remaining, "Desc");
+
+            if (ascIdx > 0 && (descIdx < 0 || ascIdx < descIdx)) {
+                final String prop = StringUtils.uncapitalize(remaining.substring(0, ascIdx));
+                orderClauses.add(new OrderClause(prop, true));
+                remaining = remaining.substring(ascIdx + 3);
+            } else if (descIdx > 0) {
+                final String prop = StringUtils.uncapitalize(remaining.substring(0, descIdx));
+                orderClauses.add(new OrderClause(prop, false));
+                remaining = remaining.substring(descIdx + 4);
+            } else if (remaining.endsWith("Desc")) {
+                final String prop = StringUtils.uncapitalize(remaining.substring(0, remaining.length() - 4));
+                orderClauses.add(new OrderClause(prop, false));
+                break;
+            } else if (remaining.endsWith("Asc")) {
+                final String prop = StringUtils.uncapitalize(remaining.substring(0, remaining.length() - 3));
+                orderClauses.add(new OrderClause(prop, true));
+                break;
+            } else {
+                // Default ascending
+                orderClauses.add(new OrderClause(StringUtils.uncapitalize(remaining), true));
+                break;
+            }
+        }
+    }
+
+    private static int findDirectionSuffix(final String input, final String suffix) {
+        int idx = input.indexOf(suffix);
+        while (idx > 0) {
+            if (idx + suffix.length() < input.length()
+                && Character.isUpperCase(input.charAt(idx + suffix.length()))) {
+                return idx;
+            }
+            idx = input.indexOf(suffix, idx + 1);
+        }
+        return -1;
+    }
+
+    public enum Action {
+        FIND, DELETE, COUNT, EXISTS
+    }
+
+    public enum Connector {
+        AND, OR
+    }
+
+    public enum Operator {
+        EQUAL("Equal"),
+        GREATER_THAN("GreaterThan"), GREATER_THAN_EQUAL("GreaterThanEqual"),
+        LESS_THAN("LessThan"), LESS_THAN_EQUAL("LessThanEqual"),
+        LIKE("Like"), STARTS_WITH("StartsWith"), ENDS_WITH("EndsWith"), CONTAINS("Contains"),
+        BETWEEN("Between"), IN("In"), NOT("Not"),
+        NULL("Null"), NOT_NULL("NotNull"),
+        TRUE("True"), FALSE("False");
+
+        private final String suffix;
+
+        Operator(final String suffix) {
+            this.suffix = suffix;
+        }
+
+        public String suffix() {
+            return suffix;
+        }
+
+        public static Operator fromSuffix(final String s) {
+            for (final Operator op : values()) {
+                if (op.suffix.equals(s)) {
+                    return op;
+                }
+            }
+            return EQUAL;
+        }
+
+        public int parameterCount() {
+            return switch (this) {
+                case NULL, NOT_NULL, TRUE, FALSE -> 0;
+                case BETWEEN -> 2;
+                default -> 1;
+            };
+        }
+    }
+
+    public record Condition(String property, Operator operator, Connector connector) {
+    }
+
+    public record OrderClause(String property, boolean ascending) {
+    }
+
+    public record ParsedQuery(Action action, List<Condition> conditions, List<OrderClause> orderClauses) {
+        public ParsedQuery {
+            conditions = Collections.unmodifiableList(conditions);
+            orderClauses = Collections.unmodifiableList(orderClauses);
+        }
+    }
+}
diff --git a/container/openejb-jakarta-data/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension b/container/openejb-jakarta-data/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension
new file mode 100644
index 0000000000..b9b81c82b9
--- /dev/null
+++ b/container/openejb-jakarta-data/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension
@@ -0,0 +1 @@
+org.apache.openejb.data.extension.DataRepositoryExtension
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/AnnotatedQueryTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/AnnotatedQueryTest.java
new file mode 100644
index 0000000000..0979ab5681
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/AnnotatedQueryTest.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.annotation.Resource;
+import jakarta.inject.Inject;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.transaction.UserTransaction;
+import org.apache.openejb.data.test.entity.Person;
+import org.apache.openejb.data.test.repo.PersonCustomRepository;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(ApplicationComposer.class)
+public class AnnotatedQueryTest {
+
+    @Inject
+    private PersonCustomRepository personRepo;
+
+    @PersistenceContext
+    private EntityManager em;
+
+    @Resource
+    private UserTransaction utx;
+
+    @Module
+    @Classes(cdi = true, value = {PersonCustomRepository.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("person-unit");
+        unit.setJtaDataSource("personDatabase");
+        unit.setNonJtaDataSource("personDatabaseUnmanaged");
+        unit.getClazz().add(Person.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("personDatabase", "new://Resource?type=DataSource");
+        p.put("personDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("personDatabase.JdbcUrl", "jdbc:hsqldb:mem:persondb-query");
+        return p;
+    }
+
+    @Test
+    public void testQueryByName() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Alice", 30, "alice@test.com"));
+            em.persist(new Person("Bob", 40, "bob@test.com"));
+            em.flush();
+
+            final List<Person> result = personRepo.queryByName("Alice");
+            assertEquals(1, result.size());
+            assertEquals("Alice", result.get(0).getName());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testQueryByMinAge() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Alice", 30, "alice@test.com"));
+            em.persist(new Person("Bob", 40, "bob@test.com"));
+            em.persist(new Person("Charlie", 20, "charlie@test.com"));
+            em.flush();
+
+            final List<Person> result = personRepo.queryByMinAge(25);
+            assertEquals(2, result.size());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testFindAnnotationByAge() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Alice", 30, "alice@test.com"));
+            em.persist(new Person("Bob", 30, "bob@test.com"));
+            em.persist(new Person("Charlie", 20, "charlie@test.com"));
+            em.flush();
+
+            final List<Person> result = personRepo.findAllByAge(30);
+            assertEquals(2, result.size());
+        } finally {
+            utx.rollback();
+        }
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/BasicCrudTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/BasicCrudTest.java
new file mode 100644
index 0000000000..863fae0bd1
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/BasicCrudTest.java
@@ -0,0 +1,395 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.annotation.Resource;
+import jakarta.inject.Inject;
+import jakarta.transaction.UserTransaction;
+import org.apache.openejb.data.test.entity.Person;
+import org.apache.openejb.data.test.repo.PersonRepository;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(ApplicationComposer.class)
+public class BasicCrudTest {
+
+    @Inject
+    private PersonRepository personRepository;
+
+    @PersistenceContext
+    private EntityManager em;
+
+    @Resource
+    private UserTransaction utx;
+
+    @Module
+    @Classes(cdi = true, value = {PersonRepository.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("person-unit");
+        unit.setJtaDataSource("personDatabase");
+        unit.setNonJtaDataSource("personDatabaseUnmanaged");
+        unit.getClazz().add(Person.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("personDatabase", "new://Resource?type=DataSource");
+        p.put("personDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("personDatabase.JdbcUrl", "jdbc:hsqldb:mem:persondb-crud");
+        return p;
+    }
+
+    @Test
+    public void testInjectRepository() {
+        assertNotNull("Repository should be injected", personRepository);
+    }
+
+    @Test
+    public void testInsertAndFindById() throws Exception {
+        utx.begin();
+        try {
+            final Person person = new Person("Alice", 30, "alice@example.com");
+            final Person saved = personRepository.insert(person);
+
+            assertNotNull("saved should not be null", saved);
+            assertNotNull("saved.getId() should not be null, saved=" + saved.getName() + " age=" + saved.getAge(), saved.getId());
+
+            final Optional<Person> found = personRepository.findById(saved.getId());
+            assertTrue(found.isPresent());
+            assertEquals("Alice", found.get().getName());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testSaveAndUpdate() throws Exception {
+        utx.begin();
+        try {
+            Person person = new Person("Bob", 25, "bob@example.com");
+            person = personRepository.insert(person);
+
+            person.setAge(26);
+            final Person updated = personRepository.save(person);
+            assertEquals(26, updated.getAge());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testDelete() throws Exception {
+        utx.begin();
+        try {
+            final Person person = new Person("Charlie", 35, "charlie@example.com");
+            final Person saved = personRepository.insert(person);
+            final Long id = saved.getId();
+
+            personRepository.delete(saved);
+
+            final Optional<Person> found = personRepository.findById(id);
+            assertFalse(found.isPresent());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- existsById tests --
+
+    @Test
+    public void testExistsByIdReturnsTrue() throws Exception {
+        utx.begin();
+        try {
+            final Person person = personRepository.insert(new Person("Diana", 28, "diana@example.com"));
+
+            assertTrue("existsById should return true for an existing entity",
+                personRepository.existsById(person.getId()));
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testExistsByIdReturnsFalse() throws Exception {
+        utx.begin();
+        try {
+            assertFalse("existsById should return false for a non-existing entity",
+                personRepository.existsById(999999L));
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testExistsByIdAfterDelete() throws Exception {
+        utx.begin();
+        try {
+            final Person person = personRepository.insert(new Person("Eve", 40, "eve@example.com"));
+            final Long id = person.getId();
+
+            assertTrue(personRepository.existsById(id));
+
+            personRepository.delete(person);
+
+            assertFalse("existsById should return false after entity is deleted",
+                personRepository.existsById(id));
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- count tests --
+
+    @Test
+    public void testCountEmpty() throws Exception {
+        utx.begin();
+        try {
+            assertEquals(0L, personRepository.count());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testCountAfterInserts() throws Exception {
+        utx.begin();
+        try {
+            personRepository.insert(new Person("Frank", 30, "frank@example.com"));
+            personRepository.insert(new Person("Grace", 25, "grace@example.com"));
+            personRepository.insert(new Person("Hank", 35, "hank@example.com"));
+
+            assertEquals(3L, personRepository.count());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testCountAfterDelete() throws Exception {
+        utx.begin();
+        try {
+            final Person p1 = personRepository.insert(new Person("Ivy", 22, "ivy@example.com"));
+            personRepository.insert(new Person("Jack", 33, "jack@example.com"));
+
+            assertEquals(2L, personRepository.count());
+
+            personRepository.delete(p1);
+
+            assertEquals(1L, personRepository.count());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- deleteAll tests --
+
+    @Test
+    public void testDeleteAllEntities() throws Exception {
+        utx.begin();
+        try {
+            personRepository.insert(new Person("Kate", 28, "kate@example.com"));
+            personRepository.insert(new Person("Leo", 31, "leo@example.com"));
+            personRepository.insert(new Person("Mia", 26, "mia@example.com"));
+
+            assertEquals(3L, personRepository.count());
+
+            personRepository.deleteAll(List.of());
+            // Deleting empty list should not affect existing entities
+            assertEquals(3L, personRepository.count());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testDeleteAllWithEntitiesArgument() throws Exception {
+        utx.begin();
+        try {
+            final Person p1 = personRepository.insert(new Person("Nick", 30, "nick@example.com"));
+            final Person p2 = personRepository.insert(new Person("Olivia", 27, "olivia@example.com"));
+            final Person p3 = personRepository.insert(new Person("Pat", 35, "pat@example.com"));
+
+            assertEquals(3L, personRepository.count());
+
+            personRepository.deleteAll(List.of(p1, p2));
+
+            assertEquals(1L, personRepository.count());
+            assertFalse(personRepository.existsById(p1.getId()));
+            assertFalse(personRepository.existsById(p2.getId()));
+            assertTrue(personRepository.existsById(p3.getId()));
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testDeleteAllNoArg() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Quinn", 29, "quinn@example.com"));
+            em.persist(new Person("Rose", 32, "rose@example.com"));
+            em.flush();
+
+            assertEquals(2L, personRepository.count());
+
+            // CrudRepository.deleteAll(List) with all entities
+            final List<Person> all = personRepository.findAll().toList();
+            personRepository.deleteAll(all);
+
+            assertEquals(0L, personRepository.count());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- saveAll tests --
+
+    @Test
+    public void testSaveAllInsertMultiple() throws Exception {
+        utx.begin();
+        try {
+            final List<Person> people = List.of(
+                new Person("Sam", 28, "sam@example.com"),
+                new Person("Tina", 33, "tina@example.com"),
+                new Person("Uma", 41, "uma@example.com")
+            );
+
+            final List<Person> saved = personRepository.saveAll(people);
+
+            assertEquals(3, saved.size());
+            assertEquals("Sam", saved.get(0).getName());
+            assertEquals("Tina", saved.get(1).getName());
+            assertEquals("Uma", saved.get(2).getName());
+            assertEquals(3L, personRepository.count());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testSaveAllUpdateExisting() throws Exception {
+        utx.begin();
+        try {
+            final Person p1 = personRepository.insert(new Person("Vera", 30, "vera@example.com"));
+            final Person p2 = personRepository.insert(new Person("Walt", 25, "walt@example.com"));
+
+            p1.setAge(31);
+            p2.setAge(26);
+
+            final List<Person> updated = personRepository.saveAll(List.of(p1, p2));
+
+            assertEquals(2, updated.size());
+            assertEquals(31, updated.get(0).getAge());
+            assertEquals(26, updated.get(1).getAge());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testSaveAllEmptyList() throws Exception {
+        utx.begin();
+        try {
+            final List<Person> result = personRepository.saveAll(List.of());
+            assertNotNull(result);
+            assertTrue(result.isEmpty());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- save with array (isArray code path) --
+
+    @Test
+    public void testSaveArrayInsertMultiple() throws Exception {
+        utx.begin();
+        try {
+            final Person[] people = new Person[]{
+                new Person("Xander", 22, "xander@example.com"),
+                new Person("Yara", 37, "yara@example.com")
+            };
+
+            final Person[] saved = personRepository.save(people);
+
+            assertEquals(2, saved.length);
+            for (final Person p : saved) {
+                assertNotNull("saved entity should have an ID", p.getId());
+            }
+            assertEquals(2L, personRepository.count());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testSaveArrayUpdateExisting() throws Exception {
+        utx.begin();
+        try {
+            final Person p1 = personRepository.insert(new Person("Zach", 29, "zach@example.com"));
+            final Person p2 = personRepository.insert(new Person("Amy", 34, "amy@example.com"));
+
+            p1.setAge(30);
+            p2.setAge(35);
+
+            final Person[] updated = personRepository.save(new Person[]{p1, p2});
+
+            assertEquals(2, updated.length);
+            assertEquals(30, updated[0].getAge());
+            assertEquals(35, updated[1].getAge());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testSaveArrayEmpty() throws Exception {
+        utx.begin();
+        try {
+            final Person[] result = personRepository.save(new Person[0]);
+            assertNotNull(result);
+            assertEquals(0, result.length);
+        } finally {
+            utx.rollback();
+        }
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/BeanValidationTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/BeanValidationTest.java
new file mode 100644
index 0000000000..afa2a64c9b
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/BeanValidationTest.java
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.annotation.Resource;
+import jakarta.inject.Inject;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.transaction.UserTransaction;
+import jakarta.validation.ConstraintViolationException;
+import org.apache.openejb.data.test.entity.Person;
+import org.apache.openejb.data.test.repo.ValidatedPersonRepository;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(ApplicationComposer.class)
+public class BeanValidationTest {
+
+    @Inject
+    private ValidatedPersonRepository repo;
+
+    @PersistenceContext
+    private EntityManager em;
+
+    @Resource
+    private UserTransaction utx;
+
+    @Module
+    @Classes(cdi = true, value = {ValidatedPersonRepository.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("person-unit");
+        unit.setJtaDataSource("personDatabase");
+        unit.setNonJtaDataSource("personDatabaseUnmanaged");
+        unit.getClazz().add(Person.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("personDatabase", "new://Resource?type=DataSource");
+        p.put("personDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("personDatabase.JdbcUrl", "jdbc:hsqldb:mem:persondb-bval");
+        return p;
+    }
+
+    @Test
+    public void testValidParametersPasses() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Alice", 30, "alice@test.com"));
+            em.flush();
+
+            final List<Person> result = repo.findByName("Alice");
+            assertEquals(1, result.size());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testNullNameThrowsConstraintViolation() throws Exception {
+        utx.begin();
+        try {
+            repo.findByName(null);
+            fail("Should throw ConstraintViolationException for null name");
+        } catch (final ConstraintViolationException e) {
+            assertNotNull(e.getConstraintViolations());
+            assertFalse(e.getConstraintViolations().isEmpty());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testEmptyNameThrowsConstraintViolation() throws Exception {
+        utx.begin();
+        try {
+            repo.findByName("");
+            fail("Should throw ConstraintViolationException for empty name");
+        } catch (final ConstraintViolationException e) {
+            assertNotNull(e.getConstraintViolations());
+            assertFalse(e.getConstraintViolations().isEmpty());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testNegativeAgeThrowsConstraintViolation() throws Exception {
+        utx.begin();
+        try {
+            repo.findByAge(-1);
+            fail("Should throw ConstraintViolationException for negative age");
+        } catch (final ConstraintViolationException e) {
+            assertNotNull(e.getConstraintViolations());
+            assertFalse(e.getConstraintViolations().isEmpty());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testZeroAgeThrowsConstraintViolation() throws Exception {
+        utx.begin();
+        try {
+            repo.findByAge(0);
+            fail("Should throw ConstraintViolationException for zero age (@Positive)");
+        } catch (final ConstraintViolationException e) {
+            assertNotNull(e.getConstraintViolations());
+            assertFalse(e.getConstraintViolations().isEmpty());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testPositiveAgeIsValid() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Bob", 25, "bob@test.com"));
+            em.flush();
+
+            final List<Person> result = repo.findByAge(25);
+            assertEquals(1, result.size());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testMultipleConstraintViolations() throws Exception {
+        utx.begin();
+        try {
+            repo.findByNameAndAge(null, -5);
+            fail("Should throw ConstraintViolationException for null name AND negative age");
+        } catch (final ConstraintViolationException e) {
+            // At least 2 violations: @NotNull on name and @Positive on age
+            assertTrue(e.getConstraintViolations().size() >= 2);
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testValidMultipleParameters() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Charlie", 35, "charlie@test.com"));
+            em.flush();
+
+            final List<Person> result = repo.findByNameAndAge("Charlie", 35);
+            assertEquals(1, result.size());
+        } finally {
+            utx.rollback();
+        }
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/DataRepositoryExtensionDisabledTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/DataRepositoryExtensionDisabledTest.java
new file mode 100644
index 0000000000..0f94361d1d
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/DataRepositoryExtensionDisabledTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.enterprise.inject.spi.BeanManager;
+import jakarta.inject.Inject;
+import org.apache.openejb.data.test.entity.Person;
+import org.apache.openejb.data.test.repo.PersonRepository;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Properties;
+
+import static org.junit.Assert.assertTrue;
+
+@RunWith(ApplicationComposer.class)
+public class DataRepositoryExtensionDisabledTest {
+
+    @Inject
+    private BeanManager beanManager;
+
+    @Module
+    @Classes(cdi = true, value = {PersonRepository.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("person-unit");
+        unit.setJtaDataSource("personDatabase");
+        unit.setNonJtaDataSource("personDatabaseUnmanaged");
+        unit.getClazz().add(Person.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("personDatabase", "new://Resource?type=DataSource");
+        p.put("personDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("personDatabase.JdbcUrl", "jdbc:hsqldb:mem:persondb-disabled");
+        // Disable the Jakarta Data extension
+        p.put("tomee.jakarta-data.active", "false");
+        return p;
+    }
+
+    @Test
+    public void testExtensionDisabledNoRepositoryBean() {
+        // When tomee.jakarta-data.active=false, the extension should not register any repository beans
+        assertTrue(
+            "No PersonRepository bean should be registered when extension is disabled",
+            beanManager.getBeans(PersonRepository.class).isEmpty());
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/DataRepositoryExtensionTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/DataRepositoryExtensionTest.java
new file mode 100644
index 0000000000..cf7b40adc8
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/DataRepositoryExtensionTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.inject.Inject;
+import org.apache.openejb.data.test.entity.Person;
+import org.apache.openejb.data.test.repo.PersonRepository;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.loader.SystemInstance;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Properties;
+
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(ApplicationComposer.class)
+public class DataRepositoryExtensionTest {
+
+    @Inject
+    private PersonRepository personRepository;
+
+    @Module
+    @Classes(cdi = true, value = {PersonRepository.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("person-unit");
+        unit.setJtaDataSource("personDatabase");
+        unit.setNonJtaDataSource("personDatabaseUnmanaged");
+        unit.getClazz().add(Person.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("personDatabase", "new://Resource?type=DataSource");
+        p.put("personDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("personDatabase.JdbcUrl", "jdbc:hsqldb:mem:persondb-ext");
+        return p;
+    }
+
+    @After
+    public void cleanup() {
+        // Ensure we don't leak property changes to other tests
+        SystemInstance.get().getProperties().remove("tomee.jakarta-data.active");
+    }
+
+    @Test
+    public void testExtensionActiveByDefault() {
+        // The extension is active by default (tomee.jakarta-data.active defaults to true)
+        // so the repository should be injectable
+        assertNotNull("Repository should be injected when extension is active", personRepository);
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/DataRepositoryExtensionUnitTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/DataRepositoryExtensionUnitTest.java
new file mode 100644
index 0000000000..1be6057715
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/DataRepositoryExtensionUnitTest.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import org.apache.openejb.data.extension.DataRepositoryExtension;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Method;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+class DataRepositoryExtensionUnitTest {
+
+    @Test
+    void hasHibernateGeneratedImplReturnsFalseWhenNoGeneratedClass() throws Exception {
+        // In our test environment, there is no DataRepositoryExtension_ generated class
+        final Method hasImpl = DataRepositoryExtension.class.getDeclaredMethod(
+            "hasHibernateGeneratedImpl", Class.class);
+        hasImpl.setAccessible(true);
+        final boolean result = (boolean) hasImpl.invoke(null, DataRepositoryExtension.class);
+        assertFalse(result, "hasHibernateGeneratedImpl() should return false when no generated class exists");
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupAmbiguousTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupAmbiguousTest.java
new file mode 100644
index 0000000000..e0bbe294af
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupAmbiguousTest.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.annotation.Resource;
+import jakarta.inject.Inject;
+import jakarta.transaction.UserTransaction;
+import org.apache.openejb.data.test.entity.Person;
+import org.apache.openejb.data.test.repo.PersonRepository;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.Persistence;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Properties;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests that EntityManagerLookup correctly throws an error when multiple
+ * persistence units exist and no dataStore is specified on the @Repository.
+ * This exercises the ambiguity detection in resolveJndiName.
+ */
+@RunWith(ApplicationComposer.class)
+public class EntityManagerLookupAmbiguousTest {
+
+    @Inject
+    private PersonRepository repo;
+
+    @Resource
+    private UserTransaction utx;
+
+    @Module
+    @Classes(cdi = true, value = {PersonRepository.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public Persistence persistence() {
+        final PersistenceUnit unit1 = new PersistenceUnit("unit-one");
+        unit1.setJtaDataSource("personDatabase");
+        unit1.setNonJtaDataSource("personDatabaseUnmanaged");
+        unit1.getClazz().add(Person.class.getName());
+        unit1.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+
+        final PersistenceUnit unit2 = new PersistenceUnit("unit-two");
+        unit2.setJtaDataSource("personDatabase");
+        unit2.setNonJtaDataSource("personDatabaseUnmanaged");
+        unit2.getClazz().add(Person.class.getName());
+        unit2.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+
+        final Persistence persistence = new Persistence();
+        persistence.getPersistenceUnit().add(unit1);
+        persistence.getPersistenceUnit().add(unit2);
+        return persistence;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("personDatabase", "new://Resource?type=DataSource");
+        p.put("personDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("personDatabase.JdbcUrl", "jdbc:hsqldb:mem:persondb-ambiguous");
+        return p;
+    }
+
+    @Test
+    public void testMultiplePersistenceUnitsThrowsAmbiguityError() throws Exception {
+        assertNotNull("Repository should be injected", repo);
+
+        utx.begin();
+        try {
+            repo.insert(new Person("Ambiguous", 30, "ambiguous@test.com"));
+            fail("Should throw IllegalStateException for ambiguous persistence units");
+        } catch (final IllegalStateException e) {
+            assertTrue("Error message should mention multiple persistence units",
+                e.getMessage().contains("Multiple persistence units"));
+        } finally {
+            utx.rollback();
+        }
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupCDINamedTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupCDINamedTest.java
new file mode 100644
index 0000000000..c7263e3ff7
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupCDINamedTest.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.annotation.Resource;
+import jakarta.inject.Inject;
+import jakarta.transaction.UserTransaction;
+import org.apache.openejb.data.test.cdi.NamedEntityManagerFactoryProducer;
+import org.apache.openejb.data.test.entity.Person;
+import org.apache.openejb.data.test.repo.NamedCDIPersonRepository;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Optional;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests the CDI named bean lookup path in EntityManagerLookup.
+ * When @Repository(dataStore="my-data-store") matches a @Named CDI-produced EMF,
+ * the lookupViaCDI method should find it by bean name.
+ */
+@RunWith(ApplicationComposer.class)
+public class EntityManagerLookupCDINamedTest {
+
+    @Inject
+    private NamedCDIPersonRepository repo;
+
+    @Resource
+    private UserTransaction utx;
+
+    @Module
+    @Classes(cdi = true, value = {NamedCDIPersonRepository.class, NamedEntityManagerFactoryProducer.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("person-unit");
+        unit.setJtaDataSource("personDatabase");
+        unit.setNonJtaDataSource("personDatabaseUnmanaged");
+        unit.getClazz().add(Person.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("personDatabase", "new://Resource?type=DataSource");
+        p.put("personDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("personDatabase.JdbcUrl", "jdbc:hsqldb:mem:persondb-cdi-named");
+        return p;
+    }
+
+    @Test
+    public void testNamedCDIBeanLookup() throws Exception {
+        // The @Repository(dataStore="my-data-store") should match the @Named("my-data-store")
+        // CDI-produced EMF via the bean name matching in lookupViaCDI
+        assertNotNull("Repository should be injected", repo);
+
+        utx.begin();
+        try {
+            final Person person = new Person("NamedCDI", 27, "named-cdi@test.com");
+            final Person saved = repo.insert(person);
+            assertNotNull("Saved person should have an id", saved.getId());
+
+            final Optional<Person> found = repo.findById(saved.getId());
+            assertTrue("Should find the saved person via named CDI EMF", found.isPresent());
+            assertEquals("NamedCDI", found.get().getName());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testNamedCDIBeanCrudOperations() throws Exception {
+        utx.begin();
+        try {
+            final Person person = repo.insert(new Person("NamedCrud", 35, "named-crud@test.com"));
+            final Long id = person.getId();
+
+            person.setAge(36);
+            repo.save(person);
+
+            final Optional<Person> found = repo.findById(id);
+            assertTrue(found.isPresent());
+            assertEquals(36, found.get().getAge());
+
+            repo.delete(found.get());
+            assertTrue(repo.findById(id).isEmpty());
+        } finally {
+            utx.rollback();
+        }
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupCDIProducerTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupCDIProducerTest.java
new file mode 100644
index 0000000000..399aa564cb
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupCDIProducerTest.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.annotation.Resource;
+import jakarta.inject.Inject;
+import jakarta.transaction.UserTransaction;
+import org.apache.openejb.data.test.cdi.EntityManagerFactoryProducer;
+import org.apache.openejb.data.test.entity.Person;
+import org.apache.openejb.data.test.repo.PersonRepository;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Optional;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests the CDI lookup path in EntityManagerLookup when a CDI-produced
+ * EntityManagerFactory bean exists. This exercises the "exactly one EMF bean"
+ * path (beans.size() == 1) in lookupViaCDI.
+ */
+@RunWith(ApplicationComposer.class)
+public class EntityManagerLookupCDIProducerTest {
+
+    @Inject
+    private PersonRepository repo;
+
+    @Resource
+    private UserTransaction utx;
+
+    @Module
+    @Classes(cdi = true, value = {PersonRepository.class, EntityManagerFactoryProducer.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("person-unit");
+        unit.setJtaDataSource("personDatabase");
+        unit.setNonJtaDataSource("personDatabaseUnmanaged");
+        unit.getClazz().add(Person.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("personDatabase", "new://Resource?type=DataSource");
+        p.put("personDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("personDatabase.JdbcUrl", "jdbc:hsqldb:mem:persondb-cdi-producer");
+        return p;
+    }
+
+    @Test
+    public void testCDIProducedEMFIsUsed() throws Exception {
+        // When a CDI-produced EMF exists, EntityManagerLookup should use it
+        // via the "beans.size() == 1" path in lookupViaCDI
+        assertNotNull("Repository should be injected", repo);
+
+        utx.begin();
+        try {
+            final Person person = new Person("CDIProducer", 33, "cdi-producer@test.com");
+            final Person saved = repo.insert(person);
+            assertNotNull("Saved person should have an id", saved.getId());
+
+            final Optional<Person> found = repo.findById(saved.getId());
+            assertTrue("Should find the saved person via CDI-produced EMF", found.isPresent());
+            assertEquals("CDIProducer", found.get().getName());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testCDIProducedEMFSupportsFullCrud() throws Exception {
+        utx.begin();
+        try {
+            // Insert
+            final Person person = repo.insert(new Person("CrudCDI", 40, "crud-cdi@test.com"));
+            final Long id = person.getId();
+
+            // Update
+            person.setAge(41);
+            repo.save(person);
+
+            // Find and verify update
+            final Optional<Person> found = repo.findById(id);
+            assertTrue(found.isPresent());
+            assertEquals(41, found.get().getAge());
+
+            // Delete
+            repo.delete(found.get());
+            assertTrue("Entity should not exist after delete", repo.findById(id).isEmpty());
+        } finally {
+            utx.rollback();
+        }
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupCDITest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupCDITest.java
new file mode 100644
index 0000000000..7411bc4088
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupCDITest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.annotation.Resource;
+import jakarta.inject.Inject;
+import jakarta.transaction.UserTransaction;
+import org.apache.openejb.data.test.entity.Person;
+import org.apache.openejb.data.test.repo.PersonRepository;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Optional;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests that EntityManagerLookup correctly resolves persistence units
+ * and that CDI-to-JNDI fallback works when no CDI-produced EntityManagerFactory exists.
+ */
+@RunWith(ApplicationComposer.class)
+public class EntityManagerLookupCDITest {
+
+    @Inject
+    private PersonRepository personRepository;
+
+    @Resource
+    private UserTransaction utx;
+
+    @Module
+    @Classes(cdi = true, value = {PersonRepository.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("person-unit");
+        unit.setJtaDataSource("personDatabase");
+        unit.setNonJtaDataSource("personDatabaseUnmanaged");
+        unit.getClazz().add(Person.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("personDatabase", "new://Resource?type=DataSource");
+        p.put("personDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("personDatabase.JdbcUrl", "jdbc:hsqldb:mem:persondb-cdi-lookup");
+        return p;
+    }
+
+    @Test
+    public void testLookupFallsBackToJNDIWhenNoCDIBean() throws Exception {
+        // In this test, no CDI-produced EntityManagerFactory exists,
+        // so EntityManagerLookup should fall back to JNDI and still work
+        assertNotNull("Repository should be injected", personRepository);
+
+        utx.begin();
+        try {
+            final Person person = new Person("CDILookupTest", 42, "cdi@test.com");
+            final Person saved = personRepository.insert(person);
+            assertNotNull("Saved person should have an id", saved.getId());
+
+            final Optional<Person> found = personRepository.findById(saved.getId());
+            assertTrue("Should find the saved person", found.isPresent());
+            assertEquals("CDILookupTest", found.get().getName());
+        } finally {
+            utx.rollback();
+        }
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupJNDITest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupJNDITest.java
new file mode 100644
index 0000000000..741cf8e7fc
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupJNDITest.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.annotation.Resource;
+import jakarta.inject.Inject;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.transaction.UserTransaction;
+import org.apache.openejb.data.test.entity.Person;
+import org.apache.openejb.data.test.repo.PersonRepository;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Optional;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests EntityManagerLookup JNDI fallback paths including:
+ * - resolveJndiName with a specific dataStore name
+ * - wrapWithJta using JtaEntityManagerRegistry
+ * - extractUnitName from JNDI path
+ *
+ * This test uses a named persistence unit to exercise the dataStore-based
+ * JNDI lookup path in resolveJndiName.
+ */
+@RunWith(ApplicationComposer.class)
+public class EntityManagerLookupJNDITest {
+
+    @Inject
+    private PersonRepository personRepository;
+
+    @PersistenceContext
+    private EntityManager em;
+
+    @Resource
+    private UserTransaction utx;
+
+    @Module
+    @Classes(cdi = true, value = {PersonRepository.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("person-unit");
+        unit.setJtaDataSource("personDatabase");
+        unit.setNonJtaDataSource("personDatabaseUnmanaged");
+        unit.getClazz().add(Person.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("personDatabase", "new://Resource?type=DataSource");
+        p.put("personDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("personDatabase.JdbcUrl", "jdbc:hsqldb:mem:persondb-jndi-lookup");
+        return p;
+    }
+
+    @Test
+    public void testJNDILookupWithDefaultDataStore() throws Exception {
+        // With empty dataStore, resolveJndiName should enumerate PUs and find "person-unit"
+        assertNotNull("Repository should be injected", personRepository);
+
+        utx.begin();
+        try {
+            final Person person = new Person("JNDIDefault", 35, "jndi-default@test.com");
+            final Person saved = personRepository.insert(person);
+            assertNotNull("Saved person should have an id", saved.getId());
+
+            final Optional<Person> found = personRepository.findById(saved.getId());
+            assertTrue("Should find the saved person", found.isPresent());
+            assertEquals("JNDIDefault", found.get().getName());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testJNDILookupReturnsJtaEntityManager() throws Exception {
+        // Verify the EntityManager is JTA-managed (wrapped by JtaEntityManager)
+        assertNotNull("Repository should be injected", personRepository);
+
+        utx.begin();
+        try {
+            // Insert and verify we can find within the same transaction (proves JTA integration)
+            final Person p1 = personRepository.insert(new Person("JtaTest1", 28, "jta1@test.com"));
+            final Person p2 = personRepository.insert(new Person("JtaTest2", 32, "jta2@test.com"));
+
+            // Both should be visible in the same transaction
+            final Optional<Person> found1 = personRepository.findById(p1.getId());
+            final Optional<Person> found2 = personRepository.findById(p2.getId());
+            assertTrue("Should find first person in same tx", found1.isPresent());
+            assertTrue("Should find second person in same tx", found2.isPresent());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testJNDILookupRollbackDoesNotPersist() throws Exception {
+        final Long id;
+        utx.begin();
+        try {
+            final Person person = personRepository.insert(new Person("RollbackTest", 40, "rollback@test.com"));
+            id = person.getId();
+            assertNotNull(id);
+        } finally {
+            utx.rollback();
+        }
+
+        // After rollback, entity should not be found
+        utx.begin();
+        try {
+            final Optional<Person> found = personRepository.findById(id);
+            assertTrue("Entity should not exist after rollback", found.isEmpty());
+        } finally {
+            utx.rollback();
+        }
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupNamedDataStoreTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupNamedDataStoreTest.java
new file mode 100644
index 0000000000..c702f6d541
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/EntityManagerLookupNamedDataStoreTest.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.annotation.Resource;
+import jakarta.inject.Inject;
+import jakarta.transaction.UserTransaction;
+import org.apache.openejb.data.test.entity.Person;
+import org.apache.openejb.data.test.repo.NamedDataStorePersonRepository;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Optional;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests EntityManagerLookup with @Repository(dataStore="person-unit")
+ * which exercises the resolveJndiName path with a specific named dataStore.
+ * This ensures the JNDI lookup path for named persistence units works correctly.
+ */
+@RunWith(ApplicationComposer.class)
+public class EntityManagerLookupNamedDataStoreTest {
+
+    @Inject
+    private NamedDataStorePersonRepository repo;
+
+    @Resource
+    private UserTransaction utx;
+
+    @Module
+    @Classes(cdi = true, value = {NamedDataStorePersonRepository.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("person-unit");
+        unit.setJtaDataSource("personDatabase");
+        unit.setNonJtaDataSource("personDatabaseUnmanaged");
+        unit.getClazz().add(Person.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("personDatabase", "new://Resource?type=DataSource");
+        p.put("personDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("personDatabase.JdbcUrl", "jdbc:hsqldb:mem:persondb-named-ds");
+        return p;
+    }
+
+    @Test
+    public void testNamedDataStoreResolvesCorrectly() throws Exception {
+        assertNotNull("Repository with named dataStore should be injected", repo);
+
+        utx.begin();
+        try {
+            final Person person = new Person("NamedDS", 38, "named-ds@test.com");
+            final Person saved = repo.insert(person);
+            assertNotNull("Saved person should have an id", saved.getId());
+
+            final Optional<Person> found = repo.findById(saved.getId());
+            assertTrue("Should find the saved person via named dataStore", found.isPresent());
+            assertEquals("NamedDS", found.get().getName());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testNamedDataStoreExistsById() throws Exception {
+        utx.begin();
+        try {
+            final Person person = repo.insert(new Person("ExistCheck", 29, "exist@test.com"));
+
+            assertTrue(repo.existsById(person.getId()));
+            assertFalse(repo.existsById(999999L));
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testNamedDataStoreCrudOperations() throws Exception {
+        utx.begin();
+        try {
+            // Insert
+            final Person person = repo.insert(new Person("CrudNamed", 45, "crud-named@test.com"));
+            final Long id = person.getId();
+
+            // Update via save
+            person.setAge(46);
+            final Person updated = repo.save(person);
+            assertEquals(46, updated.getAge());
+
+            // Find
+            final Optional<Person> found = repo.findById(id);
+            assertTrue(found.isPresent());
+            assertEquals(46, found.get().getAge());
+
+            // Delete
+            repo.delete(updated);
+            assertFalse(repo.existsById(id));
+        } finally {
+            utx.rollback();
+        }
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/ExtendedCrudTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/ExtendedCrudTest.java
new file mode 100644
index 0000000000..a605420dae
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/ExtendedCrudTest.java
@@ -0,0 +1,522 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.annotation.Resource;
+import jakarta.inject.Inject;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.transaction.UserTransaction;
+import org.apache.openejb.data.test.entity.Person;
+import org.apache.openejb.data.test.repo.PersonExtendedRepository;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.stream.Stream;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(ApplicationComposer.class)
+public class ExtendedCrudTest {
+
+    @Inject
+    private PersonExtendedRepository repo;
+
+    @PersistenceContext
+    private EntityManager em;
+
+    @Resource
+    private UserTransaction utx;
+
+    @Module
+    @Classes(cdi = true, value = {PersonExtendedRepository.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("person-unit");
+        unit.setJtaDataSource("personDatabase");
+        unit.setNonJtaDataSource("personDatabaseUnmanaged");
+        unit.getClazz().add(Person.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("personDatabase", "new://Resource?type=DataSource");
+        p.put("personDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("personDatabase.JdbcUrl", "jdbc:hsqldb:mem:persondb-extended");
+        return p;
+    }
+
+    // -- Proxy Object methods --
+
+    @Test
+    public void testProxyToString() {
+        final String str = repo.toString();
+        assertNotNull(str);
+        assertTrue(str.contains("PersonExtendedRepository"));
+    }
+
+    @Test
+    public void testProxyHashCode() {
+        final int hash = repo.hashCode();
+        // just check it doesn't throw
+        assertTrue(hash != 0 || hash == 0);
+    }
+
+    @Test
+    public void testProxyEquals() {
+        assertTrue(repo.equals(repo));
+        assertFalse(repo.equals(null));
+        assertFalse(repo.equals("not a repo"));
+    }
+
+    // -- Stream return type --
+
+    @Test
+    public void testFindByWithStreamReturn() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Alice", 30, "alice@test.com"));
+            em.persist(new Person("Bob", 20, "bob@test.com"));
+            em.persist(new Person("Charlie", 40, "charlie@test.com"));
+            em.flush();
+
+            final Stream<Person> stream = repo.findByAgeGreaterThan(25);
+            assertNotNull(stream);
+            final long count = stream.count();
+            assertEquals(2, count);
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- Optional return type --
+
+    @Test
+    public void testFindByWithOptionalReturnPresent() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Alice", 30, "alice@unique.com"));
+            em.flush();
+
+            final Optional<Person> result = repo.findByEmail("alice@unique.com");
+            assertTrue(result.isPresent());
+            assertEquals("Alice", result.get().getName());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testFindByWithOptionalReturnEmpty() throws Exception {
+        utx.begin();
+        try {
+            final Optional<Person> result = repo.findByEmail("nonexistent@test.com");
+            assertFalse(result.isPresent());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- Single entity return --
+
+    @Test
+    public void testFindBySingleEntityReturn() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("UniqueAlice", 30, "ua@test.com"));
+            em.flush();
+
+            final Person person = repo.findByName("UniqueAlice");
+            assertNotNull(person);
+            assertEquals("UniqueAlice", person.getName());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testFindBySingleEntityReturnNull() throws Exception {
+        utx.begin();
+        try {
+            final Person person = repo.findByName("DoesNotExist");
+            assertNull(person);
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- LessThanEqual operator --
+
+    @Test
+    public void testFindByLessThanEqual() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Alice", 20, "a@test.com"));
+            em.persist(new Person("Bob", 30, "b@test.com"));
+            em.persist(new Person("Charlie", 40, "c@test.com"));
+            em.flush();
+
+            final List<Person> result = repo.findByAgeLessThanEqual(30);
+            assertEquals(2, result.size());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- Contains operator --
+
+    @Test
+    public void testFindByContains() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Alice Smith", 30, "a@test.com"));
+            em.persist(new Person("Bob Jones", 30, "b@test.com"));
+            em.persist(new Person("Alice Jones", 40, "c@test.com"));
+            em.flush();
+
+            final List<Person> result = repo.findByNameContains("Jones");
+            assertEquals(2, result.size());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- deleteByName (method-name delete) --
+
+    @Test
+    public void testDeleteByName() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("ToDelete", 30, "td1@test.com"));
+            em.persist(new Person("ToDelete", 25, "td2@test.com"));
+            em.persist(new Person("ToKeep", 40, "tk@test.com"));
+            em.flush();
+
+            final int deleted = repo.deleteByName("ToDelete");
+            assertEquals(2, deleted);
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- @Query returning count --
+
+    @Test
+    public void testQueryReturningCount() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Alice", 30, "a@test.com"));
+            em.persist(new Person("Bob", 20, "b@test.com"));
+            em.persist(new Person("Charlie", 40, "c@test.com"));
+            em.flush();
+
+            final long count = repo.countOlderThan(25);
+            assertEquals(2, count);
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- @Query returning Optional --
+
+    @Test
+    public void testQueryReturningOptional() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Alice", 30, "alice-opt@test.com"));
+            em.flush();
+
+            final Optional<Person> found = repo.findOneByEmail("alice-opt@test.com");
+            assertTrue(found.isPresent());
+
+            final Optional<Person> notFound = repo.findOneByEmail("nope@test.com");
+            assertFalse(notFound.isPresent());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- @Find with @By returning Optional --
+
+    @Test
+    public void testFindAnnotationWithByReturningOptional() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Alice", 30, "alice@test.com"));
+            em.flush();
+
+            // Get the ID of the persisted entity
+            final Long id = em.createQuery("SELECT p.id FROM Person p WHERE p.name = 'Alice'", Long.class)
+                .getSingleResult();
+
+            final Optional<Person> result = repo.lookupByIdAndName(id, "Alice");
+            assertTrue(result.isPresent());
+            assertEquals("Alice", result.get().getName());
+
+            // Non-matching should be empty
+            final Optional<Person> notFound = repo.lookupByIdAndName(id, "Bob");
+            assertFalse(notFound.isPresent());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- Custom @Insert annotation --
+
+    @Test
+    public void testCustomInsertAnnotation() throws Exception {
+        utx.begin();
+        try {
+            final Person person = new Person("InsertTest", 33, "insert@test.com");
+            final Person saved = repo.customInsert(person);
+            assertNotNull(saved);
+            assertNotNull(saved.getId());
+            assertEquals("InsertTest", saved.getName());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- Custom @Update annotation --
+
+    @Test
+    public void testCustomUpdateAnnotation() throws Exception {
+        utx.begin();
+        try {
+            final Person person = new Person("UpdateTest", 33, "update@test.com");
+            em.persist(person);
+            em.flush();
+
+            person.setAge(44);
+            final Person updated = repo.customUpdate(person);
+            assertEquals(44, updated.getAge());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- Custom @Save annotation --
+
+    @Test
+    public void testCustomSaveAnnotation() throws Exception {
+        utx.begin();
+        try {
+            final Person person = new Person("SaveTest", 33, "save@test.com");
+            em.persist(person);
+            em.flush();
+
+            person.setAge(55);
+            final Person saved = repo.customSave(person);
+            assertEquals(55, saved.getAge());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- Custom @Delete annotation --
+
+    @Test
+    public void testCustomDeleteAnnotation() throws Exception {
+        utx.begin();
+        try {
+            final Person person = new Person("DeleteTest", 33, "delete@test.com");
+            em.persist(person);
+            em.flush();
+
+            repo.customDelete(person);
+
+            // Verify deleted
+            final List<?> remaining = em.createQuery("SELECT p FROM Person p WHERE p.name = 'DeleteTest'")
+                .getResultList();
+            assertEquals(0, remaining.size());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- Built-in CrudRepository: insertAll --
+
+    @Test
+    public void testInsertAll() throws Exception {
+        utx.begin();
+        try {
+            final List<Person> people = List.of(
+                new Person("P1", 20, "p1@test.com"),
+                new Person("P2", 30, "p2@test.com")
+            );
+            final List<Person> saved = repo.insertAll(people);
+            assertEquals(2, saved.size());
+            assertNotNull(saved.get(0).getId());
+            assertNotNull(saved.get(1).getId());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- Built-in CrudRepository: updateAll --
+
+    @Test
+    public void testUpdateAll() throws Exception {
+        utx.begin();
+        try {
+            final Person p1 = new Person("U1", 20, "u1@test.com");
+            final Person p2 = new Person("U2", 30, "u2@test.com");
+            em.persist(p1);
+            em.persist(p2);
+            em.flush();
+
+            p1.setAge(21);
+            p2.setAge(31);
+            final List<Person> updated = repo.updateAll(List.of(p1, p2));
+            assertEquals(2, updated.size());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- Built-in CrudRepository: save (merge for new entity) --
+
+    @Test
+    public void testSaveNewEntity() throws Exception {
+        utx.begin();
+        try {
+            final Person person = new Person("SaveNew", 25, "savenew@test.com");
+            final Person saved = repo.save(person);
+            assertNotNull(saved);
+            assertEquals("SaveNew", saved.getName());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- Built-in: deleteById --
+
+    @Test
+    public void testDeleteById() throws Exception {
+        utx.begin();
+        try {
+            final Person person = new Person("DeleteById", 33, "dbi@test.com");
+            em.persist(person);
+            em.flush();
+            final Long id = person.getId();
+
+            repo.deleteById(id);
+
+            assertFalse(repo.findById(id).isPresent());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- Built-in: deleteById non-existing --
+
+    @Test
+    public void testDeleteByIdNonExisting() throws Exception {
+        utx.begin();
+        try {
+            // Should not throw
+            repo.deleteById(999999L);
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- Built-in: findById not found --
+
+    @Test
+    public void testFindByIdNotFound() throws Exception {
+        utx.begin();
+        try {
+            final Optional<Person> result = repo.findById(999999L);
+            assertFalse(result.isPresent());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- @Delete with no args: delete all entities --
+
+    @Test
+    public void testDeleteAllNoArgs() throws Exception {
+        utx.begin();
+        try {
+            repo.customInsert(new Person("Dan", 30, "dan@example.com"));
+            repo.customInsert(new Person("Eva", 25, "eva@example.com"));
+
+            em.clear();
+            final long countBefore = em.createQuery("SELECT COUNT(p) FROM Person p", Long.class)
+                .getSingleResult();
+            assertEquals(2L, countBefore);
+
+            // deleteAll() with no args should bulk delete all
+            repo.deleteAll();
+
+            em.clear();
+            final long countAfter = em.createQuery("SELECT COUNT(p) FROM Person p", Long.class)
+                .getSingleResult();
+            assertEquals(0L, countAfter);
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testDeleteAllEntitiesAnnotation() throws Exception {
+        utx.begin();
+        try {
+            repo.customInsert(new Person("Ada", 30, "ada@example.com"));
+            repo.customInsert(new Person("Ben", 25, "ben@example.com"));
+            repo.customInsert(new Person("Cal", 40, "cal@example.com"));
+
+            // Verify entities exist
+            em.clear();
+            final long countBefore = em.createQuery("SELECT COUNT(p) FROM Person p", Long.class)
+                .getSingleResult();
+            assertEquals(3L, countBefore);
+
+            // @Delete with no args should delete all
+            repo.deleteAllEntities();
+
+            em.clear();
+            final long countAfter = em.createQuery("SELECT COUNT(p) FROM Person p", Long.class)
+                .getSingleResult();
+            assertEquals(0L, countAfter);
+        } finally {
+            utx.rollback();
+        }
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/FinderMethodTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/FinderMethodTest.java
new file mode 100644
index 0000000000..345857dd9a
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/FinderMethodTest.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.annotation.Resource;
+import jakarta.inject.Inject;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.transaction.UserTransaction;
+import org.apache.openejb.data.test.entity.Person;
+import org.apache.openejb.data.test.repo.PersonCustomRepository;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(ApplicationComposer.class)
+public class FinderMethodTest {
+
+    @Inject
+    private PersonCustomRepository personRepo;
+
+    @PersistenceContext
+    private EntityManager em;
+
+    @Resource
+    private UserTransaction utx;
+
+    @Module
+    @Classes(cdi = true, value = {PersonCustomRepository.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("person-unit");
+        unit.setJtaDataSource("personDatabase");
+        unit.setNonJtaDataSource("personDatabaseUnmanaged");
+        unit.getClazz().add(Person.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("personDatabase", "new://Resource?type=DataSource");
+        p.put("personDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("personDatabase.JdbcUrl", "jdbc:hsqldb:mem:persondb-finder");
+        return p;
+    }
+
+    @Test
+    public void testFindByName() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Alice", 30, "alice@example.com"));
+            em.persist(new Person("Alice", 25, "alice2@example.com"));
+            em.persist(new Person("Bob", 40, "bob@example.com"));
+            em.flush();
+
+            final List<Person> result = personRepo.findByName("Alice");
+            assertEquals(2, result.size());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testFindByNameAndAge() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Alice", 30, "alice@test.com"));
+            em.persist(new Person("Alice", 25, "alice2@test.com"));
+            em.flush();
+
+            final List<Person> result = personRepo.findByNameAndAge("Alice", 30);
+            assertEquals(1, result.size());
+            assertEquals(30, result.get(0).getAge());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testFindByAgeGreaterThanOrderByName() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Charlie", 50, "charlie@test.com"));
+            em.persist(new Person("Alice", 40, "alice@test.com"));
+            em.persist(new Person("Bob", 20, "bob@test.com"));
+            em.flush();
+
+            final List<Person> result = personRepo.findByAgeGreaterThanOrderByNameAsc(30);
+            assertEquals(2, result.size());
+            assertEquals("Alice", result.get(0).getName());
+            assertEquals("Charlie", result.get(1).getName());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testCountByName() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Alice", 30, "a1@test.com"));
+            em.persist(new Person("Alice", 25, "a2@test.com"));
+            em.persist(new Person("Bob", 40, "b@test.com"));
+            em.flush();
+
+            final long count = personRepo.countByName("Alice");
+            assertEquals(2, count);
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testExistsByEmail() throws Exception {
+        utx.begin();
+        try {
+            em.persist(new Person("Alice", 30, "alice@test.com"));
+            em.flush();
+
+            assertTrue(personRepo.existsByEmail("alice@test.com"));
+            assertFalse(personRepo.existsByEmail("nonexistent@test.com"));
+        } finally {
+            utx.rollback();
+        }
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/MethodMetadataTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/MethodMetadataTest.java
new file mode 100644
index 0000000000..66065f2b4c
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/MethodMetadataTest.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import org.apache.openejb.data.meta.MethodMetadata;
+import org.apache.openejb.data.query.MethodNameParser;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class MethodMetadataTest {
+
+    @Test
+    void builtinStrategyWithNullParsedQuery() {
+        final MethodMetadata meta = new MethodMetadata(MethodMetadata.Strategy.BUILTIN, null);
+        assertEquals(MethodMetadata.Strategy.BUILTIN, meta.getStrategy());
+        assertNull(meta.getParsedQuery());
+    }
+
+    @Test
+    void methodNameStrategyWithParsedQuery() {
+        final MethodNameParser.ParsedQuery parsed = MethodNameParser.parse("findByName");
+        final MethodMetadata meta = new MethodMetadata(MethodMetadata.Strategy.METHOD_NAME, parsed);
+        assertEquals(MethodMetadata.Strategy.METHOD_NAME, meta.getStrategy());
+        assertNotNull(meta.getParsedQuery());
+        assertEquals(MethodNameParser.Action.FIND, meta.getParsedQuery().action());
+    }
+
+    @Test
+    void allStrategyValuesExist() {
+        assertEquals(2, MethodMetadata.Strategy.values().length);
+        assertNotNull(MethodMetadata.Strategy.valueOf("BUILTIN"));
+        assertNotNull(MethodMetadata.Strategy.valueOf("METHOD_NAME"));
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/MethodNameParserExtendedTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/MethodNameParserExtendedTest.java
new file mode 100644
index 0000000000..500e92617e
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/MethodNameParserExtendedTest.java
@@ -0,0 +1,306 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import org.apache.openejb.data.query.MethodNameParser;
+import org.apache.openejb.data.query.MethodNameParser.Action;
+import org.apache.openejb.data.query.MethodNameParser.Connector;
+import org.apache.openejb.data.query.MethodNameParser.Operator;
+import org.apache.openejb.data.query.MethodNameParser.ParsedQuery;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class MethodNameParserExtendedTest {
+
+    // -- Or connector --
+
+    @Test
+    void parseFindByWithOrConnector() {
+        final ParsedQuery result = MethodNameParser.parse("findByNameOrAge");
+        assertNotNull(result);
+        assertEquals(Action.FIND, result.action());
+        assertEquals(2, result.conditions().size());
+        assertEquals("name", result.conditions().get(0).property());
+        assertEquals("age", result.conditions().get(1).property());
+        assertEquals(Connector.OR, result.conditions().get(1).connector());
+    }
+
+    @Test
+    void parseFindByWithMixedConnectors() {
+        final ParsedQuery result = MethodNameParser.parse("findByNameAndAgeOrEmail");
+        assertNotNull(result);
+        assertEquals(3, result.conditions().size());
+        assertEquals("name", result.conditions().get(0).property());
+        assertEquals("age", result.conditions().get(1).property());
+        assertEquals(Connector.AND, result.conditions().get(1).connector());
+        assertEquals("email", result.conditions().get(2).property());
+        assertEquals(Connector.OR, result.conditions().get(2).connector());
+    }
+
+    // -- All operators --
+
+    @Test
+    void parseFindByStartsWith() {
+        final ParsedQuery result = MethodNameParser.parse("findByNameStartsWith");
+        assertNotNull(result);
+        assertEquals(Operator.STARTS_WITH, result.conditions().get(0).operator());
+        assertEquals(1, result.conditions().get(0).operator().parameterCount());
+    }
+
+    @Test
+    void parseFindByEndsWith() {
+        final ParsedQuery result = MethodNameParser.parse("findByNameEndsWith");
+        assertNotNull(result);
+        assertEquals(Operator.ENDS_WITH, result.conditions().get(0).operator());
+    }
+
+    @Test
+    void parseFindByLike() {
+        final ParsedQuery result = MethodNameParser.parse("findByNameLike");
+        assertNotNull(result);
+        assertEquals(Operator.LIKE, result.conditions().get(0).operator());
+    }
+
+    @Test
+    void parseFindByNot() {
+        final ParsedQuery result = MethodNameParser.parse("findByNameNot");
+        assertNotNull(result);
+        assertEquals(Operator.NOT, result.conditions().get(0).operator());
+        assertEquals(1, result.conditions().get(0).operator().parameterCount());
+    }
+
+    @Test
+    void parseFindByNotNull() {
+        final ParsedQuery result = MethodNameParser.parse("findByEmailNotNull");
+        assertNotNull(result);
+        assertEquals(Operator.NOT_NULL, result.conditions().get(0).operator());
+        assertEquals(0, result.conditions().get(0).operator().parameterCount());
+    }
+
+    @Test
+    void parseFindByIn() {
+        final ParsedQuery result = MethodNameParser.parse("findByNameIn");
+        assertNotNull(result);
+        assertEquals(Operator.IN, result.conditions().get(0).operator());
+        assertEquals(1, result.conditions().get(0).operator().parameterCount());
+    }
+
+    @Test
+    void parseFindByTrue() {
+        final ParsedQuery result = MethodNameParser.parse("findByActiveTrue");
+        assertNotNull(result);
+        assertEquals(Operator.TRUE, result.conditions().get(0).operator());
+        assertEquals(0, result.conditions().get(0).operator().parameterCount());
+    }
+
+    @Test
+    void parseFindByFalse() {
+        final ParsedQuery result = MethodNameParser.parse("findByActiveFalse");
+        assertNotNull(result);
+        assertEquals(Operator.FALSE, result.conditions().get(0).operator());
+        assertEquals(0, result.conditions().get(0).operator().parameterCount());
+    }
+
+    @Test
+    void parseFindByGreaterThanEqual() {
+        final ParsedQuery result = MethodNameParser.parse("findByAgeGreaterThanEqual");
+        assertNotNull(result);
+        assertEquals(Operator.GREATER_THAN_EQUAL, result.conditions().get(0).operator());
+    }
+
+    @Test
+    void parseFindByLessThan() {
+        final ParsedQuery result = MethodNameParser.parse("findByAgeLessThan");
+        assertNotNull(result);
+        assertEquals(Operator.LESS_THAN, result.conditions().get(0).operator());
+    }
+
+    // -- OrderBy variations --
+
+    @Test
+    void parseFindByWithOrderByDesc() {
+        final ParsedQuery result = MethodNameParser.parse("findByNameOrderByAgeDesc");
+        assertNotNull(result);
+        assertEquals(1, result.conditions().size());
+        assertEquals("name", result.conditions().get(0).property());
+        assertEquals(1, result.orderClauses().size());
+        assertEquals("age", result.orderClauses().get(0).property());
+        assertFalse(result.orderClauses().get(0).ascending());
+    }
+
+    @Test
+    void parseFindByWithMultipleOrderBy() {
+        final ParsedQuery result = MethodNameParser.parse("findByAgeGreaterThanOrderByNameAscAgeDesc");
+        assertNotNull(result);
+        assertEquals(1, result.conditions().size());
+        assertEquals(2, result.orderClauses().size());
+        assertEquals("name", result.orderClauses().get(0).property());
+        assertTrue(result.orderClauses().get(0).ascending());
+        assertEquals("age", result.orderClauses().get(1).property());
+        assertFalse(result.orderClauses().get(1).ascending());
+    }
+
+    @Test
+    void parseOrderByWithoutConditions() {
+        final ParsedQuery result = MethodNameParser.parse("findByNameOrderByNameAsc");
+        assertNotNull(result);
+        assertEquals(1, result.conditions().size());
+        assertEquals(1, result.orderClauses().size());
+        assertTrue(result.orderClauses().get(0).ascending());
+    }
+
+    // -- Multiple conditions with operators --
+
+    @Test
+    void parseFindByMultipleConditionsWithOperators() {
+        final ParsedQuery result = MethodNameParser.parse("findByNameContainsAndAgeGreaterThan");
+        assertNotNull(result);
+        assertEquals(2, result.conditions().size());
+        assertEquals("name", result.conditions().get(0).property());
+        assertEquals(Operator.CONTAINS, result.conditions().get(0).operator());
+        assertEquals("age", result.conditions().get(1).property());
+        assertEquals(Operator.GREATER_THAN, result.conditions().get(1).operator());
+    }
+
+    @Test
+    void parseFindByNullAndNotNull() {
+        final ParsedQuery result = MethodNameParser.parse("findByEmailNullAndNameNotNull");
+        assertNotNull(result);
+        assertEquals(2, result.conditions().size());
+        assertEquals(Operator.NULL, result.conditions().get(0).operator());
+        assertEquals(Operator.NOT_NULL, result.conditions().get(1).operator());
+    }
+
+    // -- Delete variations --
+
+    @Test
+    void parseDeleteByMultipleConditions() {
+        final ParsedQuery result = MethodNameParser.parse("deleteByNameAndAge");
+        assertNotNull(result);
+        assertEquals(Action.DELETE, result.action());
+        assertEquals(2, result.conditions().size());
+    }
+
+    @Test
+    void parseDeleteByWithOperator() {
+        final ParsedQuery result = MethodNameParser.parse("deleteByAgeLessThan");
+        assertNotNull(result);
+        assertEquals(Action.DELETE, result.action());
+        assertEquals(Operator.LESS_THAN, result.conditions().get(0).operator());
+    }
+
+    // -- Count / Exists variations --
+
+    @Test
+    void parseCountByWithOperator() {
+        final ParsedQuery result = MethodNameParser.parse("countByAgeGreaterThan");
+        assertNotNull(result);
+        assertEquals(Action.COUNT, result.action());
+        assertEquals(Operator.GREATER_THAN, result.conditions().get(0).operator());
+    }
+
+    @Test
+    void parseExistsByWithMultipleConditions() {
+        final ParsedQuery result = MethodNameParser.parse("existsByNameAndEmail");
+        assertNotNull(result);
+        assertEquals(Action.EXISTS, result.action());
+        assertEquals(2, result.conditions().size());
+    }
+
+    // -- Non-matching --
+
+    @Test
+    void parseUnknownPrefixReturnsNull() {
+        assertNull(MethodNameParser.parse("listByName"));
+        assertNull(MethodNameParser.parse("searchByName"));
+        assertNull(MethodNameParser.parse("removeByName"));
+        assertNull(MethodNameParser.parse(""));
+        assertNull(MethodNameParser.parse("count"));
+        assertNull(MethodNameParser.parse("find"));
+    }
+
+    // -- Operator.fromSuffix coverage --
+
+    @Test
+    void operatorFromSuffixUnknownDefaultsToEqual() {
+        assertEquals(Operator.EQUAL, Operator.fromSuffix("Unknown"));
+        assertEquals(Operator.EQUAL, Operator.fromSuffix(""));
+    }
+
+    @Test
+    void operatorSuffixRoundTrips() {
+        for (final Operator op : Operator.values()) {
+            assertEquals(op, Operator.fromSuffix(op.suffix()),
+                "fromSuffix(suffix()) should round-trip for " + op);
+        }
+    }
+
+    // -- Operator.parameterCount full coverage --
+
+    @Test
+    void operatorParameterCountForAllOperators() {
+        assertEquals(1, Operator.EQUAL.parameterCount());
+        assertEquals(1, Operator.GREATER_THAN.parameterCount());
+        assertEquals(1, Operator.GREATER_THAN_EQUAL.parameterCount());
+        assertEquals(1, Operator.LESS_THAN.parameterCount());
+        assertEquals(1, Operator.LESS_THAN_EQUAL.parameterCount());
+        assertEquals(1, Operator.LIKE.parameterCount());
+        assertEquals(1, Operator.STARTS_WITH.parameterCount());
+        assertEquals(1, Operator.ENDS_WITH.parameterCount());
+        assertEquals(1, Operator.CONTAINS.parameterCount());
+        assertEquals(1, Operator.IN.parameterCount());
+        assertEquals(1, Operator.NOT.parameterCount());
+        assertEquals(2, Operator.BETWEEN.parameterCount());
+        assertEquals(0, Operator.NULL.parameterCount());
+        assertEquals(0, Operator.NOT_NULL.parameterCount());
+        assertEquals(0, Operator.TRUE.parameterCount());
+        assertEquals(0, Operator.FALSE.parameterCount());
+    }
+
+    // -- ParsedQuery immutability --
+
+    @Test
+    void parsedQueryConditionsAreImmutable() {
+        final ParsedQuery result = MethodNameParser.parse("findByName");
+        assertNotNull(result);
+        assertThrows(UnsupportedOperationException.class, () -> result.conditions().clear());
+    }
+
+    @Test
+    void parsedQueryOrderClausesAreImmutable() {
+        final ParsedQuery result = MethodNameParser.parse("findByNameOrderByAgeAsc");
+        assertNotNull(result);
+        assertThrows(UnsupportedOperationException.class, () -> result.orderClauses().clear());
+    }
+
+    // -- Edge cases --
+
+    @Test
+    void parseFindByWithBetweenAndAnd() {
+        final ParsedQuery result = MethodNameParser.parse("findByAgeBetweenAndName");
+        assertNotNull(result);
+        assertEquals(2, result.conditions().size());
+        assertEquals(Operator.BETWEEN, result.conditions().get(0).operator());
+        assertEquals(Operator.EQUAL, result.conditions().get(1).operator());
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/MethodNameParserTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/MethodNameParserTest.java
new file mode 100644
index 0000000000..30db4a6ce7
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/MethodNameParserTest.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import org.apache.openejb.data.query.MethodNameParser;
+import org.apache.openejb.data.query.MethodNameParser.Action;
+import org.apache.openejb.data.query.MethodNameParser.Operator;
+import org.apache.openejb.data.query.MethodNameParser.ParsedQuery;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class MethodNameParserTest {
+
+    @Test
+    void parseSimpleFindBy() {
+        final ParsedQuery result = MethodNameParser.parse("findByName");
+        assertNotNull(result);
+        assertEquals(Action.FIND, result.action());
+        assertEquals(1, result.conditions().size());
+        assertEquals("name", result.conditions().get(0).property());
+        assertEquals(Operator.EQUAL, result.conditions().get(0).operator());
+    }
+
+    @Test
+    void parseFindByMultipleConditions() {
+        final ParsedQuery result = MethodNameParser.parse("findByNameAndAge");
+        assertNotNull(result);
+        assertEquals(Action.FIND, result.action());
+        assertEquals(2, result.conditions().size());
+        assertEquals("name", result.conditions().get(0).property());
+        assertEquals("age", result.conditions().get(1).property());
+    }
+
+    @Test
+    void parseFindByWithOperator() {
+        final ParsedQuery result = MethodNameParser.parse("findByAgeGreaterThan");
+        assertNotNull(result);
+        assertEquals(1, result.conditions().size());
+        assertEquals("age", result.conditions().get(0).property());
+        assertEquals(Operator.GREATER_THAN, result.conditions().get(0).operator());
+    }
+
+    @Test
+    void parseFindByWithOrderBy() {
+        final ParsedQuery result = MethodNameParser.parse("findByAgeGreaterThanOrderByNameAsc");
+        assertNotNull(result);
+        assertEquals(1, result.conditions().size());
+        assertEquals("age", result.conditions().get(0).property());
+        assertEquals(Operator.GREATER_THAN, result.conditions().get(0).operator());
+        assertEquals(1, result.orderClauses().size());
+        assertEquals("name", result.orderClauses().get(0).property());
+        assertTrue(result.orderClauses().get(0).ascending());
+    }
+
+    @Test
+    void parseDeleteBy() {
+        final ParsedQuery result = MethodNameParser.parse("deleteByName");
+        assertNotNull(result);
+        assertEquals(Action.DELETE, result.action());
+        assertEquals(1, result.conditions().size());
+        assertEquals("name", result.conditions().get(0).property());
+    }
+
+    @Test
+    void parseCountBy() {
+        final ParsedQuery result = MethodNameParser.parse("countByName");
+        assertNotNull(result);
+        assertEquals(Action.COUNT, result.action());
+    }
+
+    @Test
+    void parseExistsBy() {
+        final ParsedQuery result = MethodNameParser.parse("existsByEmail");
+        assertNotNull(result);
+        assertEquals(Action.EXISTS, result.action());
+        assertEquals("email", result.conditions().get(0).property());
+    }
+
+    @Test
+    void parseFindByBetween() {
+        final ParsedQuery result = MethodNameParser.parse("findByAgeBetween");
+        assertNotNull(result);
+        assertEquals(1, result.conditions().size());
+        assertEquals(Operator.BETWEEN, result.conditions().get(0).operator());
+        assertEquals(2, result.conditions().get(0).operator().parameterCount());
+    }
+
+    @Test
+    void parseFindByNull() {
+        final ParsedQuery result = MethodNameParser.parse("findByEmailNull");
+        assertNotNull(result);
+        assertEquals(1, result.conditions().size());
+        assertEquals(Operator.NULL, result.conditions().get(0).operator());
+        assertEquals(0, result.conditions().get(0).operator().parameterCount());
+    }
+
+    @Test
+    void parseFindByContains() {
+        final ParsedQuery result = MethodNameParser.parse("findByNameContains");
+        assertNotNull(result);
+        assertEquals(1, result.conditions().size());
+        assertEquals(Operator.CONTAINS, result.conditions().get(0).operator());
+    }
+
+    @Test
+    void parseNonMatchingMethod() {
+        assertNull(MethodNameParser.parse("getByName"));
+        assertNull(MethodNameParser.parse("save"));
+    }
+
+    @Test
+    void parseFindByLessThanEqual() {
+        final ParsedQuery result = MethodNameParser.parse("findByAgeLessThanEqual");
+        assertNotNull(result);
+        assertEquals(1, result.conditions().size());
+        assertEquals(Operator.LESS_THAN_EQUAL, result.conditions().get(0).operator());
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/PaginationTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/PaginationTest.java
new file mode 100644
index 0000000000..d51a09c6de
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/PaginationTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.annotation.Resource;
+import jakarta.inject.Inject;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.transaction.UserTransaction;
+import org.apache.openejb.data.test.entity.Person;
+import org.apache.openejb.data.test.repo.PersonCustomRepository;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(ApplicationComposer.class)
+public class PaginationTest {
+
+    @Inject
+    private PersonCustomRepository personRepo;
+
+    @PersistenceContext
+    private EntityManager em;
+
+    @Resource
+    private UserTransaction utx;
+
+    @Module
+    @Classes(cdi = true, value = {PersonCustomRepository.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("person-unit");
+        unit.setJtaDataSource("personDatabase");
+        unit.setNonJtaDataSource("personDatabaseUnmanaged");
+        unit.getClazz().add(Person.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("personDatabase", "new://Resource?type=DataSource");
+        p.put("personDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("personDatabase.JdbcUrl", "jdbc:hsqldb:mem:persondb-pagination");
+        return p;
+    }
+
+    @Test
+    public void testRepositoryInjected() {
+        assertNotNull("Repository should be injected", personRepo);
+    }
+
+    @Test
+    public void testBasicQueryExecution() throws Exception {
+        utx.begin();
+        try {
+            for (int i = 0; i < 10; i++) {
+                em.persist(new Person("Person" + i, 20 + i, "person" + i + "@test.com"));
+            }
+            em.flush();
+
+            final var result = personRepo.findByAgeGreaterThanOrderByNameAsc(24);
+            assertEquals(5, result.size());
+        } finally {
+            utx.rollback();
+        }
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/QueryOperatorTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/QueryOperatorTest.java
new file mode 100644
index 0000000000..a8ac58821b
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/QueryOperatorTest.java
@@ -0,0 +1,275 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.annotation.Resource;
+import jakarta.inject.Inject;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.transaction.UserTransaction;
+import org.apache.openejb.data.test.entity.Task;
+import org.apache.openejb.data.test.repo.TaskRepository;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(ApplicationComposer.class)
+public class QueryOperatorTest {
+
+    @Inject
+    private TaskRepository repo;
+
+    @PersistenceContext
+    private EntityManager em;
+
+    @Resource
+    private UserTransaction utx;
+
+    @Module
+    @Classes(cdi = true, value = {TaskRepository.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("task-unit");
+        unit.setJtaDataSource("taskDatabase");
+        unit.setNonJtaDataSource("taskDatabaseUnmanaged");
+        unit.getClazz().add(Task.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("taskDatabase", "new://Resource?type=DataSource");
+        p.put("taskDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("taskDatabase.JdbcUrl", "jdbc:hsqldb:mem:taskdb-operators");
+        return p;
+    }
+
+    private void seedTasks() {
+        em.persist(new Task("Fix login bug", "Login page error", 3, false));
+        em.persist(new Task("Fix logout bug", "Logout page error", 2, true));
+        em.persist(new Task("Add feature X", "New feature", 5, false));
+        em.persist(new Task("Add feature Y", "New feature", 1, false));
+        em.persist(new Task("Refactor service", "Cleanup code", 4, true));
+        em.flush();
+    }
+
+    @Test
+    public void testStartsWith() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            final List<Task> result = repo.findByTitleStartsWith("Fix");
+            assertEquals(2, result.size());
+            assertTrue(result.stream().allMatch(t -> t.getTitle().startsWith("Fix")));
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testEndsWith() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            final List<Task> result = repo.findByTitleEndsWith("bug");
+            assertEquals(2, result.size());
+            assertTrue(result.stream().allMatch(t -> t.getTitle().endsWith("bug")));
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testBetween() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            final List<Task> result = repo.findByPriorityBetween(2, 4);
+            assertEquals(3, result.size());
+            assertTrue(result.stream().allMatch(t -> t.getPriority() >= 2 && t.getPriority() <= 4));
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testIn() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            final List<Task> result = repo.findByPriorityIn(List.of(1, 3, 5));
+            assertEquals(3, result.size());
+            assertTrue(result.stream().allMatch(t ->
+                    t.getPriority() == 1 || t.getPriority() == 3 || t.getPriority() == 5));
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testOrConnector() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            // completed=true OR priority > 4
+            final List<Task> result = repo.findByCompletedTrueOrPriorityGreaterThan(4);
+            // completed=true: "Fix logout bug"(p2), "Refactor service"(p4) = 2
+            // priority > 4: "Add feature X"(p5) = 1
+            // union = 3
+            assertEquals(3, result.size());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testTrue() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            final List<Task> result = repo.findByCompletedTrue();
+            assertEquals(2, result.size());
+            assertTrue(result.stream().allMatch(Task::isCompleted));
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testFalse() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            final List<Task> result = repo.findByCompletedFalse();
+            assertEquals(3, result.size());
+            assertTrue(result.stream().noneMatch(Task::isCompleted));
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testNot() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            final List<Task> result = repo.findByTitleNot("Fix login bug");
+            assertEquals(4, result.size());
+            assertTrue(result.stream().noneMatch(t -> "Fix login bug".equals(t.getTitle())));
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testStartsWithAndFalse() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            // title starts with "Fix" AND completed=false
+            final List<Task> result = repo.findByTitleStartsWithAndCompletedFalse("Fix");
+            assertEquals(1, result.size());
+            assertEquals("Fix login bug", result.get(0).getTitle());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testMultipleOrderClauses() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            // completed=false, order by priority DESC then title ASC
+            final List<Task> result = repo.findByCompletedFalseOrderByPriorityDescTitleAsc();
+            assertEquals(3, result.size());
+            // priority 5 -> "Add feature X", priority 3 -> "Fix login bug", priority 1 -> "Add feature Y"
+            assertEquals("Add feature X", result.get(0).getTitle());
+            assertEquals("Fix login bug", result.get(1).getTitle());
+            assertEquals("Add feature Y", result.get(2).getTitle());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testStartsWithNoMatch() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            final List<Task> result = repo.findByTitleStartsWith("ZZZ");
+            assertTrue(result.isEmpty());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testBetweenSingleMatch() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            final List<Task> result = repo.findByPriorityBetween(5, 5);
+            assertEquals(1, result.size());
+            assertEquals(5, result.get(0).getPriority());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testInEmptyList() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            final List<Task> result = repo.findByPriorityIn(List.of());
+            assertTrue(result.isEmpty());
+        } finally {
+            utx.rollback();
+        }
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/RepositoryBeanTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/RepositoryBeanTest.java
new file mode 100644
index 0000000000..afac91916f
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/RepositoryBeanTest.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.data.repository.CrudRepository;
+import jakarta.data.repository.Repository;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Any;
+import jakarta.enterprise.inject.Default;
+import org.apache.openejb.data.extension.RepositoryBean;
+import org.apache.openejb.data.test.entity.Person;
+import org.junit.jupiter.api.Test;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Proxy;
+import java.lang.reflect.Type;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class RepositoryBeanTest {
+
+    @Repository
+    interface TestRepo extends CrudRepository<Person, Long> {
+    }
+
+    @Test
+    void beanClassIsRepositoryInterface() {
+        final RepositoryBean<TestRepo> bean = new RepositoryBean<>(TestRepo.class);
+        assertEquals(TestRepo.class, bean.getBeanClass());
+    }
+
+    @Test
+    void scopeIsApplicationScoped() {
+        final RepositoryBean<TestRepo> bean = new RepositoryBean<>(TestRepo.class);
+        assertEquals(ApplicationScoped.class, bean.getScope());
+    }
+
+    @Test
+    void typesContainInterfaceAndObject() {
+        final RepositoryBean<TestRepo> bean = new RepositoryBean<>(TestRepo.class);
+        final Set<Type> types = bean.getTypes();
+        assertEquals(2, types.size());
+        assertTrue(types.contains(TestRepo.class));
+        assertTrue(types.contains(Object.class));
+    }
+
+    @Test
+    void qualifiersContainDefaultAndAny() {
+        final RepositoryBean<TestRepo> bean = new RepositoryBean<>(TestRepo.class);
+        final Set<Annotation> qualifiers = bean.getQualifiers();
+        assertEquals(2, qualifiers.size());
+        assertTrue(qualifiers.stream().anyMatch(a -> a.annotationType().equals(Default.class)));
+        assertTrue(qualifiers.stream().anyMatch(a -> a.annotationType().equals(Any.class)));
+    }
+
+    @Test
+    void nameIsNull() {
+        final RepositoryBean<TestRepo> bean = new RepositoryBean<>(TestRepo.class);
+        assertNull(bean.getName());
+    }
+
+    @Test
+    void isNotAlternative() {
+        final RepositoryBean<TestRepo> bean = new RepositoryBean<>(TestRepo.class);
+        assertFalse(bean.isAlternative());
+    }
+
+    @Test
+    void stereotypesAreEmpty() {
+        final RepositoryBean<TestRepo> bean = new RepositoryBean<>(TestRepo.class);
+        assertTrue(bean.getStereotypes().isEmpty());
+    }
+
+    @Test
+    void injectionPointsAreEmpty() {
+        final RepositoryBean<TestRepo> bean = new RepositoryBean<>(TestRepo.class);
+        assertTrue(bean.getInjectionPoints().isEmpty());
+    }
+
+    @Test
+    void idContainsInterfaceName() {
+        final RepositoryBean<TestRepo> bean = new RepositoryBean<>(TestRepo.class);
+        final String id = bean.getId();
+        assertNotNull(id);
+        assertTrue(id.contains(TestRepo.class.getName()));
+        assertTrue(id.startsWith("openejb-jakarta-data-"));
+    }
+
+    @Test
+    void createProducesProxyInstance() {
+        final RepositoryBean<TestRepo> bean = new RepositoryBean<>(TestRepo.class);
+        final TestRepo proxy = bean.create(null);
+        assertNotNull(proxy);
+        assertTrue(Proxy.isProxyClass(proxy.getClass()));
+        assertTrue(proxy instanceof TestRepo);
+    }
+
+    @Test
+    void createdProxyToStringContainsInterfaceName() {
+        final RepositoryBean<TestRepo> bean = new RepositoryBean<>(TestRepo.class);
+        final TestRepo proxy = bean.create(null);
+        final String str = proxy.toString();
+        assertTrue(str.contains("TestRepo"));
+        assertTrue(str.contains("Jakarta Data Repository Proxy"));
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/RepositoryMetadataTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/RepositoryMetadataTest.java
new file mode 100644
index 0000000000..b169ef6b7d
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/RepositoryMetadataTest.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.data.repository.BasicRepository;
+import jakarta.data.repository.CrudRepository;
+import jakarta.data.repository.DataRepository;
+import jakarta.data.repository.Repository;
+import org.apache.openejb.data.meta.RepositoryMetadata;
+import org.apache.openejb.data.test.entity.Person;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class RepositoryMetadataTest {
+
+    // -- Test interfaces --
+
+    @Repository
+    interface PersonCrudRepo extends CrudRepository<Person, Long> {
+    }
+
+    @Repository
+    interface PersonBasicRepo extends BasicRepository<Person, Long> {
+    }
+
+    @Repository
+    interface PersonDataRepo extends DataRepository<Person, Long> {
+    }
+
+    @Repository(dataStore = "myPU")
+    interface PersonWithDataStore extends CrudRepository<Person, Long> {
+    }
+
+    // Intermediate interface (no type args, just extends)
+    interface IntermediateRepo extends CrudRepository<Person, Long> {
+    }
+
+    @Repository
+    interface ExtendedIntermediateRepo extends IntermediateRepo {
+    }
+
+    @Repository(dataStore = "")
+    interface EmptyDataStoreRepo extends DataRepository<Person, Long> {
+    }
+
+    // -- Tests: CrudRepository --
+
+    @Test
+    void resolvesEntityClassFromCrudRepository() {
+        final RepositoryMetadata meta = new RepositoryMetadata(PersonCrudRepo.class);
+        assertEquals(Person.class, meta.getEntityClass());
+    }
+
+    @Test
+    void returnsRepositoryInterface() {
+        final RepositoryMetadata meta = new RepositoryMetadata(PersonCrudRepo.class);
+        assertEquals(PersonCrudRepo.class, meta.getRepositoryInterface());
+    }
+
+    // -- Tests: BasicRepository --
+
+    @Test
+    void resolvesEntityClassFromBasicRepository() {
+        final RepositoryMetadata meta = new RepositoryMetadata(PersonBasicRepo.class);
+        assertEquals(Person.class, meta.getEntityClass());
+    }
+
+    // -- Tests: DataRepository --
+
+    @Test
+    void resolvesEntityClassFromDataRepository() {
+        final RepositoryMetadata meta = new RepositoryMetadata(PersonDataRepo.class);
+        assertEquals(Person.class, meta.getEntityClass());
+    }
+
+    // -- Tests: dataStore --
+
+    @Test
+    void extractsDataStoreValue() {
+        final RepositoryMetadata meta = new RepositoryMetadata(PersonWithDataStore.class);
+        assertEquals("myPU", meta.getDataStore());
+    }
+
+    @Test
+    void defaultDataStoreIsEmpty() {
+        final RepositoryMetadata meta = new RepositoryMetadata(PersonCrudRepo.class);
+        assertEquals("", meta.getDataStore());
+    }
+
+    @Test
+    void emptyDataStoreAnnotation() {
+        final RepositoryMetadata meta = new RepositoryMetadata(EmptyDataStoreRepo.class);
+        assertEquals("", meta.getDataStore());
+    }
+
+    // -- Tests: intermediate interface hierarchy --
+
+    @Test
+    void resolvesTypesFromIntermediateInterface() {
+        final RepositoryMetadata meta = new RepositoryMetadata(ExtendedIntermediateRepo.class);
+        assertEquals(Person.class, meta.getEntityClass());
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/SortAndLimitTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/SortAndLimitTest.java
new file mode 100644
index 0000000000..2a52ffb282
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/SortAndLimitTest.java
@@ -0,0 +1,306 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.annotation.Resource;
+import jakarta.data.Limit;
+import jakarta.data.Order;
+import jakarta.data.Sort;
+import jakarta.data.page.Page;
+import jakarta.data.page.PageRequest;
+import jakarta.inject.Inject;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.transaction.UserTransaction;
+import org.apache.openejb.data.test.entity.Task;
+import org.apache.openejb.data.test.repo.TaskRepository;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(ApplicationComposer.class)
+public class SortAndLimitTest {
+
+    @Inject
+    private TaskRepository repo;
+
+    @PersistenceContext
+    private EntityManager em;
+
+    @Resource
+    private UserTransaction utx;
+
+    @Module
+    @Classes(cdi = true, value = {TaskRepository.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("task-unit");
+        unit.setJtaDataSource("taskDatabase");
+        unit.setNonJtaDataSource("taskDatabaseUnmanaged");
+        unit.getClazz().add(Task.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("taskDatabase", "new://Resource?type=DataSource");
+        p.put("taskDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("taskDatabase.JdbcUrl", "jdbc:hsqldb:mem:taskdb-sort");
+        return p;
+    }
+
+    private void seedTasks() {
+        em.persist(new Task("Task A", "desc", 3, false));
+        em.persist(new Task("Task B", "desc", 1, false));
+        em.persist(new Task("Task C", "desc", 5, false));
+        em.persist(new Task("Task D", "desc", 2, false));
+        em.persist(new Task("Task E", "desc", 4, false));
+        em.persist(new Task("Done 1", "desc", 1, true));
+        em.persist(new Task("Done 2", "desc", 2, true));
+        em.flush();
+    }
+
+    // -- Limit tests --
+
+    @Test
+    public void testLimitResults() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            final List<Task> result = repo.findByCompletedFalseOrderByPriorityAsc(Limit.of(3));
+            assertEquals(3, result.size());
+            // Should be priority 1, 2, 3
+            assertEquals(1, result.get(0).getPriority());
+            assertEquals(2, result.get(1).getPriority());
+            assertEquals(3, result.get(2).getPriority());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testLimitWithOffset() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            // startAt=2 means skip first 1, take 2
+            final List<Task> result = repo.findByCompletedFalseOrderByPriorityAsc(Limit.range(2, 3));
+            assertEquals(2, result.size());
+            // priority 2, 3 (skipping 1)
+            assertEquals(2, result.get(0).getPriority());
+            assertEquals(3, result.get(1).getPriority());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- Sort tests --
+
+    @Test
+    public void testSortAscending() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            final List<Task> result = repo.findByCompletedFalse(Sort.asc("priority"));
+            assertEquals(5, result.size());
+            assertEquals(1, result.get(0).getPriority());
+            assertEquals(5, result.get(4).getPriority());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testSortDescending() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            final List<Task> result = repo.findByCompletedFalse(Sort.desc("priority"));
+            assertEquals(5, result.size());
+            assertEquals(5, result.get(0).getPriority());
+            assertEquals(1, result.get(4).getPriority());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- Order tests --
+
+    @Test
+    public void testOrderWithMultipleSorts() throws Exception {
+        utx.begin();
+        try {
+            // Add tasks with same priority for deterministic ordering
+            em.persist(new Task("Zebra task", "desc", 1, false));
+            em.persist(new Task("Alpha task", "desc", 1, false));
+            em.persist(new Task("Mid task", "desc", 2, false));
+            em.flush();
+
+            @SuppressWarnings("unchecked")
+            final Order<Task> order = Order.by(Sort.asc("priority"), Sort.asc("title"));
+            final List<Task> result = repo.findByCompletedFalse(order);
+            assertEquals(3, result.size());
+            assertEquals("Alpha task", result.get(0).getTitle());
+            assertEquals("Zebra task", result.get(1).getTitle());
+            assertEquals("Mid task", result.get(2).getTitle());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- PageRequest tests --
+
+    @Test
+    public void testPageRequest() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            @SuppressWarnings("unchecked")
+            final Order<Task> order = Order.by(Sort.asc("priority"));
+            final Page<Task> page1 = repo.findByCompletedFalse(PageRequest.ofSize(2), order);
+
+            assertNotNull(page1);
+            assertEquals(2, page1.content().size());
+            assertEquals(5, page1.totalElements());
+            assertEquals(3, page1.totalPages());
+            assertEquals(1, page1.content().get(0).getPriority());
+            assertEquals(2, page1.content().get(1).getPriority());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testPageRequestSecondPage() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            @SuppressWarnings("unchecked")
+            final Order<Task> order = Order.by(Sort.asc("priority"));
+            final Page<Task> page2 = repo.findByCompletedFalse(PageRequest.ofPage(2).size(2), order);
+
+            assertNotNull(page2);
+            assertEquals(2, page2.content().size());
+            assertEquals(3, page2.content().get(0).getPriority());
+            assertEquals(4, page2.content().get(1).getPriority());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testPageRequestLastPage() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            @SuppressWarnings("unchecked")
+            final Order<Task> order = Order.by(Sort.asc("priority"));
+            final Page<Task> page3 = repo.findByCompletedFalse(PageRequest.ofPage(3).size(2), order);
+
+            assertNotNull(page3);
+            assertEquals(1, page3.content().size());
+            assertEquals(5, page3.content().get(0).getPriority());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testPageHasNextAndPrevious() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            @SuppressWarnings("unchecked")
+            final Order<Task> order = Order.by(Sort.asc("priority"));
+
+            final Page<Task> page1 = repo.findByCompletedFalse(PageRequest.ofSize(2), order);
+            assertTrue("First page should have next", page1.hasNext());
+
+            final Page<Task> page2 = repo.findByCompletedFalse(PageRequest.ofPage(2).size(2), order);
+            assertTrue("Middle page should have next", page2.hasNext());
+
+            final Page<Task> page3 = repo.findByCompletedFalse(PageRequest.ofPage(3).size(2), order);
+            // Last page should not have next (1 item, total 5, page size 2, 3 pages)
+            assertEquals(1, page3.content().size());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- @OrderBy annotation on the method --
+
+    @Test
+    public void testOrderByAnnotationDescending() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            final List<Task> result = repo.findByCompletedFalseOrderByPriorityDesc();
+            assertEquals(5, result.size());
+            assertEquals(5, result.get(0).getPriority());
+            assertEquals(4, result.get(1).getPriority());
+            assertEquals(3, result.get(2).getPriority());
+            assertEquals(2, result.get(3).getPriority());
+            assertEquals(1, result.get(4).getPriority());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    // -- findAll with Page --
+
+    @Test
+    public void testFindAllWithPageRequest() throws Exception {
+        utx.begin();
+        try {
+            seedTasks();
+
+            final Page<Task> page = repo.findAll(PageRequest.ofSize(3), Order.by());
+            assertNotNull(page);
+            assertEquals(3, page.content().size());
+            assertEquals(7, page.totalElements());
+        } finally {
+            utx.rollback();
+        }
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/TCKLikePersistenceTest.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/TCKLikePersistenceTest.java
new file mode 100644
index 0000000000..54a3065d55
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/TCKLikePersistenceTest.java
@@ -0,0 +1,337 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test;
+
+import jakarta.data.exceptions.EntityExistsException;
+import jakarta.data.exceptions.OptimisticLockingFailureException;
+import jakarta.inject.Inject;
+import org.apache.openejb.data.test.entity.Product;
+import org.apache.openejb.data.test.repo.ProductCatalog;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests that mimic the Jakarta Data TCK PersistenceEntityTests.
+ * These run locally via ApplicationComposer, much faster than the remote TCK.
+ */
+@RunWith(ApplicationComposer.class)
+public class TCKLikePersistenceTest {
+
+    @Inject
+    private ProductCatalog catalog;
+
+    @Module
+    @Classes(cdi = true, value = {ProductCatalog.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("product-unit");
+        unit.setJtaDataSource("productDatabase");
+        unit.setNonJtaDataSource("productDatabaseUnmanaged");
+        unit.getClazz().add(Product.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("productDatabase", "new://Resource?type=DataSource");
+        p.put("productDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("productDatabase.JdbcUrl", "jdbc:hsqldb:mem:productdb-tck");
+        return p;
+    }
+
+    private void cleanUp() {
+        catalog.deleteByProductNumLike("TEST-%");
+    }
+
+    // --- Mimics testEntityManager ---
+    @Test
+    public void testInsertFindDeleteByProductNumLike() {
+        cleanUp();
+        Product p = Product.of("apple", 1.29, "TEST-01");
+        catalog.add(p);
+
+        List<Product> found = catalog.findByProductNumLike("TEST-%");
+        assertEquals(1, found.size());
+        assertEquals("apple", found.get(0).getName());
+
+        long deleted = catalog.deleteByProductNumLike("TEST-%");
+        assertEquals(1, deleted);
+    }
+
+    // --- Mimics testIdAttributeWithDifferentName ---
+    @Test
+    public void testIdAttributeWithDifferentName() {
+        cleanUp();
+        Product p = Product.of("banana", 0.59, "TEST-02");
+        catalog.add(p);
+
+        Optional<Product> found = catalog.get("TEST-02");
+        assertTrue("Should find product by ID", found.isPresent());
+        assertEquals("banana", found.get().getName());
+
+        catalog.deleteByProductNumLike("TEST-%");
+    }
+
+    // --- Mimics testInsertEntityThatAlreadyExists ---
+    @Test
+    public void testInsertEntityThatAlreadyExists() {
+        cleanUp();
+        Product p = Product.of("watermelon", 6.29, "TEST-94");
+        catalog.add(p);
+
+        try {
+            catalog.add(Product.of("duplicate", 1.0, "TEST-94"));
+            fail("Should throw EntityExistsException for duplicate key");
+        } catch (EntityExistsException e) {
+            // expected
+        }
+
+        catalog.deleteByProductNumLike("TEST-%");
+    }
+
+    // --- Mimics testLike ---
+    @Test
+    public void testLike() {
+        cleanUp();
+        catalog.add(Product.of("apple", 1.29, "TEST-10"));
+        catalog.add(Product.of("apricot", 2.49, "TEST-11"));
+        catalog.add(Product.of("banana", 0.59, "TEST-12"));
+
+        List<Product> found = catalog.findByNameLike("ap%");
+        assertEquals(2, found.size());
+
+        catalog.deleteByProductNumLike("TEST-%");
+    }
+
+    // --- Mimics testMultipleInsertUpdateDelete ---
+    @Test
+    public void testMultipleInsertUpdateDelete() {
+        cleanUp();
+
+        Product[] products = catalog.addMultiple(
+            Product.of("grape", 3.49, "TEST-20"),
+            Product.of("kiwi", 2.99, "TEST-21")
+        );
+        assertEquals(2, products.length);
+
+        // Update multiple
+        products[0].setPrice(3.99);
+        products[1].setPrice(3.29);
+        Product[] updated = catalog.modifyMultiple(products);
+        assertEquals(3.99, updated[0].getPrice(), 0.001);
+        assertEquals(3.29, updated[1].getPrice(), 0.001);
+
+        // Delete
+        catalog.remove(updated[0]);
+
+        // Deleting already-removed entity should throw OptimisticLockingFailureException
+        try {
+            catalog.remove(updated[0]);
+            fail("OptimisticLockingFailureException must be raised because the entities are not found for deletion.");
+        } catch (OptimisticLockingFailureException e) {
+            // expected
+        }
+
+        catalog.deleteByProductNumLike("TEST-%");
+    }
+
+    // --- Mimics testNull ---
+    @Test
+    public void testNull() {
+        cleanUp();
+        catalog.add(Product.of("pear", null, "TEST-30"));
+        catalog.add(Product.of("plum", 1.99, "TEST-31"));
+
+        List<Product> nullPrice = catalog.findByPriceNull();
+        assertEquals(1, nullPrice.size());
+        assertEquals("pear", nullPrice.get(0).getName());
+
+        catalog.deleteByProductNumLike("TEST-%");
+    }
+
+    // --- Mimics testQueryWithPositionalParameters (JDQL: WHERE ...) ---
+    @Test
+    public void testQueryWithPositionalParameters() {
+        cleanUp();
+        catalog.add(Product.of("pear", 1.49, "TEST-40"));
+        catalog.add(Product.of("plum", 1.99, "TEST-41"));
+        catalog.add(Product.of("apple", 1.29, "TEST-42"));
+        catalog.add(Product.of("apricot", 2.49, "TEST-43"));
+
+        // findByNameLengthAndPriceBelow: WHERE LENGTH(name) = ?1 AND price < ?2 ORDER BY name
+        List<Product> found = catalog.findByNameLengthAndPriceBelow(4, 2.0);
+        assertEquals(2, found.size());
+        assertEquals("pear", found.get(0).getName());
+        assertEquals("plum", found.get(1).getName());
+
+        catalog.deleteByProductNumLike("TEST-%");
+    }
+
+    // --- Mimics testQueryWithNamedParameters (JDQL: FROM Product WHERE ...) ---
+    @Test
+    public void testQueryWithNamedParameters() {
+        cleanUp();
+        catalog.add(Product.of("apple", 1.29, "TEST-50"));
+        catalog.add(Product.of("banana", 0.59, "TEST-51"));
+        catalog.add(Product.of("cherry", 4.99, "TEST-52"));
+
+        // withTaxBetween: FROM Product WHERE (:rate * price <= :max AND :rate * price >= :min) ORDER BY name
+        Stream<Product> stream = catalog.withTaxBetween(0.50, 2.00, 1.1);
+        List<Product> found = stream.collect(Collectors.toList());
+        // apple: 1.1*1.29=1.419 -> in range [0.50, 2.00]
+        // banana: 1.1*0.59=0.649 -> in range [0.50, 2.00]
+        // cherry: 1.1*4.99=5.489 -> out of range
+        assertEquals(2, found.size());
+        assertEquals("apple", found.get(0).getName());
+        assertEquals("banana", found.get(1).getName());
+
+        catalog.deleteByProductNumLike("TEST-%");
+    }
+
+    // --- Mimics testVersionedInsertUpdateDelete ---
+    @Test
+    public void testVersionedInsertUpdateDelete() {
+        cleanUp();
+
+        Product p1 = catalog.add(Product.of("zucchini", 1.49, "TEST-91"));
+        Product p2 = catalog.add(Product.of("cucumber", 1.29, "TEST-92"));
+
+        long v1initial = p1.getVersionNum();
+        long v2initial = p2.getVersionNum();
+
+        // Modify p1
+        p1.setPrice(1.59);
+        p1 = catalog.modify(p1);
+
+        // Modify p2
+        p2.setPrice(1.39);
+        p2 = catalog.modify(p2);
+
+        // Versions must have changed
+        assertNotEquals("Version should change after update", v1initial, p1.getVersionNum());
+        assertNotEquals("Version should change after update", v2initial, p2.getVersionNum());
+
+        // Modify p1 again
+        long v1afterFirst = p1.getVersionNum();
+        p1.setPrice(1.54);
+        p1 = catalog.modify(p1);
+        assertNotEquals("Version should change again", v1afterFirst, p1.getVersionNum());
+        assertNotEquals("Version should differ from initial", v1initial, p1.getVersionNum());
+
+        // Set p2 back to old version and try to modify -> should fail
+        p2.setVersionNum(v2initial);
+        p2.setPrice(1.34);
+        try {
+            catalog.modify(p2);
+            fail("Must raise OptimisticLockingFailureException for entity instance with old version.");
+        } catch (OptimisticLockingFailureException e) {
+            // expected
+        }
+
+        // Remove p1
+        catalog.remove(p1);
+        Optional<Product> gone = catalog.get("TEST-91");
+        assertFalse("Product should be deleted", gone.isPresent());
+
+        // Removing already-removed p1 should throw OptimisticLockingFailureException
+        try {
+            catalog.remove(p1);
+            fail("Must raise OptimisticLockingFailureException for entity that was already removed from the database.");
+        } catch (OptimisticLockingFailureException e) {
+            // expected
+        }
+
+        // Remove p2 with wrong version should throw OptimisticLockingFailureException
+        p2.setVersionNum(v2initial);
+        try {
+            catalog.remove(p2);
+            fail("Must raise OptimisticLockingFailureException for entity with non-matching version.");
+        } catch (OptimisticLockingFailureException e) {
+            // expected
+        }
+
+        assertEquals(1, catalog.deleteByProductNumLike("TEST-%"));
+    }
+
+    // --- Mimics testNotRunOnNOSQL ---
+    @Test
+    public void testSaveAndCountAndDeleteBy() {
+        cleanUp();
+        catalog.save(Product.of("melon", 3.99, "TEST-60"));
+        catalog.save(Product.of("mango", 2.49, "TEST-61"));
+        catalog.save(Product.of("mint", 0.99, "TEST-62"));
+
+        long count = catalog.countByPriceGreaterThanEqual(2.0);
+        assertEquals(2, count);
+
+        catalog.deleteById("TEST-60");
+        Optional<Product> gone = catalog.get("TEST-60");
+        assertFalse("Product should be deleted", gone.isPresent());
+
+        catalog.deleteByProductNumLike("TEST-%");
+    }
+
+    // --- Test @Delete with @By annotation ---
+    @Test
+    public void testDeleteWithByAnnotation() {
+        cleanUp();
+        catalog.add(Product.of("orange", 1.99, "TEST-70"));
+
+        catalog.deleteById("TEST-70");
+        assertFalse(catalog.get("TEST-70").isPresent());
+    }
+
+    // --- Test findByPriceNotNullAndPriceLessThanEqual ---
+    @Test
+    public void testCompoundFinder() {
+        cleanUp();
+        catalog.add(Product.of("item1", 1.00, "TEST-80"));
+        catalog.add(Product.of("item2", null, "TEST-81"));
+        catalog.add(Product.of("item3", 5.00, "TEST-82"));
+        catalog.add(Product.of("item4", 3.00, "TEST-83"));
+
+        Stream<Product> found = catalog.findByPriceNotNullAndPriceLessThanEqual(3.0);
+        List<Product> list = found.collect(Collectors.toList());
+        assertEquals(2, list.size());
+
+        catalog.deleteByProductNumLike("TEST-%");
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/cdi/EntityManagerFactoryProducer.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/cdi/EntityManagerFactoryProducer.java
new file mode 100644
index 0000000000..0f1d86d6c9
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/cdi/EntityManagerFactoryProducer.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test.cdi;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
+import jakarta.persistence.EntityManagerFactory;
+import jakarta.persistence.PersistenceUnit;
+
+/**
+ * CDI producer that exposes the EntityManagerFactory as a CDI bean.
+ * This enables testing the CDI lookup path in EntityManagerLookup.
+ */
+@ApplicationScoped
+public class EntityManagerFactoryProducer {
+
+    @PersistenceUnit(unitName = "person-unit")
+    private EntityManagerFactory emf;
+
+    @Produces
+    public EntityManagerFactory produceEntityManagerFactory() {
+        return emf;
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/cdi/NamedEntityManagerFactoryProducer.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/cdi/NamedEntityManagerFactoryProducer.java
new file mode 100644
index 0000000000..e6c7f76102
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/cdi/NamedEntityManagerFactoryProducer.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test.cdi;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
+import jakarta.inject.Named;
+import jakarta.persistence.EntityManagerFactory;
+import jakarta.persistence.PersistenceUnit;
+
+/**
+ * CDI producer that exposes the EntityManagerFactory as a named CDI bean.
+ * This enables testing the named CDI lookup path in EntityManagerLookup
+ * where bean.getName() matches the dataStore value.
+ */
+@ApplicationScoped
+public class NamedEntityManagerFactoryProducer {
+
+    @PersistenceUnit(unitName = "person-unit")
+    private EntityManagerFactory emf;
+
+    @Produces
+    @Named("my-data-store")
+    public EntityManagerFactory produceNamedEntityManagerFactory() {
+        return emf;
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/entity/Person.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/entity/Person.java
new file mode 100644
index 0000000000..f363fbd04a
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/entity/Person.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test.entity;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+
+@Entity
+public class Person {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    private String name;
+
+    private int age;
+
+    private String email;
+
+    public Person() {
+    }
+
+    public Person(final String name, final int age, final String email) {
+        this.name = name;
+        this.age = age;
+        this.email = email;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(final Long id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    public int getAge() {
+        return age;
+    }
+
+    public void setAge(final int age) {
+        this.age = age;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(final String email) {
+        this.email = email;
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/entity/Product.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/entity/Product.java
new file mode 100644
index 0000000000..49c4db9ecc
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/entity/Product.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test.entity;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Version;
+
+@Entity
+public class Product {
+
+    @Id
+    private String productNum;
+
+    private String name;
+
+    private Double price;
+
+    @Version
+    private long versionNum;
+
+    public Product() {
+    }
+
+    public Product(final String productNum, final String name, final Double price) {
+        this.productNum = productNum;
+        this.name = name;
+        this.price = price;
+    }
+
+    public static Product of(final String name, final Double price, final String productNum) {
+        return new Product(productNum, name, price);
+    }
+
+    public String getProductNum() {
+        return productNum;
+    }
+
+    public void setProductNum(final String productNum) {
+        this.productNum = productNum;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    public Double getPrice() {
+        return price;
+    }
+
+    public void setPrice(final Double price) {
+        this.price = price;
+    }
+
+    public long getVersionNum() {
+        return versionNum;
+    }
+
+    public void setVersionNum(final long versionNum) {
+        this.versionNum = versionNum;
+    }
+
+    @Override
+    public String toString() {
+        return "Product[" + productNum + ", " + name + ", " + price + ", v" + versionNum + "]";
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/entity/Task.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/entity/Task.java
new file mode 100644
index 0000000000..500ee1a21a
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/entity/Task.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test.entity;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+
+@Entity
+public class Task {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    private String title;
+
+    private String description;
+
+    private int priority;
+
+    private boolean completed;
+
+    public Task() {
+    }
+
+    public Task(final String title, final String description, final int priority, final boolean completed) {
+        this.title = title;
+        this.description = description;
+        this.priority = priority;
+        this.completed = completed;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(final Long id) {
+        this.id = id;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(final String title) {
+        this.title = title;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(final String description) {
+        this.description = description;
+    }
+
+    public int getPriority() {
+        return priority;
+    }
+
+    public void setPriority(final int priority) {
+        this.priority = priority;
+    }
+
+    public boolean isCompleted() {
+        return completed;
+    }
+
+    public void setCompleted(final boolean completed) {
+        this.completed = completed;
+    }
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/NamedCDIPersonRepository.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/NamedCDIPersonRepository.java
new file mode 100644
index 0000000000..4c1995caba
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/NamedCDIPersonRepository.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test.repo;
+
+import jakarta.data.repository.CrudRepository;
+import jakarta.data.repository.Repository;
+import org.apache.openejb.data.test.entity.Person;
+
+/**
+ * Repository with dataStore matching a @Named CDI-produced EntityManagerFactory.
+ * Exercises the bean name matching path in lookupViaCDI.
+ */
+@Repository(dataStore = "my-data-store")
+public interface NamedCDIPersonRepository extends CrudRepository<Person, Long> {
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/NamedDataStorePersonRepository.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/NamedDataStorePersonRepository.java
new file mode 100644
index 0000000000..e10837f94c
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/NamedDataStorePersonRepository.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test.repo;
+
+import jakarta.data.repository.CrudRepository;
+import jakarta.data.repository.Repository;
+import org.apache.openejb.data.test.entity.Person;
+
+/**
+ * Repository with an explicit dataStore pointing to a named persistence unit.
+ * Exercises the resolveJndiName path with a specific dataStore name.
+ */
+@Repository(dataStore = "person-unit")
+public interface NamedDataStorePersonRepository extends CrudRepository<Person, Long> {
+
+    boolean existsById(Long id);
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/PersonCustomRepository.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/PersonCustomRepository.java
new file mode 100644
index 0000000000..78bf8bc5aa
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/PersonCustomRepository.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test.repo;
+
+import jakarta.data.repository.By;
+import jakarta.data.repository.DataRepository;
+import jakarta.data.repository.Find;
+import jakarta.data.repository.Param;
+import jakarta.data.repository.Query;
+import jakarta.data.repository.Repository;
+import org.apache.openejb.data.test.entity.Person;
+
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public interface PersonCustomRepository extends DataRepository<Person, Long> {
+
+    // Method-name finder
+    List<Person> findByName(String name);
+
+    // Method-name finder with multiple conditions
+    List<Person> findByNameAndAge(String name, int age);
+
+    // Method-name finder with ordering
+    List<Person> findByAgeGreaterThanOrderByNameAsc(int age);
+
+    // @Query annotation
+    @Query("SELECT p FROM Person p WHERE p.name = ?1")
+    List<Person> queryByName(String name);
+
+    // @Query with named parameter
+    @Query("SELECT p FROM Person p WHERE p.age > :minAge")
+    List<Person> queryByMinAge(@Param("minAge") int minAge);
+
+    // @Find with @By
+    @Find
+    Optional<Person> findByIdAndName(@By("id") Long id, @By("name") String name);
+
+    // @Find returning list
+    @Find
+    List<Person> findAllByAge(@By("age") int age);
+
+    // Count by method name
+    long countByName(String name);
+
+    // Exists by method name
+    boolean existsByEmail(String email);
+
+    // Delete by method name
+    int deleteByName(String name);
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/PersonExtendedRepository.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/PersonExtendedRepository.java
new file mode 100644
index 0000000000..2fbac0dc58
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/PersonExtendedRepository.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test.repo;
+
+import jakarta.data.repository.By;
+import jakarta.data.repository.CrudRepository;
+import jakarta.data.repository.Delete;
+import jakarta.data.repository.Find;
+import jakarta.data.repository.Insert;
+import jakarta.data.repository.Query;
+import jakarta.data.repository.Repository;
+import jakarta.data.repository.Save;
+import jakarta.data.repository.Update;
+import org.apache.openejb.data.test.entity.Person;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+@Repository
+public interface PersonExtendedRepository extends CrudRepository<Person, Long> {
+
+    // Stream return type via method-name finder
+    Stream<Person> findByAgeGreaterThan(int age);
+
+    // Optional return type via method-name finder
+    Optional<Person> findByEmail(String email);
+
+    // Single entity return via method-name finder
+    Person findByName(String name);
+
+    // Method name finder with LessThanEqual
+    List<Person> findByAgeLessThanEqual(int age);
+
+    // Method name finder with Contains
+    List<Person> findByNameContains(String part);
+
+    // Method name delete
+    int deleteByName(String name);
+
+    // @Query returning count
+    @Query("SELECT COUNT(p) FROM Person p WHERE p.age > ?1")
+    long countOlderThan(int age);
+
+    // @Query returning boolean-like (exists check)
+    @Query("SELECT p FROM Person p WHERE p.email = ?1")
+    Optional<Person> findOneByEmail(String email);
+
+    // @Find with @By returning Optional
+    @Find
+    Optional<Person> lookupByIdAndName(@By("id") Long id, @By("name") String name);
+
+    // Custom @Insert annotated method
+    @Insert
+    Person customInsert(Person person);
+
+    // Custom @Update annotated method
+    @Update
+    Person customUpdate(Person person);
+
+    // Custom @Save annotated method
+    @Save
+    Person customSave(Person person);
+
+    // Custom @Delete annotated method
+    @Delete
+    void customDelete(Person person);
+
+    // @Delete with no args: delete all entities
+    @Delete
+    void deleteAllEntities();
+
+    // deleteAll with no args: bulk delete all entities (exercises handleDeleteAll else branch)
+    void deleteAll();
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/PersonRepository.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/PersonRepository.java
new file mode 100644
index 0000000000..84294b140f
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/PersonRepository.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test.repo;
+
+import jakarta.data.repository.CrudRepository;
+import jakarta.data.repository.Repository;
+import jakarta.data.repository.Save;
+import org.apache.openejb.data.test.entity.Person;
+
+@Repository
+public interface PersonRepository extends CrudRepository<Person, Long> {
+
+    boolean existsById(Long id);
+
+    long count();
+
+    @Save
+    Person[] save(Person[] people);
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/ProductCatalog.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/ProductCatalog.java
new file mode 100644
index 0000000000..14b2b0d38a
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/ProductCatalog.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test.repo;
+
+import jakarta.data.repository.By;
+import jakarta.data.repository.DataRepository;
+import jakarta.data.repository.Delete;
+import jakarta.data.repository.Find;
+import jakarta.data.repository.Insert;
+import jakarta.data.repository.Param;
+import jakarta.data.repository.Query;
+import jakarta.data.repository.Repository;
+import jakarta.data.repository.Save;
+import jakarta.data.repository.Update;
+import org.apache.openejb.data.test.entity.Product;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+@Repository
+public interface ProductCatalog extends DataRepository<Product, String> {
+
+    @Insert
+    Product add(Product product);
+
+    @Insert
+    Product[] addMultiple(Product... products);
+
+    @Find
+    Optional<Product> get(@By("productNum") String productNum);
+
+    @Update
+    Product modify(Product product);
+
+    @Update
+    Product[] modifyMultiple(Product... products);
+
+    @Delete
+    void remove(Product product);
+
+    @Delete
+    void removeMultiple(Product... products);
+
+    @Save
+    void save(Product product);
+
+    @Delete
+    void deleteById(@By("productNum") String productNum);
+
+    long deleteByProductNumLike(String pattern);
+
+    long countByPriceGreaterThanEqual(Double price);
+
+    List<Product> findByNameLike(String name);
+
+    Stream<Product> findByPriceNotNullAndPriceLessThanEqual(double maxPrice);
+
+    List<Product> findByPriceNull();
+
+    List<Product> findByProductNumLike(String pattern);
+
+    @Query("WHERE LENGTH(name) = ?1 AND price < ?2 ORDER BY name")
+    List<Product> findByNameLengthAndPriceBelow(int nameLength, double maxPrice);
+
+    @Query("FROM Product WHERE (:rate * price <= :max AND :rate * price >= :min) ORDER BY name")
+    Stream<Product> withTaxBetween(@Param("min") double min, @Param("max") double max, @Param("rate") double rate);
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/TaskRepository.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/TaskRepository.java
new file mode 100644
index 0000000000..0c40859295
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/TaskRepository.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test.repo;
+
+import jakarta.data.Limit;
+import jakarta.data.Order;
+import jakarta.data.Sort;
+import jakarta.data.page.Page;
+import jakarta.data.page.PageRequest;
+import jakarta.data.repository.CrudRepository;
+import jakarta.data.repository.OrderBy;
+import jakarta.data.repository.Repository;
+import org.apache.openejb.data.test.entity.Task;
+
+import java.util.List;
+
+@Repository
+public interface TaskRepository extends CrudRepository<Task, Long> {
+
+    // StartsWith operator
+    List<Task> findByTitleStartsWith(String prefix);
+
+    // EndsWith operator
+    List<Task> findByTitleEndsWith(String suffix);
+
+    // Between operator
+    List<Task> findByPriorityBetween(int low, int high);
+
+    // In operator
+    List<Task> findByPriorityIn(List<Integer> priorities);
+
+    // Or connector
+    List<Task> findByCompletedTrueOrPriorityGreaterThan(int priority);
+
+    // True operator
+    List<Task> findByCompletedTrue();
+
+    // False operator
+    List<Task> findByCompletedFalse();
+
+    // Not operator
+    List<Task> findByTitleNot(String title);
+
+    // Combined And + Or
+    List<Task> findByTitleStartsWithAndCompletedFalse(String prefix);
+
+    // OrderBy with descending
+    @OrderBy(value = "priority", descending = true)
+    List<Task> findByCompletedFalseOrderByPriorityDesc();
+
+    // Limit parameter support
+    List<Task> findByCompletedFalseOrderByPriorityAsc(Limit limit);
+
+    // Sort parameter support
+    List<Task> findByCompletedFalse(Sort<Task> sort);
+
+    // Order parameter support
+    List<Task> findByCompletedFalse(Order<Task> order);
+
+    // PageRequest parameter support returning Page
+    Page<Task> findByCompletedFalse(PageRequest pageRequest, Order<Task> order);
+
+    // Multiple order clauses in method name
+    List<Task> findByCompletedFalseOrderByPriorityDescTitleAsc();
+}
diff --git a/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/ValidatedPersonRepository.java b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/ValidatedPersonRepository.java
new file mode 100644
index 0000000000..e7f18ada3a
--- /dev/null
+++ b/container/openejb-jakarta-data/src/test/java/org/apache/openejb/data/test/repo/ValidatedPersonRepository.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.data.test.repo;
+
+import jakarta.data.repository.DataRepository;
+import jakarta.data.repository.Repository;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.Size;
+import org.apache.openejb.data.test.entity.Person;
+
+import java.util.List;
+
+@Repository
+public interface ValidatedPersonRepository extends DataRepository<Person, Long> {
+
+    List<Person> findByName(@NotNull @Size(min = 1, max = 100) String name);
+
+    List<Person> findByAge(@Positive int age);
+
+    List<Person> findByNameAndAge(
+            @NotNull @Size(min = 1) String name,
+            @Positive int age);
+}
diff --git a/container/pom.xml b/container/pom.xml
index 7acde5d577..8fff118ba4 100644
--- a/container/pom.xml
+++ b/container/pom.xml
@@ -37,6 +37,7 @@
     <module>openejb-jee</module>
     <module>openejb-jee-accessors</module>
     <module>openejb-jpa-integration</module>
+    <module>openejb-jakarta-data</module>
     <module>openejb-junit</module>
     <module>openejb-junit5</module>
     <module>openejb-junit5-backward</module>
diff --git a/examples/jakarta-data-repository/README.adoc b/examples/jakarta-data-repository/README.adoc
new file mode 100644
index 0000000000..e5f7b0ec94
--- /dev/null
+++ b/examples/jakarta-data-repository/README.adoc
@@ -0,0 +1,235 @@
+= Jakarta Data Repository
+:index-group: JPA
+:jbake-type: page
+:jbake-status: published
+
+This example shows how to use Jakarta Data 1.0 repositories with TomEE. Instead of injecting an `EntityManager` and writing queries manually, you define a repository interface annotated with `@Repository` and let the container provide the implementation. The repository is injected into your CDI beans via `@Inject` just like any other dependency.
+
+Jakarta Data supports several ways to define queries:
+
+ * *Built-in CRUD* -- extending `CrudRepository` gives you `insert`, `findById`, `update`, `delete`, and more out of the box.
+ * *Method-name queries* -- methods like `findByDirector` are parsed automatically and translated into JPA queries.
+ * *`@Find` annotation* -- combined with `@By` and `@OrderBy` for annotation-driven finders with sorting.
+ * *`@Query` annotation* -- for custom JPQL when you need full control.
+
+== Creating the JPA Entity
+
+The entity is a simple pojo annotated with `@Entity`. We create one called `Movie` to hold movie records.
+
+[source,java]
+----
+@Entity
+public class Movie {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    private String director;
+    private String title;
+    private int year;
+
+    public Movie() {
+    }
+
+    public Movie(final String director, final String title, final int year) {
+        this.director = director;
+        this.title = title;
+        this.year = year;
+    }
+
+    // getters and setters ...
+}
+----
+
+== Defining the Repository
+
+Instead of writing a `@Stateful` or `@Stateless` bean with an `EntityManager`, we define a repository interface. The `@Repository` annotation tells the Jakarta Data provider to generate the implementation automatically.
+
+[source,java]
+----
+@Repository
+public interface MovieRepository extends CrudRepository<Movie, Long> {
+
+    // Method-name query: find all movies by a given director
+    List<Movie> findByDirector(String director);
+
+    // @Find + @OrderBy: find movies by year, ordered by title ascending
+    @Find
+    @OrderBy("title")
+    List<Movie> findByYear(@By("year") int year);
+
+    // @Query with JPQL: find movies by director within a year range
+    @Query("SELECT m FROM Movie m WHERE m.director = ?1 AND m.year BETWEEN ?2 AND ?3 ORDER BY m.year")
+    List<Movie> findByDirectorAndYearRange(String director, int startYear, int endYear);
+
+    // Method-name query: count movies by director
+    long countByDirector(String director);
+}
+----
+
+By extending `CrudRepository<Movie, Long>`, the repository inherits standard operations like `insert`, `findById`, `save`, `delete`, and `deleteById`. The custom methods above show three different query styles:
+
+ * `findByDirector` -- the method name is parsed: `findBy` + `Director` means "filter where director equals the parameter".
+ * `findByYear` -- uses `@Find` with `@By("year")` to match the parameter to the entity field, and `@OrderBy("title")` to sort results.
+ * `findByDirectorAndYearRange` -- uses `@Query` with JPQL for a more complex query involving `BETWEEN`.
+ * `countByDirector` -- method name parsing recognizes the `countBy` prefix.
+
+== Using the Repository in a CDI Bean
+
+The repository is a CDI bean and can be injected into any managed component. No `EntityManager` boilerplate is needed.
+
+[source,java]
+----
+@ApplicationScoped
+public class MovieService {
+
+    @Inject
+    private MovieRepository movieRepository;
+
+    public Movie addMovie(final String director, final String title, final int year) {
+        return movieRepository.insert(new Movie(director, title, year));
+    }
+
+    public Optional<Movie> findById(final long id) {
+        return movieRepository.findById(id);
+    }
+
+    public List<Movie> findByDirector(final String director) {
+        return movieRepository.findByDirector(director);
+    }
+
+    public long countByDirector(final String director) {
+        return movieRepository.countByDirector(director);
+    }
+
+    public void deleteMovie(final Movie movie) {
+        movieRepository.delete(movie);
+    }
+}
+----
+
+== Configuring JPA
+
+The `persistence.xml` configures the JPA persistence unit. The Jakarta Data repository uses this persistence unit to access the database.
+
+[source,xml]
+----
+<persistence version="3.0"
+             xmlns="https://jakarta.ee/xml/ns/persistence">
+  <persistence-unit name="movie-unit">
+    <jta-data-source>movieDatabase</jta-data-source>
+    <non-jta-data-source>movieDatabaseUnmanaged</non-jta-data-source>
+    <class>org.superbiz.data.Movie</class>
+    <properties>
+      <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema(ForeignKeys=true)"/>
+    </properties>
+  </persistence-unit>
+</persistence>
+----
+
+== MovieRepositoryTest
+
+Testing is straightforward with TomEE's `ApplicationComposer`. The repository and service are injected via CDI, and each test uses a `UserTransaction` to keep tests isolated via rollback.
+
+[source,java]
+----
+@RunWith(ApplicationComposer.class)
+public class MovieRepositoryTest {
+
+    @Inject
+    private MovieRepository movieRepository;
+
+    @Inject
+    private MovieService movieService;
+
+    @Resource
+    private UserTransaction utx;
+
+    @Module
+    @Classes(cdi = true, value = {MovieRepository.class, MovieService.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("movie-unit");
+        unit.setJtaDataSource("movieDatabase");
+        unit.setNonJtaDataSource("movieDatabaseUnmanaged");
+        unit.getClazz().add(Movie.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("movieDatabase", "new://Resource?type=DataSource");
+        p.put("movieDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("movieDatabase.JdbcUrl", "jdbc:hsqldb:mem:moviedb-data");
+        return p;
+    }
+
+    @Test
+    public void testInsertAndFindById() throws Exception {
+        utx.begin();
+        try {
+            final Movie movie = movieRepository.insert(new Movie("Quentin Tarantino", "Reservoir Dogs", 1992));
+            assertNotNull(movie.getId());
+
+            final Optional<Movie> found = movieRepository.findById(movie.getId());
+            assertTrue(found.isPresent());
+            assertEquals("Reservoir Dogs", found.get().getTitle());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testFindByDirector() throws Exception {
+        utx.begin();
+        try {
+            movieRepository.insert(new Movie("Joel Coen", "Fargo", 1996));
+            movieRepository.insert(new Movie("Joel Coen", "The Big Lebowski", 1998));
+            movieRepository.insert(new Movie("Quentin Tarantino", "Pulp Fiction", 1994));
+
+            final List<Movie> coenMovies = movieRepository.findByDirector("Joel Coen");
+            assertEquals(2, coenMovies.size());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testQueryAnnotation() throws Exception {
+        utx.begin();
+        try {
+            movieRepository.insert(new Movie("Joel Coen", "Blood Simple", 1984));
+            movieRepository.insert(new Movie("Joel Coen", "Fargo", 1996));
+            movieRepository.insert(new Movie("Joel Coen", "The Big Lebowski", 1998));
+            movieRepository.insert(new Movie("Joel Coen", "No Country for Old Men", 2007));
+
+            final List<Movie> nineties = movieRepository.findByDirectorAndYearRange("Joel Coen", 1990, 2000);
+            assertEquals(2, nineties.size());
+        } finally {
+            utx.rollback();
+        }
+    }
+}
+----
+
+= Running
+
+Running the example produces output similar to the following.
+
+[source,console]
+----
+-------------------------------------------------------
+ T E S T S
+-------------------------------------------------------
+Running org.superbiz.data.MovieRepositoryTest
+INFO - Discovered Jakarta Data repository: org.superbiz.data.MovieRepository
+INFO - Registering Jakarta Data repository bean: org.superbiz.data.MovieRepository
+Tests run: 9, Failures: 0, Errors: 0, Skipped: 0
+----
diff --git a/examples/jakarta-data-repository/pom.xml b/examples/jakarta-data-repository/pom.xml
new file mode 100644
index 0000000000..984525b9ab
--- /dev/null
+++ b/examples/jakarta-data-repository/pom.xml
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.superbiz</groupId>
+  <artifactId>jakarta-data-repository</artifactId>
+  <packaging>jar</packaging>
+  <version>11.0.0-SNAPSHOT</version>
+  <name>TomEE :: Examples :: Jakarta Data Repository</name>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <tomee.version>11.0.0-SNAPSHOT</tomee.version>
+    <surefire.argline />
+  </properties>
+
+  <build>
+    <defaultGoal>install</defaultGoal>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.14.1</version>
+        <configuration>
+          <source>17</source>
+          <target>17</target>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <version>3.5.3</version>
+        <configuration>
+          <argLine>${surefire.argline}</argLine>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+  <repositories>
+    <repository>
+      <id>apache-m2-snapshot</id>
+      <name>Apache Snapshot Repository</name>
+      <url>https://repository.apache.org/content/groups/snapshots</url>
+    </repository>
+  </repositories>
+
+  <dependencies>
+    <!-- Jakarta EE API (includes Jakarta Data, JPA, CDI, etc.) -->
+    <dependency>
+      <groupId>org.apache.tomee</groupId>
+      <artifactId>jakartaee-api</artifactId>
+      <version>${tomee.version}</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <!-- FIXME: needs to be shaded in jakartaee-api -->
+    <!-- Jakarta Data API (not yet in jakartaee-api shaded jar at compile time) -->
+    <dependency>
+      <groupId>jakarta.data</groupId>
+      <artifactId>jakarta.data-api</artifactId>
+      <version>1.0.1</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <!-- Test dependencies -->
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.13.2</version>
+      <scope>test</scope>
+    </dependency>
+
+    <!-- OpenEJB container for running tests -->
+    <dependency>
+      <groupId>org.apache.tomee</groupId>
+      <artifactId>openejb-core</artifactId>
+      <version>${tomee.version}</version>
+      <scope>test</scope>
+    </dependency>
+
+    <!-- TomEE Jakarta Data provider -->
+    <dependency>
+      <groupId>org.apache.tomee</groupId>
+      <artifactId>openejb-jakarta-data</artifactId>
+      <version>${tomee.version}</version>
+      <scope>test</scope>
+    </dependency>
+
+    <!-- OpenEJB JUnit integration -->
+    <dependency>
+      <groupId>org.apache.tomee</groupId>
+      <artifactId>openejb-junit</artifactId>
+      <version>${tomee.version}</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <profiles>
+    <profile>
+      <id>java17plus</id>
+      <activation>
+        <jdk>[17,</jdk>
+      </activation>
+      <properties>
+        <surefire.argline>--add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/jdk.internal.loader=ALL-UNNAMED -Djdk.attach.allowAttachSelf</surefire.argline>
+      </properties>
+    </profile>
+  </profiles>
+</project>
diff --git a/examples/jakarta-data-repository/src/main/java/org/superbiz/data/Movie.java b/examples/jakarta-data-repository/src/main/java/org/superbiz/data/Movie.java
new file mode 100644
index 0000000000..c7124890a9
--- /dev/null
+++ b/examples/jakarta-data-repository/src/main/java/org/superbiz/data/Movie.java
@@ -0,0 +1,75 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.superbiz.data;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+
+@Entity
+public class Movie {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    private String director;
+    private String title;
+    private int year;
+
+    public Movie() {
+    }
+
+    public Movie(final String director, final String title, final int year) {
+        this.director = director;
+        this.title = title;
+        this.year = year;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(final Long id) {
+        this.id = id;
+    }
+
+    public String getDirector() {
+        return director;
+    }
+
+    public void setDirector(final String director) {
+        this.director = director;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(final String title) {
+        this.title = title;
+    }
+
+    public int getYear() {
+        return year;
+    }
+
+    public void setYear(final int year) {
+        this.year = year;
+    }
+}
diff --git a/examples/jakarta-data-repository/src/main/java/org/superbiz/data/MovieRepository.java b/examples/jakarta-data-repository/src/main/java/org/superbiz/data/MovieRepository.java
new file mode 100644
index 0000000000..88b36a0e23
--- /dev/null
+++ b/examples/jakarta-data-repository/src/main/java/org/superbiz/data/MovieRepository.java
@@ -0,0 +1,61 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.superbiz.data;
+
+import jakarta.data.repository.By;
+import jakarta.data.repository.CrudRepository;
+import jakarta.data.repository.Find;
+import jakarta.data.repository.OrderBy;
+import jakarta.data.repository.Query;
+import jakarta.data.repository.Repository;
+
+import java.util.List;
+
+/**
+ * A Jakarta Data repository for Movie entities.
+ *
+ * <p>This interface extends {@link CrudRepository} which provides standard
+ * CRUD operations (insert, findById, update, delete, etc.) out of the box.
+ *
+ * <p>Custom query methods are declared using:
+ * <ul>
+ *   <li><b>Method-name convention</b>: {@code findByDirector} is parsed and
+ *       translated to a query filtering by the {@code director} field.</li>
+ *   <li><b>{@code @Find} annotation</b>: {@code findByYear} uses the {@code @Find}
+ *       annotation with {@code @OrderBy} to add sorting.</li>
+ *   <li><b>{@code @Query} annotation</b>: {@code findByDirectorAndYearRange} uses
+ *       JPQL for more complex queries.</li>
+ * </ul>
+ */
+@Repository
+public interface MovieRepository extends CrudRepository<Movie, Long> {
+
+    // Method-name query: find all movies by a given director
+    List<Movie> findByDirector(String director);
+
+    // @Find + @OrderBy: find movies by year, ordered by title ascending
+    @Find
+    @OrderBy("title")
+    List<Movie> findByYear(@By("year") int year);
+
+    // @Query with JPQL: find movies by director within a year range
+    @Query("SELECT m FROM Movie m WHERE m.director = ?1 AND m.year BETWEEN ?2 AND ?3 ORDER BY m.year")
+    List<Movie> findByDirectorAndYearRange(String director, int startYear, int endYear);
+
+    // Method-name query: count movies by director
+    long countByDirector(String director);
+}
diff --git a/examples/jakarta-data-repository/src/main/java/org/superbiz/data/MovieService.java b/examples/jakarta-data-repository/src/main/java/org/superbiz/data/MovieService.java
new file mode 100644
index 0000000000..ec280e4fab
--- /dev/null
+++ b/examples/jakarta-data-repository/src/main/java/org/superbiz/data/MovieService.java
@@ -0,0 +1,64 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.superbiz.data;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Application service demonstrating Jakarta Data repository injection.
+ *
+ * <p>The {@link MovieRepository} is injected via CDI. All data access
+ * methods are backed by the repository pattern — no manual
+ * {@code EntityManager} usage needed.
+ */
+@ApplicationScoped
+public class MovieService {
+
+    @Inject
+    private MovieRepository movieRepository;
+
+    public Movie addMovie(final String director, final String title, final int year) {
+        return movieRepository.insert(new Movie(director, title, year));
+    }
+
+    public Optional<Movie> findById(final long id) {
+        return movieRepository.findById(id);
+    }
+
+    public List<Movie> findByDirector(final String director) {
+        return movieRepository.findByDirector(director);
+    }
+
+    public List<Movie> findByYear(final int year) {
+        return movieRepository.findByYear(year);
+    }
+
+    public List<Movie> findByDirectorAndYearRange(final String director, final int startYear, final int endYear) {
+        return movieRepository.findByDirectorAndYearRange(director, startYear, endYear);
+    }
+
+    public long countByDirector(final String director) {
+        return movieRepository.countByDirector(director);
+    }
+
+    public void deleteMovie(final Movie movie) {
+        movieRepository.delete(movie);
+    }
+}
diff --git a/examples/jakarta-data-repository/src/main/resources/META-INF/persistence.xml b/examples/jakarta-data-repository/src/main/resources/META-INF/persistence.xml
new file mode 100644
index 0000000000..32a799fb17
--- /dev/null
+++ b/examples/jakarta-data-repository/src/main/resources/META-INF/persistence.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You 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.
+-->
+<persistence version="3.0"
+             xmlns="https://jakarta.ee/xml/ns/persistence"
+             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd">
+  <persistence-unit name="movie-unit">
+    <jta-data-source>movieDatabase</jta-data-source>
+    <non-jta-data-source>movieDatabaseUnmanaged</non-jta-data-source>
+    <class>org.superbiz.data.Movie</class>
+    <properties>
+      <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema(ForeignKeys=true)"/>
+    </properties>
+  </persistence-unit>
+</persistence>
diff --git a/examples/jakarta-data-repository/src/test/java/org/superbiz/data/MovieRepositoryTest.java b/examples/jakarta-data-repository/src/test/java/org/superbiz/data/MovieRepositoryTest.java
new file mode 100644
index 0000000000..71eff190b4
--- /dev/null
+++ b/examples/jakarta-data-repository/src/test/java/org/superbiz/data/MovieRepositoryTest.java
@@ -0,0 +1,213 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.superbiz.data;
+
+import jakarta.annotation.Resource;
+import jakarta.inject.Inject;
+import jakarta.transaction.UserTransaction;
+import org.apache.openejb.jee.EjbJar;
+import org.apache.openejb.jee.jpa.unit.PersistenceUnit;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Classes;
+import org.apache.openejb.testing.Configuration;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for the Jakarta Data MovieRepository.
+ *
+ * <p>Uses ApplicationComposer to bootstrap an embedded OpenEJB container
+ * with CDI, JPA, and the Jakarta Data extension. Each test runs within
+ * a JTA transaction that is rolled back afterwards to keep tests isolated.
+ */
+@RunWith(ApplicationComposer.class)
+public class MovieRepositoryTest {
+
+    @Inject
+    private MovieRepository movieRepository;
+
+    @Inject
+    private MovieService movieService;
+
+    @Resource
+    private UserTransaction utx;
+
+    @Module
+    @Classes(cdi = true, value = {MovieRepository.class, MovieService.class})
+    public EjbJar beans() {
+        return new EjbJar();
+    }
+
+    @Module
+    public PersistenceUnit persistence() {
+        final PersistenceUnit unit = new PersistenceUnit("movie-unit");
+        unit.setJtaDataSource("movieDatabase");
+        unit.setNonJtaDataSource("movieDatabaseUnmanaged");
+        unit.getClazz().add(Movie.class.getName());
+        unit.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
+        return unit;
+    }
+
+    @Configuration
+    public Properties config() {
+        final Properties p = new Properties();
+        p.put("movieDatabase", "new://Resource?type=DataSource");
+        p.put("movieDatabase.JdbcDriver", "org.hsqldb.jdbcDriver");
+        p.put("movieDatabase.JdbcUrl", "jdbc:hsqldb:mem:moviedb-data");
+        return p;
+    }
+
+    @Test
+    public void testRepositoryInjection() {
+        assertNotNull("MovieRepository should be injected via CDI", movieRepository);
+    }
+
+    @Test
+    public void testServiceInjection() {
+        assertNotNull("MovieService should be injected via CDI", movieService);
+    }
+
+    @Test
+    public void testInsertAndFindById() throws Exception {
+        utx.begin();
+        try {
+            final Movie movie = movieRepository.insert(new Movie("Quentin Tarantino", "Reservoir Dogs", 1992));
+            assertNotNull("Inserted movie should have an id", movie.getId());
+
+            final Optional<Movie> found = movieRepository.findById(movie.getId());
+            assertTrue("Should find the movie by id", found.isPresent());
+            assertEquals("Reservoir Dogs", found.get().getTitle());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testFindByDirector() throws Exception {
+        utx.begin();
+        try {
+            movieRepository.insert(new Movie("Joel Coen", "Fargo", 1996));
+            movieRepository.insert(new Movie("Joel Coen", "The Big Lebowski", 1998));
+            movieRepository.insert(new Movie("Quentin Tarantino", "Pulp Fiction", 1994));
+
+            final List<Movie> coenMovies = movieRepository.findByDirector("Joel Coen");
+            assertEquals("Joel Coen should have 2 movies", 2, coenMovies.size());
+
+            final List<Movie> tarantinoMovies = movieRepository.findByDirector("Quentin Tarantino");
+            assertEquals("Tarantino should have 1 movie", 1, tarantinoMovies.size());
+            assertEquals("Pulp Fiction", tarantinoMovies.get(0).getTitle());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testFindByYearWithOrderBy() throws Exception {
+        utx.begin();
+        try {
+            movieRepository.insert(new Movie("Joel Coen", "Fargo", 1996));
+            movieRepository.insert(new Movie("Joel Coen", "Barton Fink", 1991));
+            movieRepository.insert(new Movie("Quentin Tarantino", "Pulp Fiction", 1994));
+
+            // findByYear orders by title ascending
+            final List<Movie> movies1991 = movieRepository.findByYear(1991);
+            assertEquals(1, movies1991.size());
+            assertEquals("Barton Fink", movies1991.get(0).getTitle());
+
+            final List<Movie> movies1996 = movieRepository.findByYear(1996);
+            assertEquals(1, movies1996.size());
+            assertEquals("Fargo", movies1996.get(0).getTitle());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testQueryAnnotation() throws Exception {
+        utx.begin();
+        try {
+            movieRepository.insert(new Movie("Joel Coen", "Blood Simple", 1984));
+            movieRepository.insert(new Movie("Joel Coen", "Fargo", 1996));
+            movieRepository.insert(new Movie("Joel Coen", "The Big Lebowski", 1998));
+            movieRepository.insert(new Movie("Joel Coen", "No Country for Old Men", 2007));
+
+            final List<Movie> nineties = movieRepository.findByDirectorAndYearRange("Joel Coen", 1990, 2000);
+            assertEquals("Joel Coen had 2 movies in the 90s", 2, nineties.size());
+            assertEquals("Fargo", nineties.get(0).getTitle());
+            assertEquals("The Big Lebowski", nineties.get(1).getTitle());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testCountByDirector() throws Exception {
+        utx.begin();
+        try {
+            movieRepository.insert(new Movie("Joel Coen", "Fargo", 1996));
+            movieRepository.insert(new Movie("Joel Coen", "The Big Lebowski", 1998));
+            movieRepository.insert(new Movie("Quentin Tarantino", "Pulp Fiction", 1994));
+
+            assertEquals(2, movieRepository.countByDirector("Joel Coen"));
+            assertEquals(1, movieRepository.countByDirector("Quentin Tarantino"));
+            assertEquals(0, movieRepository.countByDirector("Steven Spielberg"));
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testDeleteMovie() throws Exception {
+        utx.begin();
+        try {
+            final Movie movie = movieRepository.insert(new Movie("Joel Coen", "Fargo", 1996));
+            final Long id = movie.getId();
+            assertTrue(movieRepository.findById(id).isPresent());
+
+            movieRepository.delete(movie);
+            assertFalse("Movie should be deleted", movieRepository.findById(id).isPresent());
+        } finally {
+            utx.rollback();
+        }
+    }
+
+    @Test
+    public void testServiceLayer() throws Exception {
+        utx.begin();
+        try {
+            final Movie movie = movieService.addMovie("Joel Coen", "Fargo", 1996);
+            assertNotNull(movie.getId());
+
+            final Optional<Movie> found = movieService.findById(movie.getId());
+            assertTrue(found.isPresent());
+
+            assertEquals(1, movieService.countByDirector("Joel Coen"));
+        } finally {
+            utx.rollback();
+        }
+    }
+}
diff --git a/examples/jpa-and-jakarta-data/README.adoc b/examples/jpa-and-jakarta-data/README.adoc
new file mode 100644
index 0000000000..2732d9601f
--- /dev/null
+++ b/examples/jpa-and-jakarta-data/README.adoc
@@ -0,0 +1,218 @@
+= JPA and Jakarta Data Combined
+:index-group: JPA
+:jbake-type: page
+:jbake-status: published
+
+This example shows how classic JPA (`EntityManager`) and Jakarta Data repositories can coexist side by side, sharing the same persistence unit and database table. Data inserted via one approach is immediately visible to the other within the same transaction.
+
+This is a realistic migration scenario: existing code uses an `EntityManager`-based DAO, while new features are added using the simpler Jakarta Data repository pattern.
+
+== The Entity
+
+A `Movie` entity with a `Genre` enum, shared by both approaches.
+
+[source,java]
+----
+@Entity
+public class Movie {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    private String director;
+    private String title;
+    private int year;
+
+    @Enumerated(EnumType.STRING)
+    private Genre genre;
+
+    public Movie() {
+    }
+
+    public Movie(final String director, final String title, final int year, final Genre genre) {
+        this.director = director;
+        this.title = title;
+        this.year = year;
+        this.genre = genre;
+    }
+
+    // getters and setters ...
+}
+----
+
+== Classic JPA: EntityManager DAO
+
+The traditional approach uses a `@Stateless` EJB with a transaction-scoped `PersistenceContext`. It provides JPQL queries and JPA Criteria API queries.
+
+[source,java]
+----
+@Stateless
+public class MovieDao {
+
+    @PersistenceContext(unitName = "movie-unit")
+    private EntityManager em;
+
+    public void addMovie(final Movie movie) {
+        em.persist(movie);
+    }
+
+    public List<Movie> findAll() {
+        return em.createQuery("SELECT m FROM Movie m ORDER BY m.title", Movie.class)
+            .getResultList();
+    }
+
+    public List<Movie> findByDirectorAndGenre(final String director, final Genre genre) {
+        final CriteriaBuilder cb = em.getCriteriaBuilder();
+        final CriteriaQuery<Movie> cq = cb.createQuery(Movie.class);
+        final Root<Movie> root = cq.from(Movie.class);
+
+        cq.where(
+            cb.and(
+                cb.equal(root.get("director"), director),
+                cb.equal(root.get("genre"), genre)
+            )
+        );
+        cq.orderBy(cb.asc(root.get("year")));
+
+        return em.createQuery(cq).getResultList();
+    }
+
+    // more methods ...
+}
+----
+
+== Jakarta Data: Repository Interface
+
+The modern approach uses a `@Repository` interface extending `CrudRepository`. The container generates the implementation automatically.
+
+[source,java]
+----
+@Repository
+public interface MovieRepository extends CrudRepository<Movie, Long> {
+
+    List<Movie> findByDirector(String director);
+
+    List<Movie> findByGenre(Genre genre);
+
+    @OrderBy("year")
+    List<Movie> findByYearGreaterThanEqual(int year);
+
+    @Query("SELECT m FROM Movie m WHERE m.genre = ?1 AND m.year BETWEEN ?2 AND ?3 ORDER BY m.title")
+    List<Movie> findByGenreAndYearRange(Genre genre, int startYear, int endYear);
+
+    long countByGenre(Genre genre);
+
+    long countByDirector(String director);
+}
+----
+
+== Using Both in a Service
+
+A single CDI bean can inject both the classic DAO and the Jakarta Data repository. This allows gradual migration -- old code stays as is while new features use the repository.
+
+[source,java]
+----
+@ApplicationScoped
+public class MovieService {
+
+    @EJB
+    private MovieDao movieDao;
+
+    @Inject
+    private MovieRepository movieRepository;
+
+    // Classic JPA
+    public void addMovieClassic(final String director, final String title, final int year, final Genre genre) {
+        movieDao.addMovie(new Movie(director, title, year, genre));
+    }
+
+    // Jakarta Data
+    public Movie addMovieData(final String director, final String title, final int year, final Genre genre) {
+        return movieRepository.insert(new Movie(director, title, year, genre));
+    }
+
+    // Query via Jakarta Data, even for JPA-inserted data
+    public long countByDirectorViaRepository(final String director) {
+        return movieRepository.countByDirector(director);
+    }
+
+    // Query via classic JPA, even for Jakarta Data-inserted data
+    public List<Movie> findByGenreViaDao(final Genre genre) {
+        return movieDao.findByGenre(genre);
+    }
+}
+----
+
+== Cross-Approach Visibility
+
+The key point of this example: data inserted through one approach is visible to the other within the same transaction. This works because both share the same persistence unit and JTA transaction.
+
+[source,java]
+----
+@Test
+public void testJpaInsertVisibleViaDataRepository() throws Exception {
+    utx.begin();
+    try {
+        // Insert using classic JPA EntityManager
+        movieDao.addMovie(new Movie("Ridley Scott", "Blade Runner", 1982, Genre.SCI_FI));
+        movieDao.addMovie(new Movie("Ridley Scott", "Alien", 1979, Genre.SCI_FI));
+
+        // Query using Jakarta Data repository -- sees JPA-persisted data
+        final List<Movie> sciFi = movieRepository.findByGenre(Genre.SCI_FI);
+        assertEquals(2, sciFi.size());
+    } finally {
+        utx.rollback();
+    }
+}
+
+@Test
+public void testMixedInsertAndQuery() throws Exception {
+    utx.begin();
+    try {
+        // Insert some via JPA, some via Jakarta Data
+        movieDao.addMovie(new Movie("Denis Villeneuve", "Arrival", 2016, Genre.SCI_FI));
+        movieRepository.insert(new Movie("Denis Villeneuve", "Dune", 2021, Genre.SCI_FI));
+
+        // Both approaches see all data
+        assertEquals(2, movieDao.count());
+        assertEquals(2, movieRepository.countByDirector("Denis Villeneuve"));
+    } finally {
+        utx.rollback();
+    }
+}
+----
+
+== Configuring JPA
+
+The `persistence.xml` configures the shared persistence unit.
+
+[source,xml]
+----
+<persistence version="3.0"
+             xmlns="https://jakarta.ee/xml/ns/persistence">
+  <persistence-unit name="movie-unit">
+    <jta-data-source>movieDatabase</jta-data-source>
+    <non-jta-data-source>movieDatabaseUnmanaged</non-jta-data-source>
+    <class>org.superbiz.combined.Movie</class>
+    <properties>
+      <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema(ForeignKeys=true)"/>
+    </properties>
+  </persistence-unit>
+</persistence>
+----
+
+= Running
+
+Running the example produces output similar to the following.
+
+[source,console]
+----
+-------------------------------------------------------
+ T E S T S
+-------------------------------------------------------
+Running org.superbiz.combined.JpaAndJakartaDataTest
+INFO - Discovered Jakarta Data repository: org.superbiz.combined.MovieRepository
+INFO - Registering Jakarta Data repository bean: org.superbiz.combined.MovieRepository
+Tests run: 10, Failures: 0, Errors: 0, Skipped: 0
+----
diff --git a/examples/jpa-and-jakarta-data/pom.xml b/examples/jpa-and-jakarta-data/pom.xml
new file mode 100644
index 0000000000..bf205998ef
--- /dev/null
+++ b/examples/jpa-and-jakarta-data/pom.xml
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.superbiz</groupId>
+  <artifactId>jpa-and-jakarta-data</artifactId>
+  <packaging>jar</packaging>
+  <version>11.0.0-SNAPSHOT</version>
+  <name>TomEE :: Examples :: JPA and Jakarta Data Combined</name>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <tomee.version>11.0.0-SNAPSHOT</tomee.version>
+    <surefire.argline />
+  </properties>
+
+  <build>
+    <defaultGoal>install</defaultGoal>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.14.1</version>
+        <configuration>
+          <source>17</source>
+          <target>17</target>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <version>3.5.3</version>
+        <configuration>
+          <argLine>${surefire.argline}</argLine>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+  <repositories>
+    <repository>
+      <id>apache-m2-snapshot</id>
+      <name>Apache Snapshot Repository</name>
+      <url>https://repository.apache.org/content/groups/snapshots</url>
+    </repository>
+  </repositories>
+
+  <dependencies>
+    <!-- Jakarta EE API (includes JPA, CDI, EJB, etc.) -->
+    <dependency>
+      <groupId>org.apache.tomee</groupId>
+      <artifactId>jakartaee-api</artifactId>
+      <version>${tomee.version}</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <!-- FIXME: needs to be shaded in jakartaee-api -->
+    <!-- Jakarta Data API (not yet in jakartaee-api shaded jar at compile time) -->
+    <dependency>
+      <groupId>jakarta.data</groupId>
+      <artifactId>jakarta.data-api</artifactId>
+      <version>1.0.1</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <!-- Test dependencies -->
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.13.2</version>
+      <scope>test</scope>
+    </dependency>
+
+    <!-- OpenEJB container for running tests -->
+    <dependency>
+      <groupId>org.apache.tomee</groupId>
+      <artifactId>openejb-core</artifactId>
+      <version>${tomee.version}</version>
+      <scope>test</scope>
+    </dependency>
+
+    <!-- TomEE Jakarta Data provider -->
+    <dependency>
+      <groupId>org.apache.tomee</groupId>
+      <artifactId>openejb-jakarta-data</artifactId>
+      <version>${tomee.version}</version>
+      <scope>test</scope>
+    </dependency>
+
+    <!-- OpenEJB JUnit integration -->
+    <dependency>
+      <groupId>org.apache.tomee</groupId>
+      <artifactId>openejb-junit</artifactId>
+      <version>${tomee.version}</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <profiles>
+    <profile>
+      <id>java17plus</id>
+      <activation>
+        <jdk>[17,</jdk>
+      </activation>
+      <properties>
+        <surefire.argline>--add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/jdk.internal.loader=ALL-UNNAMED -Djdk.attach.allowAttachSelf</surefire.argline>
+      </properties>
+    </profile>
+  </profiles>
+</project>
diff --git a/examples/jpa-and-jakarta-data/src/main/java/org/superbiz/combined/Genre.java b/examples/jpa-and-jakarta-data/src/main/java/org/superbiz/combined/Genre.java
new file mode 100644
index 0000000000..d27b405d6e
--- /dev/null
+++ b/examples/jpa-and-jakarta-data/src/main/java/org/superbiz/combined/Genre.java
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.superbiz.combined;
+
+public enum Genre {
+    DRAMA,
+    COMEDY,
+    THRILLER,
+    SCI_FI,
+    ACTION,
+    HORROR
+}
diff --git a/examples/jpa-and-jakarta-data/src/main/java/org/superbiz/combined/Movie.java b/examples/jpa-and-jakarta-data/src/main/java/org/superbiz/combined/Movie.java
new file mode 100644
index 0000000000..72f0fbec43
--- /dev/null
+++ b/examples/jpa-and-jakarta-data/src/main/java/org/superbiz/combined/Movie.java
@@ -0,0 +1,94 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.superbiz.combined;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+
+/**
+ * A JPA entity shared by both the classic EntityManager-based DAO
+ * and the Jakarta Data repository. Both approaches operate on the
+ * same database table.
+ */
+@Entity
+public class Movie {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    private String director;
+    private String title;
+    private int year;
+
+    @Enumerated(EnumType.STRING)
+    private Genre genre;
+
+    public Movie() {
+    }
+
+    public Movie(final String director, final String title, final int year, final Genre genre) {
+        this.director = director;
+        this.title = title;
+        this.year = year;
+        this.genre = genre;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(final Long id) {
+        this.id = id;
+    }
+
+    public String getDirector() {
+        return director;
+    }
+
+    public void setDirector(final String director) {
+        this.director = director;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(final String title) {
+        this.title = title;
+    }
+
+    public int getYear() {
+        return year;
+    }
+
+    public void setYear(final int year) {
+        this.year = year;
+    }
+
+    public Genre getGenre() {
+        return genre;
+    }
+
+    public void setGenre(final Genre genre) {
+        this.genre = genre;
+    }
+}
diff --git a/examples/jpa-and-jakarta-data/src/main/java/org/superbiz/combined/MovieDao.java b/examples/jpa-and-jakarta-data/src/main/java/org/superbiz/combined/MovieDao.java
new file mode 100644
index 0000000000..9b49bcc4a4
--- /dev/null
+++ b/examples/jpa-and-jakarta-data/src/main/java/org/superbiz/combined/MovieDao.java
@@ -0,0 +1,91 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.superbiz.combined;
+
+import jakarta.ejb.Stateless;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.TypedQuery;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Root;
+
+import java.util.List;
+
+/**
+ * Classic JPA data access object using EntityManager directly.
+ *
+ * <p>This demonstrates the traditional Jakarta EE approach to data access.
+ * Both this DAO and the Jakarta Data {@link MovieRepository} operate
+ * on the same persistence unit and database table, showing that both
+ * approaches coexist seamlessly.
+ */
+@Stateless
+public class MovieDao {
+
+    @PersistenceContext(unitName = "movie-unit")
+    private EntityManager em;
+
+    public void addMovie(final Movie movie) {
+        em.persist(movie);
+    }
+
+    public void deleteMovie(final Movie movie) {
+        em.remove(em.merge(movie));
+    }
+
+    public Movie findById(final long id) {
+        return em.find(Movie.class, id);
+    }
+
+    public List<Movie> findAll() {
+        return em.createQuery("SELECT m FROM Movie m ORDER BY m.title", Movie.class)
+            .getResultList();
+    }
+
+    public List<Movie> findByGenre(final Genre genre) {
+        final TypedQuery<Movie> query =
+            em.createQuery("SELECT m FROM Movie m WHERE m.genre = :genre ORDER BY m.title", Movie.class);
+        query.setParameter("genre", genre);
+        return query.getResultList();
+    }
+
+    /**
+     * Uses the JPA Criteria API to build a dynamic query.
+     * This is something that classic JPA excels at for complex dynamic queries.
+     */
+    public List<Movie> findByDirectorAndGenre(final String director, final Genre genre) {
+        final CriteriaBuilder cb = em.getCriteriaBuilder();
+        final CriteriaQuery<Movie> cq = cb.createQuery(Movie.class);
+        final Root<Movie> root = cq.from(Movie.class);
+
+        cq.where(
+            cb.and(
+                cb.equal(root.get("director"), director),
+                cb.equal(root.get("genre"), genre)
+            )
+        );
+        cq.orderBy(cb.asc(root.get("year")));
+
+        return em.createQuery(cq).getResultList();
+    }
+
+    public long count() {
+        return em.createQuery("SELECT COUNT(m) FROM Movie m", Long.class)
+            .getSingleResult();
+    }
+}
diff --git a/examples/jpa-and-jakarta-data/src/main/java/org/superbiz/combined/MovieRepository.java b/examples/jpa-and-jakarta-data/src/main/java/org/superbiz/combined/MovieRepository.java
new file mode 100644
index 0000000000..f4f7a9101b
--- /dev/null
+++ b/examples/jpa-and-jakarta-data/src/main/java/org/superbiz/combined/MovieRepository.java
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.superbiz.combined;
+
+import jakarta.data.repository.CrudRepository;
+import jakarta.data.repository.OrderBy;
+import jakarta.data.repository.Query;
+import jakarta.data.repository.Repository;
+
+import java.util.List;
+
+/**
+ * Jakarta Data repository for Movie entities.
+ *
+ * <p>This provides a modern, declarative API for data access.
+ * It operates on the same persistence unit and database table
+ * as the classic JPA {@link MovieDao}.
+ */
+@Repository
+public interface MovieRepository extends CrudRepository<Movie, Long> {
+
+    List<Movie> findByDirector(String director);
+
+    List<Movie> findByGenre(Genre genre);
+
+    @OrderBy("year")
+    List<Movie> findByYearGreaterThanEqual(int year);
+
+    @Query("SELECT m FROM Movie m WHERE m.genre = ?1 AND m.year BETWEEN ?2 AND ?3 ORDER BY m.title")
+    List<Movie> findByGenreAndYearRange(Genre genre, int startYear, int endYear);
+
+    long countByGenre(Genre genre);
+
+    long countByDirector(String director);
+}
diff --git a/examples/jpa-and-jakarta-data/src/main/java/org/superbiz/combined/MovieService.java b/examples/jpa-and-jakarta-data/src/main/java/org/superbiz/combined/MovieService.java
new file mode 100644
index 0000000000..74a2074816
--- /dev/null
+++ b/examples/jpa-and-jakarta-data/src/main/java/org/superbiz/combined/MovieService.java
@@ -0,0 +1,106 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.superbiz.combined;
+
+import jakarta.ejb.EJB;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Application service that uses BOTH classic JPA (via {@link MovieDao})
+ * and Jakarta Data (via {@link MovieRepository}) in the same class.
+ *
+ * <p>This demonstrates a realistic migration scenario: existing code
+ * uses the classic JPA DAO, while new features are added using the
+ * simpler Jakarta Data repository. Both share the same persistence
+ * unit and see each other's data within the same transaction.
+ */
+@ApplicationScoped
+public class MovieService {
+
+    @EJB
+    private MovieDao movieDao;
+
+    @Inject
+    private MovieRepository movieRepository;
+
+    // -- Classic JPA operations (via MovieDao) --
+
+    /**
+     * Add a movie using the classic JPA EntityManager approach.
+     */
+    public void addMovieClassic(final String director, final String title, final int year, final Genre genre) {
+        movieDao.addMovie(new Movie(director, title, year, genre));
+    }
+
+    /**
+     * Uses the JPA Criteria API via the classic DAO for a dynamic query.
+     */
+    public List<Movie> findByDirectorAndGenreClassic(final String director, final Genre genre) {
+        return movieDao.findByDirectorAndGenre(director, genre);
+    }
+
+    public List<Movie> findAllClassic() {
+        return movieDao.findAll();
+    }
+
+    public long countClassic() {
+        return movieDao.count();
+    }
+
+    // -- Jakarta Data operations (via MovieRepository) --
+
... 1187 lines suppressed ...