Skip to content

fix(grails-data-hibernate7): make type: 'text' produce an unbounded column - #16020

Open
borinquenkid wants to merge 3 commits into
8.0.xfrom
fix/gorm-text-type-unbounded-column
Open

fix(grails-data-hibernate7): make type: 'text' produce an unbounded column#16020
borinquenkid wants to merge 3 commits into
8.0.xfrom
fix/gorm-text-type-unbounded-column

Conversation

@borinquenkid

Copy link
Copy Markdown
Member

Summary

Fixes #16010 — a GORM property mapped with type: 'text' was producing a bounded varchar(32600) column on Postgres instead of a genuine unbounded text column, causing CommandAcceptanceException: value too long for type character varying(32600) on schema update once existing data exceeded that length.

Root cause: type: 'text' resolves through Hibernate's legacy named-type lookup to StandardBasicTypes.TEXT, whose JDBC type code is the legacy java.sql.Types.LONGVARCHAR. Dialects (e.g. Postgres) don't render that legacy code as their native unbounded text/CLOB type, falling back instead to a bounded VARCHAR at Hibernate's generic Length.LONG default (32600) once no explicit column length is set (GORM only sets an explicit length when a maxSize/inList validation constraint exists).

Fix: in SimpleValueBinder.bindSimpleValue(), when the resolved type name is "text", bind the modern SqlTypes.LONG32VARCHAR JDBC type directly via BasicValue.setExplicitJdbcTypeAccess(...) instead of going through the ambiguous legacy type name — restoring the "CLOB or TEXT depending on database dialect" behavior the mapping DSL reference docs (type.adoc) already promise for type: 'text'.

