Skip to content

Commit af2f08b

Browse files
committed
#392 Support maxFieldSize
1 parent 015ef2b commit af2f08b

File tree

21 files changed

+639
-72
lines changed

21 files changed

+639
-72
lines changed

devdoc/jdp/jdp-2025-07-statement-max-field-size.adoc

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
== Status
77

8-
* Draft
9-
* Proposed for: Jaybird 7
8+
* Published: 2025-11-13
9+
* Implemented in: Jaybird 7
1010

1111
== Type
1212

@@ -35,36 +35,57 @@ ____
3535
==== 8.3.1 Silent Truncation
3636
3737
The `Statement.setMaxFieldSize` method allows a maximum size (in bytes) to be set.
38-
This limit applies only to the `BINARY`, `VARBINARY`, `LONGVARBINARY`, `CHAR`, `VARCHAR`, `LONGVARCHAR`, `NCHAR`, `NVARCHAR`, and `LONGNVARCHAR` data types.
38+
This limit applies only to the (JDBC types) `BINARY`, `VARBINARY`, `LONGVARBINARY`, `CHAR`, `VARCHAR`, `LONGVARCHAR`, `NCHAR`, `NVARCHAR`, and `LONGNVARCHAR` data types.
3939
If a limit has been set using `setMaxFieldSize` and there is an attempt to read data that exceeds the limit, any truncation that occurs as a result of exceeding the set limit will _not_ be reported.
4040
____
4141

42-
Firebird doesn't provide a way to restrict the maximum number of bytes returned that will not result in string truncation errors for longer values, or -- for shorter values -- would result in _"`wrong`"_ client-side length derivations of variable length encodings like UTF8 (which would result in excessive truncation).
42+
Firebird doesn't provide a way to restrict the maximum number of bytes returned that will not result in string truncation errors for longer values, or -- for shorter values -- would result in "`wrong`" client-side length derivations of variable length encodings like UTF8 (which would result in excessive truncation).
4343

44-
Jaybird considers `BLOB SUB_TYPE TEXT` to be `LONGVARCHAR` and `BLOB SUB_TYPE BINARY` to be `LONGVARBINARY` (though all blob subtypes can be treated that way).
44+
Jaybird considers `BLOB SUB_TYPE TEXT` to be `LONGVARCHAR` and `BLOB SUB_TYPE BINARY` and other Firebird subtypes to be `LONGVARBINARY`.
45+
Custom subtypes (negative subtypes), are considered `Types.BLOB`
4546

4647
== Decision
4748

4849
Jaybird 7 implements support for `setMaxFieldSize`/`getMaxFieldSize` using client-side truncation.
49-
This will be implemented for `CHAR`, `VARCHAR`, `BINARY`, and `VARBINARY`.
50+
This will be implemented for `CHAR`, `VARCHAR`, `BINARY`, `VARBINARY`, `LONGVARCHAR` and `LONGVARBINARY`
5051

5152
To avoid problems with `RDB$DB_KEY` columns, implementation must ignore the limit for those columns, as indicated by `FieldDescriptor.isDbKey()`.
5253

53-
Decision for handling `LONGVARCHAR`/`LONGVARBINARY` is pending further investigation.
54-
Currently, we're leaning towards _not_ honouring the maximum field size, or only for `ResultSet.getBytes`/`getString`.
54+
Custom blob types (i.e. with a negative subtype) are not affected, as Jaybird considers those `Types.BLOB`.
5555

5656
== Consequences
5757

