You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/10/22 16:39:31 UTC

(grails-core) branch 9.0.x-h7-improvement-phase3 updated (c98ea96bef -> 5d4fdbebf2)

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

borinquenkid pushed a change to branch 9.0.x-h7-improvement-phase3
in repository https://gitbox.apache.org/repos/asf/grails-core.git


    from c98ea96bef Merge branch 'nb/16569-merge' into nb/16570-merge
     new e7b263b07c test(hibernate7): keep the 9.x refusal of an ownerless many-to-many over the 8.x warning spec
     new 5d4fdbebf2 fix(hibernate7): filter a many-to-many of a multi-tenant entity by the tenant in the native binding

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../domainbinding/jpa/GrailsDomainGenerator.groovy |  19 +-
 .../tests/ManyToManyWithoutOwnerWarningSpec.groovy | 307 ---------------------
 .../jpa/GrailsDomainGeneratorTenantSpec.groovy     |  84 +++++-
 .../resources/classic-oracle/ddl-differential.txt  |  80 +++++-
 .../classic-oracle/generator-differential.txt      |  38 ++-
 5 files changed, 206 insertions(+), 322 deletions(-)
 delete mode 100644 grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ManyToManyWithoutOwnerWarningSpec.groovy


(grails-core) 02/02: fix(hibernate7): filter a many-to-many of a multi-tenant entity by the tenant in the native binding

Posted by bo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch 9.0.x-h7-improvement-phase3
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 5d4fdbebf2b94ade092be99028af1b6d9844b525
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Thu Oct 22 11:21:51 2026 -0500

    fix(hibernate7): filter a many-to-many of a multi-tenant entity by the tenant in the native binding
    
    The native binding gave the tenant filter only to a one-to-many of a
    multi-tenant entity. The classic binder of 8.0.x (#16515) filters every
    collection of a multi-tenant entity, a many-to-many too, and loads a
    collection mapped fetch: 'join' eagerly with a select of its own, because
    Hibernate does not apply the filter to a collection it fetches by join with
    an instance loaded by id. The generator now does the same: the tenant
    condition goes on any to-many whose target is multi-tenant, and a
    join-fetched one becomes an eager @Fetch(SELECT). On a collection with a
    join table Hibernate's annotation binder turns @Filter into the many-to-many
    filter, which is what the classic binder bound.
    
    GrailsDomainGeneratorTenantSpec covers the many-to-many, the one-to-many,
    the eager select and an unfiltered collection, on the generated fields and
    in the metadata Hibernate reads from them. The classic oracle records the
    new fixtures, and the frozen fact of CMTBManyToManyOwner.items now states
    the many-to-many filter the 8.0.x classic binder binds for it.
---
 .../domainbinding/jpa/GrailsDomainGenerator.groovy | 19 +++--
 .../jpa/GrailsDomainGeneratorTenantSpec.groovy     | 84 +++++++++++++++++++++-
 .../resources/classic-oracle/ddl-differential.txt  | 76 ++++++++++++++++++++
 .../classic-oracle/generator-differential.txt      | 34 ++++++++-
 4 files changed, 206 insertions(+), 7 deletions(-)

diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGenerator.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGenerator.groovy
index 6b28281d52..32e65c0223 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGenerator.groovy
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGenerator.groovy
@@ -165,7 +165,6 @@ import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateEmbeddedPro
 import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateEnumProperty
 import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateManyToManyProperty
 import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateManyToOneProperty
-import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateOneToManyProperty
 import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateOneToOneProperty
 import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty
 import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateSimpleIdentityProperty
@@ -2123,8 +2122,18 @@ class GrailsDomainGenerator {
                         null, null, null, null, null)
             }
         }
-        String condition = property instanceof HibernateOneToManyProperty && target.isMultiTenant() ?
-                target.getMultiTenantFilterCondition(defaultColumnNames) : null
+        // a collection of a multi-tenant entity is filtered by the current tenant, whether the association is a one-to-many
+        // or a many-to-many; on a collection with a join table Hibernate applies @Filter to the element rows, which is the
+        // classic binder's many-to-many filter
+        String condition = target.isMultiTenant() ? target.getMultiTenantFilterCondition(defaultColumnNames) : null
+        boolean lazy = property.isLazy()
+        FetchMode fetchMode = FetchMode.JOIN == mapped.fetchMode ? FetchMode.JOIN : FetchMode.SELECT
+        if (condition != null && fetchMode == FetchMode.JOIN) {
+            // Hibernate does not apply the tenant filter to a collection that it fetches by join with an instance loaded
+            // by id, so the collection is loaded eagerly with a select of its own instead
+            fetchMode = FetchMode.SELECT
+            lazy = false
+        }
         if (qualifier != null && table != null && mappedBy == null) {
             table = qualifiedTableName(property, qualifier)
             keys = qualifiedKeys(keys != null ? keys : [key], keys != null ? collectionKeyReferencedColumns(property) : [], qualifier)
@@ -2141,9 +2150,9 @@ class GrailsDomainGenerator {
                 key,
                 element,
                 collectionIndexFacets(property, kind),
-                property.isLazy(),
+                lazy,
                 property.getLazy() == Boolean.TRUE,
-                FetchMode.JOIN == mapped.fetchMode ? FetchMode.JOIN : FetchMode.SELECT,
+                fetchMode,
                 Math.max(property.batchSize, 0),
                 property.cacheUsage,
                 cascadeFacets(property),
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGeneratorTenantSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGeneratorTenantSpec.groovy
index 3b4c8aa4e4..6fbff13105 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGeneratorTenantSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/GrailsDomainGeneratorTenantSpec.groovy
@@ -22,6 +22,11 @@ import jakarta.persistence.Column
 import grails.gorm.MultiTenant
 import grails.gorm.annotation.Entity
 import grails.gorm.tests.HibernateGormDatastoreSpec
+import jakarta.persistence.FetchType
+import jakarta.persistence.ManyToMany
+import jakarta.persistence.OneToMany
+import org.hibernate.annotations.Fetch
+import org.hibernate.annotations.FetchMode
 import org.hibernate.annotations.Filter
 import org.hibernate.annotations.FilterDef
 import org.hibernate.boot.Metadata
@@ -36,6 +41,7 @@ import org.hibernate.mapping.PersistentClass
 
 import org.grails.datastore.mapping.multitenancy.MultiTenancySettings
 import org.grails.datastore.mapping.multitenancy.resolvers.SystemPropertyTenantResolver
+import org.grails.datastore.mapping.model.config.GormProperties
 import org.grails.orm.hibernate.cfg.domainbinding.column.ColumnConfigToColumnBinder
 import org.grails.orm.hibernate.cfg.domainbinding.column.NumericColumnConstraintsBinder
 import org.grails.orm.hibernate.cfg.domainbinding.column.StringColumnConstraintsBinder
@@ -62,7 +68,7 @@ class GrailsDomainGeneratorTenantSpec extends HibernateGormDatastoreSpec {
                 'grails.gorm.multiTenancy.tenantResolver': new SystemPropertyTenantResolver()
         ]
         manager.registerDomainClasses(GenTenantBook, GenTenantNamed, GenTenantRoot, GenTenantLeaf, GenTenantJoinedRoot, GenTenantJoinedLeaf, GenTenantPlain,
-                GenTenantTyped)
+                GenTenantTyped, GenTenantCatalog, GenTenantEntry, GenTenantNote, GenTenantTag)
     }
 
     void cleanup() {
@@ -197,6 +203,38 @@ class GrailsDomainGeneratorTenantSpec extends HibernateGormDatastoreSpec {
         return newGenerator().generate(entity(domainClass), getClass().classLoader)
     }
 
+    void "a collection of a multi-tenant entity carries the tenant filter, and a join fetch of it becomes an eager select"() {
+        given:
+        Map<GrailsHibernatePersistentEntity, Class<?>> classes = generateAll(GenTenantCatalog, GenTenantEntry, GenTenantNote, GenTenantTag)
+        Class<?> catalog = classes[entity(GenTenantCatalog)]
+
+        expect: "the many-to-many and the one-to-many both carry the filter on the generated field"
+        catalog.getDeclaredField('entries').isAnnotationPresent(ManyToMany)
+        catalog.getDeclaredField('entries').getAnnotation(Filter).name() == GormProperties.TENANT_IDENTITY
+        catalog.getDeclaredField('notes').getAnnotation(Filter).name() == GormProperties.TENANT_IDENTITY
+
+        and: "the collection mapped fetch: 'join' is loaded eagerly with a select of its own, because a join would bypass the filter"
+        catalog.getDeclaredField('notes').getAnnotation(OneToMany).fetch() == FetchType.EAGER
+        catalog.getDeclaredField('notes').getAnnotation(Fetch).value() == FetchMode.SELECT
+
+        and: "a collection of an entity that is not multi-tenant has no filter and keeps its join fetch"
+        !catalog.getDeclaredField('tags').isAnnotationPresent(Filter)
+        catalog.getDeclaredField('tags').getAnnotation(Fetch).value() == FetchMode.JOIN
+
+        when: "Hibernate reads the generated classes"
+        Metadata metadata = annotationMetadata(classes.values())
+        org.hibernate.mapping.Collection entries = metadata.getCollectionBinding("${catalog.name}.entries")
+        org.hibernate.mapping.Collection notes = metadata.getCollectionBinding("${catalog.name}.notes")
+
+        then: "the many-to-many filters its element rows, as the classic binder's many-to-many filter did, and the one-to-many its own"
+        entries.manyToManyFilters*.name == ['tenantId']
+        entries.filters.isEmpty()
+        notes.filters*.name == ['tenantId']
+        notes.manyToManyFilters.isEmpty()
+        notes.fetchMode == org.hibernate.FetchMode.SELECT
+        !notes.lazy
+    }
+
     private GrailsHibernatePersistentEntity entity(Class<?> domainClass) {
         return getPersistentEntity(domainClass)
     }
@@ -284,3 +322,47 @@ class GenTenantTyped implements MultiTenant<GenTenantTyped> {
         }
     }
 }