Test plan

  • New GormTextTypeColumnIntegrationSpec (real Postgres 16 via Testcontainers, following the module's existing HibernateDatastoreIntegrationSpec pattern): confirmed RED against unmodified code (character varying/32600, reproducing the bug exactly), confirmed GREEN after the fix (text/unbounded)
  • Full grails-data-hibernate7-core suite: 3007 tests, 0 failures, 24 pre-existing skips
  • codenarcMain/codenarcTest/checkstyleMain/checkstyleTest: clean

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

…olumn

property type: 'text' resolved through Hibernate's legacy named-type
lookup to StandardBasicTypes.TEXT, whose JDBC type code is the legacy
java.sql.Types.LONGVARCHAR. Dialects (e.g. Postgres) don't render that
legacy code as their native unbounded text/CLOB type, falling back to a
bounded VARCHAR at Hibernate's generic Length.LONG default (32600) once
no explicit column length is set. On schema update, altering an existing
column down to that bound fails once any row already holds more text.

Bind the modern SqlTypes.LONG32VARCHAR JDBC type directly for this case
instead of going through the ambiguous legacy type name, restoring the
"CLOB or TEXT depending on dialect" behavior the mapping DSL docs already
promise for type: 'text'.

Fixes #16010

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 20, 2026 01:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a Grails Data Hibernate 7 mapping issue where type: 'text' incorrectly generated a bounded varchar(32600) on PostgreSQL, causing schema-update failures once existing data exceeded that limit. The change targets the Hibernate binding layer to force an unbounded/text-capable JDBC type, and adds an integration test that validates the generated column type against a real Postgres instance.

Changes:

  • Special-case type: 'text' during simple value binding to set an explicit JDBC type (SqlTypes.LONG32VARCHAR) so dialects render an unbounded TEXT/CLOB as appropriate.
  • Add a Testcontainers-based integration spec that asserts Postgres produces data_type = 'text' and character_maximum_length = null for a type: 'text' mapped property.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SimpleValueBinder.java Forces modern JDBC type binding for type: 'text' to avoid Postgres rendering a bounded VARCHAR.
grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/GormTextTypeColumnIntegrationSpec.groovy New Postgres Testcontainers integration test asserting type: 'text' creates an unbounded text column.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 51.4297%. Comparing base (ce1884f) to head (a8f5031).
⚠️ Report is 8 commits behind head on 8.0.x.

Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #16020        +/-   ##
==================================================
- Coverage     51.4393%   51.4297%   -0.0096%     
+ Complexity      17724      17723         -1     
==================================================
  Files            2039       2039                
  Lines           95497      95507        +10     
  Branches        16564      16564                
==================================================
- Hits            49123      49119         -4     
- Misses          39076      39088        +12     
- Partials         7298       7300         +2     

see 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

// default (32600) when no explicit column length is set - see GH-16010. The modern
// SqlTypes.LONG32VARCHAR code is what dialects actually map to an unbounded type, so
// bind that directly rather than going through the ambiguous legacy type name.
basicValue.setExplicitJdbcTypeAccess(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Isn't this dialect specific? This silently changes the DDL of type: 'text' on H2, MySQL/MariaDB, Oracle, and SQL Server:

  • H2 (the default test/dev DB): LONG32VARCHAR → character large object/clob rather than whatever LONGVARCHAR rendered before.
  • Oracle: → clob instead of the legacy path.
  • SQL Server: → varchar(max).
  • MySQL: → longtext.

i think this is ok, but I think in hiberate5 it was only supported if the dialect supported it.

@bito-code-review

Copy link
Copy Markdown

The change explicitly maps the 'text' type to SqlTypes.LONG32VARCHAR to ensure consistent, unbounded behavior across different database dialects. By bypassing the legacy java.sql.Types.LONGVARCHAR resolution, it avoids dialect-specific fallback to bounded VARCHAR types when no explicit length is defined. This approach is intended to provide a more predictable mapping to native unbounded types (like CLOB, LONGTEXT, or VARCHAR(MAX)) as supported by the underlying Hibernate 6 dialect registry.

grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/SimpleValueBinder.java

if (isUnboundedTextType(typeName) && simpleValue instanceof BasicValue basicValue) {
            basicValue.setExplicitJdbcTypeAccess(
                    typeConfiguration -> typeConfiguration.getJdbcTypeRegistry().getDescriptor(SqlTypes.LONG32VARCHAR));
        } else {
            simpleValue.setTypeName(typeName);
        }

@jdaugherty jdaugherty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I was looking at hibernate 6 and I think a better solution would be to support maxSize: null. Take a look at my comment that I generated with AI after discussing this issue.

@jdaugherty

Copy link
Copy Markdown
Contributor

The direction is right, but rather than overriding the JDBC type via setExplicitJdbcTypeAccess(SqlTypes.LONG32VARCHAR), consider leaning on Hibernate's capacity-dependent DDL type mechanism instead.

Why text fails while clob works: type: 'text' resolves to StandardBasicTypes.TEXT (Types.LONGVARCHAR), which since Hibernate 6 is no longer a LOB code — it's registered as a "long variant of varchar" and, with no explicit length, gets the implied default Length.LONG (32600). The dialect's capacity registry then correctly renders a bounded varchar(32600) because 32600 fits below the switch-over threshold. Types.CLOB has no length semantics, so dialects render their native unbounded type directly.

Hibernate 6 introduced exactly this mechanism as the replacement for the removed TextType and friends: @Column(length = Length.LONG32) is the documented way to get an unbounded string column without @Lob (see the Hibernate 6 migration guide's "Basic Types / LONGVARCHAR" changes and org.hibernate.Length). Setting column.setLength(Length.LONG32) lets every dialect's capacity-based DDL registry pick its own unbounded type (text on Postgres, longtext on MySQL, varchar(max) on SQL Server, clob on Oracle/H2) with no per-dialect assumptions.

One wrinkle worth addressing at the mapping level: GORM always defaults a length for string columns (the length is never truly "unset" by the time the column is bound), so there is currently no way for a user to express "unbounded" other than smuggling it through a type name like text. It may be cleaner to support a null/unset length in the column config as a first-class way to say unbounded — mapping it to Length.LONG32 at bind time — rather than special-casing the text type name. That gives users an explicit knob that works with any string type, and type: 'text' can then just be the case that opts into it by default.

Advantages over the explicit-JDBC-type override:

  • Uses the public, dialect-portable mechanism instead of bypassing type-name binding (setTypeName is currently skipped in the text branch while setTypeParameters still runs).
  • Composes naturally with maxSize/inList/explicit column length: — an explicit bound simply keeps the column bounded, preserving prior behavior, whereas the JDBC-type override plus StringColumnConstraintsBinder setting a length now produces dialect-dependent results.
  • Avoids forcing a character JDBC descriptor onto any property whose resolved type name happens to be text regardless of its Java type.

A couple of test asks either way:

  • The only coverage is the Postgres Testcontainers spec gated on isDockerAvailable() — a container-less CI run exercises none of this. An H2-based DDL assertion would cover the default path everywhere.
  • A test for type: 'text' combined with maxSize: — that combination previously produced varchar(maxSize) on all dialects and its behavior is now unspecified.

…eutral Length.LONG32

Address PR review feedback on the type: 'text' unbounded-column fix: instead of
overriding the JDBC type descriptor with SqlTypes.LONG32VARCHAR, set the column's
length to Hibernate 6+'s documented Length.LONG32 sentinel and let each dialect's
own capacity-dependent DDL type registry resolve the native unbounded type (text,
longtext, CLOB). This composes correctly with maxSize/inList/explicit column
length instead of racing them, and keeps SimpleValueBinder as a pure orchestrator -
the length decision now lives in StringColumnConstraintsBinder, which already owns
string column length for maxSize/inList.

Extends test coverage to close the "Postgres-only" gap: adds an H2-based spec that
runs without Docker so container-less CI still exercises this path, and extends the
Testcontainers spec to MySQL and MariaDB (Oracle excluded, matching the flaky-in-CI
precedent already established in RLikeHibernate7Spec). Reverting the fix locally
confirmed MySQL/MariaDB were independently affected (TEXT capped at 65535), not
just Postgres.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@borinquenkid

Copy link
Copy Markdown
Member Author

@jdaugherty Thanks for the review — implemented your suggested direction in 3c0350da8f5031.

Mechanism swap: dropped setExplicitJdbcTypeAccess(SqlTypes.LONG32VARCHAR) entirely. type: 'text' now resolves through the normal setTypeName/named-type path exactly as before (no longer skipped), and when no explicit length is configured, we set column.setLength(Length.LONG32) and let each dialect's own capacity-dependent DdlTypeRegistry pick the native unbounded type — text on Postgres, longtext on MySQL/MariaDB, CLOB on H2/Oracle. No JDBC type is forced onto the column regardless of its underlying Java type.

Composability: confirmed maxSize/inList/explicit column length: all still take priority and keep the column bounded — added a test for exactly the type: 'text' + maxSize combination you flagged.

Where the logic lives: moved the length decision into StringColumnConstraintsBinder (which already owned maxSize/inList length computation) rather than SimpleValueBinder reaching into Column after the fact — SimpleValueBinder just resolves and forwards the type name now, no Length/Column API surface on it at all.

Test coverage, per your ask:

  • New GormTextTypeColumnLengthSpec — H2, no Testcontainers/Docker requirement, so container-less CI now exercises this path (previously only the Postgres Testcontainers spec did).
  • Extended GormTextTypeColumnIntegrationSpec to a Postgres/MySQL/MariaDB matrix (Oracle excluded, matching the existing RLikeHibernate7Spec precedent for Oracle Testcontainers flakiness). Reverting the fix locally confirmed MySQL/MariaDB were independently broken too (TEXT capped at 65535, not just Postgres's 32600) — real regression coverage, not just Postgres.

One thing from your comment I did not implement: the broader idea of a first-class "unset length = unbounded" knob usable by any string type, not just type: 'text'. Kept this PR scoped to the reported bug; happy to open a follow-up if that's wanted.

Full grails-data-hibernate7-core suite: 3020 tests, 0 failures. codeStyle clean.

@jdaugherty jdaugherty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this solution is acceptable but it does diverge from Hibernate. Making maxSize nullable or be the larger size like hibernate did in 6 is probably a more correct solution. We effectively have added support to all databases for the text type though. It seems like we should document this divergence at a minimum.

@jdaugherty

Copy link
Copy Markdown
Contributor

@borinquenkid let's document this so we can merge this change for now? I do think a follow-up ticket needs filed, adding 'text' type on a databaes that doesn't support it really should error and hibernate has changed the semantics in later versions - we really should match their binding style or at least make it clear when gorm is "calculating" something for us vs how to override the underlying mappings.

…lumn

Addresses review feedback on #16020 asking to document that GORM
computes the concrete SQL type (text/longtext/CLOB) per dialect rather
than emitting a literal "text" type, and that every Hibernate-shipped
Dialect defines this mapping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@testlens-app

testlens-app Bot commented Jul 21, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: a5f651e
▶️ Tests: 46128 executed
⚪️ Checks: 59/59 completed


Learn more about TestLens at testlens.app.

@jamesfredley jamesfredley moved this to Todo in Apache Grails Jul 24, 2026
@borinquenkid borinquenkid added this to the grails:8.0.0-RC1 milestone Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

[Grails8 Hibernate7] Mapping a property to "type: 'text'" throws a CommandAcceptanceException

4 participants