58-
Handling the truncation for `CHAR`, `VARCHAR`, `BINARY`, and `VARBINARY` will be delegated to the `FbStatement` implementation.
59-
The truncation will be done when receiving the data from the server (from the wire, or from fbclient).
60-
61-
`FbStatement` will receive an `int` property `maxFieldSize` (getter/setter pair) with a default implementation that ignores the set value and returns 0.
62-
The actual implementation will override this, and use the set value when receiving data from the server.
63-
64-
Caveats or possible pitfalls:
65-
66-
* Given the truncation happens at a set number of bytes, values in a multibyte character set (UTF8) might end in the Unicode replacement character due to truncation before the end of the encoded codepoint.
67-
* The detection of `RDB$DB_KEY` column will also ignore "`normal`" `BINARY` columns called `DB_KEY`.
58+
For `CHAR`, `VARCHAR`, `BINARY`, and `VARBINARY`, truncation is implemented in the `FbStatement` implementations in the GDS-ng layer.
59+
The truncation is done when fetching the data from the server (wire protocol), or from fbclient (native/embedded).
60+
61+
`FbStatement` has an `int` property `maxFieldSize` with a default implementation that ignores the set value and returns `0`.
62+
The actual implementations override this, and use the set value when receiving data from the server or native client.
63+
64+
For `LONGVARCHAR`/`LONGVARBINARY`, this is implemented in the relevant `FBField` implementations and configured from `FBResultSet`.
65+
66+
* For non-cached blobs, the maximum length is applied for `getBytes` and getters relying on `getBytes`, like the getters for string, numeric, Boolean and datetime types, and `getObject` that returns any of those types, but not to getters returning `Blob`, `Clob`, `InputStream`, or `Reader`.
67+
+
68+
In case of `InputStream` and `Reader`, such behaviour doesn't conform to the JDBC requirements, but this avoids complications in the implementation.
69+
We may address this in the future (e.g. maybe if we implement support for `Blob.getBinaryStream(long, long)`).
70+
+
71+
Supporting `Blob` and `Clob` for `LONGVARCHAR` and `LONGVARBINARY` is already a non-standard extension so -- in our opinion -- not subject to these requirements.
72+
* For cached blobs (as used in holdable or emulated scrollable result sets), the maximum length is also applied to methods returning `Blob`, `Clob`, `InputStream`, or `Reader`.
73+
74+
Caveats:
75+
76+
* Given the truncation happens at a set number of bytes, values in a multibyte character set (UTF8) might end in the Unicode replacement character (U+FFFD, '`�`') due to truncation before the end of the encoded codepoint.
77+
* Jaybird will accept any non-negative value for `maxFieldSize`.
78+
The JDBC apidoc recommends using values greater than `256`.
79+
* A too small `maxFieldSize` may result in "`wrong`" values being returned without error for numeric and Boolean getters;
80+
for datetime getters it may result in missing precision without errors, or parse errors.
81+
* Setting the max field size after execute may not be immediately applied to the current result set for `CHAR`, `VARCHAR`, `BINARY`, and `VARBINARY`:
82+
** For a locally cached result set: never, as the rows were truncated to the `maxFieldSize` on execute.
83+
** Otherwise, already fetched rows are truncated to the `maxFieldSize` on their fetch, and only rows returned by a subsequent fetch will apply the new limit.
84+
* For `LONGVARCHAR`/`LONGVARBINARY`, the value will be truncated on access, with the following caveats:
85+
** For cached blobs, the limit is applied on retrieval, and applies to all methods (as the cached value is truncated).
86+
** The value of a cached blob (holdable result set or emulated scrollable result set) will be truncated to the initial `maxFieldSize`.
87+
Subsequent use of a larger `maxFieldSize` will continue to return the shorter value.
88+
* The detection of `RDB$DB_KEY` columns includes "`normal`" `BINARY`/`CHAR CHARACTER SET OCTETS` columns called `DB_KEY`.
6889

6990
[appendix]
7091
== License Notice