+
+@Entity
+class GenTenantCatalog {
+
+    String name
+    Set<GenTenantEntry> entries
+    Set<GenTenantNote> notes
+    Set<GenTenantTag> tags
+
+    static hasMany = [entries: GenTenantEntry, notes: GenTenantNote, tags: GenTenantTag]
+
+    static mapping = {
+        notes fetch: 'join'
+        tags fetch: 'join'
+    }
+}
+
+@Entity
+class GenTenantEntry implements MultiTenant<GenTenantEntry> {
+
+    String label
+    String tenantId
+    Set<GenTenantCatalog> catalogs
+
+    static hasMany = [catalogs: GenTenantCatalog]
+    static belongsTo = [GenTenantCatalog]
+}
+
+@Entity
+class GenTenantNote implements MultiTenant<GenTenantNote> {
+
+    String label
+    String tenantId
+
+    static belongsTo = [catalog: GenTenantCatalog]
+}
+
+@Entity
+class GenTenantTag {
+
+    String label
+
+    static belongsTo = [catalog: GenTenantCatalog]
+}
diff --git a/grails-data-hibernate7/core/src/test/resources/classic-oracle/ddl-differential.txt b/grails-data-hibernate7/core/src/test/resources/classic-oracle/ddl-differential.txt
index 82c6b14d7f..08917b1734 100644
--- a/grails-data-hibernate7/core/src/test/resources/classic-oracle/ddl-differential.txt
+++ b/grails-data-hibernate7/core/src/test/resources/classic-oracle/ddl-differential.txt
@@ -5240,6 +5240,26 @@ script		"ALTER TABLE \"PUBLIC\".\"GEN_SERIALIZED\" ADD CONSTRAINT \"PUBLIC\".\"C
 table	gen_tenant_book	{"checks":[],"collections":[],"columnOrder":["id","version","tenant_id","title"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"tenant_id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"title":{"checks":[],"collation":null,"comment":null,"default":null [...]
 script		"CREATE MEMORY TABLE \"PUBLIC\".\"GEN_TENANT_BOOK\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"TENANT_ID\" CHARACTER VARYING(255),\n    \"TITLE\" CHARACTER VARYING(255)\n);"
 script		"ALTER TABLE \"PUBLIC\".\"GEN_TENANT_BOOK\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+@group	org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantCatalog	{"members":["org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantCatalog","org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantEntry","org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantNote","org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantTag"],"source":"native"}
+table	gen_tenant_catalog	{"checks":[],"collections":[],"columnOrder":["id","version","name"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"name":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"version":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs [...]
+table	gen_tenant_catalog_entries	{"checks":[],"collections":["Set of ManyToOne (inverse)","Set of ManyToOne"],"columnOrder":["gen_tenant_catalog_id","gen_tenant_entry_id"],"columns":{"gen_tenant_catalog_id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":false,"type":"bigint","unique":false},"gen_tenant_entry_id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":false,"type [...]
+table	gen_tenant_entry	{"checks":[],"collections":[],"columnOrder":["id","version","label","tenant_id"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"label":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"tenant_id":{"checks":[],"collation":null,"comment":null,"default":nul [...]
+table	gen_tenant_note	{"checks":[],"collections":["Set of OneToMany (inverse)"],"columnOrder":["catalog_id","id","version","label","tenant_id"],"columns":{"catalog_id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"bigint","unique":false},"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"label":{"checks":[],"collation [...]
+table	gen_tenant_tag	{"checks":[],"collections":["Set of OneToMany (inverse)"],"columnOrder":["catalog_id","id","version","label"],"columns":{"catalog_id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"bigint","unique":false},"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"label":{"checks":[],"collation":null,"comme [...]
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"GEN_TENANT_CATALOG\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"NAME\" CHARACTER VARYING(255)\n);"
+script		"ALTER TABLE \"PUBLIC\".\"GEN_TENANT_CATALOG\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"GEN_TENANT_CATALOG_ENTRIES\"(\n    \"GEN_TENANT_CATALOG_ID\" BIGINT NOT NULL,\n    \"GEN_TENANT_ENTRY_ID\" BIGINT NOT NULL\n);"
+script		"ALTER TABLE \"PUBLIC\".\"GEN_TENANT_CATALOG_ENTRIES\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"GEN_TENANT_CATALOG_ID\", \"GEN_TENANT_ENTRY_ID\");"
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"GEN_TENANT_ENTRY\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"LABEL\" CHARACTER VARYING(255),\n    \"TENANT_ID\" CHARACTER VARYING(255)\n);"
+script		"ALTER TABLE \"PUBLIC\".\"GEN_TENANT_ENTRY\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"GEN_TENANT_NOTE\"(\n    \"CATALOG_ID\" BIGINT,\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"LABEL\" CHARACTER VARYING(255),\n    \"TENANT_ID\" CHARACTER VARYING(255)\n);"
+script		"ALTER TABLE \"PUBLIC\".\"GEN_TENANT_NOTE\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"GEN_TENANT_TAG\"(\n    \"CATALOG_ID\" BIGINT,\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"LABEL\" CHARACTER VARYING(255)\n);"
+script		"ALTER TABLE \"PUBLIC\".\"GEN_TENANT_TAG\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+script		"ALTER TABLE \"PUBLIC\".\"GEN_TENANT_NOTE\" ADD CONSTRAINT \"PUBLIC\".\"FKA4HYC4145SF6M5RSDB3T82RPI\" FOREIGN KEY(\"CATALOG_ID\") REFERENCES \"PUBLIC\".\"GEN_TENANT_CATALOG\"(\"ID\") NOCHECK;"
+script		"ALTER TABLE \"PUBLIC\".\"GEN_TENANT_TAG\" ADD CONSTRAINT \"PUBLIC\".\"FK47U3JPN71VHE8LQCBKGOH232K\" FOREIGN KEY(\"CATALOG_ID\") REFERENCES \"PUBLIC\".\"GEN_TENANT_CATALOG\"(\"ID\") NOCHECK;"
+script		"ALTER TABLE \"PUBLIC\".\"GEN_TENANT_CATALOG_ENTRIES\" ADD CONSTRAINT \"PUBLIC\".\"FK7V6QXE4D3036U3KVGHVS7BWC3\" FOREIGN KEY(\"GEN_TENANT_ENTRY_ID\") REFERENCES \"PUBLIC\".\"GEN_TENANT_ENTRY\"(\"ID\") NOCHECK;"
+script		"ALTER TABLE \"PUBLIC\".\"GEN_TENANT_CATALOG_ENTRIES\" ADD CONSTRAINT \"PUBLIC\".\"FKNBBBEPBQVO9AI9301IOBP16K1\" FOREIGN KEY(\"GEN_TENANT_CATALOG_ID\") REFERENCES \"PUBLIC\".\"GEN_TENANT_CATALOG\"(\"ID\") NOCHECK;"
 @group	org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantJoinedLeaf	{"members":["org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantJoinedLeaf","org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantJoinedRoot"]}
 table	gen_tenant_joined_leaf	{"checks":[],"collections":[],"columnOrder":["id","extra"],"columns":{"extra":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":false,"type":"bigint","unique":false}},"comment":null,"foreignKeys":{"FK51eqvkfoekh7sd60hqswid3j5":{"columns":["id"],"creat [...]
 table	gen_tenant_joined_root	{"checks":[],"collections":[],"columnOrder":["id","version","name","tenant_id"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"name":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"tenant_id":{"checks":[],"collation":null,"comment":null,"default" [...]
@@ -5837,6 +5857,46 @@ script		"ALTER TABLE \"PUBLIC\".\"DB_TENANT_WIDGET\" ADD CONSTRAINT \"PUBLIC\".\
 table	events_book	{"checks":[],"collections":[],"columnOrder":["id","version","name","time"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"name":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"time":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":n [...]
 script		"CREATE MEMORY TABLE \"PUBLIC\".\"EVENTS_BOOK\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"NAME\" CHARACTER VARYING(255),\n    \"TIME\" CHARACTER VARYING(255)\n);"
 script		"ALTER TABLE \"PUBLIC\".\"EVENTS_BOOK\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+@group	org.grails.orm.hibernate.connections.FilteredBook	{"members":["org.grails.orm.hibernate.connections.FilteredBook","org.grails.orm.hibernate.connections.FilteredShelf"],"source":"native"}
+table	filtered_book	{"checks":[],"collections":["Set of OneToMany (inverse)"],"columnOrder":["id","shelf_id","version","tenant_id","title"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"shelf_id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"bigint","unique":false},"tenant_id":{"checks":[],"collation": [...]
+table	filtered_shelf	{"checks":[],"collections":[],"columnOrder":["id","version","name"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"name":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"version":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":nu [...]
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"FILTERED_BOOK\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"SHELF_ID\" BIGINT,\n    \"VERSION\" BIGINT NOT NULL,\n    \"TENANT_ID\" CHARACTER VARYING(255),\n    \"TITLE\" CHARACTER VARYING(255)\n);"
+script		"ALTER TABLE \"PUBLIC\".\"FILTERED_BOOK\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"FILTERED_SHELF\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"NAME\" CHARACTER VARYING(255)\n);"
+script		"ALTER TABLE \"PUBLIC\".\"FILTERED_SHELF\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+script		"ALTER TABLE \"PUBLIC\".\"FILTERED_BOOK\" ADD CONSTRAINT \"PUBLIC\".\"FKGT9ODO1KLXF69KQXQR7YTWDIA\" FOREIGN KEY(\"SHELF_ID\") REFERENCES \"PUBLIC\".\"FILTERED_SHELF\"(\"ID\") NOCHECK;"
+@group	org.grails.orm.hibernate.connections.FilteredGroup	{"members":["org.grails.orm.hibernate.connections.FilteredGroup","org.grails.orm.hibernate.connections.FilteredMember"],"source":"native"}
+table	filtered_group	{"checks":[],"collections":[],"columnOrder":["id","version","name"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"name":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"version":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":nu [...]
+table	filtered_group_members	{"checks":[],"collections":["Set of ManyToOne (inverse)","Set of ManyToOne"],"columnOrder":["filtered_group_id","filtered_member_id"],"columns":{"filtered_group_id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":false,"type":"bigint","unique":false},"filtered_member_id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":false,"type":"bigint","un [...]
+table	filtered_member	{"checks":[],"collections":[],"columnOrder":["id","version","name","tenant_id"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"name":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"tenant_id":{"checks":[],"collation":null,"comment":null,"default":null," [...]
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"FILTERED_GROUP\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"NAME\" CHARACTER VARYING(255)\n);"
+script		"ALTER TABLE \"PUBLIC\".\"FILTERED_GROUP\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"FILTERED_GROUP_MEMBERS\"(\n    \"FILTERED_GROUP_ID\" BIGINT NOT NULL,\n    \"FILTERED_MEMBER_ID\" BIGINT NOT NULL\n);"
+script		"ALTER TABLE \"PUBLIC\".\"FILTERED_GROUP_MEMBERS\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"FILTERED_GROUP_ID\", \"FILTERED_MEMBER_ID\");"
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"FILTERED_MEMBER\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"NAME\" CHARACTER VARYING(255),\n    \"TENANT_ID\" CHARACTER VARYING(255)\n);"
+script		"ALTER TABLE \"PUBLIC\".\"FILTERED_MEMBER\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+script		"ALTER TABLE \"PUBLIC\".\"FILTERED_GROUP_MEMBERS\" ADD CONSTRAINT \"PUBLIC\".\"FKQMGEGBIBKTIB1R2VTWHSNJ6JD\" FOREIGN KEY(\"FILTERED_MEMBER_ID\") REFERENCES \"PUBLIC\".\"FILTERED_MEMBER\"(\"ID\") NOCHECK;"
+script		"ALTER TABLE \"PUBLIC\".\"FILTERED_GROUP_MEMBERS\" ADD CONSTRAINT \"PUBLIC\".\"FKCF2UVRU9HIU9BNC4CRFDXR07P\" FOREIGN KEY(\"FILTERED_GROUP_ID\") REFERENCES \"PUBLIC\".\"FILTERED_GROUP\"(\"ID\") NOCHECK;"
+@group	org.grails.orm.hibernate.connections.FilteredJoinBook	{"members":["org.grails.orm.hibernate.connections.FilteredJoinBook","org.grails.orm.hibernate.connections.FilteredJoinShelf"],"source":"native"}
+table	filtered_join_book	{"checks":[],"collections":["Set of OneToMany (inverse)"],"columnOrder":["id","shelf_id","version","tenant_id","title"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"shelf_id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"bigint","unique":false},"tenant_id":{"checks":[],"collat [...]
+table	filtered_join_shelf	{"checks":[],"collections":[],"columnOrder":["id","version","name"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"name":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"version":{"checks":[],"collation":null,"comment":null,"default":null,"generatedA [...]
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"FILTERED_JOIN_BOOK\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"SHELF_ID\" BIGINT,\n    \"VERSION\" BIGINT NOT NULL,\n    \"TENANT_ID\" CHARACTER VARYING(255),\n    \"TITLE\" CHARACTER VARYING(255)\n);"
+script		"ALTER TABLE \"PUBLIC\".\"FILTERED_JOIN_BOOK\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"FILTERED_JOIN_SHELF\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"NAME\" CHARACTER VARYING(255)\n);"
+script		"ALTER TABLE \"PUBLIC\".\"FILTERED_JOIN_SHELF\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+script		"ALTER TABLE \"PUBLIC\".\"FILTERED_JOIN_BOOK\" ADD CONSTRAINT \"PUBLIC\".\"FKKLX2M94UK3D9I3TNLLOE1G40E\" FOREIGN KEY(\"SHELF_ID\") REFERENCES \"PUBLIC\".\"FILTERED_JOIN_SHELF\"(\"ID\") NOCHECK;"
+@group	org.grails.orm.hibernate.connections.FilteredRack	{"members":["org.grails.orm.hibernate.connections.FilteredRack","org.grails.orm.hibernate.connections.FilteredRackBook","org.grails.orm.hibernate.connections.FilteredStore"],"source":"native"}
+table	filtered_rack	{"checks":[],"collections":[],"columnOrder":["id","version","name"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"name":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"version":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":nul [...]
+table	filtered_rack_book	{"checks":[],"collections":["Set of OneToMany (inverse)"],"columnOrder":["id","rack_id","version","tenant_id","title"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"rack_id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"bigint","unique":false},"tenant_id":{"checks":[],"collatio [...]
+table	filtered_store	{"checks":[],"collections":[],"columnOrder":["id","rack_id","version","name"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"name":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"rack_id":{"checks":[],"collation":null,"comment":null,"default":null,"gener [...]
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"FILTERED_RACK\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"NAME\" CHARACTER VARYING(255)\n);"
+script		"ALTER TABLE \"PUBLIC\".\"FILTERED_RACK\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"FILTERED_RACK_BOOK\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"RACK_ID\" BIGINT,\n    \"VERSION\" BIGINT NOT NULL,\n    \"TENANT_ID\" CHARACTER VARYING(255),\n    \"TITLE\" CHARACTER VARYING(255)\n);"
+script		"ALTER TABLE \"PUBLIC\".\"FILTERED_RACK_BOOK\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"FILTERED_STORE\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"RACK_ID\" BIGINT,\n    \"VERSION\" BIGINT NOT NULL,\n    \"NAME\" CHARACTER VARYING(255)\n);"
+script		"ALTER TABLE \"PUBLIC\".\"FILTERED_STORE\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+script		"ALTER TABLE \"PUBLIC\".\"FILTERED_STORE\" ADD CONSTRAINT \"PUBLIC\".\"FK3XUVKQ7D0YOJMC7U2P491N1MK\" FOREIGN KEY(\"RACK_ID\") REFERENCES \"PUBLIC\".\"FILTERED_RACK\"(\"ID\") NOCHECK;"
+script		"ALTER TABLE \"PUBLIC\".\"FILTERED_RACK_BOOK\" ADD CONSTRAINT \"PUBLIC\".\"FKHDQ9QMKOSA28IXS732NLK6NQD\" FOREIGN KEY(\"RACK_ID\") REFERENCES \"PUBLIC\".\"FILTERED_RACK\"(\"ID\") NOCHECK;"
 @group	org.grails.orm.hibernate.connections.Foo	{"members":["org.grails.orm.hibernate.connections.Foo"]}
 table	foo	{"checks":[],"collections":[],"columnOrder":["id","version","name"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"name":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"version":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identit [...]
 script		"CREATE MEMORY TABLE \"PUBLIC\".\"FOO\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"NAME\" CHARACTER VARYING(255)\n);"
@@ -5889,11 +5949,27 @@ script		"CREATE MEMORY TABLE \"PUBLIC\".\"MULTI_TENANT_PUBLISHER\"(\n    \"ID\"
 script		"ALTER TABLE \"PUBLIC\".\"MULTI_TENANT_PUBLISHER\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
 @group	org.grails.orm.hibernate.connections.Orange	{"members":["org.grails.orm.hibernate.connections.Orange"]}
 @group	org.grails.orm.hibernate.connections.Product	{"members":["org.grails.orm.hibernate.connections.Product"]}
+@group	org.grails.orm.hibernate.connections.ResolverAccount	{"members":["org.grails.orm.hibernate.connections.ResolverAccount","org.grails.orm.hibernate.connections.ResolverDocument"],"source":"native"}
+table	resolver_account	{"checks":[],"collections":[],"columnOrder":["id","version","host","tenant_key"],"columns":{"host":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"tenant_key":{"checks":[],"collation":null,"comment":null,"default":nul [...]
+table	resolver_document	{"checks":[],"collections":["Set of OneToMany (inverse)"],"columnOrder":["account_id","id","version","tenant_id","title"],"columns":{"account_id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"bigint","unique":false},"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"tenant_id":{"checks":[],"col [...]
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"RESOLVER_ACCOUNT\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"HOST\" CHARACTER VARYING(255),\n    \"TENANT_KEY\" CHARACTER VARYING(255)\n);"
+script		"ALTER TABLE \"PUBLIC\".\"RESOLVER_ACCOUNT\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"RESOLVER_DOCUMENT\"(\n    \"ACCOUNT_ID\" BIGINT,\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"TENANT_ID\" CHARACTER VARYING(255),\n    \"TITLE\" CHARACTER VARYING(255)\n);"
+script		"ALTER TABLE \"PUBLIC\".\"RESOLVER_DOCUMENT\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+script		"ALTER TABLE \"PUBLIC\".\"RESOLVER_DOCUMENT\" ADD CONSTRAINT \"PUBLIC\".\"FK1WTNIIQ7KE0GG1KWMSFIEV0DF\" FOREIGN KEY(\"ACCOUNT_ID\") REFERENCES \"PUBLIC\".\"RESOLVER_ACCOUNT\"(\"ID\") NOCHECK;"
+@group	org.grails.orm.hibernate.connections.ResolverNote	{"members":["org.grails.orm.hibernate.connections.ResolverNote"],"source":"native"}
+table	resolver_note	{"checks":[],"collections":[],"columnOrder":["id","version","text"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"text":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"version":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":nul [...]
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"RESOLVER_NOTE\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"TEXT\" CHARACTER VARYING(255)\n);"
+script		"ALTER TABLE \"PUBLIC\".\"RESOLVER_NOTE\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
 @group	org.grails.orm.hibernate.connections.SecondaryBook	{"members":["org.grails.orm.hibernate.connections.SecondaryBook"]}
 @group	org.grails.orm.hibernate.connections.SingleTenantAuthor	{"members":["org.grails.orm.hibernate.connections.SingleTenantAuthor"]}
 table	single_tenant_author	{"checks":[],"collections":[],"columnOrder":["id","version","name"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"name":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"version":{"checks":[],"collation":null,"comment":null,"default":null,"generated [...]
 script		"CREATE MEMORY TABLE \"PUBLIC\".\"SINGLE_TENANT_AUTHOR\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"NAME\" CHARACTER VARYING(255)\n);"
 script		"ALTER TABLE \"PUBLIC\".\"SINGLE_TENANT_AUTHOR\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
+@group	org.grails.orm.hibernate.connections.Voucher	{"members":["org.grails.orm.hibernate.connections.Voucher"],"source":"native"}
+table	voucher	{"checks":[],"collections":[],"columnOrder":["id","version","number","tenant_id"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"number":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"tenant_id":{"checks":[],"collation":null,"comment":null,"default":null,"gene [...]
+script		"CREATE MEMORY TABLE \"PUBLIC\".\"VOUCHER\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"NUMBER\" CHARACTER VARYING(255),\n    \"TENANT_ID\" CHARACTER VARYING(255)\n);"
+script		"ALTER TABLE \"PUBLIC\".\"VOUCHER\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
 @group	org.grails.orm.hibernate.dirty.DSBook	{"members":["org.grails.orm.hibernate.dirty.DSBook","org.grails.orm.hibernate.dirty.DSEmbeddedEntity"]}
 table	dsbook	{"checks":[],"collections":[],"columnOrder":["id","last_updated","version","embedded_prop_name","title"],"columns":{"embedded_prop_name":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"last_updated":{"checks":[],"collation":nul [...]
 table	dsembedded_entity	{"checks":[],"collections":[],"columnOrder":["id","version","name"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"name":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"version":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs" [...]
diff --git a/grails-data-hibernate7/core/src/test/resources/classic-oracle/generator-differential.txt b/grails-data-hibernate7/core/src/test/resources/classic-oracle/generator-differential.txt
index 1c7c7cda6f..f64d63d6c9 100644
--- a/grails-data-hibernate7/core/src/test/resources/classic-oracle/generator-differential.txt
+++ b/grails-data-hibernate7/core/src/test/resources/classic-oracle/generator-differential.txt
@@ -1810,6 +1810,12 @@ entity	org.grails.orm.hibernate.cfg.domainbinding.jpa.GenSerialized	{"abstractCl
 @group	org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantBook	{"members":["org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantBook"]}
 entity	org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantBook	{"abstractClass":false,"batchSize":0,"cached":false,"declaredNames":["version","title","tenantId"],"discriminatorInsertable":true,"dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantBook","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind":"BasicValue","identifierProperty":"id","joinTables":{} [...]
 filter	tenantId	{"appliedToLoadByKey":false,"autoEnabled":false,"name":"tenantId","parameterNames":["tenantId"],"parameterTypes":{"tenantId":"java.lang.String"}}
+@group	org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantCatalog	{"members":["org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantCatalog","org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantEntry","org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantNote","org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantTag"],"source":"native"}
+entity	org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantCatalog	{"abstractClass":false,"batchSize":-1,"cacheConcurrencyStrategy":"read-write","cached":false,"declaredNames":["entries","name","notes","tags","version"],"discriminatorInsertable":true,"discriminatorValue":"GenTenantCatalog","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantCatalog","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.gener [...]
+entity	org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantEntry	{"abstractClass":false,"batchSize":-1,"cacheConcurrencyStrategy":"read-write","cached":false,"declaredNames":["catalogs","label","tenantId","version"],"discriminatorInsertable":true,"discriminatorValue":"GenTenantEntry","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantEntry","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.Gra [...]
+entity	org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantNote	{"abstractClass":false,"batchSize":-1,"cacheConcurrencyStrategy":"read-write","cached":false,"declaredNames":["catalog","label","tenantId","version"],"discriminatorInsertable":true,"discriminatorValue":"GenTenantNote","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantNote","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsN [...]
+entity	org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantTag	{"abstractClass":false,"batchSize":-1,"cacheConcurrencyStrategy":"read-write","cached":false,"declaredNames":["catalog","label","version"],"discriminatorInsertable":true,"discriminatorValue":"GenTenantTag","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantTag","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator [...]
+filter	tenantId	{"appliedToLoadByKey":false,"autoEnabled":false,"name":"tenantId","parameterNames":["tenantId"],"parameterTypes":{"tenantId":"java.lang.String"}}
 @group	org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantJoinedLeaf	{"members":["org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantJoinedLeaf","org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantJoinedRoot"]}
 entity	org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantJoinedLeaf	{"abstractClass":false,"batchSize":-1,"declaredNames":["extra"],"dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantJoinedLeaf","identifierKind":"BasicValue","identifierProperty":"id","joinTables":{},"jpaEntityName":"GenTenantJoinedLeaf","keyColumns":["id"],"kind":"JoinedSubclass","optimisticLock":"VERSION","ownFilters":[],"ownsTable":true,"propertyNames [...]
 entity	org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantJoinedRoot	{"abstractClass":false,"batchSize":0,"cached":false,"declaredNames":["version","name","tenantId"],"discriminatorInsertable":true,"dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.cfg.domainbinding.jpa.GenTenantJoinedRoot","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind":"BasicValue","identifierProperty":"id","joi [...]
@@ -1918,7 +1924,7 @@ entity	org.grails.orm.hibernate.cfg.domainbinding.secondpass.CMTBBidirectionalOw
 filter	tenantId	{"appliedToLoadByKey":false,"autoEnabled":false,"name":"tenantId","parameterNames":["tenantId"],"parameterTypes":{"tenantId":"java.lang.Long"}}
 @group	org.grails.orm.hibernate.cfg.domainbinding.secondpass.CMTBManyToManyItem	{"members":["org.grails.orm.hibernate.cfg.domainbinding.secondpass.CMTBManyToManyItem","org.grails.orm.hibernate.cfg.domainbinding.secondpass.CMTBManyToManyOwner"]}
 entity	org.grails.orm.hibernate.cfg.domainbinding.secondpass.CMTBManyToManyItem	{"abstractClass":false,"batchSize":0,"cached":false,"declaredNames":["version","tenantId","owners"],"discriminatorInsertable":true,"dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.cfg.domainbinding.secondpass.CMTBManyToManyItem","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind":"BasicValue","identifierPrope [...]
-entity	org.grails.orm.hibernate.cfg.domainbinding.secondpass.CMTBManyToManyOwner	{"abstractClass":false,"batchSize":0,"cached":false,"declaredNames":["version","items"],"discriminatorInsertable":true,"dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.cfg.domainbinding.secondpass.CMTBManyToManyOwner","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind":"BasicValue","identifierProperty":"id", [...]
+entity	org.grails.orm.hibernate.cfg.domainbinding.secondpass.CMTBManyToManyOwner	{"abstractClass":false,"batchSize":0,"cached":false,"declaredNames":["version","items"],"discriminatorInsertable":true,"dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.cfg.domainbinding.secondpass.CMTBManyToManyOwner","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind":"BasicValue","identifierProperty":"id", [...]
 filter	tenantId	{"appliedToLoadByKey":false,"autoEnabled":false,"name":"tenantId","parameterNames":["tenantId"],"parameterTypes":{"tenantId":"java.lang.Long"}}
 @group	org.grails.orm.hibernate.cfg.domainbinding.secondpass.CMTBNonTenantItem	{"members":["org.grails.orm.hibernate.cfg.domainbinding.secondpass.CMTBNonTenantItem","org.grails.orm.hibernate.cfg.domainbinding.secondpass.CMTBNonTenantOwner"]}
 entity	org.grails.orm.hibernate.cfg.domainbinding.secondpass.CMTBNonTenantItem	{"abstractClass":false,"batchSize":0,"cached":false,"declaredNames":["version","name"],"discriminatorInsertable":true,"dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.cfg.domainbinding.secondpass.CMTBNonTenantItem","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind":"BasicValue","identifierProperty":"id","join [...]
@@ -1998,6 +2004,23 @@ entity	org.grails.orm.hibernate.connections.CachingEntity	{"abstractClass":false
 entity	org.grails.orm.hibernate.connections.DbTenantWidget	{"abstractClass":false,"batchSize":0,"cached":false,"declaredNames":["version","tenantId","name"],"discriminatorInsertable":true,"dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.connections.DbTenantWidget","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind":"BasicValue","identifierProperty":"id","joinTables":{},"jpaEntityName":"D [...]
 @group	org.grails.orm.hibernate.connections.EventsBook	{"members":["org.grails.orm.hibernate.connections.EventsBook"]}
 entity	org.grails.orm.hibernate.connections.EventsBook	{"abstractClass":false,"batchSize":0,"cached":false,"declaredNames":["version","time","name"],"discriminatorInsertable":true,"dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.connections.EventsBook","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind":"BasicValue","identifierProperty":"id","joinTables":{},"jpaEntityName":"EventsBook"," [...]
+@group	org.grails.orm.hibernate.connections.FilteredBook	{"members":["org.grails.orm.hibernate.connections.FilteredBook","org.grails.orm.hibernate.connections.FilteredShelf"],"source":"native"}
+entity	org.grails.orm.hibernate.connections.FilteredBook	{"abstractClass":false,"batchSize":-1,"cacheConcurrencyStrategy":"read-write","cached":false,"declaredNames":["shelf","tenantId","title","version"],"discriminatorInsertable":true,"discriminatorValue":"FilteredBook","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.connections.FilteredBook","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identif [...]
+entity	org.grails.orm.hibernate.connections.FilteredShelf	{"abstractClass":false,"batchSize":-1,"cacheConcurrencyStrategy":"read-write","cached":false,"declaredNames":["books","name","version"],"discriminatorInsertable":true,"discriminatorValue":"FilteredShelf","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.connections.FilteredShelf","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind": [...]
+filter	tenantId	{"appliedToLoadByKey":false,"autoEnabled":false,"name":"tenantId","parameterNames":["tenantId"],"parameterTypes":{"tenantId":"java.lang.String"}}
+@group	org.grails.orm.hibernate.connections.FilteredGroup	{"members":["org.grails.orm.hibernate.connections.FilteredGroup","org.grails.orm.hibernate.connections.FilteredMember"],"source":"native"}
+entity	org.grails.orm.hibernate.connections.FilteredGroup	{"abstractClass":false,"batchSize":-1,"cacheConcurrencyStrategy":"read-write","cached":false,"declaredNames":["members","name","version"],"discriminatorInsertable":true,"discriminatorValue":"FilteredGroup","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.connections.FilteredGroup","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind [...]
+entity	org.grails.orm.hibernate.connections.FilteredMember	{"abstractClass":false,"batchSize":-1,"cacheConcurrencyStrategy":"read-write","cached":false,"declaredNames":["groups","name","tenantId","version"],"discriminatorInsertable":true,"discriminatorValue":"FilteredMember","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.connections.FilteredMember","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"i [...]
+filter	tenantId	{"appliedToLoadByKey":false,"autoEnabled":false,"name":"tenantId","parameterNames":["tenantId"],"parameterTypes":{"tenantId":"java.lang.String"}}
+@group	org.grails.orm.hibernate.connections.FilteredJoinBook	{"members":["org.grails.orm.hibernate.connections.FilteredJoinBook","org.grails.orm.hibernate.connections.FilteredJoinShelf"],"source":"native"}
+entity	org.grails.orm.hibernate.connections.FilteredJoinBook	{"abstractClass":false,"batchSize":-1,"cacheConcurrencyStrategy":"read-write","cached":false,"declaredNames":["shelf","tenantId","title","version"],"discriminatorInsertable":true,"discriminatorValue":"FilteredJoinBook","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.connections.FilteredJoinBook","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerato [...]
+entity	org.grails.orm.hibernate.connections.FilteredJoinShelf	{"abstractClass":false,"batchSize":-1,"cacheConcurrencyStrategy":"read-write","cached":false,"declaredNames":["books","name","version"],"discriminatorInsertable":true,"discriminatorValue":"FilteredJoinShelf","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.connections.FilteredJoinShelf","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"iden [...]
+filter	tenantId	{"appliedToLoadByKey":false,"autoEnabled":false,"name":"tenantId","parameterNames":["tenantId"],"parameterTypes":{"tenantId":"java.lang.String"}}
+@group	org.grails.orm.hibernate.connections.FilteredRack	{"members":["org.grails.orm.hibernate.connections.FilteredRack","org.grails.orm.hibernate.connections.FilteredRackBook","org.grails.orm.hibernate.connections.FilteredStore"],"source":"native"}
+entity	org.grails.orm.hibernate.connections.FilteredRack	{"abstractClass":false,"batchSize":-1,"cacheConcurrencyStrategy":"read-write","cached":false,"declaredNames":["books","name","version"],"discriminatorInsertable":true,"discriminatorValue":"FilteredRack","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.connections.FilteredRack","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind":"Ba [...]
+entity	org.grails.orm.hibernate.connections.FilteredRackBook	{"abstractClass":false,"batchSize":-1,"cacheConcurrencyStrategy":"read-write","cached":false,"declaredNames":["rack","tenantId","title","version"],"discriminatorInsertable":true,"discriminatorValue":"FilteredRackBook","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.connections.FilteredRackBook","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator [...]
+entity	org.grails.orm.hibernate.connections.FilteredStore	{"abstractClass":false,"batchSize":-1,"cacheConcurrencyStrategy":"read-write","cached":false,"declaredNames":["name","rack","version"],"discriminatorInsertable":true,"discriminatorValue":"FilteredStore","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.connections.FilteredStore","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind":" [...]
+filter	tenantId	{"appliedToLoadByKey":false,"autoEnabled":false,"name":"tenantId","parameterNames":["tenantId"],"parameterTypes":{"tenantId":"java.lang.String"}}
 @group	org.grails.orm.hibernate.connections.Foo	{"members":["org.grails.orm.hibernate.connections.Foo"]}
 entity	org.grails.orm.hibernate.connections.Foo	{"abstractClass":false,"batchSize":0,"cached":false,"declaredNames":["version","name"],"discriminatorInsertable":true,"dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.connections.Foo","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind":"BasicValue","identifierProperty":"id","joinTables":{},"jpaEntityName":"Foo","kind":"RootClass","lazyPrope [...]
 @group	org.grails.orm.hibernate.connections.H7AllocationAllDatasourceTenant	{"members":["org.grails.orm.hibernate.connections.H7AllocationAllDatasourceTenant"]}
@@ -2034,9 +2057,18 @@ entity	org.grails.orm.hibernate.connections.MultiTenantPublisher	{"abstractClass
 filter	tenantId	{"appliedToLoadByKey":false,"autoEnabled":false,"name":"tenantId","parameterNames":["tenantId"],"parameterTypes":{"tenantId":"java.lang.String"}}
 @group	org.grails.orm.hibernate.connections.Orange	{"members":["org.grails.orm.hibernate.connections.Orange"]}
 @group	org.grails.orm.hibernate.connections.Product	{"members":["org.grails.orm.hibernate.connections.Product"]}
+@group	org.grails.orm.hibernate.connections.ResolverAccount	{"members":["org.grails.orm.hibernate.connections.ResolverAccount","org.grails.orm.hibernate.connections.ResolverDocument"],"source":"native"}
+entity	org.grails.orm.hibernate.connections.ResolverAccount	{"abstractClass":false,"batchSize":-1,"cacheConcurrencyStrategy":"read-write","cached":false,"declaredNames":["documents","host","tenantKey","version"],"discriminatorInsertable":true,"discriminatorValue":"ResolverAccount","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.connections.ResolverAccount","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerat [...]
+entity	org.grails.orm.hibernate.connections.ResolverDocument	{"abstractClass":false,"batchSize":-1,"cacheConcurrencyStrategy":"read-write","cached":false,"declaredNames":["account","tenantId","title","version"],"discriminatorInsertable":true,"discriminatorValue":"ResolverDocument","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.connections.ResolverDocument","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenera [...]
+filter	tenantId	{"appliedToLoadByKey":false,"autoEnabled":false,"name":"tenantId","parameterNames":["tenantId"],"parameterTypes":{"tenantId":"java.lang.String"}}
+@group	org.grails.orm.hibernate.connections.ResolverNote	{"members":["org.grails.orm.hibernate.connections.ResolverNote"],"source":"native"}
+entity	org.grails.orm.hibernate.connections.ResolverNote	{"abstractClass":false,"batchSize":-1,"cacheConcurrencyStrategy":"read-write","cached":false,"declaredNames":["text","version"],"discriminatorInsertable":true,"discriminatorValue":"ResolverNote","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.connections.ResolverNote","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind":"BasicValue [...]
 @group	org.grails.orm.hibernate.connections.SecondaryBook	{"members":["org.grails.orm.hibernate.connections.SecondaryBook"]}
 @group	org.grails.orm.hibernate.connections.SingleTenantAuthor	{"members":["org.grails.orm.hibernate.connections.SingleTenantAuthor"]}
 entity	org.grails.orm.hibernate.connections.SingleTenantAuthor	{"abstractClass":false,"batchSize":0,"cached":false,"declaredNames":["version","name"],"discriminatorInsertable":true,"dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.connections.SingleTenantAuthor","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind":"BasicValue","identifierProperty":"id","joinTables":{},"jpaEntityName":"Sing [...]
+@group	org.grails.orm.hibernate.connections.Voucher	{"members":["org.grails.orm.hibernate.connections.Voucher"],"source":"native"}
+entity	org.grails.orm.hibernate.connections.Voucher	{"abstractClass":false,"batchSize":-1,"cacheConcurrencyStrategy":"read-write","cached":false,"declaredNames":["number","tenantId","version"],"discriminatorInsertable":true,"discriminatorValue":"Voucher","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.connections.Voucher","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind":"BasicValue", [...]
+filter	tenantId	{"appliedToLoadByKey":false,"autoEnabled":false,"name":"tenantId","parameterNames":["tenantId"],"parameterTypes":{"tenantId":"java.lang.String"}}
 @group	org.grails.orm.hibernate.dirty.DSBook	{"members":["org.grails.orm.hibernate.dirty.DSBook","org.grails.orm.hibernate.dirty.DSEmbeddedEntity"]}
 entity	org.grails.orm.hibernate.dirty.DSBook	{"abstractClass":false,"batchSize":0,"cached":false,"declaredNames":["version","title","lastUpdated","embeddedProp"],"discriminatorInsertable":true,"dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.dirty.DSBook","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind":"BasicValue","identifierProperty":"id","joinTables":{},"jpaEntityName":"DSBook","k [...]
 entity	org.grails.orm.hibernate.dirty.DSEmbeddedEntity	{"abstractClass":false,"batchSize":0,"cached":false,"declaredNames":["version","name"],"discriminatorInsertable":true,"dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.dirty.DSEmbeddedEntity","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind":"BasicValue","identifierProperty":"id","joinTables":{},"jpaEntityName":"DSEmbeddedEntity","k [...]


(grails-core) 01/02: test(hibernate7): keep the 9.x refusal of an ownerless many-to-many over the 8.x warning spec

Posted by bo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch 9.0.x-h7-improvement-phase3
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit e7b263b07cf0861119ac113a215f1aa56543fdb3
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Thu Oct 22 11:21:51 2026 -0500

    test(hibernate7): keep the 9.x refusal of an ownerless many-to-many over the 8.x warning spec
    
    The 10.0.x merge brought ManyToManyWithoutOwnerWarningSpec, which expects
    the Grails 8 behaviour: a bidirectional many-to-many with no belongsTo on
    either side binds, and the binder logs one warning. On this branch such a
    mapping refuses to start with a MappingException that names both sides
    (ManyToManyOwnerValidator, documented in manyToMany.adoc and
    hibernateVersions.adoc, covered by ManyToManyWithoutOwnerSpec including the
    circular and the Map-sided cases), so the warning spec cannot pass and its
    cases are already covered. The spec goes; its Warn* fixtures went with it,
    so the classic oracle no longer expects them.
    
    The oracle also drops the record of SingleTableSubClassDefault, whose
    classic fixture left with the classic binder in the same merge.
---
 .../tests/ManyToManyWithoutOwnerWarningSpec.groovy | 307 ---------------------
 .../resources/classic-oracle/ddl-differential.txt  |   4 -
 .../classic-oracle/generator-differential.txt      |   4 -
 3 files changed, 315 deletions(-)

diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ManyToManyWithoutOwnerWarningSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ManyToManyWithoutOwnerWarningSpec.groovy
deleted file mode 100644
index ad9aedcdcd..0000000000
--- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/ManyToManyWithoutOwnerWarningSpec.groovy
+++ /dev/null
@@ -1,307 +0,0 @@
-/*
- *  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
- *
- *    https://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 grails.gorm.tests
-
-import spock.lang.Specification
-
-import grails.gorm.annotation.Entity
-import org.grails.orm.hibernate.HibernateDatastore
-
-/**
- * A many-to-many where neither side declares {@code belongsTo} has no owning side, so the relationship is never
- * stored. A warning at startup points this out.
- */
-class ManyToManyWithoutOwnerWarningSpec extends Specification {
-
-    private static final String WARNING = 'declares belongsTo, so the relationship is not stored'
-
-    void 'a many-to-many with no belongsTo on either side logs one warning'() {
-        when:
-        var output = stderrWhile {
-            new HibernateDatastore(WarnNoOwnerLeft, WarnNoOwnerRight).close()
-        }
-
-        then:
-        output.readLines().count { it.contains(WARNING) } == 1
-        output.contains('[grails.gorm.tests.WarnNoOwnerLeft.rights] and [grails.gorm.tests.WarnNoOwnerRight.lefts]')
-        output.contains('for example in WarnNoOwnerRight: static belongsTo = WarnNoOwnerLeft')
-    }
-
-    void 'a many-to-many with belongsTo on one side logs no warning'() {
-        when:
-        var output = stderrWhile {
-            new HibernateDatastore(WarnOwnedLeft, WarnOwnerRight).close()
-        }
-
-        then:
-        !output.contains(WARNING)
-    }
-
-    void 'a unidirectional self-referencing one-to-many logs no warning'() {
-        when:
-        var output = stderrWhile {
-            new HibernateDatastore(WarnSelfReferencing).close()
-        }
-
-        then:
-        !output.contains(WARNING)
-    }
-
-    void 'a circular many-to-many with no belongsTo logs one warning, because nothing is stored'() {
-        given:
-        var output = null
-        HibernateDatastore datastore = null
-
-        when:
-        output = stderrWhile {
-            datastore = new HibernateDatastore(WarnCircular)
-        }
-        WarnCircular.withNewTransaction {
-            WarnCircular followed = new WarnCircular(name: 'followed').save(failOnError: true)
-            WarnCircular follower = new WarnCircular(name: 'follower').save(failOnError: true)
-            follower.addToFollowing(followed)
-            follower.save(failOnError: true)
-        }
-
-        then:
-        output.readLines().count { it.contains(WARNING) } == 1
-        output.contains('[grails.gorm.tests.WarnCircular.followers] and [grails.gorm.tests.WarnCircular.following]')
-        output.contains('for example in WarnCircular: static belongsTo = WarnCircular')
-        WarnCircular.withNewSession {
-            WarnCircular.findByName('follower').following.isEmpty() &&
-                    WarnCircular.findByName('followed').followers.isEmpty()
-        }
-
-        cleanup:
-        datastore?.close()
-    }
-
-    void 'a circular many-to-many with belongsTo logs no warning and stores the relationship'() {
-        given:
-        var output = null
-        HibernateDatastore datastore = null
-
-        when:
-        output = stderrWhile {
-            datastore = new HibernateDatastore(WarnCircularOwned)
-        }
-        WarnCircularOwned.withNewTransaction {
-            WarnCircularOwned followed = new WarnCircularOwned(name: 'followed').save(failOnError: true)
-            WarnCircularOwned follower = new WarnCircularOwned(name: 'follower').save(failOnError: true)
-            follower.addToFollowing(followed)
-            follower.save(failOnError: true)
-        }
-
-        then:
-        !output.contains(WARNING)
-        WarnCircularOwned.withNewSession {
-            WarnCircularOwned.findByName('follower').following*.name == ['followed'] &&
-                    WarnCircularOwned.findByName('followed').followers*.name == ['follower']
-        }
-
-        cleanup:
-        datastore?.close()
-    }
-
-    void 'a unidirectional one-to-many and a basic collection log no warning'() {
-        when:
-        var output = stderrWhile {
-            new HibernateDatastore(WarnParent, WarnChild).close()
-        }
-
-        then:
-        !output.contains(WARNING)
-    }
-
-    void 'a many-to-many with a Map on one side logs no warning, because the Map side writes the join table'() {
-        when:
-        var output = stderrWhile {
-            new HibernateDatastore(WarnMapLeft, WarnMapRight).close()
-        }
-
-        then:
-        !output.contains(WARNING)
-    }
-
-    void 'a many-to-many with a Map on both sides logs no warning'() {
-        when:
-        var output = stderrWhile {
-            new HibernateDatastore(WarnMapBothLeft, WarnMapBothRight).close()
-        }
-
-        then:
-        !output.contains(WARNING)
-    }
-
-    void 'the relationship is stored when it is written through the Map side'() {
-        given:
-        HibernateDatastore datastore = new HibernateDatastore(WarnMapLeft, WarnMapRight)
-
-        when:
-        WarnMapLeft.withNewTransaction {
-            WarnMapRight right = new WarnMapRight(name: 'right').save(failOnError: true)
-            new WarnMapLeft(name: 'left', rights: [first: right]).save(failOnError: true)
-        }
-
-        then:
-        WarnMapLeft.withNewSession {
-            WarnMapLeft.findByName('left').rights.keySet() == ['first'] as Set
-        }
-        WarnMapRight.withNewSession {
-            WarnMapRight.findByName('right').lefts*.name == ['left']
-        }
-
-        cleanup:
-        datastore?.close()
-    }
-
-    private static String stderrWhile(Closure<?> work) {
-        var original = System.err
-        var buffer = new ByteArrayOutputStream()
-        System.setErr(new PrintStream(buffer, true))
-        try {
-            work()
-        }
-        finally {
-            System.setErr(original)
-        }
-        buffer.toString()
-    }
-}
-
-@Entity
-class WarnNoOwnerLeft {
-
-    String name
-    Set<WarnNoOwnerRight> rights
-
-    static hasMany = [rights: WarnNoOwnerRight]
-}
-
-@Entity
-class WarnNoOwnerRight {
-
-    String name
-    Set<WarnNoOwnerLeft> lefts
-
-    static hasMany = [lefts: WarnNoOwnerLeft]
-}
-
-@Entity
-class WarnOwnedLeft {
-
-    String name
-    Set<WarnOwnerRight> owners
-
-    static hasMany = [owners: WarnOwnerRight]
-    static belongsTo = WarnOwnerRight
-}
-
-@Entity
-class WarnOwnerRight {
-
-    String name
-    Set<WarnOwnedLeft> lefts
-
-    static hasMany = [lefts: WarnOwnedLeft]
-}
-
-@Entity
-class WarnSelfReferencing {
-
-    String name
-    Set<WarnSelfReferencing> friends
-
-    static hasMany = [friends: WarnSelfReferencing]
-}
-
-@Entity
-class WarnCircular {
-
-    String name
-    Set<WarnCircular> following
-    Set<WarnCircular> followers
-
-    static hasMany = [following: WarnCircular, followers: WarnCircular]
-    static mappedBy = [following: 'followers', followers: 'following']
-}
-
-@Entity
-class WarnCircularOwned {
-
-    String name
-    Set<WarnCircularOwned> following
-    Set<WarnCircularOwned> followers
-
-    static hasMany = [following: WarnCircularOwned, followers: WarnCircularOwned]
-    static mappedBy = [following: 'followers', followers: 'following']
-    static belongsTo = WarnCircularOwned
-}
-
-@Entity
-class WarnParent {
-
-    String name
-    Set<WarnChild> children
-    Set<String> labels
-
-    static hasMany = [children: WarnChild, labels: String]
-}
-
-@Entity
-class WarnChild {
-
-    String name
-}
-
-@Entity
-class WarnMapLeft {
-
-    String name
-    Map<String, WarnMapRight> rights
-
-    static hasMany = [rights: WarnMapRight]
-}
-
-@Entity
-class WarnMapRight {
-
-    String name
-    Set<WarnMapLeft> lefts
-
-    static hasMany = [lefts: WarnMapLeft]
-}
-
-@Entity
-class WarnMapBothLeft {
-
-    String name
-    Map<String, WarnMapBothRight> rights
-
-    static hasMany = [rights: WarnMapBothRight]
-}
-
-@Entity
-class WarnMapBothRight {
-
-    String name
-    Map<String, WarnMapBothLeft> lefts
-
-    static hasMany = [lefts: WarnMapBothLeft]
-}
diff --git a/grails-data-hibernate7/core/src/test/resources/classic-oracle/ddl-differential.txt b/grails-data-hibernate7/core/src/test/resources/classic-oracle/ddl-differential.txt
index d46d11d4b7..82c6b14d7f 100644
--- a/grails-data-hibernate7/core/src/test/resources/classic-oracle/ddl-differential.txt
+++ b/grails-data-hibernate7/core/src/test/resources/classic-oracle/ddl-differential.txt
@@ -4139,10 +4139,6 @@ table	smbssingle_super	{"checks":[],"collections":[],"columnOrder":["id","versio
 script		"CREATE MEMORY TABLE \"PUBLIC\".\"SMBSSINGLE_SUPER\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"CLASS\" CHARACTER VARYING(255) NOT NULL,\n    \"NAME\" CHARACTER VARYING(255),\n    \"SUB_NAME\" CHARACTER VARYING(255)\n);"
 script		"ALTER TABLE \"PUBLIC\".\"SMBSSINGLE_SUPER\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
 @group	org.grails.orm.hibernate.cfg.domainbinding.binder.SMBSUnionSub	{"members":["org.grails.orm.hibernate.cfg.domainbinding.binder.SMBSUnionSub","org.grails.orm.hibernate.cfg.domainbinding.binder.SMBSUnionSuper"],"unbootable":"MissingPropertyException: No such property: checkConstraints for class: org.hibernate.mapping.DenormalizedTable"}
-@group	org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubClassDefault	{"members":["org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubClassDefault","org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubClassRoot","org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubClassSub"]}
-table	single_table_sub_class_root	{"checks":[],"collections":[],"columnOrder":["id","version","class","label","name"],"columns":{"class":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":false,"type":"varchar(255)","unique":false},"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"label":{"checks":[],"collation":null,"comment":null,"d [...]
-script		"CREATE MEMORY TABLE \"PUBLIC\".\"SINGLE_TABLE_SUB_CLASS_ROOT\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"CLASS\" CHARACTER VARYING(255) NOT NULL,\n    \"LABEL\" CHARACTER VARYING(255),\n    \"NAME\" CHARACTER VARYING(255)\n);"
-script		"ALTER TABLE \"PUBLIC\".\"SINGLE_TABLE_SUB_CLASS_ROOT\" ADD CONSTRAINT \"PUBLIC\".\"CONSTRAINT_#\" PRIMARY KEY(\"ID\");"
 @group	org.grails.orm.hibernate.cfg.domainbinding.binder.TestEntity	{"members":["org.grails.orm.hibernate.cfg.domainbinding.binder.TestEntity"]}
 table	test_entity	{"checks":[],"collections":[],"columnOrder":["id","version","name"],"columns":{"id":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":true,"nullable":false,"type":"bigint","unique":false},"name":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null,"identity":false,"nullable":true,"type":"varchar(255)","unique":false},"version":{"checks":[],"collation":null,"comment":null,"default":null,"generatedAs":null, [...]
 script		"CREATE MEMORY TABLE \"PUBLIC\".\"TEST_ENTITY\"(\n    \"ID\" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL,\n    \"VERSION\" BIGINT NOT NULL,\n    \"NAME\" CHARACTER VARYING(255)\n);"
diff --git a/grails-data-hibernate7/core/src/test/resources/classic-oracle/generator-differential.txt b/grails-data-hibernate7/core/src/test/resources/classic-oracle/generator-differential.txt
index 2d31db0510..1c7c7cda6f 100644
--- a/grails-data-hibernate7/core/src/test/resources/classic-oracle/generator-differential.txt
+++ b/grails-data-hibernate7/core/src/test/resources/classic-oracle/generator-differential.txt
@@ -1456,10 +1456,6 @@ entity	org.grails.orm.hibernate.cfg.domainbinding.binder.SMBSSingleSuper	{"abstr
 @group	org.grails.orm.hibernate.cfg.domainbinding.binder.SMBSUnionSub	{"members":["org.grails.orm.hibernate.cfg.domainbinding.binder.SMBSUnionSub","org.grails.orm.hibernate.cfg.domainbinding.binder.SMBSUnionSuper"]}
 entity	org.grails.orm.hibernate.cfg.domainbinding.binder.SMBSUnionSub	{"abstractClass":false,"batchSize":-1,"declaredNames":["subName"],"dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.cfg.domainbinding.binder.SMBSUnionSub","identifierKind":"BasicValue","identifierProperty":"id","joinTables":{},"jpaEntityName":"SMBSUnionSub","kind":"UnionSubclass","optimisticLock":"VERSION","ownFilters":[],"ownsTable":true,"propertyNames":["subName"],"props":{"id":{"back [...]
 entity	org.grails.orm.hibernate.cfg.domainbinding.binder.SMBSUnionSuper	{"abstractClass":false,"batchSize":0,"cached":false,"declaredNames":["version","name"],"discriminatorInsertable":true,"dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.cfg.domainbinding.binder.SMBSUnionSuper","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsSequenceStyleGenerator","sequence":{"incrementSize":50,"name":"smbsunion_super_SEQ","optimize [...]
-@group	org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubClassDefault	{"members":["org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubClassDefault","org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubClassRoot","org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubClassSub"]}
-entity	org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubClassDefault	{"abstractClass":false,"batchSize":-1,"declaredNames":["label"],"discriminatorValue":"org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubClassDefault","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubClassDefault","identifierKind":"BasicValue","identifierProperty":"id","joinTables":{},"jpaEntityName":"SingleTableSubClass [...]
-entity	org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubClassRoot	{"abstractClass":false,"batchSize":0,"cached":false,"declaredNames":["version"],"discriminator":{"javaType":"java.lang.String","jdbcCode":12,"kind":"BasicValue","selectables":[{"name":"class","nullable":false,"quoted":false,"sqlType":"varchar(255)","text":"class","unique":false}],"typeName":"string","typeParameters":{},"types":["BasicValue"]},"discriminatorInsertable":true,"discriminatorValue":"org.grails.or [...]
-entity	org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubClassSub	{"abstractClass":false,"batchSize":-1,"declaredNames":["name"],"discriminatorValue":"SUB_CLASS","dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubClassSub","identifierKind":"BasicValue","identifierProperty":"id","joinTables":{},"jpaEntityName":"SingleTableSubClassSub","kind":"SingleTableSubclass","optimisticLock":"VERSION","ownFilters":[] [...]
 @group	org.grails.orm.hibernate.cfg.domainbinding.binder.TestEntity	{"members":["org.grails.orm.hibernate.cfg.domainbinding.binder.TestEntity"]}
 entity	org.grails.orm.hibernate.cfg.domainbinding.binder.TestEntity	{"abstractClass":false,"batchSize":0,"cached":false,"declaredNames":["version","name"],"discriminatorInsertable":true,"dynamicInsert":false,"dynamicUpdate":false,"entityName":"org.grails.orm.hibernate.cfg.domainbinding.binder.TestEntity","generator":{"className":"org.grails.orm.hibernate.cfg.domainbinding.generator.GrailsNativeGenerator"},"identifierKind":"BasicValue","identifierProperty":"id","joinTables":{},"jpaEntityN [...]
 @group	org.grails.orm.hibernate.cfg.domainbinding.binder.UnidirectionalLeft	{"members":["org.grails.orm.hibernate.cfg.domainbinding.binder.UnidirectionalLeft","org.grails.orm.hibernate.cfg.domainbinding.binder.UnidirectionalRight"]}