jaybird-native/src/main/java/org/firebirdsql/gds/ng/jna/JnaStatement.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -349,16 +349,16 @@ protected RowValue toRowValue(RowDescriptor rowDescriptor, XSQLDA xSqlDa) {
349349
if (xSqlVar.sqlind.getValue() == XSQLVAR.SQLIND_NULL) {
350350
row.setFieldData(idx, null);
351351
} else {
352-
int bufferOffset;
353-
int bufferLength;
354-
355-
if (rowDescriptor.getFieldDescriptor(idx).isVarying()) {
352+
FieldDescriptor fieldDescriptor = rowDescriptor.getFieldDescriptor(idx);
353+
int bufferOffset = 0;
354+
int bufferLength = switch (fieldDescriptor.getType() & ~1) {
355+
case ISCConstants.SQL_VARYING -> {
356356
bufferOffset = 2;
357-
bufferLength = xSqlVar.sqldata.getShort(0) & 0xffff;
358-
} else {
359-
bufferOffset = 0;
360-
bufferLength = xSqlVar.sqllen & 0xffff;
357+
yield limitToMaxFieldSize(xSqlVar.sqldata.getShort(0) & 0xffff, fieldDescriptor);
361358
}
359+
case ISCConstants.SQL_TEXT -> limitToMaxFieldSize(xSqlVar.sqllen & 0xffff, fieldDescriptor);
360+
default -> xSqlVar.sqllen & 0xffff;
361+
};
362362

363363
byte[] data = new byte[bufferLength];
364364
xSqlVar.sqldata.read(bufferOffset, data, 0, bufferLength);
@@ -368,6 +368,10 @@ protected RowValue toRowValue(RowDescriptor rowDescriptor, XSQLDA xSqlDa) {
368368
return row;
369369
}
370370

371+
private int limitToMaxFieldSize(int actualSize, FieldDescriptor fieldDescriptor) {
372+
return isApplyMaxFieldSize(fieldDescriptor) ? Math.min(actualSize, maxFieldSize()) : actualSize;
373+
}
374+
371375
/**
372376
* {@inheritDoc}
373377
* <p>

src/docs/asciidoc/release_notes.adoc

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,43 @@ The `schema` can be `null` or empty string for schemaless tables (i.e. Firebird
583583
*** `schema()` with the schema, or empty string for schemaless (Firebird 5.0 or older) or if the table was not found
584584
*** `tableReference()` with the fully qualified and quoted table reference (i.e. `[<quoted-schema>.]<quoted-table-name>`)
585585
586+
[#stmt-max-field-size]
587+
=== Statement maximum field size (`maxFieldSize`) support
588+
589+
Support for truncating values returned by a result set to the `maxFieldSize` property of a `Statement` was implemented.
590+
This is a required JDBC feature, but in previous Jaybird 6 and older, the statement implementation only recorded the value, and then ignored it.
591+
592+
The `maxFieldSize` value is defined in bytes, and is applied when the value is greater than zero.
593+
Truncation occurs for `CHAR`, `VARCHAR`, `BINARY`, `VARBINARY`, `BLOB SUB_TYPE TEXT` (JDBC `LONGVARCHAR`), and `BLOB SUB_TYPE BINARY` and other Firebird built-in (positive) blob subtypes (JDBC `LONGVARBINARY`), but not custom (negative) subtypes (Jaybird considers those JDBC `BLOB`, and JDBC specifies that type should not be limited by `maxFieldSize`).
594+
595+
In case of `CHAR`, `VARCHAR`, `BINARY`, and `VARBINARY`, the truncation is performed when fetching a row (in the GDS-ng layer) _if_ it is not an `RDB$DB_KEY` column.
596+
This is a client-side truncation, and does not reduce the number of bytes transferred, only the number of bytes retained in memory and returned from the result set.
597+
598+
In the case of built-in (non-negative) blob subtypes, the truncation is performed in the result set field implementation in the JDBC layer.
599+
This is a client-side truncation, but it may reduce the number of bytes transferred _if_ the blob wasn't already transferred as an inline blob.
600+
601+
For these blob subtypes, the truncation will not occur for methods returning a `Blob`, `Clob`, `InputStream` or `Reader` _if_ the result set is not cached locally (cached result sets are used for holdable result sets and emulated scrollable result sets).
602+
In the case of `InputStream` and `Reader`, this doesn't conform to the JDBC requirements, but avoided complications in the implementation.
603+
We may address this in the future.
604+
605+
Caveats:
606+
607+
* Given the truncation happens at a set number of bytes, values in a multibyte character set (UTF8) might end in the Unicode replacement character (U+FFFD, '`&#xFFFD;`') due to truncation before the end of the encoded codepoint.
608+
* Jaybird will accept any non-negative value for `maxFieldSize`.
609+
The JDBC apidoc recommends using values greater than `256`.
610+
* A too small `maxFieldSize` may result in "`wrong`" values being returned without error for numeric and Boolean getters;
611+
for datetime getters it may result in missing precision without errors, or parse errors.
612+
* Setting the max field size after execute may not be immediately applied to the current result set for `CHAR`, `VARCHAR`, `BINARY`, and `VARBINARY`:
613+
** For a locally cached result set: never, as the rows were truncated to the `maxFieldSize` on execute.
614+
** Otherwise, already fetched rows are truncated to the `maxFieldSize` on their fetch, and only rows returned by a subsequent fetch will apply the new limit.
615+
* For `LONGVARCHAR`/`LONGVARBINARY`, the value will be truncated on access, with the following caveats:
616+
** For cached blobs, the limit is applied on retrieval, and applies to all methods (as the cached value is truncated).
617+
** The value of a cached blob (holdable result set or emulated scrollable result set) will be truncated to the initial `maxFieldSize`.
618+
Subsequent use of a larger `maxFieldSize` will continue to return the shorter value.
619+
* The detection of `RDB$DB_KEY` columns includes "`normal`" `BINARY`/`CHAR CHARACTER SET OCTETS` columns called `DB_KEY`.
620+
621+
For further details, see https://github.com/FirebirdSQL/jaybird/blob/master/devdoc/jdp/jdp-2025-07-statement-max-field-size.adoc[jdp-2025-07: Statement Max Field Size].
622+
586623
// TODO add major changes
587624
588625
[#other-fixes-and-changes]
@@ -616,7 +653,7 @@ In Jaybird 7, it no longer ignores these parameters when querying a Firebird 6.0
616653
617654
If you currently pass the "`wrong`" value for these methods, especially `""` (empty string, i.e. only return schemaless objects), you may get no or fewer results than expected.
618655
619-
[float]
656+
[discrete]
620657
===== `schema`
621658
622659
The `schema` parameter performs an exact, case-sensitive match on the schema name, unless it's `null` (i.e. don't filter by schema).
@@ -635,7 +672,7 @@ If your code currently passes `""` (empty string), you need to either replace it
635672
636673
(Unsupported metadata methods are not listed.)
637674
638-
[float]
675+
[discrete]
639676
===== `schemaPattern`
640677
641678
The `schemaPattern` parameter performs a case-sensitive `LIKE` match on the schema name, unless it's `null` (i.e. don't filter by schema).
@@ -780,6 +817,7 @@ If you are confronted with such a change, let us know on {firebird-java}[firebir
780817
* `FbWireOperations`
781818
** The `ProcessAttachCallback` parameter of `authReceiveResponse` was removed, as all implementations did nothing, and since protocol 13, it wasn't only called for the attach response
782819
** Interface `ProcessAttachCallback` was removed
820+
* The internal interface `org.firdsql.jdbc.field.BlobListenableField` was renamed to `BlobField` due to increased responsibilities
783821
784822
[#breaking-changes-unlikely]
785823
=== Unlikely breaking changes

src/main/org/firebirdsql/gds/impl/wire/XdrInputStream.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,15 @@ public byte[] readBuffer() throws IOException {
8787
return readBuffer(fixupLength(readInt()));
8888
}
8989

90-
private static int fixupLength(int len) {
91-
// Older Firebird versions may return a 32-bit value that is a sign-extended 16-bit value.
92-
// Firebird does something similar in remote/protocol.cpp (xdr_cstring_with_limit).
90+
/**
91+
* Fix up buffer length.
92+
* <p>
93+
* In some cases, older Firebird versions may return a 32-bit buffer length that is a sign-extended 16-bit value.
94+
* This method will only return the lower 16-bits of such values. Firebird does something similar in
95+
* {@code remote/protocol.cpp} ({@code xdr_cstring_with_limit}).
96+
* </p>
97+
*/
98+
public static int fixupLength(int len) {
9399
return len >>> 16 == 0xFFFF ? len & 0xFFFF : len;
94100
}
95101

src/main/org/firebirdsql/gds/ng/AbstractFbStatement.java

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
// SPDX-FileCopyrightText: Copyright 2013-2024 Mark Rotteveel
1+
// SPDX-FileCopyrightText: Copyright 2013-2025 Mark Rotteveel
22
// SPDX-FileCopyrightText: Copyright 2019 Vasiliy Yashkov
33
// SPDX-License-Identifier: LGPL-2.1-or-later
44
package org.firebirdsql.gds.ng;
55

66
import org.firebirdsql.gds.ISCConstants;
77
import org.firebirdsql.gds.JaybirdErrorCodes;
8+
import org.firebirdsql.gds.ng.fields.FieldDescriptor;
89
import org.firebirdsql.gds.ng.fields.RowDescriptor;
910
import org.firebirdsql.gds.ng.fields.RowValue;
1011
import org.firebirdsql.gds.ng.listeners.*;
@@ -56,6 +57,7 @@ public abstract class AbstractFbStatement implements FbStatement {
5657
@SuppressWarnings("java:S3077")
5758
private volatile FbTransaction transaction;
5859
private String cursorName;
60+
private int maxFieldSize;
5961
private long timeout;
6062

6163
private final TransactionListener transactionListener = new TransactionListener() {
@@ -847,6 +849,47 @@ protected final String getCursorName() {
847849
return cursorName;
848850
}
849851

852+
@Override
853+
public final void setMaxFieldSize(int max) throws SQLException {
854+
try (var ignored = withLock()) {
855+
checkStatementValid(StatementState.NEW);
856+
if (max < 0) {
857+
throw FbExceptionBuilder.forNonTransientException(JaybirdErrorCodes.jb_invalidStringLength)
858+
.messageParameter("max", Integer.MAX_VALUE, max)
859+
.toSQLException();
860+
}
861+
maxFieldSize = max;
862+
}
863+
}
864+
865+
@Override
866+
public final int getMaxFieldSize() throws SQLException {
867+
try (var ignored = withLock()) {
868+
checkStatementValid(StatementState.NEW);
869+
return maxFieldSize;
870+
}
871+
}
872+
873+
@Override
874+
public final int maxFieldSize() {
875+
return maxFieldSize;
876+
}
877+
878+
/**
879+
* Determines if max field size should be applied for the value of {@code fieldDescriptor}.
880+
*
881+
* @param fieldDescriptor
882+
* field descriptor
883+
* @return {@code true} if max field size should be applied, {@code false} otherwise
884+
* @since 7
885+
*/
886+
protected final boolean isApplyMaxFieldSize(FieldDescriptor fieldDescriptor) {
887+
final int maxFieldSize = this.maxFieldSize;
888+
return maxFieldSize != 0 && maxFieldSize < fieldDescriptor.getLength()
889+
&& (fieldDescriptor.isVarying()
890+
|| fieldDescriptor.isFbType(ISCConstants.SQL_TEXT) && !fieldDescriptor.isDbKey());
891+
}
892+
850893
/**
851894
* @return The timeout value, or {@code 0} if the timeout is larger than supported
852895
* @throws SQLException

src/main/org/firebirdsql/gds/ng/FbStatement.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package org.firebirdsql.gds.ng;
55

66
import org.firebirdsql.gds.BatchParameterBuffer;
7+
import org.firebirdsql.gds.ng.fields.FieldDescriptor;
78
import org.firebirdsql.gds.ng.fields.RowDescriptor;
89
import org.firebirdsql.gds.ng.fields.RowValue;
910
import org.firebirdsql.gds.ng.listeners.ExceptionListenable;
@@ -644,6 +645,67 @@ default BatchParameterBuffer createBatchParameterBuffer() throws SQLException {
644645
throw new FBDriverNotCapableException("implementation does not support createBatchParameterBuffer");
645646
}
646647

648+
/**
649+
* Sets the maximum number of bytes that can be returned for a &quot;string&quot; column ({@code CHAR},
650+
* {@code VARCHAR}, {@code BINARY} or {@code VARBINARY}).
651+
* <p>
652+
* This method is used to fulfil the contract of {@link java.sql.Statement#setMaxFieldSize(int)} for
653+
* non-{@code BLOB} columns. If this method is called after a fetch, the new limit will only be applied for rows
654+
* returned by a subsequent fetch.
655+
* </p>
656+
* <p>
657+
* Firebird doesn't support limiting the column sizes without producing string truncation errors, so this will only
658+
* result in client-side truncation.
659+
* </p>
660+
* <p>
661+
* Implementations must ignore this limit for {@code RDB$DB_KEY} columns (see {@link FieldDescriptor#isDbKey()}).
662+
* </p>
663+
* <p>
664+
* The default implementation in this interface does nothing.
665+
* </p>
666+
*
667+
* @param max
668+
* maximum number of bytes, {@code 0} means there is no limit
669+
* @throws SQLException
670+
* if a database access error occurs, this method is called on a closed statement or the condition
671+
* {@code max >= 0} is not satisfied
672+
* @see #getMaxFieldSize()
673+
* @since 7
674+
*/
675+
default void setMaxFieldSize(int max) throws SQLException {
676+
}
677+
678+
/**
679+
* Gets the maximum number of bytes that can be returned for a &quot;string&quot; column ({@code CHAR},
680+
* {@code VARCHAR}, {@code BINARY} or {@code VARBINARY}).
681+
* <p>
682+
* The default implementation in this interface returns 0.
683+
* </p>
684+
*
685+
* @return maximum number of bytes, {@code 0} means there is no limit
686+
* @throws SQLException
687+
* if a database access error occurs, this method is called on a closed statement
688+
* @see #setMaxFieldSize(int)
689+
* @since 7
690+
*/
691+
default int getMaxFieldSize() throws SQLException {
692+
return 0;
693+
}
694+
695+
/**
696+
* Direct access (no locks, no validity checks) to the max field size.
697+
* <p>
698+
* The default implementation in this interface returns 0.
699+
* </p>
700+
*
701+
* @return max field size
702+
* @see #getMaxFieldSize()
703+
* @since 7
704+
*/
705+
default int maxFieldSize() {
706+
return 0;
707+
}
708+
647709
/**
648710
* Locks the lock with {@link java.util.concurrent.locks.Lock#lock()} (or equivalent).
649711
* <p>

0 commit comments

Comments
 (0)