From 9591d7d71d41b473b949b6e167c21e421f7bd6f3 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Wed, 18 Jun 2025 11:19:22 +0200
Subject: [PATCH 01/64] #882 jdp-2025-06: Schema Support
---
devdoc/jdp/jdp-2025-06-schema-support.adoc | 119 +++++++++++++++++++++
1 file changed, 119 insertions(+)
create mode 100644 devdoc/jdp/jdp-2025-06-schema-support.adoc
diff --git a/devdoc/jdp/jdp-2025-06-schema-support.adoc b/devdoc/jdp/jdp-2025-06-schema-support.adoc
new file mode 100644
index 000000000..e94008965
--- /dev/null
+++ b/devdoc/jdp/jdp-2025-06-schema-support.adoc
@@ -0,0 +1,119 @@
+= jdp-2025-06: Schema Support
+
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LicenseRef-PDL-1.0
+
+== Status
+
+* Draft
+* Proposed for: Jaybird 7, potential backport to Jaybird 6 and/or Jaybird 5
+
+== Type
+
+* Feature-Specification
+
+== Context
+
+Firebird 6.0 introduces support for schemas.
+To quote from `README.schemas.md` (of an early snapshot):
+
+____
+Firebird 6.0 introduces support for schemas in the database.
+Schemas are not an optional feature, so every Firebird 6 database has at least a `SYSTEM` schema, reserved for Firebird system objects (`RDB$*` and `MON$*`).
+
+User objects live in different schemas, which may be the automatically created `PUBLIC` schema or user-defined ones.
+It is not allowed (except for indexes) to create or modify objects in the `SYSTEM` schema.
+____
+
+Important details related to schemas:
+
+* Search path, defaults to `PUBLIC, SYSTEM`.
+The session default can be configured with `isc_dpb_search_path` (string DPB item).
+The current search path can be altered with `SET SEARCH_PATH TO ...`.
+`ALTER SESSION RESET` reverts to the session default.
+* The "`current`" schema cannot be set separately;
+the first valid schema listed in the search path is considered the current schema.
+* `CURRENT_SCHEMA` and `RDB$GET_CONTEXT('SYSTEM', 'CURRENT_SCHEMA')` return the first valid schema from the search path
+* `RDB$GET_CONTEXT('SYSTEM', 'SEARCH_PATH')` returns the current search path
+* Objects not qualified with a schema name will be resolved using the current search path.
+This is done -- with some exceptions -- at prepare time.
+* TBP has new item `isc_tpb_lock_table_schema` to specify the schema of a table to be locked (1 byte length + string data)
+* Gbak has additional options to include/exclude (skip) schema datas in backup or restore, similar to existing options to include/exclude tables
+* Gstat has additional options to specify a schema for operations involving tables
+* For validation, `val_sch_incl` and `val_sch_excl` (I don't think we use the equivalent,`val_tab_incl`/`val_tab_excl` in Jaybird, so might not be relevant)
+
+JDBC defines various methods, parameters, and return values or result set columns that are or are related to schemas.
+
+These are:
+
+* ...
+
+Jaybird 5 is the "`long-term support`" version for Java 8.
+
+[NOTE]
+====
+This document is in flux, and will be updated during implementation of the feature.
+====
+
+== Decision
+
+Jaybird 7 will implement schema support for Firebird 6.0.
+When Jaybird 7 is used on Firebird 5.0 or older, it will behave as before (no schemas at all).
+
+Further details can be found in <>.
+
+Decision on backport to Jaybird 6 and/or Jaybird 5 is pending, and may be subject of a separate JDP.
+
+[#consequences]
+== Consequences
+
+The following changes are made to Jaybird to support schemas when connecting to Firebird 6.0 or higher:
+
+* Connection property `searchPath` (alias `search_path`, `isc_dpb_search_path`) to configure the default session search path.
++
+On Firebird 5.0 and older, this will be silently ignored.
+* In internal queries in Jaybird, and fully qualified object names, we'll always use the quoted identifier `"SYSTEM"`, because `SYSTEM` is SQL:2023 reserved word.
+* `Connection.getSchema()` will return the result of `select CURRENT_SCHEMA from "SYSTEM".RDB$DATABASE`;
+the connection will not store this value
+* `Connection.setSchema(String)` will query the current search path, if not previously called, it will prepend the schema name to the search path, otherwise it will _replace_ the previously prepended schema name.
+The schema name is stored _only_ for this replacement operation (i.e. it will not be returned by `getSchema`!)
++
+** The name must match exactly as is stored in the metadata (it is always case-sensitive!)
+** Jaybird will take care of quoting, and will always quote
+** Existence of the schema is **not** checked, so it is possible the current schema does not change with this operation, as `CURRENT_SCHEMA` reports the first _valid_ schema
+** JDBC specifies that "`__Calling ``setSchema`` has no effect on previously created or prepared Statement objects.__`";
+Jaybird cannot honour this requirement for plain `Statement`, as schema resolution is on prepare time (which for plain `Statement` is on execute), and not always for `CallableStatement` (as the implementation may delay actual prepare until execution).
+* Request `isc_info_sql_relation_schema` after preparing a query, record it in `FieldDescriptor`, and return it were relevant for JDBC (e.g. `ResultSetMetaData.getSchemaName(int)`)
+** For Firebird 5.0 and older, we need to ensure that JDBC methods continue to report the correct value (i.e. ``""`` for schema-less objects)
+* A Firebird 6.0 variant of the `DatabaseMetaData` and other internal metadata queries needs to be written to address at least the following things:
+** Explicitly qualify metadata tables with `"SYSTEM"`, so the queries will work even if `SYSTEM` is not on the search path.
+** Returning schema names, and qualified object names where relevant (e.g. in `DatabaseMetaData` result sets)
+** Include schema names in joins to ensure matching the right objects
+** Allow searching for schema or schema pattern as specified in JDBC, or were needed for internal metadata queries
+** TODO: investigate need for backwards compatible behaviour for `DatabaseMetaData` parameter `schema` with value `""` ("`__[...] retrieves those without a schema__`").
+*** Maybe make it search `PUBLIC`, or `PUBLIC` and `SYSTEM`, or those on the search path?
+*** Or add a compatibility connection property to make it behave as `null` ("`__[...] means that the schema name should not be used to narrow the search__`")?
+*** Or just accept it as a breaking change?
+* TODO: Define effects for management API
+
+[appendix]
+== License Notice
+
+The contents of this Documentation are subject to the Public Documentation License Version 1.0 (the “License”);
+you may only use this Documentation if you comply with the terms of this License.
+A copy of the License is available at https://firebirdsql.org/en/public-documentation-license/.
+
+The Original Documentation is "`jdp-2025-06: Schema Support`".
+The Initial Writer of the Original Documentation is Mark Rotteveel, Copyright © 2025.
+All Rights Reserved.
+(Initial Writer contact(s): mark (at) lawinegevaar (dot) nl).
+
+////
+Contributor(s): ______________________________________.
+Portions created by ______ are Copyright © _________ [Insert year(s)].
+All Rights Reserved.
+(Contributor contact(s): ________________ [Insert hyperlink/alias]).
+////
+
+The exact file history is recorded in our Git repository;
+see https://github.com/FirebirdSQL/jaybird
\ No newline at end of file
From 9865b6bb2e8d313c9b2f383471642877ea058082 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Wed, 18 Jun 2025 12:03:11 +0200
Subject: [PATCH 02/64] #882 Fix tests failing due to Firebird 6 schemas
---
.../firebirdsql/util/FirebirdSupportInfo.java | 12 +++++++++--
.../gds/ng/AbstractStatementTest.java | 20 +++++++++++++++----
.../FBPreparedStatementGeneratedKeysTest.java | 8 ++++++--
.../jdbc/FBStatementGeneratedKeysTest.java | 8 ++++++--
.../org/firebirdsql/jdbc/FBStatementTest.java | 8 ++++++--
5 files changed, 44 insertions(+), 12 deletions(-)
diff --git a/src/main/org/firebirdsql/util/FirebirdSupportInfo.java b/src/main/org/firebirdsql/util/FirebirdSupportInfo.java
index c93a6da1e..1b566b40d 100644
--- a/src/main/org/firebirdsql/util/FirebirdSupportInfo.java
+++ b/src/main/org/firebirdsql/util/FirebirdSupportInfo.java
@@ -517,8 +517,7 @@ public int getSystemTableCount() {
case 3 -> 50;
case 4 -> 54;
case 5 -> 56;
- // Intentionally not merged with case 5 as it is likely to change during Firebird 6 development
- case 6 -> 56;
+ case 6 -> 57;
default -> -1;
};
}
@@ -785,6 +784,15 @@ public boolean supportsInlineBlobs() {
return isVersionEqualOrAbove(5, 0, 3);
}
+ /**
+ * Reports if schemas are supported.
+ *
+ * @return {@code true} if schemas are not supported, {@code false} otherwise
+ */
+ public boolean supportsSchemas() {
+ return isVersionEqualOrAbove(6);
+ }
+
/**
* @return {@code true} when this Firebird version is considered a supported version
*/
diff --git a/src/test/org/firebirdsql/gds/ng/AbstractStatementTest.java b/src/test/org/firebirdsql/gds/ng/AbstractStatementTest.java
index eb738ac96..be44ce567 100644
--- a/src/test/org/firebirdsql/gds/ng/AbstractStatementTest.java
+++ b/src/test/org/firebirdsql/gds/ng/AbstractStatementTest.java
@@ -385,7 +385,11 @@ public void test_GetExecutionPlan_withStatementPrepared() throws Exception {
String executionPlan = statement.getExecutionPlan();
- assertEquals("PLAN (RDB$DATABASE NATURAL)", executionPlan, "Unexpected plan for prepared statement");
+ String expected = getDefaultSupportInfo().supportsSchemas()
+ ? "PLAN (\"SYSTEM\".\"RDB$DATABASE\" NATURAL)"
+ : "PLAN (RDB$DATABASE NATURAL)";
+
+ assertEquals(expected, executionPlan, "Unexpected plan for prepared statement");
}
@Test
@@ -428,9 +432,17 @@ public void test_GetExplainedExecutionPlan_withStatementPrepared() throws Except
String executionPlan = statement.getExplainedExecutionPlan();
- assertEquals("""
- Select Expression
- -> Table "RDB$DATABASE" Full Scan""", executionPlan, "Unexpected plan for prepared statement");
+ //@formatter:off
+ String expected = getDefaultSupportInfo().supportsSchemas()
+ ? """
+ Select Expression
+ -> Table "SYSTEM"."RDB$DATABASE" Full Scan"""
+ : """
+ Select Expression
+ -> Table "RDB$DATABASE" Full Scan""";
+ //@formatter:on
+
+ assertEquals(expected, executionPlan, "Unexpected plan for prepared statement");
}
@Test
diff --git a/src/test/org/firebirdsql/jdbc/FBPreparedStatementGeneratedKeysTest.java b/src/test/org/firebirdsql/jdbc/FBPreparedStatementGeneratedKeysTest.java
index 99e8dab98..dd4e8b2c1 100644
--- a/src/test/org/firebirdsql/jdbc/FBPreparedStatementGeneratedKeysTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBPreparedStatementGeneratedKeysTest.java
@@ -261,7 +261,9 @@ void testPrepare_INSERT_returnGeneratedKeys_nonExistentTable() {
assertThat(exception, allOf(
errorCode(equalTo(errorCode)),
sqlState(equalTo("42S02")),
- fbMessageContains(errorCode, "TABLE_NON_EXISTENT")));
+ anyOf(
+ fbMessageContains(errorCode, "TABLE_NON_EXISTENT"),
+ fbMessageContains(errorCode, "\"TABLE_NON_EXISTENT\""))));
}
/**
@@ -377,7 +379,9 @@ void testPrepare_INSERT_columnNames_nonExistentColumn() {
assertThat(exception, allOf(
errorCode(equalTo(ISCConstants.isc_dsql_field_err)),
sqlState(equalTo("42S22")),
- message(containsString("Column unknown; NON_EXISTENT"))));
+ anyOf(
+ message(containsString("Column unknown; NON_EXISTENT")),
+ message(containsString("Column unknown; \"NON_EXISTENT\"")))));
}
// TODO In the current implementation executeUpdate uses almost identical logic as execute, decide to test separately or not
diff --git a/src/test/org/firebirdsql/jdbc/FBStatementGeneratedKeysTest.java b/src/test/org/firebirdsql/jdbc/FBStatementGeneratedKeysTest.java
index 1baf8526f..086a17ac0 100644
--- a/src/test/org/firebirdsql/jdbc/FBStatementGeneratedKeysTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBStatementGeneratedKeysTest.java
@@ -302,7 +302,9 @@ void testExecute_INSERT_returnGeneratedKeys_nonExistentTable() throws Exception
assertThat(exception, allOf(
errorCode(equalTo(errorCode)),
sqlState(equalTo("42S02")),
- fbMessageContains(errorCode, "TABLE_NON_EXISTENT")));
+ anyOf(
+ fbMessageContains(errorCode, "TABLE_NON_EXISTENT"),
+ fbMessageContains(errorCode, "\"TABLE_NON_EXISTENT\""))));
}
}
@@ -497,7 +499,9 @@ void testExecute_INSERT_columnNames_nonExistentColumn() throws Exception {
assertThat(exception, allOf(
errorCode(equalTo(ISCConstants.isc_dsql_field_err)),
sqlState(equalTo("42S22")),
- message(containsString("Column unknown; NON_EXISTENT"))));
+ anyOf(
+ message(containsString("Column unknown; NON_EXISTENT")),
+ message(containsString("Column unknown; \"NON_EXISTENT\"")))));
}
}
diff --git a/src/test/org/firebirdsql/jdbc/FBStatementTest.java b/src/test/org/firebirdsql/jdbc/FBStatementTest.java
index 48d215f87..9928cd794 100644
--- a/src/test/org/firebirdsql/jdbc/FBStatementTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBStatementTest.java
@@ -402,7 +402,9 @@ void testEscapeProcessingDisabled() throws SQLException {
SQLException exception = assertThrows(SQLException.class, () -> stmt.executeQuery(testQuery));
assertThat(exception, allOf(
- message(containsString("Column unknown; {FN")),
+ anyOf(
+ message(containsString("Column unknown; {FN")),
+ message(containsString("Column unknown; \"{FN\""))),
sqlStateEquals("42S22")));
}
}
@@ -1076,7 +1078,9 @@ void psqlExceptionWithParametersRendering() throws Exception {
end
"""));
assertThat(sqle, message(allOf(
- startsWith("exception 1; EX_PARAM; something wrong in PARAMETER_1"),
+ anyOf(
+ startsWith("exception 1; EX_PARAM; something wrong in PARAMETER_1"),
+ startsWith("exception 1; \"PUBLIC\".\"EX_PARAM\"; something wrong in PARAMETER_1")),
// The exception parameter value should not be repeated after the formatted message
not(containsString("something wrong in PARAMETER_1; PARAMETER_1")))));
}
From a3c597965e2dfced7199a13a288d3d2be129e82c Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Thu, 19 Jun 2025 10:55:49 +0200
Subject: [PATCH 03/64] #882 Implement to DatabaseMetaData.getSchemas return
schemas
---
src/docs/asciidoc/release_notes.adoc | 15 +
.../firebirdsql/jdbc/FBDatabaseMetaData.java | 10 +-
.../firebirdsql/jdbc/metadata/GetSchemas.java | 92 +++++-
.../extension/UsesDatabaseExtension.java | 6 +-
.../jdbc/FBDatabaseMetaDataSchemasTest.java | 275 ++++++++++++++++++
5 files changed, 384 insertions(+), 14 deletions(-)
create mode 100644 src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataSchemasTest.java
diff --git a/src/docs/asciidoc/release_notes.adoc b/src/docs/asciidoc/release_notes.adoc
index 9d95fd6e5..c99699374 100644
--- a/src/docs/asciidoc/release_notes.adoc
+++ b/src/docs/asciidoc/release_notes.adoc
@@ -508,6 +508,21 @@ Artificial testing on local WiFi with small blobs (200 bytes) shows a 30,000-45,
This optimization was backported to Jaybird 5.0.8 and Jaybird 6.0.2.
+[#schemas]
+=== Schema support
+
+Firebird 6.0 introduces schemas, and Jaybird 7 provides support for schemas as defined in the JDBC specification.
+
+Changes include:
+
+* `DatabaseMetaData`
+** `getSchemas()` returns all defined schemas
+** `getSchemas(String catalog, String schemaPattern)` returns all schemas matching the `LIKE` pattern `schemaPattern`, with the following caveats
+*** `catalog` non-empty will return no rows;
+we recommend to always use `null` for `catalog`.
+*** `schemaPattern` empty will return no rows (there are no schemas with an empty name);
+use `null` or `"%"` to match all schemas
+
// TODO add major changes
[#other-fixes-and-changes]
diff --git a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
index 40d560f8a..0fbacf302 100644
--- a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
@@ -1263,6 +1263,11 @@ public ResultSet getSchemas() throws SQLException {
return getSchemas(null, null);
}
+ @Override
+ public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException {
+ return GetSchemas.create(getDbMetadataMediator()).getSchemas(catalog, schemaPattern);
+ }
+
/**
* {@inheritDoc}
*
@@ -1765,11 +1770,6 @@ public ResultSet getFunctions(String catalog, String schemaPattern, String funct
return GetFunctions.create(getDbMetadataMediator()).getFunctions(catalog, functionNamePattern);
}
- @Override
- public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException {
- return GetSchemas.create(getDbMetadataMediator()).getSchemas();
- }
-
@Override
public boolean isWrapperFor(Class> iface) throws SQLException {
return iface != null && iface.isAssignableFrom(FBDatabaseMetaData.class);
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java b/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java
index a4206c29e..f3594c6de 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java
@@ -4,13 +4,14 @@
package org.firebirdsql.jdbc.metadata;
import org.firebirdsql.gds.ng.fields.RowDescriptor;
+import org.firebirdsql.gds.ng.fields.RowValue;
import org.firebirdsql.jdbc.DbMetadataMediator;
-import org.firebirdsql.jdbc.FBResultSet;
+import org.firebirdsql.jdbc.DbMetadataMediator.MetadataQuery;
+import org.firebirdsql.util.FirebirdSupportInfo;
import java.sql.ResultSet;
import java.sql.SQLException;
-import static java.util.Collections.emptyList;
import static org.firebirdsql.gds.ISCConstants.SQL_VARYING;
import static org.firebirdsql.jdbc.metadata.FbMetadataConstants.OBJECT_NAME_LENGTH;
@@ -20,23 +21,100 @@
* @author Mark Rotteveel
* @since 5
*/
-public final class GetSchemas {
+public abstract class GetSchemas extends AbstractMetadataMethod {
private static final RowDescriptor ROW_DESCRIPTOR = DbMetadataMediator.newRowDescriptorBuilder(2)
.at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_SCHEM", "TABLESCHEMAS").addField()
.at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_CATALOG", "TABLESCHEMAS").addField()
.toRowDescriptor();
- private GetSchemas() {
+ private GetSchemas(DbMetadataMediator mediator) {
+ super(ROW_DESCRIPTOR, mediator);
}
- public ResultSet getSchemas() throws SQLException {
- return new FBResultSet(ROW_DESCRIPTOR, emptyList());
+ public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException {
+ if (!(catalog == null || catalog.isEmpty()) || "".equals(schemaPattern)) {
+ // matching schema name not possible
+ return createEmpty();
+ }
+ MetadataQuery metadataQuery = createGetSchemasQuery(schemaPattern);
+ return createMetaDataResultSet(metadataQuery);
}
+ @Override
+ final RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQLException {
+ return valueBuilder
+ .at(0).setString(rs.getString("TABLE_SCHEM"))
+ .toRowValue(true);
+ }
+
+ abstract MetadataQuery createGetSchemasQuery(String schemaPattern);
+
@SuppressWarnings("unused")
public static GetSchemas create(DbMetadataMediator mediator) {
- return new GetSchemas();
+ FirebirdSupportInfo firebirdSupportInfo = mediator.getFirebirdSupportInfo();
+ // NOTE: Indirection through static method prevents unnecessary classloading
+ if (firebirdSupportInfo.isVersionEqualOrAbove(6)) {
+ return FB6.createInstance(mediator);
+ } else {
+ return FB5.createInstance(mediator);
+ }
+ }
+
+ /**
+ * Implementation for Firebird 5.0 and older.
+ */
+ private static final class FB5 extends GetSchemas {
+
+ private FB5(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetSchemas createInstance(DbMetadataMediator mediator) {
+ return new FB5(mediator);
+ }
+
+ @Override
+ public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException {
+ return createEmpty();
+ }
+
+ @Override
+ MetadataQuery createGetSchemasQuery(String schemaPattern) {
+ throw new UnsupportedOperationException("This method should not get called for Firebird 5.0 and older");
+ }
+
+ }
+
+ /**
+ * Implementation for Firebird 6.0 and higher.
+ */
+ private static final class FB6 extends GetSchemas {
+
+ private static final String GET_SCHEMAS_FRAGMENT_6 = """
+ select RDB$SCHEMA_NAME as TABLE_SCHEM
+ from "SYSTEM".RDB$SCHEMAS
+ """;
+
+ private static final String GET_SCHEMAS_ORDER_BY_6 = "\norder by RDB$SCHEMA_NAME";
+
+ private FB6(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetSchemas createInstance(DbMetadataMediator mediator) {
+ return new FB6(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetSchemasQuery(String schemaPattern) {
+ var schemaClause = new Clause("RDB$SCHEMA_NAME", schemaPattern);
+ String sql = GET_SCHEMAS_FRAGMENT_6
+ + (schemaClause.hasCondition() ? "\nwhere " + schemaClause.getCondition(false) : "")
+ + GET_SCHEMAS_ORDER_BY_6;
+ return new MetadataQuery(sql, Clause.parameters(schemaClause));
+ }
+
}
}
diff --git a/src/test/org/firebirdsql/common/extension/UsesDatabaseExtension.java b/src/test/org/firebirdsql/common/extension/UsesDatabaseExtension.java
index b7b782b4c..2c886833b 100644
--- a/src/test/org/firebirdsql/common/extension/UsesDatabaseExtension.java
+++ b/src/test/org/firebirdsql/common/extension/UsesDatabaseExtension.java
@@ -11,9 +11,10 @@
import org.junit.jupiter.api.extension.ExtensionContext;
import java.sql.SQLException;
-import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import static java.util.Collections.emptyList;
import static org.firebirdsql.common.FBTestProperties.createFBManager;
@@ -40,7 +41,7 @@ public abstract class UsesDatabaseExtension {
private final boolean initialCreate;
private FBManager fbManager = null;
private final List initStatements;
- private final List databasesToDrop = new ArrayList<>();
+ private final Set databasesToDrop = new HashSet<>();
private UsesDatabaseExtension(boolean initialCreate) {
this(initialCreate, emptyList());
@@ -72,6 +73,7 @@ void sharedAfter() {
} catch (Exception e){
System.getLogger(getClass().getName()).log(System.Logger.Level.ERROR, "Exception dropping DBs", e);
} finally {
+ databasesToDrop.clear();
try {
if (!(fbManager == null || fbManager.getState().equals("Stopped"))) {
fbManager.stop();
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataSchemasTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataSchemasTest.java
new file mode 100644
index 000000000..faf3b0cca
--- /dev/null
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataSchemasTest.java
@@ -0,0 +1,275 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jdbc;
+
+import org.firebirdsql.common.extension.UsesDatabaseExtension;
+import org.firebirdsql.util.FirebirdSupportInfo;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.NullSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import static org.firebirdsql.common.FBTestProperties.getConnectionViaDriverManager;
+import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
+import static org.firebirdsql.common.FbAssumptions.assumeFeature;
+import static org.firebirdsql.common.FbAssumptions.assumeFeatureMissing;
+import static org.firebirdsql.common.assertions.ResultSetAssertions.assertNextRow;
+import static org.firebirdsql.common.assertions.ResultSetAssertions.assertNoNextRow;
+
+/**
+ * Tests for {@link FBDatabaseMetaData} for schema related metadata.
+ */
+class FBDatabaseMetaDataSchemasTest {
+
+ @RegisterExtension
+ static final UsesDatabaseExtension.UsesDatabaseForAll usesDatabase = UsesDatabaseExtension.usesDatabaseForAll();
+
+ private static final MetadataResultSetDefinition getSchemasDefinition =
+ new MetadataResultSetDefinition(SchemaMetaData.class);
+ public static final List DEFAULT_SCHEMAS = List.of("PUBLIC", "SYSTEM");
+
+ private static Connection con;
+ private static DatabaseMetaData dbmd;
+
+ @BeforeAll
+ static void setupAll() throws SQLException {
+ con = getConnectionViaDriverManager();
+ dbmd = con.getMetaData();
+ }
+
+ @AfterEach
+ void cleanupAdditionalSchemas() throws SQLException {
+ if (!getDefaultSupportInfo().supportsSchemas()) return;
+ var schemasToDrop = new HashSet();
+ try (ResultSet schemas = dbmd.getSchemas()) {
+ while (schemas.next()) {
+ String schemaName = schemas.getString("TABLE_SCHEM");
+ if (DEFAULT_SCHEMAS.contains(schemaName)) continue;
+ schemasToDrop.add(schemaName);
+ }
+ }
+ if (schemasToDrop.isEmpty()) return;
+ try (var stmt = con.createStatement()) {
+ con.setAutoCommit(false);
+ for (String schemaName : schemasToDrop) {
+ stmt.addBatch("drop schema " + stmt.enquoteIdentifier(schemaName, false));
+ }
+ stmt.executeBatch();
+ } finally {
+ con.setAutoCommit(true);
+ }
+ }
+
+ @AfterAll
+ static void tearDownAll() throws SQLException {
+ try {
+ con.close();
+ } finally {
+ con = null;
+ dbmd = null;
+ }
+ }
+
+ /**
+ * Tests the ordinal positions and types for the metadata columns of getSchemas(...).
+ */
+ @Test
+ void testSchemaMetaDataColumns() throws Exception {
+ try (ResultSet columns = dbmd.getSchemas(null, "doesnotexist")) {
+ getSchemasDefinition.validateResultSetColumns(columns);
+ }
+ }
+
+ @Test
+ void getSchemas_noSchemaSupport_noRows() throws Exception {
+ requireNoSchemaSupport();
+ ResultSet schemas = dbmd.getSchemas();
+ assertNoNextRow(schemas);
+ }
+
+ @ParameterizedTest
+ @NullSource
+ @ValueSource(strings = "%")
+ void getSchemas_string_string_noSchemaSupport_noRows(String schemaPattern) throws Exception {
+ requireNoSchemaSupport();
+ validateSchemaMetaDataNoRow(null, schemaPattern);
+ }
+
+ @Test
+ void getSchemas_string_string_emptySchemaPattern_noRows() throws Exception {
+ // No rows expected with and without schema support
+ validateSchemaMetaDataNoRow(null, "");
+ }
+
+ @Test
+ void getSchemas_schemaSupport_defaults() throws Exception {
+ requireSchemaSupport();
+ try (ResultSet schemas = dbmd.getSchemas()) {
+ // calling getSchemas() is equivalent to calling getSchemas(null, null)
+ validateSchemaMetaData(null, schemas, DEFAULT_SCHEMAS);
+ }
+ }
+
+ @ParameterizedTest
+ @NullSource
+ @ValueSource(strings = "%")
+ void getSchemas_string_string_schemaSupport_defaults(String schemaPattern) throws Exception {
+ requireSchemaSupport();
+ validateSchemaMetaData(null, schemaPattern, List.of("PUBLIC", "SYSTEM"));
+ }
+
+ @ParameterizedTest
+ @CsvSource(useHeadersInDisplayName = true, textBlock = """
+ schemaPattern, expectedSchema
+ PUBLIC, PUBLIC
+ SYSTEM, SYSTEM
+ PUB%, PUBLIC
+ SYS%, SYSTEM
+ PUBL_C, PUBLIC
+ S_STEM, SYSTEM
+ """)
+ void getSchemas_string_string_schemaSupport_singleSchemaExpected(String schemaPattern, String expectedSchemaName)
+ throws Exception {
+ requireSchemaSupport();
+ validateSchemaMetaData(null, schemaPattern, List.of(expectedSchemaName));
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ void getSchemas_string_string_schemaSupport_catalogNullOrEmpty_defaults(String catalog) throws Exception {
+ requireSchemaSupport();
+ validateSchemaMetaData(catalog, "%", DEFAULT_SCHEMAS);
+ }
+
+ @Test
+ void getSchemas_string_string_catalogNonEmpty_noRows() throws Exception {
+ // Should return no rows with or without schema support
+ validateSchemaMetaDataNoRow("NON_EMPTY", null);
+ }
+
+ @Test
+ void getSchema_string_string_schemaSupport_returnsUserDefinedSchemas() throws Exception {
+ requireSchemaSupport();
+ try (var stmt = con.createStatement()) {
+ con.setAutoCommit(false);
+ for (String schema : List.of("ABC", "QRS", "TUV")) {
+ stmt.execute("create schema " + schema);
+ }
+ } finally {
+ con.setAutoCommit(true);
+ }
+ validateSchemaMetaData("", "%", List.of("ABC", "PUBLIC", "QRS", "SYSTEM", "TUV"));
+ }
+
+ private static void requireNoSchemaSupport() {
+ assumeFeatureMissing(FirebirdSupportInfo::supportsSchemas, "Test requires no schema support");
+ }
+
+ private static void requireSchemaSupport() {
+ assumeFeature(FirebirdSupportInfo::supportsSchemas, "Test requires schema support");
+ }
+
+ /**
+ * Helper method for test methods that retrieve metadata expecting no results.
+ *
+ * @param schemaPattern
+ * pattern of the schema name
+ */
+ private void validateSchemaMetaDataNoRow(String catalog, String schemaPattern) throws SQLException {
+ try (ResultSet schemas = dbmd.getSchemas(catalog, schemaPattern)) {
+ assertNoNextRow(schemas, "Expected empty result set for requesting " + schemaPattern);
+ }
+ }
+
+ /**
+ * Helper method for test methods that retrieve metadata expecting schemas.
+ *
+ * @param schemaPattern
+ * pattern of the schema name
+ * @param expectedSchemaNames
+ * expected schema names in order of appearance
+ */
+ private void validateSchemaMetaData(String catalog, String schemaPattern, List expectedSchemaNames)
+ throws SQLException {
+ try (ResultSet schemas = dbmd.getSchemas(catalog, schemaPattern)) {
+ validateSchemaMetaData(schemaPattern, schemas, expectedSchemaNames);
+ }
+ }
+
+ /**
+ * Helper method for test methods that retrieve metadata expecting schemas.
+ *
+ * @param schemaPattern
+ * pattern of the schema name (for diagnostics only)
+ * @param schemas
+ * schema result set as returned by one of the database metadata {@code getSchema} methods
+ * @param expectedSchemaNames
+ * expected schema names in order of appearance
+ */
+ private static void validateSchemaMetaData(String schemaPattern, ResultSet schemas,
+ List expectedSchemaNames) throws SQLException {
+ for (String expectedSchemaName : expectedSchemaNames) {
+ assertNextRow(schemas,
+ "Pattern '%s', expected row for schema name %s".formatted(schemaPattern, expectedSchemaName));
+ Map valueRules = getDefaultValueValidationRules();
+ valueRules.put(SchemaMetaData.TABLE_SCHEM, expectedSchemaName);
+ getSchemasDefinition.validateRowValues(schemas, valueRules);
+ }
+ assertNoNextRow(schemas, "Expected no more schema names for pattern " + schemaPattern);
+ }
+
+ private static final Map DEFAULT_COLUMN_VALUES;
+ static {
+ Map defaults = new EnumMap<>(SchemaMetaData.class);
+ defaults.put(SchemaMetaData.TABLE_CATALOG, null);
+
+ DEFAULT_COLUMN_VALUES = Collections.unmodifiableMap(defaults);
+ }
+
+ private static Map getDefaultValueValidationRules() {
+ return new EnumMap<>(DEFAULT_COLUMN_VALUES);
+ }
+
+ /**
+ * Columns defined for the getTables() metadata.
+ */
+ private enum SchemaMetaData implements MetaDataInfo {
+ TABLE_SCHEM(1, String.class),
+ TABLE_CATALOG(2, String.class),
+ ;
+
+ private final int position;
+ private final Class> columnClass;
+
+ SchemaMetaData(int position, Class> columnClass) {
+ this.position = position;
+ this.columnClass = columnClass;
+ }
+
+ @Override
+ public int getPosition() {
+ return position;
+ }
+
+ @Override
+ public Class> getColumnClass() {
+ return columnClass;
+ }
+ }
+}
From 7deb6ed741afe9375e1c6f4adbaaf4135bcbd06c Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Thu, 19 Jun 2025 11:23:45 +0200
Subject: [PATCH 04/64] #882 Implement DatabaseMetaData informational methods
to report schema support
---
.../firebirdsql/jdbc/FBDatabaseMetaData.java | 17 ++++++------
.../jdbc/FBDatabaseMetaDataTest.java | 27 +++++++++++++++++++
2 files changed, 35 insertions(+), 9 deletions(-)
diff --git a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
index 0fbacf302..b346877ff 100644
--- a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
@@ -764,12 +764,11 @@ public boolean supportsLimitedOuterJoins() throws SQLException {
/**
* {@inheritDoc}
*
- * @return the vendor term, always {@code null} because schemas are not supported by database server (see JDBC CTS
- * for details).
+ * @return the vendor term; for Firebird 5.0 and older always {@code null} because schemas are not supported
*/
@Override
public String getSchemaTerm() throws SQLException {
- return null;
+ return firebirdSupportInfo.supportsSchemas() ? "SCHEMA" : null;
}
@Override
@@ -814,27 +813,27 @@ public String getCatalogSeparator() throws SQLException {
@Override
public boolean supportsSchemasInDataManipulation() throws SQLException {
- return false;
+ return firebirdSupportInfo.supportsSchemas();
}
@Override
public boolean supportsSchemasInProcedureCalls() throws SQLException {
- return false;
+ return firebirdSupportInfo.supportsSchemas();
}
@Override
public boolean supportsSchemasInTableDefinitions() throws SQLException {
- return false;
+ return firebirdSupportInfo.supportsSchemas();
}
@Override
public boolean supportsSchemasInIndexDefinitions() throws SQLException {
- return false;
+ return firebirdSupportInfo.supportsSchemas();
}
@Override
public boolean supportsSchemasInPrivilegeDefinitions() throws SQLException {
- return false;
+ return firebirdSupportInfo.supportsSchemas();
}
/**
@@ -1031,7 +1030,7 @@ public int getMaxIndexLength() throws SQLException {
@Override
public int getMaxSchemaNameLength() throws SQLException {
- return 0; //No schemas
+ return firebirdSupportInfo.supportsSchemas() ? getMaxObjectNameLength() : 0;
}
/**
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataTest.java
index 25032199e..ddadb9088 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataTest.java
@@ -9,6 +9,7 @@
import org.firebirdsql.common.DdlHelper;
import org.firebirdsql.common.extension.UsesDatabaseExtension;
+import org.firebirdsql.util.FirebirdSupportInfo;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -809,6 +810,32 @@ void testGetIdentifierQuoteString_dialect3Db(String connectionDialect, String ex
}
}
+ @Test
+ void testGetSchemaTerm() throws Exception {
+ final String expected = getDefaultSupportInfo().supportsSchemas() ? "SCHEMA" : null;
+ assertEquals(expected, dmd.getSchemaTerm(), "schemaTerm");
+ }
+
+ @Test
+ void testGetMaxSchemaNameLength() throws Exception {
+ FirebirdSupportInfo supportInfo = getDefaultSupportInfo();
+ final int expected = getDefaultSupportInfo().supportsSchemas()
+ ? supportInfo.maxIdentifierLengthCharacters() : 0;
+ assertEquals(expected, dmd.getMaxSchemaNameLength(), "maxSchemaNameLength");
+ }
+
+ @Test
+ void testSupportsSchemasInXXX() {
+ final boolean expected = getDefaultSupportInfo().supportsSchemas();
+ assertAll(
+ () -> assertEquals(expected, dmd.supportsSchemasInDataManipulation(), "DataManipulation"),
+ () -> assertEquals(expected, dmd.supportsSchemasInIndexDefinitions(), "IndexDefinitions"),
+ () -> assertEquals(expected, dmd.supportsSchemasInPrivilegeDefinitions(), "PrivilegeDefinitions"),
+ () -> assertEquals(expected, dmd.supportsSchemasInProcedureCalls(), "ProcedureCalls"),
+ () -> assertEquals(expected, dmd.supportsSchemasInTableDefinitions(), "TableDefinitions")
+ );
+ }
+
@SuppressWarnings("SameParameterValue")
private void createPackage(String packageName, String procedureName) throws Exception {
try (Statement stmt = connection.createStatement()) {
From 740ce4da7cd89690ca543ef8a2e3d7b52c8f6d0d Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Thu, 19 Jun 2025 12:54:40 +0200
Subject: [PATCH 05/64] Use isNullOrEmpty
---
src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java | 7 ++++---
src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataTest.java | 2 +-
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java b/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java
index f3594c6de..d878190e8 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2023 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2022-2023 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -13,6 +13,7 @@
import java.sql.SQLException;
import static org.firebirdsql.gds.ISCConstants.SQL_VARYING;
+import static org.firebirdsql.jaybird.util.StringUtils.isNullOrEmpty;
import static org.firebirdsql.jdbc.metadata.FbMetadataConstants.OBJECT_NAME_LENGTH;
/**
@@ -33,7 +34,7 @@ private GetSchemas(DbMetadataMediator mediator) {
}
public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException {
- if (!(catalog == null || catalog.isEmpty()) || "".equals(schemaPattern)) {
+ if (!isNullOrEmpty(catalog) || "".equals(schemaPattern)) {
// matching schema name not possible
return createEmpty();
}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataTest.java
index ddadb9088..278080ab1 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataTest.java
@@ -2,7 +2,7 @@
SPDX-FileCopyrightText: Copyright 2001-2002 David Jencks
SPDX-FileCopyrightText: Copyright 2002-2010 Roman Rokytskyy
SPDX-FileCopyrightText: Copyright 2002-2003 Blas Rodriguez Somoza
- SPDX-FileCopyrightText: Copyright 2011-2023 Mark Rotteveel
+ SPDX-FileCopyrightText: Copyright 2011-2025 Mark Rotteveel
SPDX-License-Identifier: LGPL-2.1-or-later
*/
package org.firebirdsql.jdbc;
From 286eea0444870ffd9ab7d967f28a93b654901698 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Thu, 19 Jun 2025 13:15:26 +0200
Subject: [PATCH 06/64] #882 Schema support for DatabaseMetaData.getProcedures
---
src/docs/asciidoc/release_notes.adoc | 6 +-
.../firebirdsql/jdbc/FBDatabaseMetaData.java | 5 +-
.../jdbc/metadata/GetProcedures.java | 127 ++++++++++++++++--
.../firebirdsql/jdbc/metadata/NameHelper.java | 62 +++++++--
.../FBDatabaseMetaDataProceduresTest.java | 27 ++--
.../jdbc/metadata/NameHelperTest.java | 18 ++-
6 files changed, 204 insertions(+), 41 deletions(-)
diff --git a/src/docs/asciidoc/release_notes.adoc b/src/docs/asciidoc/release_notes.adoc
index c99699374..f820b919b 100644
--- a/src/docs/asciidoc/release_notes.adoc
+++ b/src/docs/asciidoc/release_notes.adoc
@@ -516,10 +516,14 @@ Firebird 6.0 introduces schemas, and Jaybird 7 provides support for schemas as d
Changes include:
* `DatabaseMetaData`
+** `getProcedures` now uses the `schemaPattern` to filter by schema, with the following caveats
+*** `schemaPattern` empty will return no rows on Firebird 6.0 and higher (all procedures are in a schema);
+use `null` or `"%"` to match all schemas
+*** `catalog` is (still) ignored if `useCatalogAsPackage` is `false`
** `getSchemas()` returns all defined schemas
** `getSchemas(String catalog, String schemaPattern)` returns all schemas matching the `LIKE` pattern `schemaPattern`, with the following caveats
*** `catalog` non-empty will return no rows;
-we recommend to always use `null` for `catalog`.
+we recommend to always use `null` for `catalog`
*** `schemaPattern` empty will return no rows (there are no schemas with an empty name);
use `null` or `"%"` to match all schemas
diff --git a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
index b346877ff..0915d2b93 100644
--- a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
@@ -8,7 +8,7 @@
SPDX-FileCopyrightText: Copyright 2005 Michael Romankiewicz
SPDX-FileCopyrightText: Copyright 2005 Steven Jardine
SPDX-FileCopyrightText: Copyright 2007 Gabriel Reid
- SPDX-FileCopyrightText: Copyright 2011-2024 Mark Rotteveel
+ SPDX-FileCopyrightText: Copyright 2011-2025 Mark Rotteveel
SPDX-License-Identifier: LGPL-2.1-or-later
*/
package org.firebirdsql.jdbc;
@@ -1203,7 +1203,8 @@ public boolean dataDefinitionIgnoredInTransactions() throws SQLException {
@Override
public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern)
throws SQLException {
- return GetProcedures.create(getDbMetadataMediator()).getProcedures(catalog, procedureNamePattern);
+ return GetProcedures.create(getDbMetadataMediator())
+ .getProcedures(catalog, schemaPattern, procedureNamePattern);
}
/**
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java b/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java
index 22d0f7b5e..22e5f8bff 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -33,10 +33,12 @@ public abstract class GetProcedures extends AbstractMetadataMethod {
private static final String PROCEDURES = "PROCEDURES";
private static final String COLUMN_PROCEDURE_NAME = "RDB$PROCEDURE_NAME";
+ private static final String COLUMN_SCHEMA_NAME = "RDB$SCHEMA_NAME";
+ private static final String COLUMN_PACKAGE_NAME = "RDB$PACKAGE_NAME";
private static final RowDescriptor ROW_DESCRIPTOR = DbMetadataMediator.newRowDescriptorBuilder(9)
.at(0).simple(SQL_VARYING | 1, OBJECT_NAME_LENGTH, "PROCEDURE_CAT", PROCEDURES).addField()
- .at(1).simple(SQL_VARYING | 1, OBJECT_NAME_LENGTH, "PROCEDURE_SCHEM", "ROCEDURES").addField()
+ .at(1).simple(SQL_VARYING | 1, OBJECT_NAME_LENGTH, "PROCEDURE_SCHEM", PROCEDURES).addField()
.at(2).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PROCEDURE_NAME", PROCEDURES).addField()
.at(3).simple(SQL_VARYING, 31, "FUTURE1", PROCEDURES).addField()
.at(4).simple(SQL_VARYING, 31, "FUTURE2", PROCEDURES).addField()
@@ -55,34 +57,42 @@ private GetProcedures(DbMetadataMediator mediator) {
/**
* @see DatabaseMetaData#getProcedures(String, String, String)
*/
- public final ResultSet getProcedures(String catalog, String procedureNamePattern) throws SQLException {
+ public final ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern)
+ throws SQLException {
if ("".equals(procedureNamePattern)) {
return createEmpty();
}
- MetadataQuery metadataQuery = createGetProceduresQuery(catalog, procedureNamePattern);
+ MetadataQuery metadataQuery = createGetProceduresQuery(catalog, schemaPattern, procedureNamePattern);
return createMetaDataResultSet(metadataQuery);
}
@Override
final RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQLException {
String catalog = rs.getString("PROCEDURE_CAT");
+ String schema = rs.getString("PROCEDURE_SCHEM");
String procedureName = rs.getString("PROCEDURE_NAME");
return valueBuilder
.at(0).setString(catalog)
+ .at(1).setString(schema)
.at(2).setString(procedureName)
.at(6).setString(rs.getString("REMARKS"))
.at(7).setShort(rs.getShort("PROCEDURE_TYPE") == 0 ? procedureNoResult : procedureReturnsResult)
- .at(8).setString(toSpecificName(catalog, procedureName))
+ .at(8).setString(toSpecificName(catalog, schema, procedureName))
.toRowValue(true);
}
- abstract MetadataQuery createGetProceduresQuery(String catalog, String procedureNamePattern);
+ abstract MetadataQuery createGetProceduresQuery(String catalog, String schemaPattern, String procedureNamePattern);
public static GetProcedures create(DbMetadataMediator mediator) {
FirebirdSupportInfo firebirdSupportInfo = mediator.getFirebirdSupportInfo();
// NOTE: Indirection through static method prevents unnecessary classloading
- if (firebirdSupportInfo.isVersionEqualOrAbove(3)) {
+ if (firebirdSupportInfo.isVersionEqualOrAbove(6)) {
+ if (mediator.isUseCatalogAsPackage()) {
+ return FB6CatalogAsPackage.createInstance(mediator);
+ }
+ return FB6.createInstance(mediator);
+ }else if (firebirdSupportInfo.isVersionEqualOrAbove(3)) {
if (mediator.isUseCatalogAsPackage()) {
return FB3CatalogAsPackage.createInstance(mediator);
}
@@ -98,6 +108,7 @@ private static final class FB2_5 extends GetProcedures {
private static final String GET_PROCEDURES_FRAGMENT_2_5 = """
select
null as PROCEDURE_CAT,
+ null as PROCEDURE_SCHEM,
RDB$PROCEDURE_NAME as PROCEDURE_NAME,
RDB$DESCRIPTION as REMARKS,
RDB$PROCEDURE_OUTPUTS as PROCEDURE_TYPE
@@ -114,7 +125,7 @@ private static GetProcedures createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createGetProceduresQuery(String catalog, String procedureNamePattern) {
+ MetadataQuery createGetProceduresQuery(String catalog, String schemaPattern, String procedureNamePattern) {
Clause procedureNameClause = new Clause(COLUMN_PROCEDURE_NAME, procedureNamePattern);
String queryText = GET_PROCEDURES_FRAGMENT_2_5
+ procedureNameClause.getCondition("\nwhere ", "")
@@ -128,6 +139,7 @@ private static final class FB3 extends GetProcedures {
private static final String GET_PROCEDURES_FRAGMENT_3 = """
select
null as PROCEDURE_CAT,
+ null as PROCEDURE_SCHEM,
trim(trailing from RDB$PROCEDURE_NAME) as PROCEDURE_NAME,
RDB$DESCRIPTION as REMARKS,
RDB$PROCEDURE_OUTPUTS as PROCEDURE_TYPE
@@ -146,7 +158,7 @@ private static GetProcedures createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createGetProceduresQuery(String catalog, String procedureNamePattern) {
+ MetadataQuery createGetProceduresQuery(String catalog, String schemaPattern, String procedureNamePattern) {
Clause procedureNameClause = new Clause(COLUMN_PROCEDURE_NAME, procedureNamePattern);
String queryText = GET_PROCEDURES_FRAGMENT_3
+ procedureNameClause.getCondition("\nand ", "")
@@ -160,12 +172,12 @@ private static final class FB3CatalogAsPackage extends GetProcedures {
private static final String GET_PROCEDURES_FRAGMENT_3_W_PKG = """
select
coalesce(trim(trailing from RDB$PACKAGE_NAME), '') as PROCEDURE_CAT,
+ null as PROCEDURE_SCHEM,
trim(trailing from RDB$PROCEDURE_NAME) as PROCEDURE_NAME,
RDB$DESCRIPTION as REMARKS,
RDB$PROCEDURE_OUTPUTS as PROCEDURE_TYPE
from RDB$PROCEDURES""";
- // NOTE: Including RDB$PACKAGE_NAME so index can be used to sort
private static final String GET_PROCEDURES_ORDER_BY_3_W_PKG =
"\norder by RDB$PACKAGE_NAME nulls first, RDB$PROCEDURE_NAME";
@@ -178,16 +190,16 @@ private static GetProcedures createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createGetProceduresQuery(String catalog, String procedureNamePattern) {
+ MetadataQuery createGetProceduresQuery(String catalog, String schemaPattern, String procedureNamePattern) {
var clauses = new ArrayList(2);
if (catalog != null) {
// To quote from the JDBC API: "" retrieves those without a catalog; null means that the catalog name
// should not be used to narrow the search
if (catalog.isEmpty()) {
- clauses.add(Clause.isNullClause("RDB$PACKAGE_NAME"));
+ clauses.add(Clause.isNullClause(COLUMN_PACKAGE_NAME));
} else {
// Exact matches only
- clauses.add(Clause.equalsClause("RDB$PACKAGE_NAME", catalog));
+ clauses.add(Clause.equalsClause(COLUMN_PACKAGE_NAME, catalog));
}
}
clauses.add(new Clause(COLUMN_PROCEDURE_NAME, procedureNamePattern));
@@ -201,4 +213,91 @@ MetadataQuery createGetProceduresQuery(String catalog, String procedureNamePatte
return new MetadataQuery(sql, Clause.parameters(clauses));
}
}
+
+ private static final class FB6 extends GetProcedures {
+
+ private static final String GET_PROCEDURES_FRAGMENT_6 = """
+ select
+ null as PROCEDURE_CAT,
+ trim(trailing from RDB$SCHEMA_NAME) as PROCEDURE_SCHEM,
+ trim(trailing from RDB$PROCEDURE_NAME) as PROCEDURE_NAME,
+ RDB$DESCRIPTION as REMARKS,
+ RDB$PROCEDURE_OUTPUTS as PROCEDURE_TYPE
+ from "SYSTEM".RDB$PROCEDURES
+ where RDB$PACKAGE_NAME is null""";
+
+ // NOTE: Including RDB$PACKAGE_NAME so index can be used to sort
+ private static final String GET_PROCEDURES_ORDER_BY_6 =
+ "\norder by RDB$SCHEMA_NAME, RDB$PACKAGE_NAME, RDB$PROCEDURE_NAME";
+
+ private FB6(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetProcedures createInstance(DbMetadataMediator mediator) {
+ return new FB6(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetProceduresQuery(String catalog, String schemaPattern, String procedureNamePattern) {
+ var schemaNameClause = new Clause(COLUMN_SCHEMA_NAME, schemaPattern);
+ var procedureNameClause = new Clause(COLUMN_PROCEDURE_NAME, procedureNamePattern);
+ //@formatter:off
+ String queryText = GET_PROCEDURES_FRAGMENT_6
+ + (Clause.anyCondition(schemaNameClause, procedureNameClause)
+ ? "\nand " + Clause.conjunction(schemaNameClause, procedureNameClause)
+ : "")
+ + GET_PROCEDURES_ORDER_BY_6;
+ //@formatter:on
+ return new MetadataQuery(queryText, Clause.parameters(schemaNameClause, procedureNameClause));
+ }
+ }
+
+ private static final class FB6CatalogAsPackage extends GetProcedures {
+
+ private static final String GET_PROCEDURES_FRAGMENT_6_W_PKG = """
+ select
+ coalesce(trim(trailing from RDB$PACKAGE_NAME), '') as PROCEDURE_CAT,
+ trim(trailing from RDB$SCHEMA_NAME) as PROCEDURE_SCHEM,
+ trim(trailing from RDB$PROCEDURE_NAME) as PROCEDURE_NAME,
+ RDB$DESCRIPTION as REMARKS,
+ RDB$PROCEDURE_OUTPUTS as PROCEDURE_TYPE
+ from "SYSTEM".RDB$PROCEDURES""";
+
+ private static final String GET_PROCEDURES_ORDER_BY_6_W_PKG =
+ "\norder by RDB$PACKAGE_NAME nulls first, RDB$SCHEMA_NAME, RDB$PROCEDURE_NAME";
+
+ private FB6CatalogAsPackage(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetProcedures createInstance(DbMetadataMediator mediator) {
+ return new FB6CatalogAsPackage(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetProceduresQuery(String catalog, String schemaPattern, String procedureNamePattern) {
+ var clauses = new ArrayList(3);
+ clauses.add(new Clause(COLUMN_SCHEMA_NAME, schemaPattern));
+ if (catalog != null) {
+ // To quote from the JDBC API: "" retrieves those without a catalog; null means that the catalog name
+ // should not be used to narrow the search
+ if (catalog.isEmpty()) {
+ clauses.add(Clause.isNullClause(COLUMN_PACKAGE_NAME));
+ } else {
+ // Exact matches only
+ clauses.add(Clause.equalsClause(COLUMN_PACKAGE_NAME, catalog));
+ }
+ }
+ clauses.add(new Clause(COLUMN_PROCEDURE_NAME, procedureNamePattern));
+ //@formatter:off
+ String sql = GET_PROCEDURES_FRAGMENT_6_W_PKG
+ + (Clause.anyCondition(clauses)
+ ? "\nwhere " + Clause.conjunction(clauses)
+ : "")
+ + GET_PROCEDURES_ORDER_BY_6_W_PKG;
+ //@formatter:on
+ return new MetadataQuery(sql, Clause.parameters(clauses));
+ }
+ }
}
diff --git a/src/main/org/firebirdsql/jdbc/metadata/NameHelper.java b/src/main/org/firebirdsql/jdbc/metadata/NameHelper.java
index ec807a550..ef7651e3d 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/NameHelper.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/NameHelper.java
@@ -1,8 +1,12 @@
-// SPDX-FileCopyrightText: Copyright 2023 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2023-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
import org.firebirdsql.jdbc.QuoteStrategy;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+import static org.firebirdsql.jaybird.util.StringUtils.isNullOrEmpty;
/**
* Helper methods for generating object names.
@@ -10,35 +14,75 @@
* @author Mark Rotteveel
* @since 6
*/
+@NullMarked
final class NameHelper {
private NameHelper() {
// no instances
}
+ // TODO Remove once all metadata methods have been rewritten to support schemas
+ @Deprecated
+ static String toSpecificName(@Nullable String catalog, String routineName) {
+ return toSpecificName(catalog, null, routineName);
+ }
+
/**
* Generates a name for the {@code SPECIFIC_NAME} column of {@code getFunctions}, {@code getFunctionColumns},
* {@code getProcedures} and {@code getProcedureColumns}.
+ *
+ * The specific name is generated as follows:
+ *
+ *
*
* @param catalog
* generally {@code null}, or — when {@code useCatalogAsPackage = true} — an empty string (no
* package) or a package name
+ * @param schema
+ * schema name, or {@code null} for Firebird versions without schema support, empty string is handled same
+ * as {@code null}
* @param routineName
* name of the routine (procedure or function)
- * @return specific name: for non-packaged routines the {@code routineName}, of packaged routines, both
- * {@code catalog} (package name) and {@code routineName} are transformed to quoted identifiers and separated by
- * {@code .} (period)
+ * @return specific name
*/
- static String toSpecificName(String catalog, String routineName) {
- if (catalog == null || catalog.isEmpty()) {
+ static String toSpecificName(@Nullable String catalog, @Nullable String schema, String routineName) {
+ if (isNullOrEmpty(catalog) && isNullOrEmpty(schema)) {
return routineName;
}
var quoteStrategy = QuoteStrategy.DIALECT_3;
- // 5: 4 quotes + 1 separator
- var sb = new StringBuilder(catalog.length() + routineName.length() + 5);
- quoteStrategy.appendQuoted(catalog, sb).append('.');
+ // 8: 6 quotes + 2 separators
+ var sb = new StringBuilder(length(catalog) + length(schema) + routineName.length() + 8);
+ if (!isNullOrEmpty(schema)) {
+ quoteStrategy.appendQuoted(schema, sb).append('.');
+ }
+ // this order assumes the catalog actually represents the package name
+ if (!isNullOrEmpty(catalog)) {
+ quoteStrategy.appendQuoted(catalog, sb).append('.');
+ }
quoteStrategy.appendQuoted(routineName, sb);
return sb.toString();
}
+ private static int length(@Nullable String value) {
+ return value != null ? value.length() : 0;
+ }
+
}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataProceduresTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataProceduresTest.java
index ad4e593bd..3f56d38b2 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataProceduresTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataProceduresTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2012-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2012-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -37,6 +37,8 @@
*/
class FBDatabaseMetaDataProceduresTest {
+ // TODO Add tests for filtering by schema
+
private static final String CREATE_NORMAL_PROC_NO_RETURN = """
CREATE PROCEDURE normal_proc_no_return
( param1 VARCHAR(100))
@@ -302,10 +304,11 @@ private void validateProcedures(ResultSet procedures, List ex
}
static boolean isIgnoredProcedure(String specificName) {
- class Ignored {
+ final class Ignored {
// Skipping procedures from system packages (when testing with useCatalogAsPackage=true)
private static final List PREFIXES_TO_IGNORE =
- List.of("\"RDB$BLOB_UTIL\".", "\"RDB$PROFILER\".", "\"RDB$TIME_ZONE_UTIL\".", "\"RDB$SQL\".");
+ List.of("\"SYSTEM\".\"RDB$", "\"RDB$BLOB_UTIL\".", "\"RDB$PROFILER\".", "\"RDB$TIME_ZONE_UTIL\".",
+ "\"RDB$SQL\".");
}
return Ignored.PREFIXES_TO_IGNORE.stream().anyMatch(specificName::startsWith);
}
@@ -322,7 +325,7 @@ static Map modifyForUseCatalogAsPackage(ProcedureTest
static {
Map defaults = new EnumMap<>(ProcedureMetaData.class);
defaults.put(ProcedureMetaData.PROCEDURE_CAT, null);
- defaults.put(ProcedureMetaData.PROCEDURE_SCHEM, null);
+ defaults.put(ProcedureMetaData.PROCEDURE_SCHEM, ifSchemaElse("PUBLIC", null));
defaults.put(ProcedureMetaData.FUTURE1, null);
defaults.put(ProcedureMetaData.FUTURE2, null);
defaults.put(ProcedureMetaData.FUTURE3, null);
@@ -369,13 +372,18 @@ public Class> getColumnClass() {
}
}
+ private static String ifSchemaElse(String forSchema, String withoutSchema) {
+ return getDefaultSupportInfo().supportsSchemas() ? forSchema : withoutSchema;
+ }
+
private enum ProcedureTestData {
NORMAL_PROC_NO_RETURN("normal_proc_no_return", List.of(CREATE_NORMAL_PROC_NO_RETURN)) {
@Override
Map getSpecificValidationRules(Map rules) {
rules.put(ProcedureMetaData.PROCEDURE_NAME, "NORMAL_PROC_NO_RETURN");
rules.put(ProcedureMetaData.PROCEDURE_TYPE, DatabaseMetaData.procedureNoResult);
- rules.put(ProcedureMetaData.SPECIFIC_NAME, "NORMAL_PROC_NO_RETURN");
+ rules.put(ProcedureMetaData.SPECIFIC_NAME,
+ ifSchemaElse("\"PUBLIC\".\"NORMAL_PROC_NO_RETURN\"", "NORMAL_PROC_NO_RETURN"));
return rules;
}
},
@@ -386,7 +394,8 @@ Map getSpecificValidationRules(Map getSpecificValidationRules(Map getSpecificValidationRules(Map rules) {
rules.put(ProcedureMetaData.PROCEDURE_NAME, "quoted_proc_no_return");
rules.put(ProcedureMetaData.PROCEDURE_TYPE, DatabaseMetaData.procedureNoResult);
- rules.put(ProcedureMetaData.SPECIFIC_NAME, "quoted_proc_no_return");
+ rules.put(ProcedureMetaData.SPECIFIC_NAME,
+ ifSchemaElse("\"PUBLIC\".\"quoted_proc_no_return\"", "quoted_proc_no_return"));
return rules;
}
},
@@ -407,7 +417,8 @@ Map getSpecificValidationRules(Map, ROUTINE, ROUTINE
- PACKAGE, ROUTINE, "PACKAGE"."ROUTINE"
- WITH"DOUBLE, DOUBLE"QUOTE, "WITH""DOUBLE"."DOUBLE""QUOTE"
+ @CsvSource(useHeadersInDisplayName = true, textBlock = """
+ catalog, schema, routineName, expectedSpecificName
+ , , ROUTINE, ROUTINE
+ , PUBLIC, ROUTINE, "PUBLIC"."ROUTINE"
+ PACKAGE, , ROUTINE, "PACKAGE"."ROUTINE"
+ PACKAGE, PUBLIC, ROUTINE, "PUBLIC"."PACKAGE"."ROUTINE"
+ WITH"DOUBLE, , DOUBLE"QUOTE, "WITH""DOUBLE"."DOUBLE""QUOTE"
+ WITH"DOUBLE, PUBLIC, DOUBLE"QUOTE, "PUBLIC"."WITH""DOUBLE"."DOUBLE""QUOTE"
""", nullValues = "")
- void testToSpecificName(String catalog, String routineName, String expectedResult) {
- assertEquals(expectedResult, NameHelper.toSpecificName(catalog, routineName));
+ void testToSpecificName(String catalog, String schema, String routineName, String expectedResult) {
+ assertEquals(expectedResult, NameHelper.toSpecificName(catalog, schema, routineName));
}
}
\ No newline at end of file
From 44cccc2c80630bf10929e31f52e35435b042b47b Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Thu, 19 Jun 2025 17:09:40 +0200
Subject: [PATCH 07/64] #882 Use SYSTEM unquoted due to dialect 1 support
---
devdoc/jdp/jdp-2025-06-schema-support.adoc | 6 ++++--
src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java | 4 ++--
src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java | 6 ++----
3 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/devdoc/jdp/jdp-2025-06-schema-support.adoc b/devdoc/jdp/jdp-2025-06-schema-support.adoc
index e94008965..467553bb6 100644
--- a/devdoc/jdp/jdp-2025-06-schema-support.adoc
+++ b/devdoc/jdp/jdp-2025-06-schema-support.adoc
@@ -72,8 +72,8 @@ The following changes are made to Jaybird to support schemas when connecting to
* Connection property `searchPath` (alias `search_path`, `isc_dpb_search_path`) to configure the default session search path.
+
On Firebird 5.0 and older, this will be silently ignored.
-* In internal queries in Jaybird, and fully qualified object names, we'll always use the quoted identifier `"SYSTEM"`, because `SYSTEM` is SQL:2023 reserved word.
-* `Connection.getSchema()` will return the result of `select CURRENT_SCHEMA from "SYSTEM".RDB$DATABASE`;
+* In internal queries in Jaybird, and fully qualified object names, we'll use the regular -- unquoted -- identifier `SYSTEM`, even though `SYSTEM` is a SQL:2023 reserved word, to preserve dialect 1 compatibility.
+* `Connection.getSchema()` will return the result of `select CURRENT_SCHEMA from SYSTEM.RDB$DATABASE`;
the connection will not store this value
* `Connection.setSchema(String)` will query the current search path, if not previously called, it will prepend the schema name to the search path, otherwise it will _replace_ the previously prepended schema name.
The schema name is stored _only_ for this replacement operation (i.e. it will not be returned by `getSchema`!)
@@ -96,6 +96,8 @@ Jaybird cannot honour this requirement for plain `Statement`, as schema resoluti
*** Or just accept it as a breaking change?
* TODO: Define effects for management API
+Note to self: use `// TODO Add schema support` in places that you identify need to get/improve schema support, while working on schema support elsewhere
+
[appendix]
== License Notice
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java b/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java
index 22e5f8bff..d27deef10 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java
@@ -223,7 +223,7 @@ private static final class FB6 extends GetProcedures {
trim(trailing from RDB$PROCEDURE_NAME) as PROCEDURE_NAME,
RDB$DESCRIPTION as REMARKS,
RDB$PROCEDURE_OUTPUTS as PROCEDURE_TYPE
- from "SYSTEM".RDB$PROCEDURES
+ from SYSTEM.RDB$PROCEDURES
where RDB$PACKAGE_NAME is null""";
// NOTE: Including RDB$PACKAGE_NAME so index can be used to sort
@@ -262,7 +262,7 @@ private static final class FB6CatalogAsPackage extends GetProcedures {
trim(trailing from RDB$PROCEDURE_NAME) as PROCEDURE_NAME,
RDB$DESCRIPTION as REMARKS,
RDB$PROCEDURE_OUTPUTS as PROCEDURE_TYPE
- from "SYSTEM".RDB$PROCEDURES""";
+ from SYSTEM.RDB$PROCEDURES""";
private static final String GET_PROCEDURES_ORDER_BY_6_W_PKG =
"\norder by RDB$PACKAGE_NAME nulls first, RDB$SCHEMA_NAME, RDB$PROCEDURE_NAME";
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java b/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java
index d878190e8..514a924d0 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java
@@ -7,7 +7,6 @@
import org.firebirdsql.gds.ng.fields.RowValue;
import org.firebirdsql.jdbc.DbMetadataMediator;
import org.firebirdsql.jdbc.DbMetadataMediator.MetadataQuery;
-import org.firebirdsql.util.FirebirdSupportInfo;
import java.sql.ResultSet;
import java.sql.SQLException;
@@ -53,9 +52,8 @@ final RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) thr
@SuppressWarnings("unused")
public static GetSchemas create(DbMetadataMediator mediator) {
- FirebirdSupportInfo firebirdSupportInfo = mediator.getFirebirdSupportInfo();
// NOTE: Indirection through static method prevents unnecessary classloading
- if (firebirdSupportInfo.isVersionEqualOrAbove(6)) {
+ if (mediator.getFirebirdSupportInfo().isVersionEqualOrAbove(6)) {
return FB6.createInstance(mediator);
} else {
return FB5.createInstance(mediator);
@@ -94,7 +92,7 @@ private static final class FB6 extends GetSchemas {
private static final String GET_SCHEMAS_FRAGMENT_6 = """
select RDB$SCHEMA_NAME as TABLE_SCHEM
- from "SYSTEM".RDB$SCHEMAS
+ from SYSTEM.RDB$SCHEMAS
""";
private static final String GET_SCHEMAS_ORDER_BY_6 = "\norder by RDB$SCHEMA_NAME";
From d34af6183c90a2b322eaed8135b15c1efa68ea1b Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Sat, 21 Jun 2025 11:10:42 +0200
Subject: [PATCH 08/64] #882 Schema support for getBestRowIdentifier
---
.../firebirdsql/jdbc/FBDatabaseMetaData.java | 9 +-
.../org/firebirdsql/jdbc/FBRowUpdater.java | 5 +-
.../jdbc/metadata/GetBestRowIdentifier.java | 170 +++++++++---
.../firebirdsql/common/FBTestProperties.java | 13 +
...DatabaseMetaDataBestRowIdentifierTest.java | 244 ++++++++++++++++++
.../jdbc/FBDatabaseMetaDataTest.java | 34 ---
6 files changed, 396 insertions(+), 79 deletions(-)
create mode 100644 src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataBestRowIdentifierTest.java
diff --git a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
index 0915d2b93..2af73d71c 100644
--- a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
@@ -1372,14 +1372,19 @@ public ResultSet getTablePrivileges(String catalog, String schemaPattern, String
/**
* {@inheritDoc}
*
- * Jaybird considers the primary key (scoped as {@code bestRowSession} as the best identifier for all scopes.
- * Pseudo column {@code RDB$DB_KEY} (scoped as {@code bestRowTransaction} is considered the second-best alternative
+ * Jaybird considers the primary key (scoped as {@code bestRowSession}) as the best identifier for all scopes.
+ * Pseudo column {@code RDB$DB_KEY} (scoped as {@code bestRowTransaction}) is considered the second-best alternative
* for scopes {@code bestRowTemporary} and {@code bestRowTransaction} if {@code table} has no primary key.
*
*
* Jaybird currently considers {@code RDB$DB_KEY} to be {@link DatabaseMetaData#bestRowTransaction} even if the
* dbkey_scope is set to 1 (session). This may change in the future. See also {@link #getRowIdLifetime()}.
*
+ *
+ * On Firebird 6.0 and higher, passing {@code null} for {@code schema} will return columns of all
+ * tables with name {@code name}. It is recommended to always specify a non-{@code null} {@code schema} on
+ * Firebird 6.0 and higher.
+ *
*/
@Override
public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable)
diff --git a/src/main/org/firebirdsql/jdbc/FBRowUpdater.java b/src/main/org/firebirdsql/jdbc/FBRowUpdater.java
index 37060e979..5a55715c8 100644
--- a/src/main/org/firebirdsql/jdbc/FBRowUpdater.java
+++ b/src/main/org/firebirdsql/jdbc/FBRowUpdater.java
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2005-2011 Roman Rokytskyy
-// SPDX-FileCopyrightText: Copyright 2012-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2012-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -250,8 +250,9 @@ private static List deriveKeyColumns(String tableName, RowDescr
*/
private static List keyColumnsOfBestRowIdentifier(String tableName, RowDescriptor rowDescriptor,
DatabaseMetaData dbmd) throws SQLException {
+ // TODO Add schema support
try (ResultSet bestRowIdentifier = dbmd
- .getBestRowIdentifier("", "", tableName, DatabaseMetaData.bestRowTransaction, true)) {
+ .getBestRowIdentifier("", null, tableName, DatabaseMetaData.bestRowTransaction, true)) {
int bestRowIdentifierColumnCount = 0;
List keyColumns = new ArrayList<>();
while (bestRowIdentifier.next()) {
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java b/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java
index d631431df..6960432cd 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -41,7 +41,7 @@
* @author Mark Rotteveel
* @since 5
*/
-public final class GetBestRowIdentifier extends AbstractMetadataMethod {
+public abstract class GetBestRowIdentifier extends AbstractMetadataMethod {
private static final String ROWIDENTIFIER = "ROWIDENTIFIER";
@@ -56,31 +56,6 @@ public final class GetBestRowIdentifier extends AbstractMetadataMethod {
.at(7).simple(SQL_SHORT, 0, "PSEUDO_COLUMN", ROWIDENTIFIER).addField()
.toRowDescriptor();
- //@formatter:off
- private static final String GET_BEST_ROW_IDENT_START =
- "select\n"
- + " RF.RDB$FIELD_NAME as COLUMN_NAME,\n"
- + " F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n"
- + " F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n"
- + " F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n"
- + " F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n"
- + " F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n"
- + " F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n"
- + " F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + "\n"
- + "from RDB$RELATION_CONSTRAINTS RC\n"
- + "inner join RDB$INDEX_SEGMENTS IDX\n"
- + " on IDX.RDB$INDEX_NAME = RC.RDB$INDEX_NAME\n"
- + "inner join RDB$RELATION_FIELDS RF\n"
- + " on RF.RDB$FIELD_NAME = IDX.RDB$FIELD_NAME and RF.RDB$RELATION_NAME = RC.RDB$RELATION_NAME\n"
- + "inner join RDB$FIELDS F\n"
- + " on F.RDB$FIELD_NAME = RF.RDB$FIELD_SOURCE\n"
- + "where RC.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY'\n"
- + "and ";
-
- private static final String GET_BEST_ROW_IDENT_END =
- "\norder by IDX.RDB$FIELD_POSITION";
- //@formatter:on
-
private GetBestRowIdentifier(DbMetadataMediator mediator) {
super(ROW_DESCRIPTOR, mediator);
}
@@ -93,7 +68,7 @@ public ResultSet getBestRowIdentifier(String catalog, String schema, String tabl
}
RowValueBuilder valueBuilder = new RowValueBuilder(ROW_DESCRIPTOR);
- List rows = getPrimaryKeyIdentifier(table, valueBuilder);
+ List rows = getPrimaryKeyIdentifier(schema, table, valueBuilder);
// if no primary key exists, add RDB$DB_KEY as pseudo-column
if (rows.isEmpty()) {
@@ -105,8 +80,8 @@ public ResultSet getBestRowIdentifier(String catalog, String schema, String tabl
return createEmpty();
}
- try (ResultSet pseudoColumns =
- dbmd.getPseudoColumns(catalog, schema, escapeWildcards(table), "RDB$DB\\_KEY")) {
+ try (ResultSet pseudoColumns = dbmd.getPseudoColumns(
+ catalog, escapeWildcards(schema), escapeWildcards(table), "RDB$DB\\_KEY")) {
if (!pseudoColumns.next()) {
return createEmpty();
}
@@ -118,7 +93,7 @@ public ResultSet getBestRowIdentifier(String catalog, String schema, String tabl
.at(1).setString("RDB$DB_KEY")
.at(2).setInt(Types.ROWID)
.at(3).setString(getDataTypeName(char_type, 0, CS_BINARY))
- .at(4).setInt(pseudoColumns.getInt(8))
+ .at(4).setInt(pseudoColumns.getInt(6))
.at(5).set(null)
.at(6).set(null)
.at(7).setShort(DatabaseMetaData.bestRowPseudo)
@@ -132,8 +107,10 @@ public ResultSet getBestRowIdentifier(String catalog, String schema, String tabl
/**
* Get primary key of the table as best row identifier.
*
+ * @param schema
+ * name of the schema
* @param table
- * name of the table.
+ * name of the table
* @param valueBuilder
* builder for row values
* @return list of result set values, when empty, no primary key has been defined for a table or the table does not
@@ -141,13 +118,9 @@ public ResultSet getBestRowIdentifier(String catalog, String schema, String tabl
* @throws SQLException
* if something went wrong.
*/
- private List getPrimaryKeyIdentifier(String table, RowValueBuilder valueBuilder) throws SQLException {
- Clause tableClause = Clause.equalsClause("RC.RDB$RELATION_NAME", table);
- String sql = GET_BEST_ROW_IDENT_START
- + tableClause.getCondition(false)
- + GET_BEST_ROW_IDENT_END;
-
- MetadataQuery metadataQuery = new MetadataQuery(sql, Clause.parameters(tableClause));
+ private List getPrimaryKeyIdentifier(String schema, String table, RowValueBuilder valueBuilder)
+ throws SQLException {
+ MetadataQuery metadataQuery = createGetPrimaryKeyIdentifierQuery(schema, table);
try (ResultSet rs = mediator.performMetaDataQuery(metadataQuery)) {
List rows = new ArrayList<>();
while (rs.next()) {
@@ -157,6 +130,8 @@ private List getPrimaryKeyIdentifier(String table, RowValueBuilder val
}
}
+ abstract MetadataQuery createGetPrimaryKeyIdentifierQuery(String schema, String table);
+
@Override
RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQLException {
TypeMetadata typeMetadata = TypeMetadata.builder(mediator.getFirebirdSupportInfo())
@@ -176,6 +151,119 @@ RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQ
}
public static GetBestRowIdentifier create(DbMetadataMediator mediator) {
- return new GetBestRowIdentifier(mediator);
+ // NOTE: Indirection through static method prevents unnecessary classloading
+ if (mediator.getFirebirdSupportInfo().isVersionEqualOrAbove(6)) {
+ return FB6.createInstance(mediator);
+ } else {
+ return FB5.createInstance(mediator);
+ }
}
+
+ /**
+ * Implementation for Firebird 5.0 and older.
+ */
+ private static final class FB5 extends GetBestRowIdentifier {
+
+ //@formatter:off
+ private static final String GET_BEST_ROW_IDENT_START = """
+ select
+ RF.RDB$FIELD_NAME as COLUMN_NAME,
+ """ +
+ " F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n" +
+ " F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n" +
+ " F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n" +
+ " F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n" +
+ " F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n" +
+ " F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n" +
+ " F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + "\n" + """
+ from RDB$RELATION_CONSTRAINTS RC
+ inner join RDB$INDEX_SEGMENTS IDX
+ on IDX.RDB$INDEX_NAME = RC.RDB$INDEX_NAME
+ inner join RDB$RELATION_FIELDS RF
+ on RF.RDB$FIELD_NAME = IDX.RDB$FIELD_NAME and RF.RDB$RELATION_NAME = RC.RDB$RELATION_NAME
+ inner join RDB$FIELDS F
+ on F.RDB$FIELD_NAME = RF.RDB$FIELD_SOURCE
+ where RC.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY'
+ and\s""";
+ //@formatter:on
+
+ private static final String GET_BEST_ROW_IDENT_END = "\norder by IDX.RDB$FIELD_POSITION";
+
+
+ private FB5(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetBestRowIdentifier createInstance(DbMetadataMediator mediator) {
+ return new FB5(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetPrimaryKeyIdentifierQuery(String schema, String table) {
+ Clause tableClause = Clause.equalsClause("RC.RDB$RELATION_NAME", table);
+ String sql = GET_BEST_ROW_IDENT_START
+ + tableClause.getCondition(false)
+ + GET_BEST_ROW_IDENT_END;
+
+ return new MetadataQuery(sql, Clause.parameters(tableClause));
+ }
+ }
+
+ /**
+ * Implementation for Firebird 6.0 and higher.
+ */
+ private static final class FB6 extends GetBestRowIdentifier {
+
+ //@formatter:off
+ private static final String GET_BEST_ROW_IDENT_START_6 = """
+ select
+ RF.RDB$FIELD_NAME as COLUMN_NAME,
+ """ +
+ " F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n" +
+ " F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n" +
+ " F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n" +
+ " F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n" +
+ " F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n" +
+ " F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n" +
+ " F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + "\n" + """
+ from SYSTEM.RDB$RELATION_CONSTRAINTS RC
+ inner join SYSTEM.RDB$INDEX_SEGMENTS IDX
+ on IDX.RDB$SCHEMA_NAME = RC.RDB$SCHEMA_NAME and IDX.RDB$INDEX_NAME = RC.RDB$INDEX_NAME
+ inner join SYSTEM.RDB$RELATION_FIELDS RF
+ on RF.RDB$FIELD_NAME = IDX.RDB$FIELD_NAME and RF.RDB$SCHEMA_NAME = RC.RDB$SCHEMA_NAME and RF.RDB$RELATION_NAME = RC.RDB$RELATION_NAME
+ inner join SYSTEM.RDB$FIELDS F
+ on F.RDB$SCHEMA_NAME = RF.RDB$FIELD_SOURCE_SCHEMA_NAME and F.RDB$FIELD_NAME = RF.RDB$FIELD_SOURCE
+ where RC.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY'
+ and\s""";
+ //@formatter:on
+
+ // The order by schema name is to ensure a consistent order when this is called with schema = null, as that will
+ // not narrow the search by schema, so can return columns of multiple same named tables in different schemas.
+ private static final String GET_BEST_ROW_IDENT_END_6 = "\norder by RC.RDB$SCHEMA_NAME, IDX.RDB$FIELD_POSITION";
+
+ private FB6(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetBestRowIdentifier createInstance(DbMetadataMediator mediator) {
+ return new FB6(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetPrimaryKeyIdentifierQuery(String schema, String table) {
+ List clauses = new ArrayList<>(2);
+ if (schema != null) {
+ // NOTE: empty string will return no rows as required ("" retrieves those without a schema)
+ clauses.add(Clause.equalsClause("RC.RDB$SCHEMA_NAME", schema));
+ }
+ clauses.add(Clause.equalsClause("RC.RDB$RELATION_NAME", table));
+ String sql = GET_BEST_ROW_IDENT_START_6
+ + Clause.conjunction(clauses)
+ + GET_BEST_ROW_IDENT_END_6;
+
+ return new MetadataQuery(sql, Clause.parameters(clauses));
+ }
+
+ }
+
}
diff --git a/src/test/org/firebirdsql/common/FBTestProperties.java b/src/test/org/firebirdsql/common/FBTestProperties.java
index f6d8077b3..d211e1d86 100644
--- a/src/test/org/firebirdsql/common/FBTestProperties.java
+++ b/src/test/org/firebirdsql/common/FBTestProperties.java
@@ -366,6 +366,19 @@ public static void defaultDatabaseTearDown(FBManager fbManager) throws Exception
}
}
+ /**
+ * If schema support is available, returns {@code forSchema}, otherwise returns {@code withoutSchema}.
+ *
+ * @param forSchema
+ * value to return when schema support is available
+ * @param withoutSchema
+ * value to return when schema support is not available
+ * @return {@code forSchema} if schema support is available, otherwise {@code withoutSchema}
+ */
+ public static T ifSchemaElse(T forSchema, T withoutSchema) {
+ return getDefaultSupportInfo().supportsSchemas() ? forSchema : withoutSchema;
+ }
+
private FBTestProperties() {
// No instantiation
}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataBestRowIdentifierTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataBestRowIdentifierTest.java
new file mode 100644
index 000000000..f55ce737c
--- /dev/null
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataBestRowIdentifierTest.java
@@ -0,0 +1,244 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jdbc;
+
+import org.firebirdsql.common.extension.UsesDatabaseExtension;
+import org.firebirdsql.util.FirebirdSupportInfo;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.firebirdsql.common.FBTestProperties.getConnectionViaDriverManager;
+import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
+import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
+import static org.firebirdsql.common.FbAssumptions.assumeFeature;
+import static org.firebirdsql.common.assertions.ResultSetAssertions.assertNextRow;
+import static org.firebirdsql.common.assertions.ResultSetAssertions.assertNoNextRow;
+
+class FBDatabaseMetaDataBestRowIdentifierTest {
+
+ @RegisterExtension
+ static final UsesDatabaseExtension.UsesDatabaseForAll usesDatabase = UsesDatabaseExtension.usesDatabaseForAll(
+ getInitStatements());
+
+ private static final String TABLE_BEST_ROW_PK = """
+ create table BEST_ROW_PK (
+ C1 integer constraint PK_BEST_ROW_PK primary key
+ )""";
+
+ private static final String TABLE_BEST_ROW_NO_PK = """
+ create table BEST_ROW_NO_PK (
+ C1 integer not null
+ )""";
+
+ private static final String CREATE_OTHER_SCHEMA = "create schema OTHER_SCHEMA";
+
+ private static final String TABLE_BEST_ROW_PK_OTHER_SCHEMA = """
+ create table OTHER_SCHEMA.BEST_ROW_PK (
+ ID1 integer not null,
+ ID2 bigint not null,
+ constraint PK_BEST_ROW_PK primary key (ID1, ID2)
+ )""";
+
+ private static final MetadataResultSetDefinition getBestRowIdentifierDefinition =
+ new MetadataResultSetDefinition(BestRowIdentifierMetaData.class);
+
+ private static Connection con;
+ private static DatabaseMetaData dbmd;
+
+ @BeforeAll
+ static void setupAll() throws SQLException {
+ con = getConnectionViaDriverManager();
+ dbmd = con.getMetaData();
+ }
+
+ @AfterAll
+ static void tearDownAll() throws SQLException {
+ try {
+ con.close();
+ } finally {
+ con = null;
+ dbmd = null;
+ }
+ }
+
+ private static List getInitStatements() {
+ var statements = new ArrayList<>(
+ List.of(TABLE_BEST_ROW_PK,
+ TABLE_BEST_ROW_NO_PK));
+ if (getDefaultSupportInfo().supportsSchemas()) {
+ statements.add(CREATE_OTHER_SCHEMA);
+ statements.add(TABLE_BEST_ROW_PK_OTHER_SCHEMA);
+ }
+ return statements;
+ }
+
+ /**
+ * Tests the ordinal positions and types for the metadata columns of getBestRowIdentifier(...).
+ */
+ @Test
+ void testSchemaMetaDataColumns() throws Exception {
+ try (ResultSet columns = dbmd.getBestRowIdentifier("", "", "", DatabaseMetaData.bestRowTransaction, true)) {
+ getBestRowIdentifierDefinition.validateResultSetColumns(columns);
+ }
+ }
+
+ @Test
+ void testGetBestRowIdentifier() throws Exception {
+ for (int scope : new int[] { DatabaseMetaData.bestRowTemporary, DatabaseMetaData.bestRowTransaction,
+ DatabaseMetaData.bestRowSession }) {
+ try (ResultSet rs = dbmd.getBestRowIdentifier("", ifSchemaElse("PUBLIC", ""), "BEST_ROW_PK", scope, true)) {
+ validate(rs, rules_BEST_ROW_PK());
+ }
+ }
+
+ for (int scope : new int[] { DatabaseMetaData.bestRowTemporary, DatabaseMetaData.bestRowTransaction }) {
+ try (ResultSet rs = dbmd.getBestRowIdentifier(
+ "", ifSchemaElse("PUBLIC", ""), "BEST_ROW_NO_PK", scope, true)) {
+ validate(rs, rules_BEST_ROW_NO_PK());
+ }
+ }
+
+ try (ResultSet rs = dbmd.getBestRowIdentifier(
+ "", ifSchemaElse("PUBLIC", ""), "BEST_ROW_NO_PK", DatabaseMetaData.bestRowSession, true)) {
+ validate(rs, List.of());
+ }
+ }
+
+ @Test
+ void testGetBestRowIdentifier_otherSchema() throws Exception {
+ assumeFeature(FirebirdSupportInfo::supportsSchemas, "Test requires schema support");
+ for (int scope : new int[] { DatabaseMetaData.bestRowTemporary, DatabaseMetaData.bestRowTransaction,
+ DatabaseMetaData.bestRowSession }) {
+ try (ResultSet rs = dbmd.getBestRowIdentifier("", "OTHER_SCHEMA", "BEST_ROW_PK", scope, true)) {
+ validate(rs, rules_BEST_ROW_PK_OTHER_SCHEMA());
+ }
+ }
+ }
+
+ @Test
+ void testGetBestRowIdentifier_allSchemas() throws Exception {
+ assumeFeature(FirebirdSupportInfo::supportsSchemas, "Test requires schema support");
+ /* JDBC specifies that "null means that the schema name should not be used to narrow the search". This seems
+ like useless behaviour to me for this method (you don't know to which table the columns are actually referring),
+ but let's verify it (our implementation returns the columns of all tables with the same name, ordered by schema
+ and field position). */
+ for (int scope : new int[] { DatabaseMetaData.bestRowTemporary, DatabaseMetaData.bestRowTransaction,
+ DatabaseMetaData.bestRowSession }) {
+ try (ResultSet rs = dbmd.getBestRowIdentifier("", null, "BEST_ROW_PK", scope, true)) {
+ var combinedRules = new ArrayList
*
* NOTE: This implementation returns all privileges, not just applicable to the current user. It is
- * unclear if this complies with the JDBC requirements. This may change in the future to only return only privileges
+ * unclear if this complies with the JDBC requirements. This may change in the future to only return privileges
* applicable to the current user, user {@code PUBLIC} and — maybe — active roles.
*
+ *
+ * Contrary to specified in the JDBC API, the result set is ordered by {@code TABLE_SCHEM}, {@code COLUMN_NAME},
+ * {@code PRIVILEGE}, and {@code GRANTEE} (JDBC specifies ordering by {@code COLUMN_NAME} and {@code PRIVILEGE}).
+ * This only makes a difference when specifying {@code null} for {@code schema} (search all schemas) and there are
+ * multiples tables with the same {@code name}.
+ *
*/
@Override
public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern)
throws SQLException {
- return GetColumnPrivileges.create(getDbMetadataMediator()).getColumnPrivileges(table, columnNamePattern);
+ return GetColumnPrivileges.create(getDbMetadataMediator()).getColumnPrivileges(schema, table, columnNamePattern);
}
/**
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java b/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java
index 6960432cd..5ff6c7670 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java
@@ -22,6 +22,7 @@
import static org.firebirdsql.gds.ISCConstants.SQL_LONG;
import static org.firebirdsql.gds.ISCConstants.SQL_SHORT;
import static org.firebirdsql.gds.ISCConstants.SQL_VARYING;
+import static org.firebirdsql.jaybird.util.StringUtils.isNullOrEmpty;
import static org.firebirdsql.jdbc.metadata.FbMetadataConstants.OBJECT_NAME_LENGTH;
import static org.firebirdsql.jdbc.metadata.FbMetadataConstants.char_type;
import static org.firebirdsql.jdbc.metadata.MetadataPattern.escapeWildcards;
@@ -63,7 +64,7 @@ private GetBestRowIdentifier(DbMetadataMediator mediator) {
@SuppressWarnings("unused")
public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable)
throws SQLException {
- if (table == null || table.isEmpty()) {
+ if (isNullOrEmpty(table)) {
return createEmpty();
}
@@ -204,7 +205,6 @@ MetadataQuery createGetPrimaryKeyIdentifierQuery(String schema, String table) {
String sql = GET_BEST_ROW_IDENT_START
+ tableClause.getCondition(false)
+ GET_BEST_ROW_IDENT_END;
-
return new MetadataQuery(sql, Clause.parameters(tableClause));
}
}
@@ -217,7 +217,7 @@ private static final class FB6 extends GetBestRowIdentifier {
//@formatter:off
private static final String GET_BEST_ROW_IDENT_START_6 = """
select
- RF.RDB$FIELD_NAME as COLUMN_NAME,
+ trim(trailing from RF.RDB$FIELD_NAME) as COLUMN_NAME,
""" +
" F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n" +
" F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n" +
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java b/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java
index f57bac6fd..0416d1ba4 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -10,8 +10,11 @@
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
import static org.firebirdsql.gds.ISCConstants.SQL_VARYING;
+import static org.firebirdsql.jaybird.util.StringUtils.isNullOrEmpty;
import static org.firebirdsql.jdbc.metadata.FbMetadataConstants.OBJECT_NAME_LENGTH;
import static org.firebirdsql.jdbc.metadata.PrivilegeMapping.mapPrivilege;
@@ -27,7 +30,7 @@
* @author Mark Rotteveel
* @since 5
*/
-public final class GetColumnPrivileges extends AbstractMetadataMethod {
+public abstract class GetColumnPrivileges extends AbstractMetadataMethod {
private static final String COLUMNPRIV = "COLUMNPRIV";
private static final RowDescriptor ROW_DESCRIPTOR = DbMetadataMediator.newRowDescriptorBuilder(9)
@@ -42,33 +45,6 @@ public final class GetColumnPrivileges extends AbstractMetadataMethod {
.at(8).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "JB_GRANTEE_TYPE", COLUMNPRIV).addField()
.toRowDescriptor();
- //@formatter:off
- private static final String GET_COLUMN_PRIVILEGES_START =
- "select distinct \n"
- + " RF.RDB$RELATION_NAME as TABLE_NAME, \n"
- + " RF.RDB$FIELD_NAME as COLUMN_NAME, \n"
- + " UP.RDB$GRANTOR as GRANTOR, \n"
- + " UP.RDB$USER as GRANTEE, \n"
- + " UP.RDB$PRIVILEGE as PRIVILEGE, \n"
- + " UP.RDB$GRANT_OPTION as IS_GRANTABLE,\n"
- + " T.RDB$TYPE_NAME as JB_GRANTEE_TYPE\n"
- + "from RDB$RELATION_FIELDS RF\n"
- + "inner join RDB$USER_PRIVILEGES UP\n"
- + " on UP.RDB$RELATION_NAME = RF.RDB$RELATION_NAME \n"
- + " and (UP.RDB$FIELD_NAME is null or UP.RDB$FIELD_NAME = RF.RDB$FIELD_NAME) \n"
- + "left join RDB$TYPES T\n"
- + " on T.RDB$FIELD_NAME = 'RDB$OBJECT_TYPE' and T.RDB$TYPE = UP.RDB$USER_TYPE \n"
- // Other privileges don't make sense for column privileges
- + "where UP.RDB$PRIVILEGE in ('A', 'D', 'I', 'R', 'S', 'U')\n"
- // Only tables and views
- + "and UP.RDB$OBJECT_TYPE in (0, 1)\n"
- + "and ";
-
- // NOTE: Sort by user is not defined in JDBC, but we do this to ensure a consistent order for tests
- private static final String GET_COLUMN_PRIVILEGES_END =
- "\norder by RF.RDB$FIELD_NAME, UP.RDB$PRIVILEGE, UP.RDB$USER";
- //@formatter:on
-
GetColumnPrivileges(DbMetadataMediator mediator) {
super(ROW_DESCRIPTOR, mediator);
}
@@ -76,26 +52,21 @@ public final class GetColumnPrivileges extends AbstractMetadataMethod {
/**
* @see java.sql.DatabaseMetaData#getColumnPrivileges(String, String, String, String)
*/
- public ResultSet getColumnPrivileges(String table, String columnNamePattern) throws SQLException {
- if (table == null || "".equals(columnNamePattern)) {
+ public ResultSet getColumnPrivileges(String schema, String table, String columnNamePattern) throws SQLException {
+ if (isNullOrEmpty(table) || "".equals(columnNamePattern)) {
return createEmpty();
}
- Clause tableClause = Clause.equalsClause("RF.RDB$RELATION_NAME", table);
- Clause columnNameClause = new Clause("RF.RDB$FIELD_NAME", columnNamePattern);
-
- String sql = GET_COLUMN_PRIVILEGES_START
- + tableClause.getCondition(columnNameClause.hasCondition())
- + columnNameClause.getCondition(false)
- + GET_COLUMN_PRIVILEGES_END;
- MetadataQuery metadataQuery = new MetadataQuery(sql, Clause.parameters(tableClause, columnNameClause));
+ MetadataQuery metadataQuery = createMetadataQuery(schema, table, columnNamePattern);
return createMetaDataResultSet(metadataQuery);
}
+ abstract MetadataQuery createMetadataQuery(String schema, String table, String columnNamePattern);
+
@Override
RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQLException {
return valueBuilder
.at(0).set(null)
- .at(1).set(null)
+ .at(1).setString(rs.getString("TABLE_SCHEM"))
.at(2).setString(rs.getString("TABLE_NAME"))
.at(3).setString(rs.getString("COLUMN_NAME"))
.at(4).setString(rs.getString("GRANTOR"))
@@ -107,6 +78,115 @@ RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQ
}
public static GetColumnPrivileges create(DbMetadataMediator mediator) {
- return new GetColumnPrivileges(mediator);
+ if (mediator.getFirebirdSupportInfo().isVersionEqualOrAbove(6)) {
+ return FB6.createInstance(mediator);
+ } else {
+ return FB5.createInstance(mediator);
+ }
+ }
+
+ /**
+ * Implementation for Firebird 5.0 and older.
+ */
+ private static final class FB5 extends GetColumnPrivileges {
+
+ private static final String GET_COLUMN_PRIVILEGES_START_5 = """
+ select distinct
+ cast(null as char(1)) as TABLE_SCHEM,
+ RF.RDB$RELATION_NAME as TABLE_NAME,
+ RF.RDB$FIELD_NAME as COLUMN_NAME,
+ UP.RDB$GRANTOR as GRANTOR,
+ UP.RDB$USER as GRANTEE,
+ UP.RDB$PRIVILEGE as PRIVILEGE,
+ UP.RDB$GRANT_OPTION as IS_GRANTABLE,
+ T.RDB$TYPE_NAME as JB_GRANTEE_TYPE
+ from RDB$RELATION_FIELDS RF
+ inner join RDB$USER_PRIVILEGES UP
+ on UP.RDB$RELATION_NAME = RF.RDB$RELATION_NAME
+ and (UP.RDB$FIELD_NAME is null or UP.RDB$FIELD_NAME = RF.RDB$FIELD_NAME)
+ left join RDB$TYPES T
+ on T.RDB$FIELD_NAME = 'RDB$OBJECT_TYPE' and T.RDB$TYPE = UP.RDB$USER_TYPE
+ where UP.RDB$PRIVILEGE in ('A', 'D', 'I', 'R', 'S', 'U') -- privileges relevant for columns
+ and UP.RDB$OBJECT_TYPE in (0, 1) -- only tables and views
+ and\s""";
+
+ // NOTE: Sort by user is not defined in JDBC, but we do this to ensure a consistent order for tests
+ private static final String GET_COLUMN_PRIVILEGES_END_5 =
+ "\norder by RF.RDB$FIELD_NAME, UP.RDB$PRIVILEGE, UP.RDB$USER";
+
+ private FB5(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetColumnPrivileges createInstance(DbMetadataMediator mediator) {
+ return new FB5(mediator);
+ }
+
+ @Override
+ MetadataQuery createMetadataQuery(String schema, String table, String columnNamePattern) {
+ var clauses = List.of(
+ Clause.equalsClause("RF.RDB$RELATION_NAME", table),
+ new Clause("RF.RDB$FIELD_NAME", columnNamePattern));
+ String sql = GET_COLUMN_PRIVILEGES_START_5
+ + Clause.conjunction(clauses)
+ + GET_COLUMN_PRIVILEGES_END_5;
+ return new MetadataQuery(sql, Clause.parameters(clauses));
+ }
+
}
+
+ /**
+ * Implementation for Firebird 6.0 and higher.
+ */
+ private static final class FB6 extends GetColumnPrivileges {
+
+ private static final String GET_COLUMN_PRIVILEGES_START_6 = """
+ select distinct
+ trim(trailing from RF.RDB$SCHEMA_NAME) as TABLE_SCHEM,
+ trim(trailing from RF.RDB$RELATION_NAME) as TABLE_NAME,
+ trim(trailing from RF.RDB$FIELD_NAME) as COLUMN_NAME,
+ trim(trailing from UP.RDB$GRANTOR) as GRANTOR,
+ trim(trailing from UP.RDB$USER) as GRANTEE,
+ UP.RDB$PRIVILEGE as PRIVILEGE,
+ UP.RDB$GRANT_OPTION as IS_GRANTABLE,
+ T.RDB$TYPE_NAME as JB_GRANTEE_TYPE
+ from SYSTEM.RDB$RELATION_FIELDS RF
+ inner join SYSTEM.RDB$USER_PRIVILEGES UP
+ on UP.RDB$RELATION_SCHEMA_NAME = RF.RDB$SCHEMA_NAME and UP.RDB$RELATION_NAME = RF.RDB$RELATION_NAME
+ and (UP.RDB$FIELD_NAME is null or UP.RDB$FIELD_NAME = RF.RDB$FIELD_NAME)
+ left join SYSTEM.RDB$TYPES T
+ on T.RDB$FIELD_NAME = 'RDB$OBJECT_TYPE' and T.RDB$TYPE = UP.RDB$USER_TYPE
+ where UP.RDB$PRIVILEGE in ('A', 'D', 'I', 'R', 'S', 'U') -- privileges relevant for columns
+ and UP.RDB$OBJECT_TYPE in (0, 1) -- only tables and views
+ and\s""";
+
+ // NOTE: Sort by user and schema is not defined in JDBC, but we do this to ensure a consistent order for tests
+ private static final String GET_COLUMN_PRIVILEGES_END_6 =
+ "\norder by RF.RDB$FIELD_NAME, UP.RDB$PRIVILEGE, UP.RDB$USER, RF.RDB$SCHEMA_NAME";
+
+ private FB6(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetColumnPrivileges createInstance(DbMetadataMediator mediator) {
+ return new FB6(mediator);
+ }
+
+ @Override
+ MetadataQuery createMetadataQuery(String schema, String table, String columnNamePattern) {
+ var clauses = new ArrayList(3);
+ if (schema != null) {
+ // NOTE: empty string will return no rows as required ("" retrieves those without a schema)
+ clauses.add(Clause.equalsClause("RF.RDB$SCHEMA_NAME", schema));
+ }
+ clauses.add(Clause.equalsClause("RF.RDB$RELATION_NAME", table));
+ clauses.add(new Clause("RF.RDB$FIELD_NAME", columnNamePattern));
+ String sql = GET_COLUMN_PRIVILEGES_START_6
+ + Clause.conjunction(clauses)
+ + GET_COLUMN_PRIVILEGES_END_6;
+ return new MetadataQuery(sql, Clause.parameters(clauses));
+ }
+
+ }
+
}
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java b/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java
index d27deef10..8f87e009d 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java
@@ -107,8 +107,8 @@ private static final class FB2_5 extends GetProcedures {
private static final String GET_PROCEDURES_FRAGMENT_2_5 = """
select
- null as PROCEDURE_CAT,
- null as PROCEDURE_SCHEM,
+ cast(null as char(1)) as PROCEDURE_CAT,
+ cast(null as char(1)) as PROCEDURE_SCHEM,
RDB$PROCEDURE_NAME as PROCEDURE_NAME,
RDB$DESCRIPTION as REMARKS,
RDB$PROCEDURE_OUTPUTS as PROCEDURE_TYPE
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnPrivilegesTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnPrivilegesTest.java
index 616f6a9f0..9fba1fa3b 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnPrivilegesTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnPrivilegesTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2022-2023 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -24,6 +24,7 @@
import static org.firebirdsql.common.FBTestProperties.getConnectionViaDriverManager;
import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
+import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
import static org.firebirdsql.common.matchers.MatcherAssume.assumeThat;
import static org.hamcrest.Matchers.equalToIgnoringCase;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -35,6 +36,8 @@
*/
class FBDatabaseMetaDataColumnPrivilegesTest {
+ // TODO Add schema support: tests involving other schema
+
private static final String SYSDBA = "SYSDBA";
private static final String USER1 = "USER1";
private static final String user2 = getDefaultSupportInfo().supportsCaseSensitiveUserNames() ? "user2" : "USER2";
@@ -125,7 +128,9 @@ void testColumnPrivileges_TBL1_all(String allPattern) throws Exception {
createRule("TBL1", "val3", USER1, false, "UPDATE"),
createRule("TBL1", "val3", user2, false, "UPDATE"));
- validateExpectedColumnPrivileges("TBL1", allPattern, rules);
+ validateExpectedColumnPrivileges(ifSchemaElse("PUBLIC", ""), "TBL1", allPattern, rules);
+ // schema = null should also find it:
+ validateExpectedColumnPrivileges(null, "TBL1", allPattern, rules);
}
@Test
@@ -155,7 +160,9 @@ void testColumnPrivileges_TBL1_COL_wildcard() throws Exception {
createRule("TBL1", "COL2", SYSDBA, true, "UPDATE"),
createRule("TBL1", "COL2", USER1, false, "UPDATE"));
- validateExpectedColumnPrivileges("TBL1", "COL%", rules);
+ validateExpectedColumnPrivileges(ifSchemaElse("PUBLIC", ""), "TBL1", "COL%", rules);
+ // schema = null should also find it:
+ validateExpectedColumnPrivileges(null, "TBL1", "COL%", rules);
}
@Test
@@ -181,13 +188,19 @@ void testColumnPrivileges_tbl2_all() throws Exception {
createRule("tbl2", "val3", user2, true, "SELECT"),
createRule("tbl2", "val3", SYSDBA, true, "UPDATE"));
- validateExpectedColumnPrivileges("tbl2", "%", rules);
+ validateExpectedColumnPrivileges(ifSchemaElse("PUBLIC", ""), "tbl2", "%", rules);
}
- private Map createRule(String tableName, String columnName, String grantee,
+ private Map createRule(String table, String columnName, String grantee,
boolean grantable, String privilege) {
+ return createRule(ifSchemaElse("PUBLIC", null), table, columnName, grantee, grantable, privilege);
+ }
+
+ private Map createRule(String schema, String table, String columnName,
+ String grantee, boolean grantable, String privilege) {
Map rules = getDefaultValueValidationRules();
- rules.put(ColumnPrivilegesMetadata.TABLE_NAME, tableName);
+ rules.put(ColumnPrivilegesMetadata.TABLE_SCHEM, schema);
+ rules.put(ColumnPrivilegesMetadata.TABLE_NAME, table);
rules.put(ColumnPrivilegesMetadata.COLUMN_NAME, columnName);
rules.put(ColumnPrivilegesMetadata.GRANTEE, grantee);
rules.put(ColumnPrivilegesMetadata.PRIVILEGE, privilege);
@@ -195,9 +208,9 @@ private Map createRule(String tableName, Strin
return rules;
}
- private void validateExpectedColumnPrivileges(String tableName, String columnNamePattern,
+ private void validateExpectedColumnPrivileges(String schema, String table, String columnNamePattern,
List> expectedColumnPrivileges) throws SQLException {
- try (ResultSet columnPrivileges = dbmd.getColumnPrivileges(null, null, tableName, columnNamePattern)) {
+ try (ResultSet columnPrivileges = dbmd.getColumnPrivileges(null, schema, table, columnNamePattern)) {
int privilegeCount = 0;
while (columnPrivileges.next()) {
if (privilegeCount < expectedColumnPrivileges.size()) {
@@ -215,7 +228,7 @@ private void validateExpectedColumnPrivileges(String tableName, String columnNam
static {
Map defaults = new EnumMap<>(ColumnPrivilegesMetadata.class);
defaults.put(ColumnPrivilegesMetadata.TABLE_CAT, null);
- defaults.put(ColumnPrivilegesMetadata.TABLE_SCHEM, null);
+ defaults.put(ColumnPrivilegesMetadata.TABLE_SCHEM, ifSchemaElse("PUBLIC", null));
defaults.put(ColumnPrivilegesMetadata.GRANTOR, SYSDBA);
defaults.put(ColumnPrivilegesMetadata.JB_GRANTEE_TYPE, "USER");
From 76992340e53b9f2ced707a7133c0b25ca20ac5e3 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Sat, 21 Jun 2025 14:15:35 +0200
Subject: [PATCH 10/64] #882 Schema support for getColumns
---
.../firebirdsql/jdbc/FBDatabaseMetaData.java | 3 +-
.../firebirdsql/jdbc/metadata/GetColumns.java | 183 ++++++++++++------
.../jdbc/FBDatabaseMetaDataColumnsTest.java | 95 +++++----
.../FBDatabaseMetaDataProceduresTest.java | 2 +-
4 files changed, 175 insertions(+), 108 deletions(-)
diff --git a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
index fcc6762e4..72e8edb2c 100644
--- a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
@@ -1319,7 +1319,8 @@ public String[] getTableTypeNames() throws SQLException {
@Override
public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern)
throws SQLException {
- return GetColumns.create(getDbMetadataMediator()).getColumns(tableNamePattern, columnNamePattern);
+ return GetColumns.create(getDbMetadataMediator())
+ .getColumns(schemaPattern, tableNamePattern, columnNamePattern);
}
/**
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetColumns.java b/src/main/org/firebirdsql/jdbc/metadata/GetColumns.java
index 39ee3d221..4cb1c46f4 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetColumns.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetColumns.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -12,6 +12,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
+import java.util.List;
import java.util.Objects;
import static java.sql.DatabaseMetaData.columnNoNulls;
@@ -78,13 +79,13 @@ private GetColumns(DbMetadataMediator mediator) {
* @see java.sql.DatabaseMetaData#getColumns(String, String, String, String)
* @see org.firebirdsql.jdbc.FBDatabaseMetaData#getColumns(String, String, String, String)
*/
- public final ResultSet getColumns(String tableNamePattern, String columnNamePattern) throws SQLException {
+ public final ResultSet getColumns(String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException {
if ("".equals(tableNamePattern) || "".equals(columnNamePattern)) {
// Matching table name or column not possible
return createEmpty();
}
- MetadataQuery metadataQuery = createGetColumnsQuery(tableNamePattern, columnNamePattern);
+ MetadataQuery metadataQuery = createGetColumnsQuery(schemaPattern, tableNamePattern, columnNamePattern);
return createMetaDataResultSet(metadataQuery);
}
@@ -98,6 +99,7 @@ final RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) thr
boolean isComputed = rs.getBoolean("IS_COMPUTED");
boolean isIdentity = rs.getBoolean("IS_IDENTITY");
return valueBuilder
+ .at(1).setString(rs.getString("SCHEMA_NAME"))
.at(2).setString(rs.getString("RELATION_NAME"))
.at(3).setString(rs.getString("FIELD_NAME"))
.at(4).setInt(typeMetadata.getJdbcType())
@@ -138,12 +140,15 @@ private String getIsAutoIncrementValue(boolean isIdentity, TypeMetadata typeMeta
};
}
- abstract MetadataQuery createGetColumnsQuery(String tableNamePattern, String columnNamePattern);
+ abstract MetadataQuery createGetColumnsQuery(String schemaPattern, String tableNamePattern,
+ String columnNamePattern);
public static GetColumns create(DbMetadataMediator mediator) {
FirebirdSupportInfo firebirdSupportInfo = mediator.getFirebirdSupportInfo();
// NOTE: Indirection through static method prevents unnecessary classloading
- if (firebirdSupportInfo.isVersionEqualOrAbove(3, 0)) {
+ if (firebirdSupportInfo.isVersionEqualOrAbove(6)) {
+ return FB6.createInstance(mediator);
+ } else if (firebirdSupportInfo.isVersionEqualOrAbove(3)) {
return FB3.createInstance(mediator);
} else {
return FB2_5.createInstance(mediator);
@@ -153,29 +158,29 @@ public static GetColumns create(DbMetadataMediator mediator) {
@SuppressWarnings("java:S101")
private static class FB2_5 extends GetColumns {
- //@formatter:off
- private static final String GET_COLUMNS_FRAGMENT_2_5 =
- "select\n"
- + " RF.RDB$RELATION_NAME as RELATION_NAME,\n"
- + " RF.RDB$FIELD_NAME as FIELD_NAME,\n"
- + " F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n"
- + " F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n"
- + " F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n"
- + " F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n"
- + " F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n"
- + " F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n"
- + " F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + ",\n"
- + " RF.RDB$DESCRIPTION as REMARKS,\n"
- + " coalesce(RF.RDB$DEFAULT_SOURCE, F.RDB$DEFAULT_SOURCE) as DEFAULT_SOURCE,\n"
- + " RF.RDB$FIELD_POSITION + 1 as FIELD_POSITION,\n"
- + " iif(coalesce(RF.RDB$NULL_FLAG, 0) + coalesce(F.RDB$NULL_FLAG, 0) = 0, 'T', 'F') as IS_NULLABLE,\n"
- + " iif(F.RDB$COMPUTED_BLR is not NULL, 'T', 'F') as IS_COMPUTED,\n"
- + " 'F' as IS_IDENTITY,\n"
- + " cast(NULL as VARCHAR(10)) as JB_IDENTITY_TYPE\n"
- + "from RDB$RELATION_FIELDS RF inner join RDB$FIELDS F on RF.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME";
+ private static final String GET_COLUMNS_FRAGMENT_2_5 = """
+ select
+ cast(null as char(1)) AS SCHEMA_NAME,
+ RF.RDB$RELATION_NAME as RELATION_NAME,
+ RF.RDB$FIELD_NAME as FIELD_NAME,
+ """ +
+ " F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n" +
+ " F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n" +
+ " F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n" +
+ " F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n" +
+ " F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n" +
+ " F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n" +
+ " F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + ",\n" + """
+ RF.RDB$DESCRIPTION as REMARKS,
+ coalesce(RF.RDB$DEFAULT_SOURCE, F.RDB$DEFAULT_SOURCE) as DEFAULT_SOURCE,
+ RF.RDB$FIELD_POSITION + 1 as FIELD_POSITION,
+ iif(coalesce(RF.RDB$NULL_FLAG, 0) + coalesce(F.RDB$NULL_FLAG, 0) = 0, 'T', 'F') as IS_NULLABLE,
+ iif(F.RDB$COMPUTED_BLR is not NULL, 'T', 'F') as IS_COMPUTED,
+ 'F' as IS_IDENTITY,
+ cast(NULL as VARCHAR(10)) as JB_IDENTITY_TYPE
+ from RDB$RELATION_FIELDS RF inner join RDB$FIELDS F on RF.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME""";
private static final String GET_COLUMNS_ORDER_BY_2_5 = "\norder by RF.RDB$RELATION_NAME, RF.RDB$FIELD_POSITION";
- //@formatter:on
private FB2_5(DbMetadataMediator mediator) {
super(mediator);
@@ -186,44 +191,43 @@ private static GetColumns createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createGetColumnsQuery(String tableNamePattern, String columnNamePattern) {
- Clause tableNameClause = new Clause("RF.RDB$RELATION_NAME", tableNamePattern);
- Clause columnNameClause = new Clause("RF.RDB$FIELD_NAME", columnNamePattern);
+ MetadataQuery createGetColumnsQuery(String schemaPattern, String tableNamePattern, String columnNamePattern) {
+ var clauses = List.of(
+ new Clause("RF.RDB$RELATION_NAME", tableNamePattern),
+ new Clause("RF.RDB$FIELD_NAME", columnNamePattern));
String sql = GET_COLUMNS_FRAGMENT_2_5
- + (Clause.anyCondition(tableNameClause, columnNameClause)
- ? "\nwhere " + tableNameClause.getCondition(columnNameClause.hasCondition())
- + columnNameClause.getCondition(false)
- : "")
+ + (Clause.anyCondition(clauses) ? "\nwhere " + Clause.conjunction(clauses) : "")
+ GET_COLUMNS_ORDER_BY_2_5;
- return new MetadataQuery(sql, Clause.parameters(tableNameClause, columnNameClause));
+ return new MetadataQuery(sql, Clause.parameters(clauses));
}
+
}
private static class FB3 extends GetColumns {
- //@formatter:off
- private static final String GET_COLUMNS_FRAGMENT_3 =
- "select\n"
- + " trim(trailing from RF.RDB$RELATION_NAME) as RELATION_NAME,\n"
- + " trim(trailing from RF.RDB$FIELD_NAME) as FIELD_NAME,\n"
- + " F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n"
- + " F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n"
- + " F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n"
- + " F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n"
- + " F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n"
- + " F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n"
- + " F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + ",\n"
- + " RF.RDB$DESCRIPTION as REMARKS,\n"
- + " coalesce(RF.RDB$DEFAULT_SOURCE, F.RDB$DEFAULT_SOURCE) as DEFAULT_SOURCE,\n"
- + " RF.RDB$FIELD_POSITION + 1 as FIELD_POSITION,\n"
- + " (coalesce(RF.RDB$NULL_FLAG, 0) + coalesce(F.RDB$NULL_FLAG, 0) = 0) as IS_NULLABLE,\n"
- + " (F.RDB$COMPUTED_BLR is not NULL) as IS_COMPUTED,\n"
- + " (RF.RDB$IDENTITY_TYPE IS NOT NULL) as IS_IDENTITY,\n"
- + " trim(trailing from decode(RF.RDB$IDENTITY_TYPE, 0, 'ALWAYS', 1, 'BY DEFAULT')) as JB_IDENTITY_TYPE\n"
- + "from RDB$RELATION_FIELDS RF inner join RDB$FIELDS F on RF.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME";
+ private static final String GET_COLUMNS_FRAGMENT_3 = """
+ select
+ null AS SCHEMA_NAME,
+ trim(trailing from RF.RDB$RELATION_NAME) as RELATION_NAME,
+ trim(trailing from RF.RDB$FIELD_NAME) as FIELD_NAME,
+ """ +
+ " F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n" +
+ " F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n" +
+ " F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n" +
+ " F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n" +
+ " F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n" +
+ " F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n" +
+ " F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + ",\n" + """
+ RF.RDB$DESCRIPTION as REMARKS,
+ coalesce(RF.RDB$DEFAULT_SOURCE, F.RDB$DEFAULT_SOURCE) as DEFAULT_SOURCE,
+ RF.RDB$FIELD_POSITION + 1 as FIELD_POSITION,
+ (coalesce(RF.RDB$NULL_FLAG, 0) + coalesce(F.RDB$NULL_FLAG, 0) = 0) as IS_NULLABLE,
+ (F.RDB$COMPUTED_BLR is not NULL) as IS_COMPUTED,
+ (RF.RDB$IDENTITY_TYPE IS NOT NULL) as IS_IDENTITY,
+ trim(trailing from decode(RF.RDB$IDENTITY_TYPE, 0, 'ALWAYS', 1, 'BY DEFAULT')) as JB_IDENTITY_TYPE
+ from RDB$RELATION_FIELDS RF inner join RDB$FIELDS F on RF.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME""";
private static final String GET_COLUMNS_ORDER_BY_3 = "\norder by RF.RDB$RELATION_NAME, RF.RDB$FIELD_POSITION";
- //@formatter:on
private FB3(DbMetadataMediator mediator) {
super(mediator);
@@ -234,16 +238,67 @@ private static GetColumns createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createGetColumnsQuery(String tableNamePattern, String columnNamePattern) {
- Clause tableNameClause = new Clause("RF.RDB$RELATION_NAME", tableNamePattern);
- Clause columnNameClause = new Clause("RF.RDB$FIELD_NAME", columnNamePattern);
+ MetadataQuery createGetColumnsQuery(String schemaPattern, String tableNamePattern, String columnNamePattern) {
+ var clauses = List.of(
+ new Clause("RF.RDB$RELATION_NAME", tableNamePattern),
+ new Clause("RF.RDB$FIELD_NAME", columnNamePattern));
String sql = GET_COLUMNS_FRAGMENT_3
- + (Clause.anyCondition(tableNameClause, columnNameClause)
- ? "\nwhere " + tableNameClause.getCondition(columnNameClause.hasCondition())
- + columnNameClause.getCondition(false)
- : "")
+ + (Clause.anyCondition(clauses) ? "\nwhere " + Clause.conjunction(clauses) : "")
+ GET_COLUMNS_ORDER_BY_3;
- return new MetadataQuery(sql, Clause.parameters(tableNameClause, columnNameClause));
+ return new MetadataQuery(sql, Clause.parameters(clauses));
}
+
}
+
+ private static class FB6 extends GetColumns {
+
+ private static final String GET_COLUMNS_FRAGMENT_6 = """
+ select
+ trim(trailing from RF.RDB$SCHEMA_NAME) AS SCHEMA_NAME,
+ trim(trailing from RF.RDB$RELATION_NAME) as RELATION_NAME,
+ trim(trailing from RF.RDB$FIELD_NAME) as FIELD_NAME,
+ """ +
+ " F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n" +
+ " F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n" +
+ " F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n" +
+ " F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n" +
+ " F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n" +
+ " F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n" +
+ " F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + ",\n" + """
+ RF.RDB$DESCRIPTION as REMARKS,
+ coalesce(RF.RDB$DEFAULT_SOURCE, F.RDB$DEFAULT_SOURCE) as DEFAULT_SOURCE,
+ RF.RDB$FIELD_POSITION + 1 as FIELD_POSITION,
+ (coalesce(RF.RDB$NULL_FLAG, 0) + coalesce(F.RDB$NULL_FLAG, 0) = 0) as IS_NULLABLE,
+ (F.RDB$COMPUTED_BLR is not NULL) as IS_COMPUTED,
+ (RF.RDB$IDENTITY_TYPE IS NOT NULL) as IS_IDENTITY,
+ trim(trailing from decode(RF.RDB$IDENTITY_TYPE, 0, 'ALWAYS', 1, 'BY DEFAULT')) as JB_IDENTITY_TYPE
+ from SYSTEM.RDB$RELATION_FIELDS RF
+ inner join SYSTEM.RDB$FIELDS F
+ on RF.RDB$FIELD_SOURCE_SCHEMA_NAME = F.RDB$SCHEMA_NAME and RF.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME""";
+
+ private static final String GET_COLUMNS_ORDER_BY_6 =
+ "\norder by RF.RDB$SCHEMA_NAME, RF.RDB$RELATION_NAME, RF.RDB$FIELD_POSITION";
+
+ private FB6(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetColumns createInstance(DbMetadataMediator mediator) {
+ return new FB6(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetColumnsQuery(String schemaPattern, String tableNamePattern, String columnNamePattern) {
+ var clauses = List.of(
+ new Clause("RF.RDB$SCHEMA_NAME", schemaPattern),
+ new Clause("RF.RDB$RELATION_NAME", tableNamePattern),
+ new Clause("RF.RDB$FIELD_NAME", columnNamePattern));
+ String sql = GET_COLUMNS_FRAGMENT_6
+ + (Clause.anyCondition(clauses) ? "\nwhere " + Clause.conjunction(clauses) : "")
+ + GET_COLUMNS_ORDER_BY_6;
+ return new MetadataQuery(sql, Clause.parameters(clauses));
+ }
+
+ }
+
}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnsTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnsTest.java
index 5482ecae6..01f384573 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnsTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnsTest.java
@@ -17,6 +17,8 @@
import static org.firebirdsql.common.FBTestProperties.getConnectionViaDriverManager;
import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
+import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
+import static org.firebirdsql.jaybird.util.StringUtils.trimToNull;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@@ -28,6 +30,8 @@
*/
class FBDatabaseMetaDataColumnsTest {
+ // TODO Add schema support: tests involving other schema
+
private static final String TEST_TABLE = "TEST_COLUMN_METADATA";
private static final String CREATE_DOMAIN_WITH_DEFAULT =
@@ -175,7 +179,7 @@ void testIntegerColumn() throws Exception {
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 1);
validationRules.put(ColumnMetaData.IS_AUTOINCREMENT, "");
- validate(TEST_TABLE, "COL_INTEGER", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_INTEGER", validationRules);
}
/**
@@ -193,7 +197,7 @@ void testInteger_DefaultNullColumn() throws Exception {
validationRules.put(ColumnMetaData.IS_AUTOINCREMENT, "");
validationRules.put(ColumnMetaData.COLUMN_DEF, "NULL");
- validate(TEST_TABLE, "COL_INTEGER_DEFAULT_NULL", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_INTEGER_DEFAULT_NULL", validationRules);
}
/**
@@ -210,7 +214,7 @@ void testInteger_Default999Column() throws Exception {
validationRules.put(ColumnMetaData.IS_AUTOINCREMENT, "");
validationRules.put(ColumnMetaData.COLUMN_DEF, "999");
- validate(TEST_TABLE, "COL_INTEGER_DEFAULT_999", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_INTEGER_DEFAULT_999", validationRules);
}
/**
@@ -229,7 +233,7 @@ void testInteger_NotNullColumn() throws Exception {
validationRules.put(ColumnMetaData.NULLABLE, DatabaseMetaData.columnNoNulls);
validationRules.put(ColumnMetaData.IS_NULLABLE, "NO");
- validate(TEST_TABLE, "COL_INTEGER_NOT_NULL", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_INTEGER_NOT_NULL", validationRules);
}
/**
@@ -247,7 +251,7 @@ void testBigintColumn() throws Exception {
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 2);
validationRules.put(ColumnMetaData.IS_AUTOINCREMENT, "");
- validate(TEST_TABLE, "COL_BIGINT", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_BIGINT", validationRules);
}
/**
@@ -264,7 +268,7 @@ void testSmallintColumn() throws Exception {
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 3);
validationRules.put(ColumnMetaData.IS_AUTOINCREMENT, "");
- validate(TEST_TABLE, "COL_SMALLINT", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_SMALLINT", validationRules);
}
/**
@@ -281,7 +285,7 @@ void testDoublePrecisionColumn() throws Exception {
validationRules.put(ColumnMetaData.NUM_PREC_RADIX, supportsFloatBinaryPrecision ? 2 : 10);
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 4);
- validate(TEST_TABLE, "COL_DOUBLE", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_DOUBLE", validationRules);
}
/**
@@ -298,7 +302,7 @@ void testFloatColumn() throws Exception {
validationRules.put(ColumnMetaData.NUM_PREC_RADIX, supportsFloatBinaryPrecision ? 2 : 10);
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 5);
- validate(TEST_TABLE, "COL_FLOAT", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_FLOAT", validationRules);
}
/**
@@ -326,7 +330,7 @@ void testDecimalColumn(String columnName, int expectedSize, int expectedDecDigit
validationRules.put(ColumnMetaData.IS_AUTOINCREMENT, "");
}
- validate(TEST_TABLE, columnName, validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, columnName, validationRules);
}
/**
@@ -354,7 +358,7 @@ void testNumericColumn(String columnName, int expectedSize, int expectedDecDigit
validationRules.put(ColumnMetaData.IS_AUTOINCREMENT, "");
}
- validate(TEST_TABLE, columnName, validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, columnName, validationRules);
}
/**
@@ -370,7 +374,7 @@ void testDateColumn() throws Exception {
validationRules.put(ColumnMetaData.COLUMN_SIZE, 10);
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 18);
- validate(TEST_TABLE, "COL_DATE", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_DATE", validationRules);
}
/**
@@ -385,7 +389,7 @@ void testTimeColumn() throws Exception {
validationRules.put(ColumnMetaData.COLUMN_SIZE, 13);
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 19);
- validate(TEST_TABLE, "COL_TIME", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_TIME", validationRules);
}
/**
@@ -400,7 +404,7 @@ void testTimestampColumn() throws Exception {
validationRules.put(ColumnMetaData.COLUMN_SIZE, 24);
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 20);
- validate(TEST_TABLE, "COL_TIMESTAMP", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_TIMESTAMP", validationRules);
}
/**
@@ -416,7 +420,7 @@ void testChar10_UTF8Column() throws Exception {
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 21);
validationRules.put(ColumnMetaData.CHAR_OCTET_LENGTH, 40);
- validate(TEST_TABLE, "COL_CHAR_10_UTF8", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_CHAR_10_UTF8", validationRules);
}
/**
@@ -432,7 +436,7 @@ void testChar10_ISO8859_1Column() throws Exception {
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 22);
validationRules.put(ColumnMetaData.CHAR_OCTET_LENGTH, 10);
- validate(TEST_TABLE, "COL_CHAR_10_ISO8859_1", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_CHAR_10_ISO8859_1", validationRules);
}
/**
@@ -448,7 +452,7 @@ void testChar10_OCTETSColumn() throws Exception {
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 23);
validationRules.put(ColumnMetaData.CHAR_OCTET_LENGTH, 10);
- validate(TEST_TABLE, "COL_CHAR_10_OCTETS", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_CHAR_10_OCTETS", validationRules);
}
/**
@@ -464,7 +468,7 @@ void testVarchar10_UTF8Column() throws Exception {
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 24);
validationRules.put(ColumnMetaData.CHAR_OCTET_LENGTH, 40);
- validate(TEST_TABLE, "COL_VARCHAR_10_UTF8", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_VARCHAR_10_UTF8", validationRules);
}
/**
@@ -480,7 +484,7 @@ void testVarchar10_ISO8859_1Column() throws Exception {
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 25);
validationRules.put(ColumnMetaData.CHAR_OCTET_LENGTH, 10);
- validate(TEST_TABLE, "COL_VARCHAR_10_ISO8859_1", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_VARCHAR_10_ISO8859_1", validationRules);
}
/**
@@ -496,7 +500,7 @@ void testVarchar10_OCTETSColumn() throws Exception {
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 26);
validationRules.put(ColumnMetaData.CHAR_OCTET_LENGTH, 10);
- validate(TEST_TABLE, "COL_VARCHAR_10_OCTETS", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_VARCHAR_10_OCTETS", validationRules);
}
/**
@@ -518,7 +522,7 @@ void testVarchar_Default(String columnName, int expectedPosition, String expecte
validationRules.put(ColumnMetaData.CHAR_OCTET_LENGTH, 100);
validationRules.put(ColumnMetaData.COLUMN_DEF, expectedDefault);
- validate(TEST_TABLE, columnName, validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, columnName, validationRules);
}
/**
@@ -534,7 +538,7 @@ void testVarchar_Generated() throws Exception {
validationRules.put(ColumnMetaData.CHAR_OCTET_LENGTH, 200);
validationRules.put(ColumnMetaData.IS_GENERATEDCOLUMN, "YES");
- validate(TEST_TABLE, "COL_VARCHAR_GENERATED", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_VARCHAR_GENERATED", validationRules);
}
/**
@@ -552,7 +556,7 @@ void testVarchar_NotNullColumn() throws Exception {
validationRules.put(ColumnMetaData.NULLABLE, DatabaseMetaData.columnNoNulls);
validationRules.put(ColumnMetaData.IS_NULLABLE, "NO");
- validate(TEST_TABLE, "COL_VARCHAR_NOT_NULL", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_VARCHAR_NOT_NULL", validationRules);
}
/**
@@ -567,7 +571,7 @@ void testTextBlob_UTF8Column() throws Exception {
validationRules.put(ColumnMetaData.COLUMN_SIZE, null);
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 27);
- validate(TEST_TABLE, "COL_BLOB_TEXT_UTF8", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_BLOB_TEXT_UTF8", validationRules);
}
/**
@@ -582,7 +586,7 @@ void testTextBlob_ISO8859_1Column() throws Exception {
validationRules.put(ColumnMetaData.COLUMN_SIZE, null);
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 28);
- validate(TEST_TABLE, "COL_BLOB_TEXT_ISO8859_1", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_BLOB_TEXT_ISO8859_1", validationRules);
}
/**
@@ -597,7 +601,7 @@ void testBlobColumn() throws Exception {
validationRules.put(ColumnMetaData.COLUMN_SIZE, null);
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 29);
- validate(TEST_TABLE, "COL_BLOB_BINARY", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_BLOB_BINARY", validationRules);
}
/**
@@ -614,7 +618,7 @@ void testDomainWithDefaultColumn() throws Exception {
validationRules.put(ColumnMetaData.CHAR_OCTET_LENGTH, 100);
validationRules.put(ColumnMetaData.COLUMN_DEF, "'this is a default'");
- validate(TEST_TABLE, "COL_DOMAIN_WITH_DEFAULT", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_DOMAIN_WITH_DEFAULT", validationRules);
}
/**
@@ -631,7 +635,7 @@ void testDomainWithDefaultOverriddenColumn() throws Exception {
validationRules.put(ColumnMetaData.CHAR_OCTET_LENGTH, 100);
validationRules.put(ColumnMetaData.COLUMN_DEF, "'overridden default'");
- validate(TEST_TABLE, "COL_DOMAIN_W_DEFAULT_OVERRIDDEN", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_DOMAIN_W_DEFAULT_OVERRIDDEN", validationRules);
}
@Test
@@ -644,7 +648,7 @@ void testBooleanColumn() throws Exception {
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 40);
validationRules.put(ColumnMetaData.NUM_PREC_RADIX, 2);
- validate(TEST_TABLE, "COL_BOOLEAN", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_BOOLEAN", validationRules);
}
@Test
@@ -656,7 +660,7 @@ void testDecfloat16Column() throws Exception {
validationRules.put(ColumnMetaData.COLUMN_SIZE, 16);
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 41);
- validate(TEST_TABLE, "COL_DECFLOAT16", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_DECFLOAT16", validationRules);
}
@Test
@@ -668,7 +672,7 @@ void testDecfloat34Column() throws Exception {
validationRules.put(ColumnMetaData.COLUMN_SIZE, 34);
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 42);
- validate(TEST_TABLE, "COL_DECFLOAT34", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_DECFLOAT34", validationRules);
}
/**
@@ -686,7 +690,7 @@ void testNumeric25_20Column() throws Exception {
validationRules.put(ColumnMetaData.DECIMAL_DIGITS, 20);
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 43);
- validate(TEST_TABLE, "COL_NUMERIC25_20", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_NUMERIC25_20", validationRules);
}
/**
@@ -704,7 +708,7 @@ void testDecimal30_5Column() throws Exception {
validationRules.put(ColumnMetaData.DECIMAL_DIGITS, 5);
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 44);
- validate(TEST_TABLE, "COL_DECIMAL30_5", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_DECIMAL30_5", validationRules);
}
@Test
@@ -717,7 +721,7 @@ void testTimeWithTimezoneColumn() throws Exception {
validationRules.put(ColumnMetaData.COLUMN_SIZE, 19);
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 45);
- validate(TEST_TABLE, "COL_TIMETZ", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_TIMETZ", validationRules);
}
@Test
@@ -730,7 +734,7 @@ void testTimestampWithTimezoneColumn() throws Exception {
validationRules.put(ColumnMetaData.COLUMN_SIZE, 30);
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 46);
- validate(TEST_TABLE, "COL_TIMESTAMPTZ", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_TIMESTAMPTZ", validationRules);
}
@Test
@@ -744,25 +748,32 @@ void testInt128Column() throws Exception {
validationRules.put(ColumnMetaData.DECIMAL_DIGITS, 0);
validationRules.put(ColumnMetaData.ORDINAL_POSITION, 47);
- validate(TEST_TABLE, "COL_INT128", validationRules);
+ validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_INT128", validationRules);
}
// TODO: Add more extensive tests of patterns
/**
* Method to validate the column metadata for a single column of a table (does not support quoted identifiers).
- *
- * @param tableName Name of the able
- * @param columnName Name of the column
- * @param validationRules Map of validationRules
+ *
+ * @param schema
+ * Name of the schema
+ * @param tableName
+ * Name of the table
+ * @param columnName
+ * Name of the column
+ * @param validationRules
+ * Map of validationRules
*/
@SuppressWarnings("SameParameterValue")
- private void validate(String tableName, String columnName, Map validationRules) throws Exception {
+ private void validate(String schema, String tableName, String columnName,
+ Map validationRules) throws Exception {
+ validationRules.put(ColumnMetaData.TABLE_SCHEM, trimToNull(schema));
validationRules.put(ColumnMetaData.TABLE_NAME, tableName);
validationRules.put(ColumnMetaData.COLUMN_NAME, columnName);
getColumnsDefinition.checkValidationRulesComplete(validationRules);
- try (ResultSet columns = dbmd.getColumns(null, null, tableName, columnName)) {
+ try (ResultSet columns = dbmd.getColumns(null, schema, tableName, columnName)) {
assertTrue(columns.next(), "Expected row in column metadata");
getColumnsDefinition.validateRowValues(columns, validationRules);
assertFalse(columns.next(), "Expected only one row in resultset");
@@ -773,7 +784,7 @@ private void validate(String tableName, String columnName, Map defaults = new EnumMap<>(ColumnMetaData.class);
defaults.put(ColumnMetaData.TABLE_CAT, null);
- defaults.put(ColumnMetaData.TABLE_SCHEM, null);
+ defaults.put(ColumnMetaData.TABLE_SCHEM, ifSchemaElse("PUBLIC", null));
defaults.put(ColumnMetaData.BUFFER_LENGTH, null);
defaults.put(ColumnMetaData.DECIMAL_DIGITS, null);
defaults.put(ColumnMetaData.NUM_PREC_RADIX, 10);
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataProceduresTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataProceduresTest.java
index 3f56d38b2..e53d880d2 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataProceduresTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataProceduresTest.java
@@ -37,7 +37,7 @@
*/
class FBDatabaseMetaDataProceduresTest {
- // TODO Add tests for filtering by schema
+ // TODO Add schema support: tests involving other schema
private static final String CREATE_NORMAL_PROC_NO_RETURN = """
CREATE PROCEDURE normal_proc_no_return
From 1d30a6d4182ff1e4b335b732d8081de39069a0d8 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Sat, 21 Jun 2025 15:56:32 +0200
Subject: [PATCH 11/64] #882 Schema support for
getImportedKeys/getExportedKeys/getCrossReference
---
.../firebirdsql/jdbc/FBDatabaseMetaData.java | 9 +-
.../jdbc/metadata/AbstractKeysMethod.java | 6 +-
.../jdbc/metadata/GetBestRowIdentifier.java | 2 +-
.../jdbc/metadata/GetColumnPrivileges.java | 2 +-
.../jdbc/metadata/GetCrossReference.java | 138 +++++++++++++++---
.../jdbc/metadata/GetExportedKeys.java | 126 +++++++++++++---
.../jdbc/metadata/GetImportedKeys.java | 127 +++++++++++++---
.../FBDatabaseMetaDataAbstractKeysTest.java | 15 +-
.../FBDatabaseMetaDataCrossReferenceTest.java | 12 +-
.../FBDatabaseMetaDataExportedKeysTest.java | 17 ++-
.../FBDatabaseMetaDataImportedKeysTest.java | 17 ++-
.../FBDatabaseMetaDataProceduresTest.java | 5 +-
12 files changed, 394 insertions(+), 82 deletions(-)
diff --git a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
index 72e8edb2c..15a25c8da 100644
--- a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
@@ -1443,7 +1443,7 @@ public ResultSet getPrimaryKeys(String catalog, String schema, String table) thr
*/
@Override
public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException {
- return GetImportedKeys.create(getDbMetadataMediator()).getImportedKeys(table);
+ return GetImportedKeys.create(getDbMetadataMediator()).getImportedKeys(schema, table);
}
/**
@@ -1458,7 +1458,7 @@ public ResultSet getImportedKeys(String catalog, String schema, String table) th
*/
@Override
public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException {
- return GetExportedKeys.create(getDbMetadataMediator()).getExportedKeys(table);
+ return GetExportedKeys.create(getDbMetadataMediator()).getExportedKeys(schema, table);
}
/**
@@ -1473,9 +1473,10 @@ public ResultSet getExportedKeys(String catalog, String schema, String table) th
*/
@Override
public ResultSet getCrossReference(
- String primaryCatalog, String primarySchema, String primaryTable,
+ String parentCatalog, String parentSchema, String parentTable,
String foreignCatalog, String foreignSchema, String foreignTable) throws SQLException {
- return GetCrossReference.create(getDbMetadataMediator()).getCrossReference(primaryTable, foreignTable);
+ return GetCrossReference.create(getDbMetadataMediator())
+ .getCrossReference(parentSchema, parentTable, foreignSchema, foreignTable);
}
@Override
diff --git a/src/main/org/firebirdsql/jdbc/metadata/AbstractKeysMethod.java b/src/main/org/firebirdsql/jdbc/metadata/AbstractKeysMethod.java
index a263b9e2a..6f08fde7c 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/AbstractKeysMethod.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/AbstractKeysMethod.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -51,8 +51,10 @@ abstract class AbstractKeysMethod extends AbstractMetadataMethod {
@Override
final RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQLException {
return valueBuilder
+ .at(1).setString(rs.getString("PKTABLE_SCHEM"))
.at(2).setString(rs.getString("PKTABLE_NAME"))
.at(3).setString(rs.getString("PKCOLUMN_NAME"))
+ .at(5).setString(rs.getString("FKTABLE_SCHEM"))
.at(6).setString(rs.getString("FKTABLE_NAME"))
.at(7).setString(rs.getString("FKCOLUMN_NAME"))
.at(8).setShort(rs.getShort("KEY_SEQ"))
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java b/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java
index 5ff6c7670..870307cae 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java
@@ -62,7 +62,7 @@ private GetBestRowIdentifier(DbMetadataMediator mediator) {
}
@SuppressWarnings("unused")
- public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable)
+ public final ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable)
throws SQLException {
if (isNullOrEmpty(table)) {
return createEmpty();
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java b/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java
index 0416d1ba4..1912e254e 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java
@@ -52,7 +52,7 @@ public abstract class GetColumnPrivileges extends AbstractMetadataMethod {
/**
* @see java.sql.DatabaseMetaData#getColumnPrivileges(String, String, String, String)
*/
- public ResultSet getColumnPrivileges(String schema, String table, String columnNamePattern) throws SQLException {
+ public final ResultSet getColumnPrivileges(String schema, String table, String columnNamePattern) throws SQLException {
if (isNullOrEmpty(table) || "".equals(columnNamePattern)) {
return createEmpty();
}
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetCrossReference.java b/src/main/org/firebirdsql/jdbc/metadata/GetCrossReference.java
index 7c2a8dd4f..47369e451 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetCrossReference.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetCrossReference.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -8,6 +8,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.ArrayList;
import static org.firebirdsql.jaybird.util.StringUtils.isNullOrEmpty;
@@ -17,12 +18,44 @@
* @author Mark Rotteveel
* @since 5
*/
-public final class GetCrossReference extends AbstractKeysMethod {
+public abstract class GetCrossReference extends AbstractKeysMethod {
- private static final String GET_CROSS_KEYS_START = """
+ private GetCrossReference(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ public final ResultSet getCrossReference(String parentSchema, String parentTable,
+ String foreignSchema, String foreignTable) throws SQLException {
+ if (isNullOrEmpty(parentTable) || isNullOrEmpty(foreignTable)) {
+ return createEmpty();
+ }
+ MetadataQuery metadataQuery = createGetCrossReferenceQuery(parentSchema, parentTable, foreignSchema, foreignTable);
+ return createMetaDataResultSet(metadataQuery);
+ }
+
+ abstract MetadataQuery createGetCrossReferenceQuery(String parentSchema, String parentTable,
+ String foreignSchema, String foreignTable);
+
+ public static GetCrossReference create(DbMetadataMediator mediator) {
+ // NOTE: Indirection through static method prevents unnecessary classloading
+ if (mediator.getFirebirdSupportInfo().isVersionEqualOrAbove(6)) {
+ return FB6.createInstance(mediator);
+ } else {
+ return FB5.createInstance(mediator);
+ }
+ }
+
+ /**
+ * Implementation for Firebird 5.0 and older.
+ */
+ private static final class FB5 extends GetCrossReference {
+
+ private static final String GET_CROSS_KEYS_START_5 = """
select
+ cast(null as char(1)) AS PKTABLE_SCHEM,
PK.RDB$RELATION_NAME as PKTABLE_NAME,
ISP.RDB$FIELD_NAME as PKCOLUMN_NAME,
+ cast(null as char(1)) AS FKTABLE_SCHEM,
FK.RDB$RELATION_NAME as FKTABLE_NAME,
ISF.RDB$FIELD_NAME as FKCOLUMN_NAME,
ISP.RDB$FIELD_POSITION + 1 as KEY_SEQ,
@@ -43,27 +76,92 @@ public final class GetCrossReference extends AbstractKeysMethod {
on ISF.RDB$INDEX_NAME = FK.RDB$INDEX_NAME and ISP.RDB$FIELD_POSITION = ISF.RDB$FIELD_POSITION
where\s""";
- private static final String GET_CROSS_KEYS_END = "\norder by FK.RDB$RELATION_NAME, ISP.RDB$FIELD_POSITION";
+ private static final String GET_CROSS_KEYS_END_5 = "\norder by FK.RDB$RELATION_NAME, ISP.RDB$FIELD_POSITION";
- private GetCrossReference(DbMetadataMediator mediator) {
- super(mediator);
- }
+ private FB5(DbMetadataMediator mediator) {
+ super(mediator);
+ }
- public ResultSet getCrossReference(String primaryTable, String foreignTable) throws SQLException {
- if (isNullOrEmpty(primaryTable) || isNullOrEmpty(foreignTable)) {
- return createEmpty();
+ private static GetCrossReference createInstance(DbMetadataMediator mediator) {
+ return new FB5(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetCrossReferenceQuery(String parentSchema, String parentTable,
+ String foreignSchema, String foreignTable) {
+ Clause parentTableClause = Clause.equalsClause("PK.RDB$RELATION_NAME", parentTable);
+ Clause foreignTableCause = Clause.equalsClause("FK.RDB$RELATION_NAME", foreignTable);
+ String sql = GET_CROSS_KEYS_START_5
+ + Clause.conjunction(parentTableClause, foreignTableCause)
+ + GET_CROSS_KEYS_END_5;
+ return new MetadataQuery(sql, Clause.parameters(parentTableClause, foreignTableCause));
}
- Clause primaryTableClause = Clause.equalsClause("PK.RDB$RELATION_NAME", primaryTable);
- Clause foreignTableCause = Clause.equalsClause("FK.RDB$RELATION_NAME", foreignTable);
- String sql = GET_CROSS_KEYS_START
- + Clause.conjunction(primaryTableClause, foreignTableCause)
- + GET_CROSS_KEYS_END;
- MetadataQuery metadataQuery = new MetadataQuery(sql, Clause.parameters(primaryTableClause, foreignTableCause));
- return createMetaDataResultSet(metadataQuery);
}
- public static GetCrossReference create(DbMetadataMediator mediator) {
- return new GetCrossReference(mediator);
+ /**
+ * Implementation for Firebird 6.0 and higher.
+ */
+ private static final class FB6 extends GetCrossReference {
+
+ private static final String GET_CROSS_KEYS_START_6 = """
+ select
+ trim(trailing from PK.RDB$SCHEMA_NAME) as PKTABLE_SCHEM,
+ trim(trailing from PK.RDB$RELATION_NAME) as PKTABLE_NAME,
+ trim(trailing from ISP.RDB$FIELD_NAME) as PKCOLUMN_NAME,
+ trim(trailing from FK.RDB$SCHEMA_NAME) as FKTABLE_SCHEM,
+ trim(trailing from FK.RDB$RELATION_NAME) as FKTABLE_NAME,
+ trim(trailing from ISF.RDB$FIELD_NAME) as FKCOLUMN_NAME,
+ ISP.RDB$FIELD_POSITION + 1 as KEY_SEQ,
+ RC.RDB$UPDATE_RULE as UPDATE_RULE,
+ RC.RDB$DELETE_RULE as DELETE_RULE,
+ trim(trailing from PK.RDB$CONSTRAINT_NAME) as PK_NAME,
+ trim(trailing from FK.RDB$CONSTRAINT_NAME) as FK_NAME,
+ trim(trailing from PK.RDB$INDEX_NAME) as JB_PK_INDEX_NAME,
+ trim(trailing from FK.RDB$INDEX_NAME) as JB_FK_INDEX_NAME
+ from SYSTEM.RDB$RELATION_CONSTRAINTS PK
+ inner join SYSTEM.RDB$REF_CONSTRAINTS RC
+ on PK.RDB$SCHEMA_NAME = RC.RDB$CONST_SCHEMA_NAME_UQ and PK.RDB$CONSTRAINT_NAME = RC.RDB$CONST_NAME_UQ
+ inner join SYSTEM.RDB$RELATION_CONSTRAINTS FK
+ on FK.RDB$SCHEMA_NAME = RC.RDB$SCHEMA_NAME and FK.RDB$CONSTRAINT_NAME = RC.RDB$CONSTRAINT_NAME
+ inner join SYSTEM.RDB$INDEX_SEGMENTS ISP
+ on ISP.RDB$SCHEMA_NAME = PK.RDB$SCHEMA_NAME and ISP.RDB$INDEX_NAME = PK.RDB$INDEX_NAME
+ inner join SYSTEM.RDB$INDEX_SEGMENTS ISF
+ on ISF.RDB$SCHEMA_NAME = FK.RDB$SCHEMA_NAME and ISF.RDB$INDEX_NAME = FK.RDB$INDEX_NAME
+ and ISP.RDB$FIELD_POSITION = ISF.RDB$FIELD_POSITION
+ where\s""";
+
+ private static final String GET_CROSS_KEYS_END_6 =
+ "\norder by FK.RDB$SCHEMA_NAME, FK.RDB$RELATION_NAME, ISP.RDB$FIELD_POSITION";
+
+ private FB6(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetCrossReference createInstance(DbMetadataMediator mediator) {
+ return new FB6(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetCrossReferenceQuery(String parentSchema, String parentTable,
+ String foreignSchema, String foreignTable) {
+ var clauses = new ArrayList(4);
+ if (parentSchema != null) {
+ // NOTE: empty string will return no rows as required ("" retrieves those without a schema)
+ clauses.add(Clause.equalsClause("PK.RDB$SCHEMA_NAME", parentSchema));
+ }
+ clauses.add(Clause.equalsClause("PK.RDB$RELATION_NAME", parentTable));
+ if (foreignSchema != null) {
+ // NOTE: empty string will return no rows as required ("" retrieves those without a schema)
+ clauses.add(Clause.equalsClause("FK.RDB$SCHEMA_NAME", foreignSchema));
+ }
+ clauses.add(Clause.equalsClause("FK.RDB$RELATION_NAME", foreignTable));
+ String sql = GET_CROSS_KEYS_START_6
+ + Clause.conjunction(clauses)
+ + GET_CROSS_KEYS_END_6;
+ return new MetadataQuery(sql, Clause.parameters(clauses));
+ }
+
}
+
}
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetExportedKeys.java b/src/main/org/firebirdsql/jdbc/metadata/GetExportedKeys.java
index 357013984..0434e8853 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetExportedKeys.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetExportedKeys.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -8,6 +8,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.ArrayList;
import static org.firebirdsql.jaybird.util.StringUtils.isNullOrEmpty;
@@ -17,12 +18,42 @@
* @author Mark Rotteveel
* @since 5
*/
-public final class GetExportedKeys extends AbstractKeysMethod {
+public abstract class GetExportedKeys extends AbstractKeysMethod {
- private static final String GET_EXPORTED_KEYS_START = """
+ private GetExportedKeys(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ public final ResultSet getExportedKeys(String schema, String table) throws SQLException {
+ if (isNullOrEmpty(table)) {
+ return createEmpty();
+ }
+ MetadataQuery metadataQuery = createGetExportedKeysQuery(schema, table);
+ return createMetaDataResultSet(metadataQuery);
+ }
+
+ abstract MetadataQuery createGetExportedKeysQuery(String schema, String table);
+
+ public static GetExportedKeys create(DbMetadataMediator mediator) {
+ // NOTE: Indirection through static method prevents unnecessary classloading
+ if (mediator.getFirebirdSupportInfo().isVersionEqualOrAbove(6)) {
+ return FB6.createInstance(mediator);
+ } else {
+ return FB5.createInstance(mediator);
+ }
+ }
+
+ /**
+ * Implementation for Firebird 5.0 and older.
+ */
+ private static final class FB5 extends GetExportedKeys {
+
+ private static final String GET_EXPORTED_KEYS_START_5 = """
select
+ cast(null as char(1)) as PKTABLE_SCHEM,
PK.RDB$RELATION_NAME as PKTABLE_NAME,
ISP.RDB$FIELD_NAME as PKCOLUMN_NAME,
+ cast(null as char(1)) as FKTABLE_SCHEM,
FK.RDB$RELATION_NAME as FKTABLE_NAME,
ISF.RDB$FIELD_NAME as FKCOLUMN_NAME,
ISP.RDB$FIELD_POSITION + 1 as KEY_SEQ,
@@ -43,26 +74,83 @@ public final class GetExportedKeys extends AbstractKeysMethod {
on ISF.RDB$INDEX_NAME = FK.RDB$INDEX_NAME and ISP.RDB$FIELD_POSITION = ISF.RDB$FIELD_POSITION
where\s""";
- private static final String GET_EXPORTED_KEYS_END = "\norder by FK.RDB$RELATION_NAME, ISP.RDB$FIELD_POSITION";
+ private static final String GET_EXPORTED_KEYS_END_5 = "\norder by FK.RDB$RELATION_NAME, ISP.RDB$FIELD_POSITION";
- private GetExportedKeys(DbMetadataMediator mediator) {
- super(mediator);
- }
+ private FB5(DbMetadataMediator mediator) {
+ super(mediator);
+ }
- public ResultSet getExportedKeys(String table) throws SQLException {
- if (isNullOrEmpty(table)) {
- return createEmpty();
+ private static GetExportedKeys createInstance(DbMetadataMediator mediator) {
+ return new FB5(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetExportedKeysQuery(String schema, String table) {
+ Clause tableClause = Clause.equalsClause("PK.RDB$RELATION_NAME", table);
+ String sql = GET_EXPORTED_KEYS_START_5
+ + tableClause.getCondition(false)
+ + GET_EXPORTED_KEYS_END_5;
+ return new MetadataQuery(sql, Clause.parameters(tableClause));
}
- Clause tableClause = Clause.equalsClause("PK.RDB$RELATION_NAME", table);
- String sql = GET_EXPORTED_KEYS_START
- + tableClause.getCondition(false)
- + GET_EXPORTED_KEYS_END;
- MetadataQuery metadataQuery = new MetadataQuery(sql, Clause.parameters(tableClause));
- return createMetaDataResultSet(metadataQuery);
}
- public static GetExportedKeys create(DbMetadataMediator mediator) {
- return new GetExportedKeys(mediator);
+ /**
+ * Implementation for Firebird 6.0 and higher.
+ */
+ private static final class FB6 extends GetExportedKeys {
+
+ private static final String GET_EXPORTED_KEYS_START_6 = """
+ select
+ trim(trailing from PK.RDB$SCHEMA_NAME) as PKTABLE_SCHEM,
+ trim(trailing from PK.RDB$RELATION_NAME) as PKTABLE_NAME,
+ trim(trailing from ISP.RDB$FIELD_NAME) as PKCOLUMN_NAME,
+ trim(trailing from FK.RDB$SCHEMA_NAME) as FKTABLE_SCHEM,
+ trim(trailing from FK.RDB$RELATION_NAME) as FKTABLE_NAME,
+ trim(trailing from ISF.RDB$FIELD_NAME) as FKCOLUMN_NAME,
+ ISP.RDB$FIELD_POSITION + 1 as KEY_SEQ,
+ RC.RDB$UPDATE_RULE as UPDATE_RULE,
+ RC.RDB$DELETE_RULE as DELETE_RULE,
+ trim(trailing from PK.RDB$CONSTRAINT_NAME) as PK_NAME,
+ trim(trailing from FK.RDB$CONSTRAINT_NAME) as FK_NAME,
+ trim(trailing from PK.RDB$INDEX_NAME) as JB_PK_INDEX_NAME,
+ trim(trailing from FK.RDB$INDEX_NAME) as JB_FK_INDEX_NAME
+ from SYSTEM.RDB$RELATION_CONSTRAINTS PK
+ inner join SYSTEM.RDB$REF_CONSTRAINTS RC
+ on PK.RDB$SCHEMA_NAME = RC.RDB$CONST_SCHEMA_NAME_UQ and PK.RDB$CONSTRAINT_NAME = RC.RDB$CONST_NAME_UQ
+ inner join SYSTEM.RDB$RELATION_CONSTRAINTS FK
+ on FK.RDB$SCHEMA_NAME = RC.RDB$SCHEMA_NAME and FK.RDB$CONSTRAINT_NAME = RC.RDB$CONSTRAINT_NAME
+ inner join SYSTEM.RDB$INDEX_SEGMENTS ISP
+ on ISP.RDB$SCHEMA_NAME = PK.RDB$SCHEMA_NAME and ISP.RDB$INDEX_NAME = PK.RDB$INDEX_NAME
+ inner join SYSTEM.RDB$INDEX_SEGMENTS ISF
+ on ISF.RDB$SCHEMA_NAME = FK.RDB$SCHEMA_NAME and ISF.RDB$INDEX_NAME = FK.RDB$INDEX_NAME
+ and ISP.RDB$FIELD_POSITION = ISF.RDB$FIELD_POSITION
+ where\s""";
+
+ private static final String GET_EXPORTED_KEYS_END_6 =
+ "\norder by FK.RDB$SCHEMA_NAME, FK.RDB$RELATION_NAME, ISP.RDB$FIELD_POSITION";
+
+ private FB6(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetExportedKeys createInstance(DbMetadataMediator mediator) {
+ return new FB6(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetExportedKeysQuery(String schema, String table) {
+ var clauses = new ArrayList(2);
+ if (schema != null) {
+ // NOTE: empty string will return no rows as required ("" retrieves those without a schema)
+ clauses.add(Clause.equalsClause("PK.RDB$SCHEMA_NAME", schema));
+ }
+ clauses.add(Clause.equalsClause("PK.RDB$RELATION_NAME", table));
+ String sql = GET_EXPORTED_KEYS_START_6
+ + Clause.conjunction(clauses)
+ + GET_EXPORTED_KEYS_END_6;
+ return new MetadataQuery(sql, Clause.parameters(clauses));
+ }
+
}
}
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetImportedKeys.java b/src/main/org/firebirdsql/jdbc/metadata/GetImportedKeys.java
index fd3343a3e..8c264b6fb 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetImportedKeys.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetImportedKeys.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -8,6 +8,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.ArrayList;
import static org.firebirdsql.jaybird.util.StringUtils.isNullOrEmpty;
@@ -17,12 +18,42 @@
* @author Mark Rotteveel
* @since 5
*/
-public final class GetImportedKeys extends AbstractKeysMethod {
+public abstract class GetImportedKeys extends AbstractKeysMethod {
- private static final String GET_IMPORTED_KEYS_START = """
+ private GetImportedKeys(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ public final ResultSet getImportedKeys(String schema, String table) throws SQLException {
+ if (isNullOrEmpty(table)) {
+ return createEmpty();
+ }
+ MetadataQuery metadataQuery = createGetImportedKeysQuery(schema, table);
+ return createMetaDataResultSet(metadataQuery);
+ }
+
+ abstract MetadataQuery createGetImportedKeysQuery(String schema, String table);
+
+ public static GetImportedKeys create(DbMetadataMediator mediator) {
+ // NOTE: Indirection through static method prevents unnecessary classloading
+ if (mediator.getFirebirdSupportInfo().isVersionEqualOrAbove(6)) {
+ return FB6.createInstance(mediator);
+ } else {
+ return FB5.createInstance(mediator);
+ }
+ }
+
+ /**
+ * Implementation for Firebird 5.0 and older.
+ */
+ private static final class FB5 extends GetImportedKeys {
+
+ private static final String GET_IMPORTED_KEYS_START_5 = """
select
+ cast(null as char(1)) as PKTABLE_SCHEM,
PK.RDB$RELATION_NAME as PKTABLE_NAME,
ISP.RDB$FIELD_NAME as PKCOLUMN_NAME,
+ cast(null as char(1)) as FKTABLE_SCHEM,
FK.RDB$RELATION_NAME as FKTABLE_NAME,
ISF.RDB$FIELD_NAME as FKCOLUMN_NAME,
ISP.RDB$FIELD_POSITION + 1 as KEY_SEQ,
@@ -43,26 +74,84 @@ public final class GetImportedKeys extends AbstractKeysMethod {
on ISF.RDB$INDEX_NAME = FK.RDB$INDEX_NAME and ISP.RDB$FIELD_POSITION = ISF.RDB$FIELD_POSITION
where\s""";
- private static final String GET_IMPORTED_KEYS_END = "\norder by PK.RDB$RELATION_NAME, ISP.RDB$FIELD_POSITION";
+ private static final String GET_IMPORTED_KEYS_END_5 = "\norder by PK.RDB$RELATION_NAME, ISP.RDB$FIELD_POSITION";
- private GetImportedKeys(DbMetadataMediator mediator) {
- super(mediator);
- }
+ private FB5(DbMetadataMediator mediator) {
+ super(mediator);
+ }
- public ResultSet getImportedKeys(String table) throws SQLException {
- if (isNullOrEmpty(table)) {
- return createEmpty();
+ private static GetImportedKeys createInstance(DbMetadataMediator mediator) {
+ return new FB5(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetImportedKeysQuery(String schema, String table) {
+ Clause tableClause = Clause.equalsClause("FK.RDB$RELATION_NAME", table);
+ String sql = GET_IMPORTED_KEYS_START_5
+ + tableClause.getCondition(false)
+ + GET_IMPORTED_KEYS_END_5;
+ return new MetadataQuery(sql, Clause.parameters(tableClause));
}
- Clause tableClause = Clause.equalsClause("FK.RDB$RELATION_NAME", table);
- String sql = GET_IMPORTED_KEYS_START
- + tableClause.getCondition(false)
- + GET_IMPORTED_KEYS_END;
- MetadataQuery metadataQuery = new MetadataQuery(sql, Clause.parameters(tableClause));
- return createMetaDataResultSet(metadataQuery);
}
- public static GetImportedKeys create(DbMetadataMediator mediator) {
- return new GetImportedKeys(mediator);
+ /**
+ * Implementation for Firebird 6.0 and higher.
+ */
+ private static final class FB6 extends GetImportedKeys {
+
+ private static final String GET_IMPORTED_KEYS_START_6 = """
+ select
+ trim(trailing from PK.RDB$SCHEMA_NAME) as PKTABLE_SCHEM,
+ trim(trailing from PK.RDB$RELATION_NAME) as PKTABLE_NAME,
+ trim(trailing from ISP.RDB$FIELD_NAME) as PKCOLUMN_NAME,
+ trim(trailing from FK.RDB$SCHEMA_NAME) as FKTABLE_SCHEM,
+ trim(trailing from FK.RDB$RELATION_NAME) as FKTABLE_NAME,
+ trim(trailing from ISF.RDB$FIELD_NAME) as FKCOLUMN_NAME,
+ ISP.RDB$FIELD_POSITION + 1 as KEY_SEQ,
+ RC.RDB$UPDATE_RULE as UPDATE_RULE,
+ RC.RDB$DELETE_RULE as DELETE_RULE,
+ trim(trailing from PK.RDB$CONSTRAINT_NAME) as PK_NAME,
+ trim(trailing from FK.RDB$CONSTRAINT_NAME) as FK_NAME,
+ trim(trailing from PK.RDB$INDEX_NAME) as JB_PK_INDEX_NAME,
+ trim(trailing from FK.RDB$INDEX_NAME) as JB_FK_INDEX_NAME
+ from SYSTEM.RDB$RELATION_CONSTRAINTS PK
+ inner join SYSTEM.RDB$REF_CONSTRAINTS RC
+ on PK.RDB$SCHEMA_NAME = RC.RDB$CONST_SCHEMA_NAME_UQ and PK.RDB$CONSTRAINT_NAME = RC.RDB$CONST_NAME_UQ
+ inner join SYSTEM.RDB$RELATION_CONSTRAINTS FK
+ on FK.RDB$SCHEMA_NAME = RC.RDB$SCHEMA_NAME and FK.RDB$CONSTRAINT_NAME = RC.RDB$CONSTRAINT_NAME
+ inner join SYSTEM.RDB$INDEX_SEGMENTS ISP
+ on ISP.RDB$SCHEMA_NAME = PK.RDB$SCHEMA_NAME and ISP.RDB$INDEX_NAME = PK.RDB$INDEX_NAME
+ inner join SYSTEM.RDB$INDEX_SEGMENTS ISF
+ on ISF.RDB$SCHEMA_NAME = FK.RDB$SCHEMA_NAME and ISF.RDB$INDEX_NAME = FK.RDB$INDEX_NAME
+ and ISP.RDB$FIELD_POSITION = ISF.RDB$FIELD_POSITION
+ where\s""";
+
+ private static final String GET_IMPORTED_KEYS_END_6 =
+ "\norder by PK.RDB$SCHEMA_NAME, PK.RDB$RELATION_NAME, ISP.RDB$FIELD_POSITION";
+
+ private FB6(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetImportedKeys createInstance(DbMetadataMediator mediator) {
+ return new FB6(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetImportedKeysQuery(String schema, String table) {
+ var clauses = new ArrayList(2);
+ if (schema != null) {
+ // NOTE: empty string will return no rows as required ("" retrieves those without a schema)
+ clauses.add(Clause.equalsClause("FK.RDB$SCHEMA_NAME", schema));
+ }
+ clauses.add(Clause.equalsClause("FK.RDB$RELATION_NAME", table));
+ String sql = GET_IMPORTED_KEYS_START_6
+ + Clause.conjunction(clauses)
+ + GET_IMPORTED_KEYS_END_6;
+ return new MetadataQuery(sql, Clause.parameters(clauses));
+ }
+
}
+
}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataAbstractKeysTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataAbstractKeysTest.java
index a1714d1df..c25a1d6ab 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataAbstractKeysTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataAbstractKeysTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2024-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -19,8 +19,10 @@
import static java.util.Collections.unmodifiableMap;
import static org.firebirdsql.common.FBTestProperties.getConnectionViaDriverManager;
+import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
import static org.firebirdsql.common.assertions.ResultSetAssertions.assertNextRow;
import static org.firebirdsql.common.assertions.ResultSetAssertions.assertNoNextRow;
+import static org.firebirdsql.jaybird.util.StringUtils.trimToNull;
/**
* Base test class for subclasses of {@code org.firebirdsql.jdbc.metadata.AbstractKeysMethod}.
@@ -180,9 +182,18 @@ protected void validateExpectedKeys(ResultSet keys, List createKeysTestData(String pkTable, String pkColumn, String fkTable,
String fkColumn, int keySeq, int updateRule, int deleteRule, String pkName, String fkName,
String pkIndexName, String fkIndexName) {
+ return createKeysTestData(ifSchemaElse("PUBLIC", null), pkTable, pkColumn, ifSchemaElse("PUBLIC", null),
+ fkTable, fkColumn, keySeq, updateRule, deleteRule, pkName, fkName, pkIndexName, fkIndexName);
+ }
+
+ protected static Map createKeysTestData(String pkSchema, String pkTable, String pkColumn,
+ String fkSchema, String fkTable, String fkColumn, int keySeq, int updateRule, int deleteRule,
+ String pkName, String fkName, String pkIndexName, String fkIndexName) {
Map rules = getDefaultValidationRules();
+ rules.put(KeysMetaData.PKTABLE_SCHEM, trimToNull(pkSchema));
rules.put(KeysMetaData.PKTABLE_NAME, pkTable);
rules.put(KeysMetaData.PKCOLUMN_NAME, pkColumn);
+ rules.put(KeysMetaData.FKTABLE_SCHEM, trimToNull(fkSchema));
rules.put(KeysMetaData.FKTABLE_NAME, fkTable);
rules.put(KeysMetaData.FKCOLUMN_NAME, fkColumn);
rules.put(KeysMetaData.KEY_SEQ, (short) keySeq);
@@ -207,6 +218,8 @@ private static Object constraintNameValidation(String constraintName) {
static {
var defaults = new EnumMap<>(KeysMetaData.class);
Arrays.stream(KeysMetaData.values()).forEach(key -> defaults.put(key, null));
+ defaults.put(KeysMetaData.PKTABLE_SCHEM, ifSchemaElse("PUBLIC", null));
+ defaults.put(KeysMetaData.FKTABLE_SCHEM, ifSchemaElse("PUBLIC", null));
defaults.put(KeysMetaData.UPDATE_RULE, DatabaseMetaData.importedKeyNoAction);
defaults.put(KeysMetaData.DELETE_RULE, DatabaseMetaData.importedKeyNoAction);
defaults.put(KeysMetaData.DEFERRABILITY, DatabaseMetaData.importedKeyNotDeferrable);
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataCrossReferenceTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataCrossReferenceTest.java
index 8320c5ce2..041bc72a6 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataCrossReferenceTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataCrossReferenceTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2024-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -12,6 +12,8 @@
import java.util.Map;
import java.util.stream.Stream;
+import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
+
/**
* Tests for {@link FBDatabaseMetaData#getCrossReference(String, String, String, String, String, String)}.
*
@@ -19,9 +21,12 @@
*/
class FBDatabaseMetaDataCrossReferenceTest extends FBDatabaseMetaDataAbstractKeysTest {
+ // TODO Add schema support: tests involving other schema
+
@Test
void testCrossReferenceMetaDataColumns() throws Exception {
- try (ResultSet crossReference = dbmd.getCrossReference(null, null, "doesnotexit", null, null, "doesnotexist")) {
+ try (ResultSet crossReference = dbmd.getCrossReference(
+ null, null, "doesnotexist", null, null, "doesnotexist")) {
keysDefinition.validateResultSetColumns(crossReference);
}
}
@@ -30,7 +35,8 @@ void testCrossReferenceMetaDataColumns() throws Exception {
@MethodSource
void testCrossReference(String parentTable, String foreignTable, List> expectedKeys)
throws Exception {
- try (ResultSet crossReference = dbmd.getCrossReference(null, null, parentTable, null, null, foreignTable)) {
+ try (ResultSet crossReference = dbmd.getCrossReference(null, ifSchemaElse("PUBLIC", ""), parentTable, null,
+ ifSchemaElse("PUBLIC", ""), foreignTable)) {
validateExpectedKeys(crossReference, expectedKeys);
}
}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataExportedKeysTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataExportedKeysTest.java
index e1f0f3ebd..3b4217ebe 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataExportedKeysTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataExportedKeysTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2024-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -14,6 +14,8 @@
import java.util.Map;
import java.util.stream.Stream;
+import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
+
/**
* Tests for {@link java.sql.DatabaseMetaData#getExportedKeys(String, String, String)}.
*
@@ -21,6 +23,8 @@
*/
class FBDatabaseMetaDataExportedKeysTest extends FBDatabaseMetaDataAbstractKeysTest {
+ // TODO Add schema support: tests involving other schema
+
@Test
void testExportedKeysMetaDataColumns() throws Exception {
try (ResultSet exportedKeys = dbmd.getExportedKeys(null, null, "doesnotexit")) {
@@ -30,8 +34,8 @@ void testExportedKeysMetaDataColumns() throws Exception {
@ParameterizedTest
@MethodSource
- void testExportedKeys(String table, List> expectedKeys) throws Exception {
- try (ResultSet exportedKeys = dbmd.getExportedKeys(null, null, table)) {
+ void testExportedKeys(String schema, String table, List> expectedKeys) throws Exception {
+ try (ResultSet exportedKeys = dbmd.getExportedKeys(null, schema, table)) {
validateExpectedKeys(exportedKeys, expectedKeys);
}
}
@@ -46,7 +50,12 @@ static Stream testExportedKeys() {
}
private static Arguments exportedKeysTestCase(String table, List> expectedKeys) {
- return Arguments.of(table, expectedKeys);
+ return exportedKeysTestCase(ifSchemaElse("PUBLIC", ""), table, expectedKeys);
+ }
+
+ private static Arguments exportedKeysTestCase(String schema, String table,
+ List> expectedKeys) {
+ return Arguments.of(schema, table, expectedKeys);
}
@SuppressWarnings("SameParameterValue")
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataImportedKeysTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataImportedKeysTest.java
index ad64ea532..296cd3b42 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataImportedKeysTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataImportedKeysTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2024-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -12,6 +12,8 @@
import java.util.Map;
import java.util.stream.Stream;
+import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
+
/**
* Tests for {@link java.sql.DatabaseMetaData#getImportedKeys(String, String, String)}.
*
@@ -19,6 +21,8 @@
*/
class FBDatabaseMetaDataImportedKeysTest extends FBDatabaseMetaDataAbstractKeysTest {
+ // TODO Add schema support: tests involving other schema
+
@Test
void testExportedKeysMetaDataColumns() throws Exception {
try (ResultSet importedKeys = dbmd.getImportedKeys(null, null, "doesnotexit")) {
@@ -28,8 +32,8 @@ void testExportedKeysMetaDataColumns() throws Exception {
@ParameterizedTest
@MethodSource
- void testImportedKeys(String table, List> expectedKeys) throws Exception {
- try (ResultSet importedKeys = dbmd.getImportedKeys(null, null, table)) {
+ void testImportedKeys(String schema, String table, List> expectedKeys) throws Exception {
+ try (ResultSet importedKeys = dbmd.getImportedKeys(null, schema, table)) {
validateExpectedKeys(importedKeys, expectedKeys);
}
}
@@ -47,7 +51,12 @@ static Stream testImportedKeys() {
}
private static Arguments importedKeysTestCase(String table, List> expectedKeys) {
- return Arguments.of(table, expectedKeys);
+ return importedKeysTestCase(ifSchemaElse("PUBLIC", ""), table, expectedKeys);
+ }
+
+ private static Arguments importedKeysTestCase(String schema, String table,
+ List> expectedKeys) {
+ return Arguments.of(schema, table, expectedKeys);
}
}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataProceduresTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataProceduresTest.java
index e53d880d2..293416ab0 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataProceduresTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataProceduresTest.java
@@ -26,6 +26,7 @@
import static org.firebirdsql.common.FBTestProperties.getDefaultPropertiesForConnection;
import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
import static org.firebirdsql.common.FBTestProperties.getUrl;
+import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
import static org.firebirdsql.common.JdbcResourceHelper.closeQuietly;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@@ -372,10 +373,6 @@ public Class> getColumnClass() {
}
}
- private static String ifSchemaElse(String forSchema, String withoutSchema) {
- return getDefaultSupportInfo().supportsSchemas() ? forSchema : withoutSchema;
- }
-
private enum ProcedureTestData {
NORMAL_PROC_NO_RETURN("normal_proc_no_return", List.of(CREATE_NORMAL_PROC_NO_RETURN)) {
@Override
From 69effebb07ec51dde743c38a3ce638524135e9b6 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Sun, 22 Jun 2025 13:10:17 +0200
Subject: [PATCH 12/64] #882 Schema support for getFunctionColumns
---
.../firebirdsql/jdbc/FBDatabaseMetaData.java | 2 +-
.../jdbc/metadata/GetFunctionColumns.java | 389 +++++++++++++-----
...FBDatabaseMetaDataFunctionColumnsTest.java | 24 +-
.../jdbc/FBDatabaseMetaDataFunctionsTest.java | 7 +-
4 files changed, 297 insertions(+), 125 deletions(-)
diff --git a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
index 15a25c8da..043d311db 100644
--- a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
@@ -1742,7 +1742,7 @@ public ResultSet getClientInfoProperties() throws SQLException {
public ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern,
String columnNamePattern) throws SQLException {
return GetFunctionColumns.create(getDbMetadataMediator())
- .getFunctionColumns(catalog, functionNamePattern, columnNamePattern);
+ .getFunctionColumns(catalog, schemaPattern, functionNamePattern, columnNamePattern);
}
/**
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetFunctionColumns.java b/src/main/org/firebirdsql/jdbc/metadata/GetFunctionColumns.java
index 1b4860dc5..c7c464109 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetFunctionColumns.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetFunctionColumns.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2019-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2019-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -13,6 +13,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
+import java.util.List;
import static java.sql.DatabaseMetaData.functionColumnIn;
import static java.sql.DatabaseMetaData.functionNoNulls;
@@ -73,14 +74,15 @@ private GetFunctionColumns(DbMetadataMediator mediator) {
/**
* @see java.sql.DatabaseMetaData#getFunctionColumns(String, String, String, String)
*/
- public final ResultSet getFunctionColumns(String catalog, String functionNamePattern, String columnNamePattern)
- throws SQLException {
+ public final ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern,
+ String columnNamePattern) throws SQLException {
if ("".equals(functionNamePattern) || "".equals(columnNamePattern)) {
// Matching function name or column name not possible
return createEmpty();
}
- MetadataQuery metadataQuery = createGetFunctionColumnsQuery(catalog, functionNamePattern, columnNamePattern);
+ MetadataQuery metadataQuery = createGetFunctionColumnsQuery(catalog, schemaPattern, functionNamePattern,
+ columnNamePattern);
return createMetaDataResultSet(metadataQuery);
}
@@ -90,12 +92,13 @@ final RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) thr
.fromCurrentRow(rs)
.build();
String catalog = rs.getString("FUNCTION_CAT");
+ String schema = rs.getString("FUNCTION_SCHEM");
String functionName = rs.getString("FUNCTION_NAME");
int ordinalPosition = rs.getInt("ORDINAL_POSITION");
boolean nullable = rs.getBoolean("IS_NULLABLE");
return valueBuilder
.at(0).setString(catalog)
- .at(1).set(null)
+ .at(1).setString(schema)
.at(2).setString(functionName)
.at(3).setString(rs.getString("COLUMN_NAME"))
.at(4).setShort(ordinalPosition == 0 ? functionReturn : functionColumnIn)
@@ -111,17 +114,22 @@ final RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) thr
.at(13).setInt(typeMetadata.getCharOctetLength())
.at(14).setInt(ordinalPosition)
.at(15).setString(nullable ? "YES" : "NO")
- .at(16).setString(toSpecificName(catalog, functionName))
+ .at(16).setString(toSpecificName(catalog, schema, functionName))
.toRowValue(false);
}
- abstract MetadataQuery createGetFunctionColumnsQuery(String catalog, String functionNamePattern,
- String columnNamePattern);
+ abstract MetadataQuery createGetFunctionColumnsQuery(String catalog, String schemaPattern,
+ String functionNamePattern, String columnNamePattern);
public static GetFunctionColumns create(DbMetadataMediator mediator) {
FirebirdSupportInfo firebirdSupportInfo = mediator.getFirebirdSupportInfo();
// NOTE: Indirection through static method prevents unnecessary classloading
- if (firebirdSupportInfo.isVersionEqualOrAbove(3)) {
+ if (firebirdSupportInfo.isVersionEqualOrAbove(6)) {
+ if (mediator.isUseCatalogAsPackage()) {
+ return FB6CatalogAsPackage.createInstance(mediator);
+ }
+ return FB6.createInstance(mediator);
+ } else if (firebirdSupportInfo.isVersionEqualOrAbove(3)) {
if (mediator.isUseCatalogAsPackage()) {
return FB3CatalogAsPackage.createInstance(mediator);
}
@@ -135,31 +143,33 @@ public static GetFunctionColumns create(DbMetadataMediator mediator) {
private static final class FB2_5 extends GetFunctionColumns {
//@formatter:off
- private static final String GET_FUNCTION_COLUMNS_FRAGMENT_2_5 =
- "select\n"
- + " null as FUNCTION_CAT,\n"
- + " FUN.RDB$FUNCTION_NAME as FUNCTION_NAME,\n"
- // Firebird 2.5 and earlier have no parameter name: derive one
- + " 'PARAM_' || FUNA.RDB$ARGUMENT_POSITION as COLUMN_NAME,\n"
- + " FUNA.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n"
- + " FUNA.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n"
- + " FUNA.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n"
- + " FUNA.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n"
- + " FUNA.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n"
- + " FUNA.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n"
- + " FUNA.RDB$CHARACTER_SET_ID as " + CHARSET_ID + ",\n"
- + " case\n"
- + " when FUN.RDB$RETURN_ARGUMENT = FUNA.RDB$ARGUMENT_POSITION then 0\n"
- + " else FUNA.RDB$ARGUMENT_POSITION\n"
- + " end as ORDINAL_POSITION,\n"
- + " case FUNA.RDB$MECHANISM\n"
- + " when 0 then 'F'\n"
- + " when 1 then 'F'\n"
- + " else 'T'\n"
- + " end as IS_NULLABLE\n"
- + "from RDB$FUNCTIONS FUN\n"
- + "inner join RDB$FUNCTION_ARGUMENTS FUNA\n"
- + " on FUNA.RDB$FUNCTION_NAME = FUN.RDB$FUNCTION_NAME";
+ private static final String GET_FUNCTION_COLUMNS_FRAGMENT_2_5 = """
+ select
+ cast(null as char(1)) as FUNCTION_CAT,
+ cast(null as char(1)) as FUNCTION_SCHEM,
+ FUN.RDB$FUNCTION_NAME as FUNCTION_NAME,
+ -- Firebird 2.5 and earlier have no parameter name: derive one
+ 'PARAM_' || FUNA.RDB$ARGUMENT_POSITION as COLUMN_NAME,
+ """ +
+ " FUNA.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n" +
+ " FUNA.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n" +
+ " FUNA.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n" +
+ " FUNA.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n" +
+ " FUNA.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n" +
+ " FUNA.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n" +
+ " FUNA.RDB$CHARACTER_SET_ID as " + CHARSET_ID + ",\n" + """
+ case
+ when FUN.RDB$RETURN_ARGUMENT = FUNA.RDB$ARGUMENT_POSITION then 0
+ else FUNA.RDB$ARGUMENT_POSITION
+ end as ORDINAL_POSITION,
+ case FUNA.RDB$MECHANISM
+ when 0 then 'F'
+ when 1 then 'F'
+ else 'T'
+ end as IS_NULLABLE
+ from RDB$FUNCTIONS FUN
+ inner join RDB$FUNCTION_ARGUMENTS FUNA
+ on FUNA.RDB$FUNCTION_NAME = FUN.RDB$FUNCTION_NAME""";
//@formatter:on
private static final String GET_FUNCTION_COLUMNS_ORDER_BY_2_5 = """
@@ -178,53 +188,54 @@ private static GetFunctionColumns createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createGetFunctionColumnsQuery(String catalog, String functionNamePattern,
+ MetadataQuery createGetFunctionColumnsQuery(String catalog, String schemaPattern, String functionNamePattern,
String columnNamePattern) {
- Clause functionNameClause = new Clause("FUN.RDB$FUNCTION_NAME", functionNamePattern);
- Clause columnNameClause = new Clause("'PARAM_' || FUNA.RDB$ARGUMENT_POSITION", columnNamePattern);
+ var clauses = List.of(
+ new Clause("FUN.RDB$FUNCTION_NAME", functionNamePattern),
+ new Clause("'PARAM_' || FUNA.RDB$ARGUMENT_POSITION", columnNamePattern));
String query = GET_FUNCTION_COLUMNS_FRAGMENT_2_5
- + (anyCondition(functionNameClause, columnNameClause)
- ? "\nwhere " + functionNameClause.getCondition(columnNameClause.hasCondition())
- + columnNameClause.getCondition(false)
- : "")
+ + (anyCondition(clauses) ? "\nwhere " + Clause.conjunction(clauses) : "")
+ GET_FUNCTION_COLUMNS_ORDER_BY_2_5;
- return new MetadataQuery(query, Clause.parameters(functionNameClause, columnNameClause));
+ return new MetadataQuery(query, Clause.parameters(clauses));
}
+
}
private static final class FB3 extends GetFunctionColumns {
//@formatter:off
- private static final String GET_FUNCTION_COLUMNS_FRAGMENT_3 =
- "select\n"
- + " null as FUNCTION_CAT,\n"
- + " trim(trailing from FUN.RDB$FUNCTION_NAME) as FUNCTION_NAME,\n"
- + " -- legacy UDF and return value have no parameter name: derive one\n"
- + " coalesce(FUNA.RDB$ARGUMENT_NAME, 'PARAM_' || FUNA.RDB$ARGUMENT_POSITION) as COLUMN_NAME,\n"
- + " coalesce(FUNA.RDB$FIELD_TYPE, F.RDB$FIELD_TYPE) as " + FIELD_TYPE + ",\n"
- + " coalesce(FUNA.RDB$FIELD_SUB_TYPE, F.RDB$FIELD_SUB_TYPE) as " + FIELD_SUB_TYPE + ",\n"
- + " coalesce(FUNA.RDB$FIELD_PRECISION, F.RDB$FIELD_PRECISION) as " + FIELD_PRECISION + ",\n"
- + " coalesce(FUNA.RDB$FIELD_SCALE, F.RDB$FIELD_SCALE) as " + FIELD_SCALE + ",\n"
- + " coalesce(FUNA.RDB$FIELD_LENGTH, F.RDB$FIELD_LENGTH) as " + FIELD_LENGTH + ",\n"
- + " coalesce(FUNA.RDB$CHARACTER_LENGTH, F.RDB$CHARACTER_LENGTH) as " + CHAR_LEN + ",\n"
- + " coalesce(FUNA.RDB$CHARACTER_SET_ID, F.RDB$CHARACTER_SET_ID) as " + CHARSET_ID + ",\n"
- + " case\n"
- + " when FUN.RDB$RETURN_ARGUMENT = FUNA.RDB$ARGUMENT_POSITION then 0\n"
- + " else FUNA.RDB$ARGUMENT_POSITION\n"
- + " end as ORDINAL_POSITION,\n"
- + " case \n"
- + " when coalesce(FUNA.RDB$NULL_FLAG, F.RDB$NULL_FLAG) = 1 then false\n"
- + " when FUNA.RDB$MECHANISM = 0 then false\n"
- + " when FUNA.RDB$MECHANISM = 1 then false\n"
- + " else true\n"
- + " end as IS_NULLABLE\n"
- + "from RDB$FUNCTIONS FUN\n"
- + "inner join RDB$FUNCTION_ARGUMENTS FUNA\n"
- + " on FUNA.RDB$FUNCTION_NAME = FUN.RDB$FUNCTION_NAME \n"
- + " and FUNA.RDB$PACKAGE_NAME is not distinct from FUN.RDB$PACKAGE_NAME\n"
- + "left join RDB$FIELDS F\n"
- + " on F.RDB$FIELD_NAME = FUNA.RDB$FIELD_SOURCE\n"
- + "where FUN.RDB$PACKAGE_NAME is null";
+ private static final String GET_FUNCTION_COLUMNS_FRAGMENT_3 = """
+ select
+ null as FUNCTION_CAT,
+ null as FUNCTION_SCHEM,
+ trim(trailing from FUN.RDB$FUNCTION_NAME) as FUNCTION_NAME,
+ -- legacy UDF and return value have no parameter name: derive one
+ trim(trailing from coalesce(FUNA.RDB$ARGUMENT_NAME, 'PARAM_' || FUNA.RDB$ARGUMENT_POSITION)) as COLUMN_NAME,
+ """ +
+ " coalesce(FUNA.RDB$FIELD_TYPE, F.RDB$FIELD_TYPE) as " + FIELD_TYPE + ",\n" +
+ " coalesce(FUNA.RDB$FIELD_SUB_TYPE, F.RDB$FIELD_SUB_TYPE) as " + FIELD_SUB_TYPE + ",\n" +
+ " coalesce(FUNA.RDB$FIELD_PRECISION, F.RDB$FIELD_PRECISION) as " + FIELD_PRECISION + ",\n" +
+ " coalesce(FUNA.RDB$FIELD_SCALE, F.RDB$FIELD_SCALE) as " + FIELD_SCALE + ",\n" +
+ " coalesce(FUNA.RDB$FIELD_LENGTH, F.RDB$FIELD_LENGTH) as " + FIELD_LENGTH + ",\n" +
+ " coalesce(FUNA.RDB$CHARACTER_LENGTH, F.RDB$CHARACTER_LENGTH) as " + CHAR_LEN + ",\n" +
+ " coalesce(FUNA.RDB$CHARACTER_SET_ID, F.RDB$CHARACTER_SET_ID) as " + CHARSET_ID + ",\n" + """
+ case
+ when FUN.RDB$RETURN_ARGUMENT = FUNA.RDB$ARGUMENT_POSITION then 0
+ else FUNA.RDB$ARGUMENT_POSITION
+ end as ORDINAL_POSITION,
+ case
+ when coalesce(FUNA.RDB$NULL_FLAG, F.RDB$NULL_FLAG) = 1 then false
+ when FUNA.RDB$MECHANISM = 0 then false
+ when FUNA.RDB$MECHANISM = 1 then false
+ else true
+ end as IS_NULLABLE
+ from RDB$FUNCTIONS FUN
+ inner join RDB$FUNCTION_ARGUMENTS FUNA
+ on FUNA.RDB$FUNCTION_NAME = FUN.RDB$FUNCTION_NAME
+ and FUNA.RDB$PACKAGE_NAME is not distinct from FUN.RDB$PACKAGE_NAME
+ left join RDB$FIELDS F
+ on F.RDB$FIELD_NAME = FUNA.RDB$FIELD_SOURCE
+ where FUN.RDB$PACKAGE_NAME is null""";
//@formatter:on
// NOTE: Including RDB$PACKAGE_NAME so index can be used to sort
@@ -245,51 +256,54 @@ private static GetFunctionColumns createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createGetFunctionColumnsQuery(String catalog, String functionNamePattern,
+ MetadataQuery createGetFunctionColumnsQuery(String catalog, String schemaPattern, String functionNamePattern,
String columnNamePattern) {
- Clause functionNameClause = new Clause("FUN.RDB$FUNCTION_NAME", functionNamePattern);
- Clause columnNameClause = new Clause(
- "coalesce(FUNA.RDB$ARGUMENT_NAME, 'PARAM_' || FUNA.RDB$ARGUMENT_POSITION)", columnNamePattern);
+ var clauses = List.of(
+ new Clause("FUN.RDB$FUNCTION_NAME", functionNamePattern),
+ new Clause("coalesce(FUNA.RDB$ARGUMENT_NAME, 'PARAM_' || FUNA.RDB$ARGUMENT_POSITION)",
+ columnNamePattern));
String query = GET_FUNCTION_COLUMNS_FRAGMENT_3
- + functionNameClause.getCondition("\nand ", "")
- + columnNameClause.getCondition("\nand ", "")
+ + (Clause.anyCondition(clauses) ? "\nand " + Clause.conjunction(clauses) : "")
+ GET_FUNCTION_COLUMNS_ORDER_BY_3;
- return new MetadataQuery(query, Clause.parameters(functionNameClause, columnNameClause));
+ return new MetadataQuery(query, Clause.parameters(clauses));
}
+
}
private static final class FB3CatalogAsPackage extends GetFunctionColumns {
//@formatter:off
- private static final String GET_FUNCTION_COLUMNS_FRAGMENT_3_W_PKG =
- "select\n"
- + " coalesce(trim(trailing from FUN.RDB$PACKAGE_NAME), '') as FUNCTION_CAT,\n"
- + " trim(trailing from FUN.RDB$FUNCTION_NAME) as FUNCTION_NAME,\n"
- + " -- legacy UDF and return value have no parameter name: derive one\n"
- + " coalesce(FUNA.RDB$ARGUMENT_NAME, 'PARAM_' || FUNA.RDB$ARGUMENT_POSITION) as COLUMN_NAME,\n"
- + " coalesce(FUNA.RDB$FIELD_TYPE, F.RDB$FIELD_TYPE) as " + FIELD_TYPE + ",\n"
- + " coalesce(FUNA.RDB$FIELD_SUB_TYPE, F.RDB$FIELD_SUB_TYPE) as " + FIELD_SUB_TYPE + ",\n"
- + " coalesce(FUNA.RDB$FIELD_PRECISION, F.RDB$FIELD_PRECISION) as " + FIELD_PRECISION + ",\n"
- + " coalesce(FUNA.RDB$FIELD_SCALE, F.RDB$FIELD_SCALE) as " + FIELD_SCALE + ",\n"
- + " coalesce(FUNA.RDB$FIELD_LENGTH, F.RDB$FIELD_LENGTH) as " + FIELD_LENGTH + ",\n"
- + " coalesce(FUNA.RDB$CHARACTER_LENGTH, F.RDB$CHARACTER_LENGTH) as " + CHAR_LEN + ",\n"
- + " coalesce(FUNA.RDB$CHARACTER_SET_ID, F.RDB$CHARACTER_SET_ID) as " + CHARSET_ID + ",\n"
- + " case\n"
- + " when FUN.RDB$RETURN_ARGUMENT = FUNA.RDB$ARGUMENT_POSITION then 0\n"
- + " else FUNA.RDB$ARGUMENT_POSITION\n"
- + " end as ORDINAL_POSITION,\n"
- + " case \n"
- + " when coalesce(FUNA.RDB$NULL_FLAG, F.RDB$NULL_FLAG) = 1 then false\n"
- + " when FUNA.RDB$MECHANISM = 0 then false\n"
- + " when FUNA.RDB$MECHANISM = 1 then false\n"
- + " else true\n"
- + " end as IS_NULLABLE\n"
- + "from RDB$FUNCTIONS FUN\n"
- + "inner join RDB$FUNCTION_ARGUMENTS FUNA\n"
- + " on FUNA.RDB$FUNCTION_NAME = FUN.RDB$FUNCTION_NAME \n"
- + " and FUNA.RDB$PACKAGE_NAME is not distinct from FUN.RDB$PACKAGE_NAME\n"
- + "left join RDB$FIELDS F\n"
- + " on F.RDB$FIELD_NAME = FUNA.RDB$FIELD_SOURCE";
+ private static final String GET_FUNCTION_COLUMNS_FRAGMENT_3_W_PKG = """
+ select
+ coalesce(trim(trailing from FUN.RDB$PACKAGE_NAME), '') as FUNCTION_CAT,
+ null as FUNCTION_SCHEM,
+ trim(trailing from FUN.RDB$FUNCTION_NAME) as FUNCTION_NAME,
+ -- legacy UDF and return value have no parameter name: derive one
+ trim(trailing from coalesce(FUNA.RDB$ARGUMENT_NAME, 'PARAM_' || FUNA.RDB$ARGUMENT_POSITION)) as COLUMN_NAME,
+ """ +
+ " coalesce(FUNA.RDB$FIELD_TYPE, F.RDB$FIELD_TYPE) as " + FIELD_TYPE + ",\n" +
+ " coalesce(FUNA.RDB$FIELD_SUB_TYPE, F.RDB$FIELD_SUB_TYPE) as " + FIELD_SUB_TYPE + ",\n" +
+ " coalesce(FUNA.RDB$FIELD_PRECISION, F.RDB$FIELD_PRECISION) as " + FIELD_PRECISION + ",\n" +
+ " coalesce(FUNA.RDB$FIELD_SCALE, F.RDB$FIELD_SCALE) as " + FIELD_SCALE + ",\n" +
+ " coalesce(FUNA.RDB$FIELD_LENGTH, F.RDB$FIELD_LENGTH) as " + FIELD_LENGTH + ",\n" +
+ " coalesce(FUNA.RDB$CHARACTER_LENGTH, F.RDB$CHARACTER_LENGTH) as " + CHAR_LEN + ",\n" +
+ " coalesce(FUNA.RDB$CHARACTER_SET_ID, F.RDB$CHARACTER_SET_ID) as " + CHARSET_ID + ",\n" + """
+ case
+ when FUN.RDB$RETURN_ARGUMENT = FUNA.RDB$ARGUMENT_POSITION then 0
+ else FUNA.RDB$ARGUMENT_POSITION
+ end as ORDINAL_POSITION,
+ case
+ when coalesce(FUNA.RDB$NULL_FLAG, F.RDB$NULL_FLAG) = 1 then false
+ when FUNA.RDB$MECHANISM = 0 then false
+ when FUNA.RDB$MECHANISM = 1 then false
+ else true
+ end as IS_NULLABLE
+ from RDB$FUNCTIONS FUN
+ inner join RDB$FUNCTION_ARGUMENTS FUNA
+ on FUNA.RDB$FUNCTION_NAME = FUN.RDB$FUNCTION_NAME
+ and FUNA.RDB$PACKAGE_NAME is not distinct from FUN.RDB$PACKAGE_NAME
+ left join RDB$FIELDS F
+ on F.RDB$FIELD_NAME = FUNA.RDB$FIELD_SOURCE""";
//@formatter:on
// NOTE: Including RDB$PACKAGE_NAME so index can be used to sort
@@ -309,7 +323,7 @@ private static GetFunctionColumns createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createGetFunctionColumnsQuery(String catalog, String functionNamePattern,
+ MetadataQuery createGetFunctionColumnsQuery(String catalog, String schemaPattern, String functionNamePattern,
String columnNamePattern) {
var clauses = new ArrayList(3);
if (catalog != null) {
@@ -327,12 +341,161 @@ MetadataQuery createGetFunctionColumnsQuery(String catalog, String functionNameP
"coalesce(FUNA.RDB$ARGUMENT_NAME, 'PARAM_' || FUNA.RDB$ARGUMENT_POSITION)", columnNamePattern));
//@formatter:off
String sql = GET_FUNCTION_COLUMNS_FRAGMENT_3_W_PKG
- + (Clause.anyCondition(clauses)
- ? "\nwhere " + Clause.conjunction(clauses)
- : "")
+ + (Clause.anyCondition(clauses) ? "\nwhere " + Clause.conjunction(clauses) : "")
+ GET_FUNCTION_COLUMNS_ORDER_BY_3_W_PKG;
//@formatter:on
return new MetadataQuery(sql, Clause.parameters(clauses));
}
+
+ }
+
+ private static final class FB6 extends GetFunctionColumns {
+
+ //@formatter:off
+ private static final String GET_FUNCTION_COLUMNS_FRAGMENT_6 = """
+ select
+ null as FUNCTION_CAT,
+ trim(trailing from FUN.RDB$SCHEMA_NAME) as FUNCTION_SCHEM,
+ trim(trailing from FUN.RDB$FUNCTION_NAME) as FUNCTION_NAME,
+ -- legacy UDF and return value have no parameter name: derive one
+ trim(trailing from coalesce(FUNA.RDB$ARGUMENT_NAME, 'PARAM_' || FUNA.RDB$ARGUMENT_POSITION)) as COLUMN_NAME,
+ """ +
+ " coalesce(FUNA.RDB$FIELD_TYPE, F.RDB$FIELD_TYPE) as " + FIELD_TYPE + ",\n" +
+ " coalesce(FUNA.RDB$FIELD_SUB_TYPE, F.RDB$FIELD_SUB_TYPE) as " + FIELD_SUB_TYPE + ",\n" +
+ " coalesce(FUNA.RDB$FIELD_PRECISION, F.RDB$FIELD_PRECISION) as " + FIELD_PRECISION + ",\n" +
+ " coalesce(FUNA.RDB$FIELD_SCALE, F.RDB$FIELD_SCALE) as " + FIELD_SCALE + ",\n" +
+ " coalesce(FUNA.RDB$FIELD_LENGTH, F.RDB$FIELD_LENGTH) as " + FIELD_LENGTH + ",\n" +
+ " coalesce(FUNA.RDB$CHARACTER_LENGTH, F.RDB$CHARACTER_LENGTH) as " + CHAR_LEN + ",\n" +
+ " coalesce(FUNA.RDB$CHARACTER_SET_ID, F.RDB$CHARACTER_SET_ID) as " + CHARSET_ID + ",\n" + """
+ case
+ when FUN.RDB$RETURN_ARGUMENT = FUNA.RDB$ARGUMENT_POSITION then 0
+ else FUNA.RDB$ARGUMENT_POSITION
+ end as ORDINAL_POSITION,
+ case
+ when coalesce(FUNA.RDB$NULL_FLAG, F.RDB$NULL_FLAG) = 1 then false
+ when FUNA.RDB$MECHANISM = 0 then false
+ when FUNA.RDB$MECHANISM = 1 then false
+ else true
+ end as IS_NULLABLE
+ from SYSTEM.RDB$FUNCTIONS FUN
+ inner join SYSTEM.RDB$FUNCTION_ARGUMENTS FUNA
+ on FUNA.RDB$SCHEMA_NAME = FUN.RDB$SCHEMA_NAME and FUNA.RDB$FUNCTION_NAME = FUN.RDB$FUNCTION_NAME
+ and FUNA.RDB$PACKAGE_NAME is not distinct from FUN.RDB$PACKAGE_NAME
+ left join SYSTEM.RDB$FIELDS F
+ on F.RDB$SCHEMA_NAME = FUNA.RDB$FIELD_SOURCE_SCHEMA_NAME and F.RDB$FIELD_NAME = FUNA.RDB$FIELD_SOURCE
+ where FUN.RDB$PACKAGE_NAME is null""";
+ //@formatter:on
+
+ // NOTE: Including RDB$PACKAGE_NAME so index can be used to sort
+ private static final String GET_FUNCTION_COLUMNS_ORDER_BY_6 = """
+ \norder by FUN.RDB$SCHEMA_NAME, FUN.RDB$PACKAGE_NAME, FUN.RDB$FUNCTION_NAME,
+ case
+ when FUN.RDB$RETURN_ARGUMENT = FUNA.RDB$ARGUMENT_POSITION then -1
+ else FUNA.RDB$ARGUMENT_POSITION
+ end""";
+
+
+ private FB6(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetFunctionColumns createInstance(DbMetadataMediator mediator) {
+ return new FB6(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetFunctionColumnsQuery(String catalog, String schemaPattern, String functionNamePattern,
+ String columnNamePattern) {
+ var clauses = List.of(
+ new Clause("FUN.RDB$SCHEMA_NAME", schemaPattern),
+ new Clause("FUN.RDB$FUNCTION_NAME", functionNamePattern),
+ new Clause("coalesce(FUNA.RDB$ARGUMENT_NAME, 'PARAM_' || FUNA.RDB$ARGUMENT_POSITION)",
+ columnNamePattern));
+ String query = GET_FUNCTION_COLUMNS_FRAGMENT_6
+ + (Clause.anyCondition(clauses) ? "\nand " + Clause.conjunction(clauses) : "")
+ + GET_FUNCTION_COLUMNS_ORDER_BY_6;
+ return new MetadataQuery(query, Clause.parameters(clauses));
+ }
+
+ }
+
+ private static final class FB6CatalogAsPackage extends GetFunctionColumns {
+
+ //@formatter:off
+ private static final String GET_FUNCTION_COLUMNS_FRAGMENT_6_W_PKG = """
+ select
+ coalesce(trim(trailing from FUN.RDB$PACKAGE_NAME), '') as FUNCTION_CAT,
+ trim(trailing from FUN.RDB$SCHEMA_NAME) as FUNCTION_SCHEM,
+ trim(trailing from FUN.RDB$FUNCTION_NAME) as FUNCTION_NAME,
+ -- legacy UDF and return value have no parameter name: derive one
+ trim(trailing from coalesce(FUNA.RDB$ARGUMENT_NAME, 'PARAM_' || FUNA.RDB$ARGUMENT_POSITION)) as COLUMN_NAME,
+ """ +
+ " coalesce(FUNA.RDB$FIELD_TYPE, F.RDB$FIELD_TYPE) as " + FIELD_TYPE + ",\n" +
+ " coalesce(FUNA.RDB$FIELD_SUB_TYPE, F.RDB$FIELD_SUB_TYPE) as " + FIELD_SUB_TYPE + ",\n" +
+ " coalesce(FUNA.RDB$FIELD_PRECISION, F.RDB$FIELD_PRECISION) as " + FIELD_PRECISION + ",\n" +
+ " coalesce(FUNA.RDB$FIELD_SCALE, F.RDB$FIELD_SCALE) as " + FIELD_SCALE + ",\n" +
+ " coalesce(FUNA.RDB$FIELD_LENGTH, F.RDB$FIELD_LENGTH) as " + FIELD_LENGTH + ",\n" +
+ " coalesce(FUNA.RDB$CHARACTER_LENGTH, F.RDB$CHARACTER_LENGTH) as " + CHAR_LEN + ",\n" +
+ " coalesce(FUNA.RDB$CHARACTER_SET_ID, F.RDB$CHARACTER_SET_ID) as " + CHARSET_ID + ",\n" + """
+ case
+ when FUN.RDB$RETURN_ARGUMENT = FUNA.RDB$ARGUMENT_POSITION then 0
+ else FUNA.RDB$ARGUMENT_POSITION
+ end as ORDINAL_POSITION,
+ case
+ when coalesce(FUNA.RDB$NULL_FLAG, F.RDB$NULL_FLAG) = 1 then false
+ when FUNA.RDB$MECHANISM = 0 then false
+ when FUNA.RDB$MECHANISM = 1 then false
+ else true
+ end as IS_NULLABLE
+ from SYSTEM.RDB$FUNCTIONS FUN
+ inner join SYSTEM.RDB$FUNCTION_ARGUMENTS FUNA
+ on FUNA.RDB$SCHEMA_NAME = FUN.RDB$SCHEMA_NAME and FUNA.RDB$FUNCTION_NAME = FUN.RDB$FUNCTION_NAME
+ and FUNA.RDB$PACKAGE_NAME is not distinct from FUN.RDB$PACKAGE_NAME
+ left join SYSTEM.RDB$FIELDS F
+ on F.RDB$SCHEMA_NAME = FUNA.RDB$FIELD_SOURCE_SCHEMA_NAME and F.RDB$FIELD_NAME = FUNA.RDB$FIELD_SOURCE""";
+ //@formatter:on
+
+ private static final String GET_FUNCTION_COLUMNS_ORDER_BY_6_W_PKG = """
+ \norder by FUN.RDB$PACKAGE_NAME nulls first, FUN.RDB$SCHEMA_NAME, FUN.RDB$FUNCTION_NAME,
+ case
+ when FUN.RDB$RETURN_ARGUMENT = FUNA.RDB$ARGUMENT_POSITION then -1
+ else FUNA.RDB$ARGUMENT_POSITION
+ end""";
+
+ private FB6CatalogAsPackage(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetFunctionColumns createInstance(DbMetadataMediator mediator) {
+ return new FB6CatalogAsPackage(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetFunctionColumnsQuery(String catalog, String schemaPattern, String functionNamePattern,
+ String columnNamePattern) {
+ var clauses = new ArrayList(4);
+ clauses.add(new Clause("FUN.RDB$SCHEMA_NAME", schemaPattern));
+ if (catalog != null) {
+ // To quote from the JDBC API: "" retrieves those without a catalog; null means that the catalog name
+ // should not be used to narrow the search
+ if (catalog.isEmpty()) {
+ clauses.add(Clause.isNullClause("FUN.RDB$PACKAGE_NAME"));
+ } else {
+ // Exact matches only
+ clauses.add(Clause.equalsClause("FUN.RDB$PACKAGE_NAME", catalog));
+ }
+ }
+ clauses.add(new Clause("FUN.RDB$FUNCTION_NAME", functionNamePattern));
+ clauses.add(new Clause(
+ "coalesce(FUNA.RDB$ARGUMENT_NAME, 'PARAM_' || FUNA.RDB$ARGUMENT_POSITION)", columnNamePattern));
+ //@formatter:off
+ String sql = GET_FUNCTION_COLUMNS_FRAGMENT_6_W_PKG
+ + (Clause.anyCondition(clauses) ? "\nwhere " + Clause.conjunction(clauses) : "")
+ + GET_FUNCTION_COLUMNS_ORDER_BY_6_W_PKG;
+ //@formatter:on
+ return new MetadataQuery(sql, Clause.parameters(clauses));
+ }
+
}
+
}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionColumnsTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionColumnsTest.java
index d0d175490..d08b24349 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionColumnsTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionColumnsTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2019-2023 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2019-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -22,9 +22,10 @@
import static org.firebirdsql.common.FBTestProperties.getDefaultPropertiesForConnection;
import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
import static org.firebirdsql.common.FBTestProperties.getUrl;
+import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
+import static org.firebirdsql.common.assertions.ResultSetAssertions.assertNextRow;
import static org.firebirdsql.jdbc.FBDatabaseMetaDataFunctionsTest.isIgnoredFunction;
import static org.firebirdsql.jdbc.metadata.FbMetadataConstants.*;
-import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@@ -353,7 +354,7 @@ void testFunctionColumnMetaData_useCatalogAsPackage_specificPackageProcedureColu
try (var connection = DriverManager.getConnection(getUrl(), props)) {
dbmd = connection.getMetaData();
List> expectedColumns = withCatalog("WITH$FUNCTION",
- withSpecificName("\"WITH$FUNCTION\".\"IN$PACKAGE\"",
+ withSpecificName(ifSchemaElse("\"PUBLIC\".", "") + "\"WITH$FUNCTION\".\"IN$PACKAGE\"",
List.of(createNumericalType(Types.INTEGER, "IN$PACKAGE", "PARAM1", 1, 10, 0, true))));
validateExpectedFunctionColumns(catalog, "IN$PACKAGE", "PARAM1", expectedColumns);
}
@@ -386,7 +387,13 @@ private void validateExpectedFunctionColumns(String functionNamePattern, String
private void validateExpectedFunctionColumns(String catalog, String functionNamePattern, String columnNamePattern,
List> expectedColumns) throws Exception {
- try (ResultSet columns = dbmd.getFunctionColumns(catalog, null, functionNamePattern, columnNamePattern)) {
+ validateExpectedFunctionColumns(catalog, null, functionNamePattern, columnNamePattern, expectedColumns);
+ }
+
+ private void validateExpectedFunctionColumns(String catalog, String schemaPattern, String functionNamePattern,
+ String columnNamePattern, List> expectedColumns) throws Exception {
+ try (ResultSet columns = dbmd.getFunctionColumns(
+ catalog, schemaPattern, functionNamePattern, columnNamePattern)) {
for (Map expectedColumn : expectedColumns) {
expectNextFunctionColumn(columns);
getFunctionColumnsDefinition.validateRowValues(columns, expectedColumn);
@@ -397,7 +404,7 @@ private void validateExpectedFunctionColumns(String catalog, String functionName
private static void expectNextFunctionColumn(ResultSet rs) throws SQLException {
do {
- assertTrue(rs.next(), "Expected a row");
+ assertNextRow(rs);
} while (isIgnoredFunction(rs.getString("SPECIFIC_NAME")));
}
@@ -477,7 +484,7 @@ private static List> getUdfExample2Columns()
private static List> getWithFunctionInPackageColumns() {
return withCatalog("WITH$FUNCTION",
- withSpecificName("\"WITH$FUNCTION\".\"IN$PACKAGE\"",
+ withSpecificName(ifSchemaElse("\"PUBLIC\".", "") + "\"WITH$FUNCTION\".\"IN$PACKAGE\"",
List.of(
withColumnTypeFunctionReturn(
createNumericalType(Types.INTEGER, "IN$PACKAGE", "PARAM_0", 0, 10, 0, true)),
@@ -510,7 +517,8 @@ private static Map createColumn(String functionN
int ordinalPosition, boolean nullable) {
Map rules = getDefaultValidationRules();
rules.put(FunctionColumnMetaData.FUNCTION_NAME, functionName);
- rules.put(FunctionColumnMetaData.SPECIFIC_NAME, functionName);
+ rules.put(FunctionColumnMetaData.SPECIFIC_NAME, ifSchemaElse(
+ "\"PUBLIC\"." + QuoteStrategy.DIALECT_3.quoteObjectName(functionName), functionName));
rules.put(FunctionColumnMetaData.COLUMN_NAME, columnName);
rules.put(FunctionColumnMetaData.ORDINAL_POSITION, ordinalPosition);
if (nullable) {
@@ -674,7 +682,7 @@ private static Map createDecfloat(String functio
static {
Map defaults = new EnumMap<>(FunctionColumnMetaData.class);
defaults.put(FunctionColumnMetaData.FUNCTION_CAT, null);
- defaults.put(FunctionColumnMetaData.FUNCTION_SCHEM, null);
+ defaults.put(FunctionColumnMetaData.FUNCTION_SCHEM, ifSchemaElse("PUBLIC", null));
defaults.put(FunctionColumnMetaData.PRECISION, null);
defaults.put(FunctionColumnMetaData.SCALE, null);
defaults.put(FunctionColumnMetaData.RADIX, 10);
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionsTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionsTest.java
index fa5ff223f..91d2688e2 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionsTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionsTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2019-2023 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2019-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -338,9 +338,10 @@ class Ignored {
// Skipping RDB$GET_CONTEXT and RDB$SET_CONTEXT as that seems to be an implementation artifact:
// present in FB 2.5, absent in FB 3.0
private static final Set FUNCTIONS_TO_IGNORE = Set.of("RDB$GET_CONTEXT", "RDB$SET_CONTEXT");
- // Also skipping functions from system packages (when testing with useCatalogAsPackage=true)
+ // Also skipping functions from system packages (when testing with useCatalogAsPackage=true),
+ // and schema SYSTEM (Firebird 6+)
private static final List PREFIXES_TO_IGNORE =
- List.of("\"RDB$BLOB_UTIL\".", "\"RDB$PROFILER\".", "\"RDB$TIME_ZONE_UTIL\".");
+ List.of("\"SYSTEM\".\"RDB$", "\"RDB$BLOB_UTIL\".", "\"RDB$PROFILER\".", "\"RDB$TIME_ZONE_UTIL\".");
}
if (Ignored.FUNCTIONS_TO_IGNORE.contains(specificName)) return true;
return Ignored.PREFIXES_TO_IGNORE.stream().anyMatch(specificName::startsWith);
From a5d8434595cb55d70e2f01be4e7842ec4206889c Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Sun, 22 Jun 2025 13:36:53 +0200
Subject: [PATCH 13/64] #882 Schema support for getFunctions
---
.../firebirdsql/jdbc/FBDatabaseMetaData.java | 2 +-
.../jdbc/metadata/GetFunctions.java | 146 +++++++++++++++---
...FBDatabaseMetaDataFunctionColumnsTest.java | 2 +
.../jdbc/FBDatabaseMetaDataFunctionsTest.java | 11 +-
4 files changed, 136 insertions(+), 25 deletions(-)
diff --git a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
index 043d311db..b7961796f 100644
--- a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
@@ -1780,7 +1780,7 @@ public ResultSet getFunctionColumns(String catalog, String schemaPattern, String
@Override
public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern)
throws SQLException {
- return GetFunctions.create(getDbMetadataMediator()).getFunctions(catalog, functionNamePattern);
+ return GetFunctions.create(getDbMetadataMediator()).getFunctions(catalog, schemaPattern, functionNamePattern);
}
@Override
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetFunctions.java b/src/main/org/firebirdsql/jdbc/metadata/GetFunctions.java
index ea9243cf3..9f6c0dfba 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetFunctions.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetFunctions.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2019-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2019-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -13,6 +13,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
+import java.util.List;
import static java.sql.DatabaseMetaData.functionNoTable;
import static org.firebirdsql.gds.ISCConstants.SQL_SHORT;
@@ -30,8 +31,10 @@
public abstract class GetFunctions extends AbstractMetadataMethod {
private static final String FUNCTIONS = "FUNCTIONS";
+ private static final String COLUMN_CATALOG_NAME = "RDB$PACKAGE_NAME";
+ private static final String COLUMN_SCHEMA_NAME = "RDB$SCHEMA_NAME";
private static final String COLUMN_FUNCTION_NAME = "RDB$FUNCTION_NAME";
-
+
private static final RowDescriptor ROW_DESCRIPTOR = DbMetadataMediator.newRowDescriptorBuilder(11)
.at(0).simple(SQL_VARYING | 1, OBJECT_NAME_LENGTH, "FUNCTION_CAT", FUNCTIONS).addField()
.at(1).simple(SQL_VARYING | 1, OBJECT_NAME_LENGTH, "FUNCTION_SCHEM", FUNCTIONS).addField()
@@ -56,27 +59,29 @@ private GetFunctions(DbMetadataMediator mediator) {
/**
* @see java.sql.DatabaseMetaData#getFunctions(String, String, String)
*/
- public final ResultSet getFunctions(String catalog, String functionNamePattern) throws SQLException {
+ public final ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern)
+ throws SQLException {
if ("".equals(functionNamePattern)) {
// Matching function name not possible
return createEmpty();
}
- MetadataQuery metadataQuery = createGetFunctionsQuery(catalog, functionNamePattern);
+ MetadataQuery metadataQuery = createGetFunctionsQuery(catalog, schemaPattern, functionNamePattern);
return createMetaDataResultSet(metadataQuery);
}
@Override
final RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQLException {
String catalog = rs.getString("FUNCTION_CAT");
+ String schema = rs.getString("FUNCTION_SCHEM");
String functionName = rs.getString("FUNCTION_NAME");
return valueBuilder
.at(0).setString(catalog)
- .at(1).set(null)
+ .at(1).setString(schema)
.at(2).setString(functionName)
.at(3).setString(rs.getString("REMARKS"))
.at(4).setShort(functionNoTable)
- .at(5).setString(toSpecificName(catalog, functionName))
+ .at(5).setString(toSpecificName(catalog, schema, functionName))
.at(6).setString(rs.getString("JB_FUNCTION_SOURCE"))
.at(7).setString(rs.getString("JB_FUNCTION_KIND"))
.at(8).setString(rs.getString("JB_MODULE_NAME"))
@@ -85,7 +90,7 @@ final RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) thr
.toRowValue(false);
}
- abstract MetadataQuery createGetFunctionsQuery(String catalog, String functionNamePattern);
+ abstract MetadataQuery createGetFunctionsQuery(String catalog, String schemaPattern, String functionNamePattern);
/**
* Creates an instance of {@code GetFunctions}.
@@ -97,7 +102,12 @@ final RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) thr
public static GetFunctions create(DbMetadataMediator mediator) {
FirebirdSupportInfo firebirdSupportInfo = mediator.getFirebirdSupportInfo();
// NOTE: Indirection through static method prevents unnecessary classloading
- if (firebirdSupportInfo.isVersionEqualOrAbove(3)) {
+ if (firebirdSupportInfo.isVersionEqualOrAbove(6)) {
+ if (mediator.isUseCatalogAsPackage()) {
+ return FB6CatalogAsPackage.createInstance(mediator);
+ }
+ return FB6.createInstance(mediator);
+ } else if (firebirdSupportInfo.isVersionEqualOrAbove(3)) {
if (mediator.isUseCatalogAsPackage()) {
return FB3CatalogAsPackage.createInstance(mediator);
}
@@ -115,7 +125,8 @@ private static final class FB2_5 extends GetFunctions {
private static final String GET_FUNCTIONS_FRAGMENT_2_5 = """
select
- null as FUNCTION_CAT,
+ cast(null as char(1)) as FUNCTION_CAT,
+ cast(null as char(1)) as FUNCTION_SCHEM,
RDB$FUNCTION_NAME as FUNCTION_NAME,
RDB$DESCRIPTION as REMARKS,
cast(null as blob sub_type text) as JB_FUNCTION_SOURCE,
@@ -137,7 +148,7 @@ private static GetFunctions createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createGetFunctionsQuery(String catalog, String functionNamePattern) {
+ MetadataQuery createGetFunctionsQuery(String catalog, String schemaPattern, String functionNamePattern) {
Clause functionNameClause = new Clause(COLUMN_FUNCTION_NAME, functionNamePattern);
String queryText = GET_FUNCTIONS_FRAGMENT_2_5
+ functionNameClause.getCondition("\nwhere ", "")
@@ -154,6 +165,7 @@ private static final class FB3 extends GetFunctions {
private static final String GET_FUNCTIONS_FRAGMENT_3 = """
select
null as FUNCTION_CAT,
+ null as FUNCTION_SCHEM,
trim(trailing from RDB$FUNCTION_NAME) as FUNCTION_NAME,
RDB$DESCRIPTION as REMARKS,
RDB$FUNCTION_SOURCE as JB_FUNCTION_SOURCE,
@@ -180,7 +192,7 @@ private static GetFunctions createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createGetFunctionsQuery(String catalog, String functionNamePattern) {
+ MetadataQuery createGetFunctionsQuery(String catalog, String schemaPattern, String functionNamePattern) {
Clause functionNameClause = new Clause(COLUMN_FUNCTION_NAME, functionNamePattern);
String queryText = GET_FUNCTIONS_FRAGMENT_3
+ functionNameClause.getCondition("\nand ", "")
@@ -194,6 +206,7 @@ private static final class FB3CatalogAsPackage extends GetFunctions {
private static final String GET_FUNCTIONS_FRAGMENT_3_W_PKG = """
select
coalesce(trim(trailing from RDB$PACKAGE_NAME), '') as FUNCTION_CAT,
+ null as FUNCTION_SCHEM,
trim(trailing from RDB$FUNCTION_NAME) as FUNCTION_NAME,
RDB$DESCRIPTION as REMARKS,
RDB$FUNCTION_SOURCE as JB_FUNCTION_SOURCE,
@@ -210,8 +223,6 @@ private static final class FB3CatalogAsPackage extends GetFunctions {
private static final String GET_FUNCTIONS_ORDER_BY_3_W_PKG =
"\norder by RDB$PACKAGE_NAME nulls first, RDB$FUNCTION_NAME";
- private static final String COLUMN_CATALOG_NAME = "RDB$PACKAGE_NAME";
-
private FB3CatalogAsPackage(DbMetadataMediator mediator) {
super(mediator);
}
@@ -221,7 +232,7 @@ private static GetFunctions createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createGetFunctionsQuery(String catalog, String functionNamePattern) {
+ MetadataQuery createGetFunctionsQuery(String catalog, String schemaPattern, String functionNamePattern) {
var clauses = new ArrayList(2);
if (catalog != null) {
// To quote from the JDBC API: "" retrieves those without a catalog; null means that the catalog name
@@ -234,14 +245,109 @@ MetadataQuery createGetFunctionsQuery(String catalog, String functionNamePattern
}
}
clauses.add(new Clause(COLUMN_FUNCTION_NAME, functionNamePattern));
- //@formatter:off
String sql = GET_FUNCTIONS_FRAGMENT_3_W_PKG
- + (Clause.anyCondition(clauses)
- ? "\nwhere " + Clause.conjunction(clauses)
- : "")
+ + (Clause.anyCondition(clauses) ? "\nwhere " + Clause.conjunction(clauses) : "")
+ GET_FUNCTIONS_ORDER_BY_3_W_PKG;
- //@formatter:on
return new MetadataQuery(sql, Clause.parameters(clauses));
}
}
+
+ private static final class FB6 extends GetFunctions {
+
+ private static final String GET_FUNCTIONS_FRAGMENT_6 = """
+ select
+ null as FUNCTION_CAT,
+ trim(trailing from RDB$SCHEMA_NAME) as FUNCTION_SCHEM,
+ trim(trailing from RDB$FUNCTION_NAME) as FUNCTION_NAME,
+ RDB$DESCRIPTION as REMARKS,
+ RDB$FUNCTION_SOURCE as JB_FUNCTION_SOURCE,
+ case
+ when RDB$LEGACY_FLAG = 1 then 'UDF'
+ when RDB$ENGINE_NAME is not null then 'UDR'
+ else 'PSQL'
+ end as JB_FUNCTION_KIND,
+ trim(trailing from RDB$MODULE_NAME) as JB_MODULE_NAME,
+ trim(trailing from RDB$ENTRYPOINT) as JB_ENTRYPOINT,
+ trim(trailing from RDB$ENGINE_NAME) as JB_ENGINE_NAME
+ from SYSTEM.RDB$FUNCTIONS
+ where RDB$PACKAGE_NAME is null""";
+
+ // NOTE: Including RDB$PACKAGE_NAME so index can be used to sort
+ private static final String GET_FUNCTIONS_ORDER_BY_6 =
+ "\norder by RDB$SCHEMA_NAME, RDB$PACKAGE_NAME, RDB$FUNCTION_NAME";
+
+ private FB6(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetFunctions createInstance(DbMetadataMediator mediator) {
+ return new FB6(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetFunctionsQuery(String catalog, String schemaPattern, String functionNamePattern) {
+ var clauses = List.of(
+ new Clause(COLUMN_SCHEMA_NAME, schemaPattern),
+ new Clause(COLUMN_FUNCTION_NAME, functionNamePattern));
+ String queryText = GET_FUNCTIONS_FRAGMENT_6
+ + (Clause.anyCondition(clauses) ? "\nand " + Clause.conjunction(clauses) : "")
+ + GET_FUNCTIONS_ORDER_BY_6;
+ return new MetadataQuery(queryText, Clause.parameters(clauses));
+ }
+
+ }
+
+ private static final class FB6CatalogAsPackage extends GetFunctions {
+
+ private static final String GET_FUNCTIONS_FRAGMENT_6_W_PKG = """
+ select
+ coalesce(trim(trailing from RDB$PACKAGE_NAME), '') as FUNCTION_CAT,
+ trim(trailing from RDB$SCHEMA_NAME) as FUNCTION_SCHEM,
+ trim(trailing from RDB$FUNCTION_NAME) as FUNCTION_NAME,
+ RDB$DESCRIPTION as REMARKS,
+ RDB$FUNCTION_SOURCE as JB_FUNCTION_SOURCE,
+ case
+ when RDB$LEGACY_FLAG = 1 then 'UDF'
+ when RDB$ENGINE_NAME is not null then 'UDR'
+ else 'PSQL'
+ end as JB_FUNCTION_KIND,
+ trim(trailing from RDB$MODULE_NAME) as JB_MODULE_NAME,
+ trim(trailing from RDB$ENTRYPOINT) as JB_ENTRYPOINT,
+ trim(trailing from RDB$ENGINE_NAME) as JB_ENGINE_NAME
+ from SYSTEM.RDB$FUNCTIONS""";
+
+ private static final String GET_FUNCTIONS_ORDER_BY_6_W_PKG =
+ "\norder by RDB$PACKAGE_NAME nulls first, RDB$SCHEMA_NAME, RDB$FUNCTION_NAME";
+
+ private FB6CatalogAsPackage(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetFunctions createInstance(DbMetadataMediator mediator) {
+ return new FB6CatalogAsPackage(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetFunctionsQuery(String catalog, String schemaPattern, String functionNamePattern) {
+ var clauses = new ArrayList(3);
+ clauses.add(new Clause(COLUMN_SCHEMA_NAME, schemaPattern));
+ if (catalog != null) {
+ // To quote from the JDBC API: "" retrieves those without a catalog; null means that the catalog name
+ // should not be used to narrow the search
+ if (catalog.isEmpty()) {
+ clauses.add(Clause.isNullClause(COLUMN_CATALOG_NAME));
+ } else {
+ // Exact matches only
+ clauses.add(Clause.equalsClause(COLUMN_CATALOG_NAME, catalog));
+ }
+ }
+ clauses.add(new Clause(COLUMN_FUNCTION_NAME, functionNamePattern));
+ String sql = GET_FUNCTIONS_FRAGMENT_6_W_PKG
+ + (Clause.anyCondition(clauses) ? "\nwhere " + Clause.conjunction(clauses) : "")
+ + GET_FUNCTIONS_ORDER_BY_6_W_PKG;
+ return new MetadataQuery(sql, Clause.parameters(clauses));
+ }
+
+ }
+
}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionColumnsTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionColumnsTest.java
index d08b24349..9d20e0a40 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionColumnsTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionColumnsTest.java
@@ -36,6 +36,8 @@
*/
class FBDatabaseMetaDataFunctionColumnsTest {
+ // TODO Add schema support: tests involving other schema
+
private static final String PSQL_EXAMPLE_1 = "PSQL$EXAMPLE$1";
private static final String PSQL_EXAMPLE_2 = "PSQL$EXAMPLE$2";
private static final String UDF_EXAMPLE_1 = "UDF$EXAMPLE$1";
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionsTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionsTest.java
index 91d2688e2..ccd821777 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionsTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionsTest.java
@@ -24,6 +24,7 @@
import static org.firebirdsql.common.FBTestProperties.getDefaultPropertiesForConnection;
import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
import static org.firebirdsql.common.FBTestProperties.getUrl;
+import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -36,6 +37,8 @@
*/
class FBDatabaseMetaDataFunctionsTest {
+ // TODO Add schema support: tests involving other schema
+
private static final String CREATE_UDF_EXAMPLE = """
declare external function UDF$EXAMPLE
int by descriptor, int by descriptor
@@ -274,7 +277,7 @@ private void validatePsqlExample(ResultSet functions, boolean useCatalogAsPackag
rules.put(FunctionMetaData.FUNCTION_CAT, "");
}
rules.put(FunctionMetaData.FUNCTION_NAME, "PSQL$EXAMPLE");
- rules.put(FunctionMetaData.SPECIFIC_NAME, "PSQL$EXAMPLE");
+ rules.put(FunctionMetaData.SPECIFIC_NAME, ifSchemaElse("\"PUBLIC\".\"PSQL$EXAMPLE\"", "PSQL$EXAMPLE"));
if (supportsComments) {
rules.put(FunctionMetaData.REMARKS, "Comment on PSQL$EXAMPLE");
}
@@ -298,7 +301,7 @@ private void validateUdfExample(ResultSet functions, boolean useCatalogAsPackage
rules.put(FunctionMetaData.FUNCTION_CAT, "");
}
rules.put(FunctionMetaData.FUNCTION_NAME, "UDF$EXAMPLE");
- rules.put(FunctionMetaData.SPECIFIC_NAME, "UDF$EXAMPLE");
+ rules.put(FunctionMetaData.SPECIFIC_NAME, ifSchemaElse("\"PUBLIC\".\"UDF$EXAMPLE\"", "UDF$EXAMPLE"));
if (supportsComments) {
rules.put(FunctionMetaData.REMARKS, "Comment on UDF$EXAMPLE");
}
@@ -312,7 +315,7 @@ private void validatePackageFunctionExample(ResultSet functions) throws SQLExcep
Map rules = getDefaultValidationRules();
rules.put(FunctionMetaData.FUNCTION_CAT, "WITH$FUNCTION");
rules.put(FunctionMetaData.FUNCTION_NAME, "IN$PACKAGE");
- rules.put(FunctionMetaData.SPECIFIC_NAME, "\"WITH$FUNCTION\".\"IN$PACKAGE\"");
+ rules.put(FunctionMetaData.SPECIFIC_NAME, ifSchemaElse("\"PUBLIC\".", "") + "\"WITH$FUNCTION\".\"IN$PACKAGE\"");
// Stored with package
rules.put(FunctionMetaData.JB_FUNCTION_SOURCE, null);
rules.put(FunctionMetaData.JB_FUNCTION_KIND, "PSQL");
@@ -351,7 +354,7 @@ class Ignored {
static {
Map defaults = new EnumMap<>(FunctionMetaData.class);
defaults.put(FunctionMetaData.FUNCTION_CAT, null);
- defaults.put(FunctionMetaData.FUNCTION_SCHEM, null);
+ defaults.put(FunctionMetaData.FUNCTION_SCHEM, ifSchemaElse("PUBLIC", null));
defaults.put(FunctionMetaData.REMARKS, null);
defaults.put(FunctionMetaData.FUNCTION_TYPE, (short) DatabaseMetaData.functionNoTable);
defaults.put(FunctionMetaData.JB_FUNCTION_SOURCE, null);
From c08df8bec4cdb3232e1147146e0fbaab91cbb801 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Sun, 22 Jun 2025 13:56:10 +0200
Subject: [PATCH 14/64] #882 Schema support for getIndexInfo
---
.../firebirdsql/jdbc/FBDatabaseMetaData.java | 2 +-
.../jdbc/metadata/GetIndexInfo.java | 75 ++++++++++++++++---
.../jdbc/FBDatabaseMetaDataIndexInfoTest.java | 16 +++-
3 files changed, 78 insertions(+), 15 deletions(-)
diff --git a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
index b7961796f..05511c9c0 100644
--- a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
@@ -1497,7 +1497,7 @@ public ResultSet getTypeInfo() throws SQLException {
@Override
public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate)
throws SQLException {
- return GetIndexInfo.create(getDbMetadataMediator()).getIndexInfo(table, unique, approximate);
+ return GetIndexInfo.create(getDbMetadataMediator()).getIndexInfo(schema, table, unique, approximate);
}
@Override
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetIndexInfo.java b/src/main/org/firebirdsql/jdbc/metadata/GetIndexInfo.java
index 09f59e071..40ddce9df 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetIndexInfo.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetIndexInfo.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -12,6 +12,7 @@
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.ArrayList;
import static org.firebirdsql.gds.ISCConstants.SQL_LONG;
import static org.firebirdsql.gds.ISCConstants.SQL_SHORT;
@@ -53,22 +54,22 @@ private GetIndexInfo(DbMetadataMediator mediator) {
}
@SuppressWarnings("unused")
- public ResultSet getIndexInfo(String table, boolean unique, boolean approximate) throws SQLException {
+ public ResultSet getIndexInfo(String schema, String table, boolean unique, boolean approximate) throws SQLException {
if (isNullOrEmpty(table)) {
return createEmpty();
}
- MetadataQuery metadataQuery = createIndexInfoQuery(table, unique);
+ MetadataQuery metadataQuery = createIndexInfoQuery(schema, table, unique);
return createMetaDataResultSet(metadataQuery);
}
- abstract MetadataQuery createIndexInfoQuery(String table, boolean unique);
+ abstract MetadataQuery createIndexInfoQuery(String schema, String table, boolean unique);
@Override
RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQLException {
valueBuilder
.at(0).set(null)
- .at(1).set(null)
+ .at(1).setString(rs.getString("TABLE_SCHEM"))
.at(2).setString(rs.getString("TABLE_NAME"))
.at(3).setString(rs.getInt("UNIQUE_FLAG") == 0 ? "T" : "F")
.at(4).set(null)
@@ -107,7 +108,9 @@ RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQ
public static GetIndexInfo create(DbMetadataMediator mediator) {
// NOTE: Indirection through static method prevents unnecessary classloading
- if (mediator.getOdsVersion().compareTo(ODS_13_1) >= 0) {
+ if (mediator.getFirebirdSupportInfo().isVersionEqualOrAbove(6)) {
+ return FB6.createInstance(mediator);
+ } else if (mediator.getOdsVersion().compareTo(ODS_13_1) >= 0) {
return FB5.createInstance(mediator);
} else {
return FB2_5.createInstance(mediator);
@@ -118,6 +121,7 @@ private static final class FB2_5 extends GetIndexInfo {
private static final String GET_INDEX_INFO_START_2_5 = """
select
+ cast(null as char(1)) as TABLE_SCHEM,
IND.RDB$RELATION_NAME as TABLE_NAME,
IND.RDB$UNIQUE_FLAG as UNIQUE_FLAG,
IND.RDB$INDEX_NAME as INDEX_NAME,
@@ -127,7 +131,8 @@ private static final class FB2_5 extends GetIndexInfo {
IND.RDB$INDEX_TYPE as ASC_OR_DESC,
null as CONDITION_SOURCE
from RDB$INDICES IND
- left join RDB$INDEX_SEGMENTS ISE on IND.RDB$INDEX_NAME = ISE.RDB$INDEX_NAME where\s""";
+ left join RDB$INDEX_SEGMENTS ISE on IND.RDB$INDEX_NAME = ISE.RDB$INDEX_NAME
+ where\s""";
private static final String GET_INDEX_INFO_END_2_5 =
"\norder by IND.RDB$UNIQUE_FLAG, IND.RDB$INDEX_NAME, ISE.RDB$FIELD_POSITION";
@@ -141,7 +146,7 @@ private static GetIndexInfo createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createIndexInfoQuery(String table, boolean unique) {
+ MetadataQuery createIndexInfoQuery(String schema, String table, boolean unique) {
Clause tableClause = Clause.equalsClause("IND.RDB$RELATION_NAME", table);
String sql = GET_INDEX_INFO_START_2_5
+ tableClause.getCondition(unique)
@@ -156,6 +161,7 @@ private static final class FB5 extends GetIndexInfo {
private static final String GET_INDEX_INFO_START_5 = """
select
+ null as TABLE_SCHEM,
trim(trailing from IND.RDB$RELATION_NAME) as TABLE_NAME,
IND.RDB$UNIQUE_FLAG as UNIQUE_FLAG,
trim(trailing from IND.RDB$INDEX_NAME) as INDEX_NAME,
@@ -165,7 +171,8 @@ private static final class FB5 extends GetIndexInfo {
IND.RDB$INDEX_TYPE as ASC_OR_DESC,
IND.RDB$CONDITION_SOURCE as CONDITION_SOURCE
from RDB$INDICES IND
- left join RDB$INDEX_SEGMENTS ISE on IND.RDB$INDEX_NAME = ISE.RDB$INDEX_NAME where\s""";
+ left join RDB$INDEX_SEGMENTS ISE on IND.RDB$INDEX_NAME = ISE.RDB$INDEX_NAME
+ where\s""";
private static final String GET_INDEX_INFO_END_5 =
"\norder by IND.RDB$UNIQUE_FLAG, IND.RDB$INDEX_NAME, ISE.RDB$FIELD_POSITION";
@@ -179,7 +186,7 @@ private static GetIndexInfo createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createIndexInfoQuery(String table, boolean unique) {
+ MetadataQuery createIndexInfoQuery(String schema, String table, boolean unique) {
Clause tableClause = Clause.equalsClause("IND.RDB$RELATION_NAME", table);
String sql = GET_INDEX_INFO_START_5
+ tableClause.getCondition(unique)
@@ -190,4 +197,50 @@ MetadataQuery createIndexInfoQuery(String table, boolean unique) {
}
+ private static final class FB6 extends GetIndexInfo {
+
+ private static final String GET_INDEX_INFO_START_6 = """
+ select
+ trim(trailing from IND.RDB$SCHEMA_NAME) as TABLE_SCHEM,
+ trim(trailing from IND.RDB$RELATION_NAME) as TABLE_NAME,
+ IND.RDB$UNIQUE_FLAG as UNIQUE_FLAG,
+ trim(trailing from IND.RDB$INDEX_NAME) as INDEX_NAME,
+ ISE.RDB$FIELD_POSITION + 1 as ORDINAL_POSITION,
+ trim(trailing from ISE.RDB$FIELD_NAME) as COLUMN_NAME,
+ IND.RDB$EXPRESSION_SOURCE as EXPRESSION_SOURCE,
+ IND.RDB$INDEX_TYPE as ASC_OR_DESC,
+ IND.RDB$CONDITION_SOURCE as CONDITION_SOURCE
+ from SYSTEM.RDB$INDICES IND
+ left join SYSTEM.RDB$INDEX_SEGMENTS ISE
+ on IND.RDB$SCHEMA_NAME = ISE.RDB$SCHEMA_NAME and IND.RDB$INDEX_NAME = ISE.RDB$INDEX_NAME
+ where\s""";
+
+ private static final String GET_INDEX_INFO_END_6 =
+ "\norder by IND.RDB$UNIQUE_FLAG, IND.RDB$INDEX_NAME, ISE.RDB$FIELD_POSITION";
+
+ private FB6(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetIndexInfo createInstance(DbMetadataMediator mediator) {
+ return new FB6(mediator);
+ }
+
+ @Override
+ MetadataQuery createIndexInfoQuery(String schema, String table, boolean unique) {
+ var clauses = new ArrayList(2);
+ if (schema != null) {
+ // NOTE: empty string will return no rows as required ("" retrieves those without a schema)
+ clauses.add(Clause.equalsClause("IND.RDB$SCHEMA_NAME", schema));
+ }
+ clauses.add(Clause.equalsClause("IND.RDB$RELATION_NAME", table));
+ String sql = GET_INDEX_INFO_START_6
+ + Clause.conjunction(clauses)
+ + (unique ? "\n and IND.RDB$UNIQUE_FLAG = 1" : "")
+ + GET_INDEX_INFO_END_6;
+ return new MetadataQuery(sql, Clause.parameters(clauses));
+ }
+
+ }
+
}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataIndexInfoTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataIndexInfoTest.java
index 99cb63d1b..22780881c 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataIndexInfoTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataIndexInfoTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2012-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2012-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -26,6 +26,7 @@
import static org.firebirdsql.common.FBTestProperties.getDefaultPropertiesForConnection;
import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
import static org.firebirdsql.common.FBTestProperties.getUrl;
+import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
import static org.firebirdsql.common.JdbcResourceHelper.closeQuietly;
import static org.firebirdsql.common.matchers.MatcherAssume.assumeThat;
import static org.hamcrest.Matchers.equalTo;
@@ -40,6 +41,8 @@
*/
class FBDatabaseMetaDataIndexInfoTest {
+ // TODO Add schema support: tests involving other schema
+
private static final String CREATE_INDEX_TEST_TABLE_1 = """
CREATE TABLE index_test_table_1 (
id INTEGER CONSTRAINT pk_idx_test_1_id PRIMARY KEY,
@@ -283,9 +286,16 @@ private void validate(ResultSet indexInfo, List>
}
}
- private Map createRule(String tableName, boolean nonUnique, String indexName,
+ private Map createRule(String tableName, boolean nonUnique, String indexName,
String columnName, Integer ordinalPosition, boolean ascending) {
+ return createRule(ifSchemaElse("PUBLIC", null), tableName, nonUnique, indexName, columnName, ordinalPosition,
+ ascending);
+ }
+
+ private Map createRule(String schema, String tableName, boolean nonUnique,
+ String indexName, String columnName, Integer ordinalPosition, boolean ascending) {
Map indexRules = getDefaultValueValidationRules();
+ indexRules.put(IndexInfoMetaData.TABLE_SCHEM, schema);
indexRules.put(IndexInfoMetaData.TABLE_NAME, tableName);
indexRules.put(IndexInfoMetaData.NON_UNIQUE, nonUnique ? "T" : "F");
indexRules.put(IndexInfoMetaData.INDEX_NAME, indexName);
@@ -314,7 +324,7 @@ private Map withFilterCondition(Map defaults = new EnumMap<>(IndexInfoMetaData.class);
defaults.put(IndexInfoMetaData.TABLE_CAT, null);
- defaults.put(IndexInfoMetaData.TABLE_SCHEM, null);
+ defaults.put(IndexInfoMetaData.TABLE_SCHEM, ifSchemaElse("PUBLIC", null));
defaults.put(IndexInfoMetaData.INDEX_QUALIFIER, null);
defaults.put(IndexInfoMetaData.TYPE, DatabaseMetaData.tableIndexOther);
defaults.put(IndexInfoMetaData.CARDINALITY, null);
From a1655eb99b489f1229aa377704f0a6176688cb24 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Sun, 22 Jun 2025 15:11:46 +0200
Subject: [PATCH 15/64] #882 Schema support for getPrimaryKeys
---
.../firebirdsql/jdbc/FBDatabaseMetaData.java | 2 +-
.../jdbc/metadata/GetBestRowIdentifier.java | 2 +-
.../jdbc/metadata/GetPrimaryKeys.java | 129 ++++++++++++++----
.../FBDatabaseMetaDataPrimaryKeysTest.java | 6 +-
4 files changed, 110 insertions(+), 29 deletions(-)
diff --git a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
index 05511c9c0..578e7df3b 100644
--- a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
@@ -1428,7 +1428,7 @@ public ResultSet getVersionColumns(String catalog, String schema, String table)
*/
@Override
public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException {
- return GetPrimaryKeys.create(getDbMetadataMediator()).getPrimaryKeys(table);
+ return GetPrimaryKeys.create(getDbMetadataMediator()).getPrimaryKeys(schema, table);
}
/**
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java b/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java
index 870307cae..eed40154f 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java
@@ -251,7 +251,7 @@ private static GetBestRowIdentifier createInstance(DbMetadataMediator mediator)
@Override
MetadataQuery createGetPrimaryKeyIdentifierQuery(String schema, String table) {
- List clauses = new ArrayList<>(2);
+ var clauses = new ArrayList(2);
if (schema != null) {
// NOTE: empty string will return no rows as required ("" retrieves those without a schema)
clauses.add(Clause.equalsClause("RC.RDB$SCHEMA_NAME", schema));
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetPrimaryKeys.java b/src/main/org/firebirdsql/jdbc/metadata/GetPrimaryKeys.java
index 777638300..4908edb89 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetPrimaryKeys.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetPrimaryKeys.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -10,6 +10,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.ArrayList;
import static org.firebirdsql.gds.ISCConstants.SQL_SHORT;
import static org.firebirdsql.gds.ISCConstants.SQL_VARYING;
@@ -22,7 +23,7 @@
* @author Mark Rotteveel
* @since 5
*/
-public final class GetPrimaryKeys extends AbstractMetadataMethod {
+public abstract class GetPrimaryKeys extends AbstractMetadataMethod {
private static final String COLUMNINFO = "COLUMNINFO";
@@ -36,42 +37,25 @@ public final class GetPrimaryKeys extends AbstractMetadataMethod {
.at(6).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "JB_PK_INDEX_NAME", COLUMNINFO).addField()
.toRowDescriptor();
- private static final String GET_PRIMARY_KEYS_START = """
- select
- RC.RDB$RELATION_NAME as TABLE_NAME,
- ISGMT.RDB$FIELD_NAME as COLUMN_NAME,
- ISGMT.RDB$FIELD_POSITION + 1 as KEY_SEQ,
- RC.RDB$CONSTRAINT_NAME as PK_NAME,
- RC.RDB$INDEX_NAME as JB_PK_INDEX_NAME
- from RDB$RELATION_CONSTRAINTS RC
- inner join RDB$INDEX_SEGMENTS ISGMT
- on RC.RDB$INDEX_NAME = ISGMT.RDB$INDEX_NAME
- where RC.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY'
- and\s""";
-
- private static final String GET_PRIMARY_KEYS_END = "\norder by ISGMT.RDB$FIELD_NAME ";
-
private GetPrimaryKeys(DbMetadataMediator mediator) {
super(ROW_DESCRIPTOR, mediator);
}
- public ResultSet getPrimaryKeys(String table) throws SQLException {
+ public final ResultSet getPrimaryKeys(String schema, String table) throws SQLException {
if (isNullOrEmpty(table)) {
return createEmpty();
}
- Clause tableClause = Clause.equalsClause("RC.RDB$RELATION_NAME", table);
- String sql = GET_PRIMARY_KEYS_START
- + tableClause.getCondition(false)
- + GET_PRIMARY_KEYS_END;
- MetadataQuery metadataQuery = new MetadataQuery(sql, Clause.parameters(tableClause));
+ MetadataQuery metadataQuery = createGetPrimaryKeysQuery(schema, table);
return createMetaDataResultSet(metadataQuery);
}
+ abstract MetadataQuery createGetPrimaryKeysQuery(String schema, String table);
+
@Override
RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQLException {
return valueBuilder
.at(0).set(null)
- .at(1).set(null)
+ .at(1).setString(rs.getString("TABLE_SCHEM"))
.at(2).setString(rs.getString("TABLE_NAME"))
.at(3).setString(rs.getString("COLUMN_NAME"))
.at(4).setShort(rs.getShort("KEY_SEQ"))
@@ -81,6 +65,99 @@ RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQ
}
public static GetPrimaryKeys create(DbMetadataMediator mediator) {
- return new GetPrimaryKeys(mediator);
+ // NOTE: Indirection through static method prevents unnecessary classloading
+ if (mediator.getFirebirdSupportInfo().isVersionEqualOrAbove(6)) {
+ return FB6.createInstance(mediator);
+ } else {
+ return FB5.createInstance(mediator);
+ }
}
+
+ /**
+ * Implementation for Firebird 5.0 and older.
+ */
+ private static final class FB5 extends GetPrimaryKeys {
+
+ private static final String GET_PRIMARY_KEYS_START_5 = """
+ select
+ cast(null as char(1)) as TABLE_SCHEM,
+ RC.RDB$RELATION_NAME as TABLE_NAME,
+ ISGMT.RDB$FIELD_NAME as COLUMN_NAME,
+ ISGMT.RDB$FIELD_POSITION + 1 as KEY_SEQ,
+ RC.RDB$CONSTRAINT_NAME as PK_NAME,
+ RC.RDB$INDEX_NAME as JB_PK_INDEX_NAME
+ from RDB$RELATION_CONSTRAINTS RC
+ inner join RDB$INDEX_SEGMENTS ISGMT
+ on RC.RDB$INDEX_NAME = ISGMT.RDB$INDEX_NAME
+ where RC.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY'
+ and\s""";
+
+ private static final String GET_PRIMARY_KEYS_END_5 = "\norder by ISGMT.RDB$FIELD_NAME";
+
+ private FB5(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetPrimaryKeys createInstance(DbMetadataMediator mediator) {
+ return new FB5(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetPrimaryKeysQuery(String schema, String table) {
+ Clause tableClause = Clause.equalsClause("RC.RDB$RELATION_NAME", table);
+ String sql = GET_PRIMARY_KEYS_START_5
+ + tableClause.getCondition(false)
+ + GET_PRIMARY_KEYS_END_5;
+ return new MetadataQuery(sql, Clause.parameters(tableClause));
+ }
+
+ }
+
+ /**
+ * Implementation for Firebird 6.0 and higher.
+ */
+ private static final class FB6 extends GetPrimaryKeys {
+
+ private static final String GET_PRIMARY_KEYS_START_6 = """
+ select
+ trim(trailing from RC.RDB$SCHEMA_NAME) as TABLE_SCHEM,
+ trim(trailing from RC.RDB$RELATION_NAME) as TABLE_NAME,
+ trim(trailing from ISGMT.RDB$FIELD_NAME) as COLUMN_NAME,
+ ISGMT.RDB$FIELD_POSITION + 1 as KEY_SEQ,
+ trim(trailing from RC.RDB$CONSTRAINT_NAME) as PK_NAME,
+ trim(trailing from RC.RDB$INDEX_NAME) as JB_PK_INDEX_NAME
+ from SYSTEM.RDB$RELATION_CONSTRAINTS RC
+ inner join SYSTEM.RDB$INDEX_SEGMENTS ISGMT
+ on RC.RDB$SCHEMA_NAME = ISGMT.RDB$SCHEMA_NAME and RC.RDB$INDEX_NAME = ISGMT.RDB$INDEX_NAME
+ where RC.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY'
+ and\s""";
+
+ // For consistent order (e.g. for tests), we're also sorting on schema name.
+ // JDBC specifies that the result set is sorted on COLUMN_NAME, so we can't sort on schema first
+ private static final String GET_PRIMARY_KEYS_END_6 = "\norder by ISGMT.RDB$FIELD_NAME, ISGMT.RDB$SCHEMA_NAME";
+
+ private FB6(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetPrimaryKeys createInstance(DbMetadataMediator mediator) {
+ return new FB6(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetPrimaryKeysQuery(String schema, String table) {
+ var clauses = new ArrayList(2);
+ if (schema != null) {
+ // NOTE: empty string will return no rows as required ("" retrieves those without a schema)
+ clauses.add(Clause.equalsClause("RC.RDB$SCHEMA_NAME", schema));
+ }
+ clauses.add(Clause.equalsClause("RC.RDB$RELATION_NAME", table));
+ String sql = GET_PRIMARY_KEYS_START_6
+ + Clause.conjunction(clauses)
+ + GET_PRIMARY_KEYS_END_6;
+ return new MetadataQuery(sql, Clause.parameters(clauses));
+ }
+
+ }
+
}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataPrimaryKeysTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataPrimaryKeysTest.java
index 85b661c73..735fa96e7 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataPrimaryKeysTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataPrimaryKeysTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2024-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -20,6 +20,7 @@
import static java.util.Collections.unmodifiableMap;
import static org.firebirdsql.common.FBTestProperties.getConnectionViaDriverManager;
+import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
import static org.firebirdsql.common.assertions.ResultSetAssertions.assertNextRow;
import static org.firebirdsql.common.assertions.ResultSetAssertions.assertNoNextRow;
@@ -30,6 +31,8 @@
*/
class FBDatabaseMetaDataPrimaryKeysTest {
+ // TODO Add schema support: tests involving other schema
+
private static final String UNNAMED_CONSTRAINT_PREFIX = "INTEG_";
private static final String UNNAMED_PK_INDEX_PREFIX = "RDB$PRIMARY";
@@ -170,6 +173,7 @@ private void validateExpectedPrimaryKeys(String tableName, List(PrimaryKeysMetaData.class);
Arrays.stream(PrimaryKeysMetaData.values()).forEach(key -> defaults.put(key, null));
+ defaults.put(PrimaryKeysMetaData.TABLE_SCHEM, ifSchemaElse("PUBLIC", null));
DEFAULT_COLUMN_VALUES = unmodifiableMap(defaults);
}
From 6b55727bdfdcab5a50631c50f21f9c575a12c72f Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Sun, 22 Jun 2025 15:50:12 +0200
Subject: [PATCH 16/64] #882 Schema support for getProcedureColumns
---
.../firebirdsql/jdbc/FBDatabaseMetaData.java | 2 +-
.../jdbc/metadata/GetProcedureColumns.java | 314 +++++++++++++-----
.../jdbc/metadata/GetProcedures.java | 6 +-
...BDatabaseMetaDataProcedureColumnsTest.java | 31 +-
4 files changed, 250 insertions(+), 103 deletions(-)
diff --git a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
index 578e7df3b..4192cef86 100644
--- a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
@@ -1230,7 +1230,7 @@ public ResultSet getProcedures(String catalog, String schemaPattern, String proc
public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern,
String columnNamePattern) throws SQLException {
return GetProcedureColumns.create(getDbMetadataMediator())
- .getProcedureColumns(catalog, procedureNamePattern, columnNamePattern);
+ .getProcedureColumns(catalog, schemaPattern, procedureNamePattern, columnNamePattern);
}
public static final String TABLE = "TABLE";
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetProcedureColumns.java b/src/main/org/firebirdsql/jdbc/metadata/GetProcedureColumns.java
index 83fca3254..6d1fb9ca5 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetProcedureColumns.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetProcedureColumns.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -13,6 +13,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
+import java.util.List;
import static java.sql.DatabaseMetaData.procedureColumnIn;
import static java.sql.DatabaseMetaData.procedureColumnOut;
@@ -21,7 +22,6 @@
import static org.firebirdsql.gds.ISCConstants.SQL_LONG;
import static org.firebirdsql.gds.ISCConstants.SQL_SHORT;
import static org.firebirdsql.gds.ISCConstants.SQL_VARYING;
-import static org.firebirdsql.jdbc.metadata.Clause.anyCondition;
import static org.firebirdsql.jdbc.metadata.FbMetadataConstants.OBJECT_NAME_LENGTH;
import static org.firebirdsql.jdbc.metadata.NameHelper.toSpecificName;
import static org.firebirdsql.jdbc.metadata.TypeMetadata.CHARSET_ID;
@@ -42,6 +42,8 @@
public abstract class GetProcedureColumns extends AbstractMetadataMethod {
private static final String COLUMNINFO = "COLUMNINFO";
+ private static final String COLUMN_SCHEMA_NAME = "PP.RDB$SCHEMA_NAME";
+ private static final String COLUMN_PACKAGE_NAME = "PP.RDB$PACKAGE_NAME";
private static final String COLUMN_PROCEDURE_NAME = "PP.RDB$PROCEDURE_NAME";
private static final String COLUMN_PARAMETER_NAME = "PP.RDB$PARAMETER_NAME";
@@ -76,14 +78,15 @@ private GetProcedureColumns(DbMetadataMediator mediator) {
/**
* @see DatabaseMetaData#getProcedureColumns(String, String, String, String)
*/
- public final ResultSet getProcedureColumns(String catalog, String procedureNamePattern, String columnNamePattern)
- throws SQLException {
+ public final ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern,
+ String columnNamePattern) throws SQLException {
if ("".equals(procedureNamePattern) || "".equals(columnNamePattern)) {
// Matching procedure name or column name not possible
return createEmpty();
}
- MetadataQuery metadataQuery = createGetProcedureColumnsQuery(catalog, procedureNamePattern, columnNamePattern);
+ MetadataQuery metadataQuery = createGetProcedureColumnsQuery(catalog, schemaPattern, procedureNamePattern,
+ columnNamePattern);
return createMetaDataResultSet(metadataQuery);
}
@@ -96,10 +99,11 @@ final RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) thr
.fromCurrentRow(rs)
.build();
String catalog = rs.getString("PROCEDURE_CAT");
+ String schema = rs.getString("PROCEDURE_SCHEM");
String procedureName = rs.getString("PROCEDURE_NAME");
return valueBuilder
.at(0).setString(catalog)
- .at(1).set(null)
+ .at(1).setString(schema)
.at(2).setString(procedureName)
.at(3).setString(rs.getString("COLUMN_NAME"))
// TODO: Unsure if procedureColumnOut is correct, maybe procedureColumnResult, or need ODS dependent use of RDB$PROCEDURE_TYPE to decide on selectable or executable?
@@ -123,17 +127,22 @@ final RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) thr
.at(17).setInt(rs.getInt("PARAMETER_NUMBER"))
// TODO: Find out if there is a conceptual difference with NULLABLE (idx 11)
.at(18).setString(nullFlag == 1 ? "NO" : "YES")
- .at(19).setString(toSpecificName(catalog, procedureName))
+ .at(19).setString(toSpecificName(catalog, schema, procedureName))
.toRowValue(false);
}
- abstract MetadataQuery createGetProcedureColumnsQuery(String catalog, String procedureNamePattern,
- String columnNamePattern);
+ abstract MetadataQuery createGetProcedureColumnsQuery(String catalog, String schemaPattern,
+ String procedureNamePattern, String columnNamePattern);
public static GetProcedureColumns create(DbMetadataMediator mediator) {
FirebirdSupportInfo firebirdSupportInfo = mediator.getFirebirdSupportInfo();
// NOTE: Indirection through static method prevents unnecessary classloading
- if (firebirdSupportInfo.isVersionEqualOrAbove(3)) {
+ if (firebirdSupportInfo.isVersionEqualOrAbove(6)) {
+ if (mediator.isUseCatalogAsPackage()) {
+ return FB6CatalogAsPackage.createInstance(mediator);
+ }
+ return FB6.createInstance(mediator);
+ } else if (firebirdSupportInfo.isVersionEqualOrAbove(3)) {
if (mediator.isUseCatalogAsPackage()) {
return FB3CatalogAsPackage.createInstance(mediator);
}
@@ -147,27 +156,30 @@ public static GetProcedureColumns create(DbMetadataMediator mediator) {
private static class FB2_5 extends GetProcedureColumns {
//@formatter:off
- private static final String GET_PROCEDURE_COLUMNS_FRAGMENT_2_5 =
- "select\n"
- + " null as PROCEDURE_CAT,\n"
- + " PP.RDB$PROCEDURE_NAME as PROCEDURE_NAME,\n"
- + " PP.RDB$PARAMETER_NAME as COLUMN_NAME,\n"
- + " PP.RDB$PARAMETER_TYPE as COLUMN_TYPE,\n"
- + " F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n"
- + " F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n"
- + " F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n"
- + " F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n"
- + " F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n"
- + " F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n"
- + " F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + ",\n"
- + " F.RDB$NULL_FLAG as NULL_FLAG,\n"
- + " PP.RDB$DESCRIPTION as REMARKS,\n"
- + " PP.RDB$PARAMETER_NUMBER + 1 as PARAMETER_NUMBER,\n"
- + " coalesce(PP.RDB$DEFAULT_SOURCE, F.RDB$DEFAULT_SOURCE) as COLUMN_DEF\n"
- + "from RDB$PROCEDURE_PARAMETERS PP inner join RDB$FIELDS F on PP.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME";
+ private static final String GET_PROCEDURE_COLUMNS_FRAGMENT_2_5 = """
+ select
+ cast(null as CHAR(1)) as PROCEDURE_CAT,
+ cast(null as CHAR(1)) as PROCEDURE_SCHEM,
+ PP.RDB$PROCEDURE_NAME as PROCEDURE_NAME,
+ PP.RDB$PARAMETER_NAME as COLUMN_NAME,
+ PP.RDB$PARAMETER_TYPE as COLUMN_TYPE,
+ """ +
+ " F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n" +
+ " F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n" +
+ " F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n" +
+ " F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n" +
+ " F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n" +
+ " F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n" +
+ " F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + ",\n" + """
+ F.RDB$NULL_FLAG as NULL_FLAG,
+ PP.RDB$DESCRIPTION as REMARKS,
+ PP.RDB$PARAMETER_NUMBER + 1 as PARAMETER_NUMBER,
+ coalesce(PP.RDB$DEFAULT_SOURCE, F.RDB$DEFAULT_SOURCE) as COLUMN_DEF
+ from RDB$PROCEDURE_PARAMETERS PP inner join RDB$FIELDS F on PP.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME""";
+ //@formatter:on
+
private static final String GET_PROCEDURE_COLUMNS_END_2_5 =
"\norder by PP.RDB$PROCEDURE_NAME, PP.RDB$PARAMETER_TYPE desc, PP.RDB$PARAMETER_NUMBER";
- //@formatter:on
private FB2_5(DbMetadataMediator mediator) {
super(mediator);
@@ -178,47 +190,50 @@ private static GetProcedureColumns createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createGetProcedureColumnsQuery(String catalog, String procedureNamePattern,
+ MetadataQuery createGetProcedureColumnsQuery(String catalog, String schemaPattern, String procedureNamePattern,
String columnNamePattern) {
- Clause procedureClause = new Clause(COLUMN_PROCEDURE_NAME, procedureNamePattern);
- Clause columnClause = new Clause(COLUMN_PARAMETER_NAME, columnNamePattern);
+ var clauses = List.of(
+ new Clause(COLUMN_PROCEDURE_NAME, procedureNamePattern),
+ new Clause(COLUMN_PARAMETER_NAME, columnNamePattern));
String query = GET_PROCEDURE_COLUMNS_FRAGMENT_2_5
- + (anyCondition(procedureClause, columnClause)
- ? "\nwhere " + procedureClause.getCondition(columnClause.hasCondition())
- + columnClause.getCondition(false)
- : "")
+ + (Clause.anyCondition(clauses) ? "\nwhere " + Clause.conjunction(clauses) : "")
+ GET_PROCEDURE_COLUMNS_END_2_5;
- return new MetadataQuery(query, Clause.parameters(procedureClause, columnClause));
+ return new MetadataQuery(query, Clause.parameters(clauses));
}
+
}
private static class FB3 extends GetProcedureColumns {
//@formatter:off
- private static final String GET_PROCEDURE_COLUMNS_FRAGMENT_3 =
- "select\n"
- + " null as PROCEDURE_CAT,\n"
- + " trim(trailing from PP.RDB$PROCEDURE_NAME) as PROCEDURE_NAME,\n"
- + " trim(trailing from PP.RDB$PARAMETER_NAME) as COLUMN_NAME,\n"
- + " PP.RDB$PARAMETER_TYPE as COLUMN_TYPE,\n"
- + " F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n"
- + " F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n"
- + " F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n"
- + " F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n"
- + " F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n"
- + " F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n"
- + " F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + ",\n"
- + " F.RDB$NULL_FLAG as NULL_FLAG,\n"
- + " PP.RDB$DESCRIPTION as REMARKS,\n"
- + " PP.RDB$PARAMETER_NUMBER + 1 as PARAMETER_NUMBER,\n"
- + " coalesce(PP.RDB$DEFAULT_SOURCE, F.RDB$DEFAULT_SOURCE) as COLUMN_DEF\n"
- + "from RDB$PROCEDURE_PARAMETERS PP inner join RDB$FIELDS F on PP.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME\n"
- + "where PP.RDB$PACKAGE_NAME is null";
+ private static final String GET_PROCEDURE_COLUMNS_FRAGMENT_3 = """
+ select
+ null as PROCEDURE_CAT,
+ null as PROCEDURE_SCHEM,
+ trim(trailing from PP.RDB$PROCEDURE_NAME) as PROCEDURE_NAME,
+ trim(trailing from PP.RDB$PARAMETER_NAME) as COLUMN_NAME,
+ PP.RDB$PARAMETER_TYPE as COLUMN_TYPE,
+ """ +
+ " F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n" +
+ " F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n" +
+ " F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n" +
+ " F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n" +
+ " F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n" +
+ " F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n" +
+ " F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + ",\n" + """
+ F.RDB$NULL_FLAG as NULL_FLAG,
+ PP.RDB$DESCRIPTION as REMARKS,
+ PP.RDB$PARAMETER_NUMBER + 1 as PARAMETER_NUMBER,
+ coalesce(PP.RDB$DEFAULT_SOURCE, F.RDB$DEFAULT_SOURCE) as COLUMN_DEF
+ from RDB$PROCEDURE_PARAMETERS PP inner join RDB$FIELDS F on PP.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME
+ where PP.RDB$PACKAGE_NAME is null""";
+ //@formatter:on
+
// NOTE: Including RDB$PACKAGE_NAME so index can be used to sort
private static final String GET_PROCEDURE_COLUMNS_END_3 =
"\norder by PP.RDB$PACKAGE_NAME, PP.RDB$PROCEDURE_NAME, PP.RDB$PARAMETER_TYPE desc, "
+ "PP.RDB$PARAMETER_NUMBER";
- //@formatter:on
+
private FB3(DbMetadataMediator mediator) {
super(mediator);
@@ -229,46 +244,47 @@ private static GetProcedureColumns createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createGetProcedureColumnsQuery(String catalog, String procedureNamePattern,
+ MetadataQuery createGetProcedureColumnsQuery(String catalog, String schemaPattern, String procedureNamePattern,
String columnNamePattern) {
- Clause procedureClause = new Clause(COLUMN_PROCEDURE_NAME, procedureNamePattern);
- Clause columnClause = new Clause(COLUMN_PARAMETER_NAME, columnNamePattern);
+ var clauses = List.of(
+ new Clause(COLUMN_PROCEDURE_NAME, procedureNamePattern),
+ new Clause(COLUMN_PARAMETER_NAME, columnNamePattern));
String query = GET_PROCEDURE_COLUMNS_FRAGMENT_3
- + procedureClause.getCondition("\nand ", "")
- + columnClause.getCondition("\nand ", "")
+ + (Clause.anyCondition(clauses) ? "\nand " + Clause.conjunction(clauses) : "")
+ GET_PROCEDURE_COLUMNS_END_3;
- return new MetadataQuery(query, Clause.parameters(procedureClause, columnClause));
+ return new MetadataQuery(query, Clause.parameters(clauses));
}
+
}
private static final class FB3CatalogAsPackage extends GetProcedureColumns {
//@formatter:off
- private static final String GET_PROCEDURE_COLUMNS_FRAGMENT_3_W_PKG =
- "select\n"
- + " coalesce(trim(trailing from PP.RDB$PACKAGE_NAME), '') as PROCEDURE_CAT,\n"
- + " trim(trailing from PP.RDB$PROCEDURE_NAME) as PROCEDURE_NAME,\n"
- + " trim(trailing from PP.RDB$PARAMETER_NAME) as COLUMN_NAME,\n"
- + " PP.RDB$PARAMETER_TYPE as COLUMN_TYPE,\n"
- + " F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n"
- + " F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n"
- + " F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n"
- + " F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n"
- + " F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n"
- + " F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n"
- + " F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + ",\n"
- + " F.RDB$NULL_FLAG as NULL_FLAG,\n"
- + " PP.RDB$DESCRIPTION as REMARKS,\n"
- + " PP.RDB$PARAMETER_NUMBER + 1 as PARAMETER_NUMBER,\n"
- + " coalesce(PP.RDB$DEFAULT_SOURCE, F.RDB$DEFAULT_SOURCE) as COLUMN_DEF\n"
- + "from RDB$PROCEDURE_PARAMETERS PP inner join RDB$FIELDS F on PP.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME";
+ private static final String GET_PROCEDURE_COLUMNS_FRAGMENT_3_W_PKG = """
+ select
+ coalesce(trim(trailing from PP.RDB$PACKAGE_NAME), '') as PROCEDURE_CAT,
+ null as PROCEDURE_SCHEM,
+ trim(trailing from PP.RDB$PROCEDURE_NAME) as PROCEDURE_NAME,
+ trim(trailing from PP.RDB$PARAMETER_NAME) as COLUMN_NAME,
+ PP.RDB$PARAMETER_TYPE as COLUMN_TYPE,
+ """ +
+ " F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n" +
+ " F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n" +
+ " F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n" +
+ " F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n" +
+ " F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n" +
+ " F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n" +
+ " F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + ",\n" + """
+ F.RDB$NULL_FLAG as NULL_FLAG,
+ PP.RDB$DESCRIPTION as REMARKS,
+ PP.RDB$PARAMETER_NUMBER + 1 as PARAMETER_NUMBER,
+ coalesce(PP.RDB$DEFAULT_SOURCE, F.RDB$DEFAULT_SOURCE) as COLUMN_DEF
+ from RDB$PROCEDURE_PARAMETERS PP inner join RDB$FIELDS F on PP.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME""";
+ //@formatter:on
private static final String GET_PROCEDURE_COLUMNS_END_3_W_PKG =
"\norder by PP.RDB$PACKAGE_NAME nulls first, PP.RDB$PROCEDURE_NAME, PP.RDB$PARAMETER_TYPE desc, "
+ "PP.RDB$PARAMETER_NUMBER";
- //@formatter:on
-
- private static final String COLUMN_PACKAGE_NAME = "PP.RDB$PACKAGE_NAME";
private FB3CatalogAsPackage(DbMetadataMediator mediator) {
super(mediator);
@@ -279,7 +295,7 @@ private static GetProcedureColumns createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createGetProcedureColumnsQuery(String catalog, String procedureNamePattern,
+ MetadataQuery createGetProcedureColumnsQuery(String catalog, String schemaPattern, String procedureNamePattern,
String columnNamePattern) {
var clauses = new ArrayList(3);
if (catalog != null) {
@@ -294,14 +310,130 @@ MetadataQuery createGetProcedureColumnsQuery(String catalog, String procedureNam
}
clauses.add(new Clause(COLUMN_PROCEDURE_NAME, procedureNamePattern));
clauses.add(new Clause(COLUMN_PARAMETER_NAME, columnNamePattern));
- //@formatter:off
String sql = GET_PROCEDURE_COLUMNS_FRAGMENT_3_W_PKG
- + (Clause.anyCondition(clauses)
- ? "\nwhere " + Clause.conjunction(clauses)
- : "")
+ + (Clause.anyCondition(clauses) ? "\nwhere " + Clause.conjunction(clauses) : "")
+ GET_PROCEDURE_COLUMNS_END_3_W_PKG;
- //@formatter:on
return new MetadataQuery(sql, Clause.parameters(clauses));
}
+
+ }
+
+ private static class FB6 extends GetProcedureColumns {
+
+ //@formatter:off
+ private static final String GET_PROCEDURE_COLUMNS_FRAGMENT_6 = """
+ select
+ null as PROCEDURE_CAT,
+ trim(trailing from PP.RDB$SCHEMA_NAME) as PROCEDURE_SCHEM,
+ trim(trailing from PP.RDB$PROCEDURE_NAME) as PROCEDURE_NAME,
+ trim(trailing from PP.RDB$PARAMETER_NAME) as COLUMN_NAME,
+ PP.RDB$PARAMETER_TYPE as COLUMN_TYPE,
+ """ +
+ " F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n" +
+ " F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n" +
+ " F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n" +
+ " F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n" +
+ " F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n" +
+ " F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n" +
+ " F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + ",\n" + """
+ F.RDB$NULL_FLAG as NULL_FLAG,
+ PP.RDB$DESCRIPTION as REMARKS,
+ PP.RDB$PARAMETER_NUMBER + 1 as PARAMETER_NUMBER,
+ coalesce(PP.RDB$DEFAULT_SOURCE, F.RDB$DEFAULT_SOURCE) as COLUMN_DEF
+ from SYSTEM.RDB$PROCEDURE_PARAMETERS PP
+ inner join SYSTEM.RDB$FIELDS F
+ on PP.RDB$FIELD_SOURCE_SCHEMA_NAME = F.RDB$SCHEMA_NAME and PP.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME
+ where PP.RDB$PACKAGE_NAME is null""";
+ //@formatter:on
+
+ private static final String GET_PROCEDURE_COLUMNS_END_6 =
+ "\norder by PP.RDB$PACKAGE_NAME, PP.RDB$SCHEMA_NAME, PP.RDB$PROCEDURE_NAME, "
+ + "PP.RDB$PARAMETER_TYPE desc, PP.RDB$PARAMETER_NUMBER";
+
+ private FB6(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetProcedureColumns createInstance(DbMetadataMediator mediator) {
+ return new FB6(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetProcedureColumnsQuery(String catalog, String schemaPattern, String procedureNamePattern,
+ String columnNamePattern) {
+ var clauses = List.of(
+ new Clause(COLUMN_SCHEMA_NAME, schemaPattern),
+ new Clause(COLUMN_PROCEDURE_NAME, procedureNamePattern),
+ new Clause(COLUMN_PARAMETER_NAME, columnNamePattern));
+ String query = GET_PROCEDURE_COLUMNS_FRAGMENT_6
+ + (Clause.anyCondition(clauses) ? "\nand " + Clause.conjunction(clauses) : "")
+ + GET_PROCEDURE_COLUMNS_END_6;
+ return new MetadataQuery(query, Clause.parameters(clauses));
+ }
+
+ }
+
+ private static final class FB6CatalogAsPackage extends GetProcedureColumns {
+
+ //@formatter:off
+ private static final String GET_PROCEDURE_COLUMNS_FRAGMENT_6_W_PKG = """
+ select
+ coalesce(trim(trailing from PP.RDB$PACKAGE_NAME), '') as PROCEDURE_CAT,
+ trim(trailing from PP.RDB$SCHEMA_NAME) as PROCEDURE_SCHEM,
+ trim(trailing from PP.RDB$PROCEDURE_NAME) as PROCEDURE_NAME,
+ trim(trailing from PP.RDB$PARAMETER_NAME) as COLUMN_NAME,
+ PP.RDB$PARAMETER_TYPE as COLUMN_TYPE,
+ """ +
+ " F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n" +
+ " F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n" +
+ " F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n" +
+ " F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n" +
+ " F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n" +
+ " F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n" +
+ " F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + ",\n" + """
+ F.RDB$NULL_FLAG as NULL_FLAG,
+ PP.RDB$DESCRIPTION as REMARKS,
+ PP.RDB$PARAMETER_NUMBER + 1 as PARAMETER_NUMBER,
+ coalesce(PP.RDB$DEFAULT_SOURCE, F.RDB$DEFAULT_SOURCE) as COLUMN_DEF
+ from SYSTEM.RDB$PROCEDURE_PARAMETERS PP
+ inner join SYSTEM.RDB$FIELDS F
+ on PP.RDB$FIELD_SOURCE_SCHEMA_NAME = F.RDB$SCHEMA_NAME and PP.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME""";
+ //@formatter:on
+
+ private static final String GET_PROCEDURE_COLUMNS_END_6_W_PKG =
+ "\norder by PP.RDB$PACKAGE_NAME nulls first, PP.RDB$SCHEMA_NAME, PP.RDB$PROCEDURE_NAME,"
+ + "PP.RDB$PARAMETER_TYPE desc, PP.RDB$PARAMETER_NUMBER";
+
+ private FB6CatalogAsPackage(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetProcedureColumns createInstance(DbMetadataMediator mediator) {
+ return new FB6CatalogAsPackage(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetProcedureColumnsQuery(String catalog, String schemaPattern, String procedureNamePattern,
+ String columnNamePattern) {
+ var clauses = new ArrayList(4);
+ clauses.add(new Clause(COLUMN_SCHEMA_NAME, schemaPattern));
+ if (catalog != null) {
+ // To quote from the JDBC API: "" retrieves those without a catalog; null means that the catalog name
+ // should not be used to narrow the search
+ if (catalog.isEmpty()) {
+ clauses.add(Clause.isNullClause(COLUMN_PACKAGE_NAME));
+ } else {
+ // Exact matches only
+ clauses.add(Clause.equalsClause(COLUMN_PACKAGE_NAME, catalog));
+ }
+ }
+ clauses.add(new Clause(COLUMN_PROCEDURE_NAME, procedureNamePattern));
+ clauses.add(new Clause(COLUMN_PARAMETER_NAME, columnNamePattern));
+ String sql = GET_PROCEDURE_COLUMNS_FRAGMENT_6_W_PKG
+ + (Clause.anyCondition(clauses) ? "\nwhere " + Clause.conjunction(clauses) : "")
+ + GET_PROCEDURE_COLUMNS_END_6_W_PKG;
+ return new MetadataQuery(sql, Clause.parameters(clauses));
+ }
+
}
}
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java b/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java
index 8f87e009d..2d277313e 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java
@@ -290,13 +290,9 @@ MetadataQuery createGetProceduresQuery(String catalog, String schemaPattern, Str
}
}
clauses.add(new Clause(COLUMN_PROCEDURE_NAME, procedureNamePattern));
- //@formatter:off
String sql = GET_PROCEDURES_FRAGMENT_6_W_PKG
- + (Clause.anyCondition(clauses)
- ? "\nwhere " + Clause.conjunction(clauses)
- : "")
+ + (Clause.anyCondition(clauses) ? "\nwhere " + Clause.conjunction(clauses) : "")
+ GET_PROCEDURES_ORDER_BY_6_W_PKG;
- //@formatter:on
return new MetadataQuery(sql, Clause.parameters(clauses));
}
}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataProcedureColumnsTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataProcedureColumnsTest.java
index 628ae9a5f..a8fe6f42d 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataProcedureColumnsTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataProcedureColumnsTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2012-2023 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2012-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -23,6 +23,7 @@
import static org.firebirdsql.common.FBTestProperties.getDefaultPropertiesForConnection;
import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
import static org.firebirdsql.common.FBTestProperties.getUrl;
+import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
import static org.firebirdsql.common.JdbcResourceHelper.closeQuietly;
import static org.firebirdsql.jdbc.FBDatabaseMetaDataProceduresTest.isIgnoredProcedure;
import static org.firebirdsql.jdbc.metadata.FbMetadataConstants.*;
@@ -36,7 +37,8 @@
* @author Mark Rotteveel
*/
class FBDatabaseMetaDataProcedureColumnsTest {
-
+
+ // TODO Add schema support: tests involving other schema
// TODO This test will need to be expanded with version dependent features
// (eg TYPE OF (2.1), TYPE OF COLUMN (2.5), NOT NULL (2.1), DEFAULT (2.0)
@@ -329,7 +331,7 @@ void testProcedureColumns_useCatalogAsPackage_specificPackageProcedureColumn(Str
List> expectedColumns =
withCatalog("WITH$PROCEDURE",
- withSpecificName("\"WITH$PROCEDURE\".\"IN$PACKAGE\"",
+ withSpecificName(ifSchemaElse("\"PUBLIC\".", "") + "\"WITH$PROCEDURE\".\"IN$PACKAGE\"",
List.of(createNumericalType(Types.INTEGER, "IN$PACKAGE", "RETURN1", 1, 10, 0, true,
DatabaseMetaData.procedureColumnOut))));
@@ -359,7 +361,7 @@ void testProcedureColumns_useCatalogAsPackage_nonPackagedOnly() throws Exception
private static List> getInPackage_allColumns() {
return withCatalog("WITH$PROCEDURE",
- withSpecificName("\"WITH$PROCEDURE\".\"IN$PACKAGE\"",
+ withSpecificName(ifSchemaElse("\"PUBLIC\".", "") + "\"WITH$PROCEDURE\".\"IN$PACKAGE\"",
// TODO Having result columns first might be against JDBC spec
// TODO Describing result columns as procedureColumnOut might be against JDBC spec
List.of(
@@ -392,9 +394,16 @@ private void validate(ResultSet procedureColumns, List createColumn(String procedureName, String columnName,
int ordinalPosition, boolean nullable, int columnType) {
+ return createColumn(ifSchemaElse("PUBLIC", null), procedureName, columnName, ordinalPosition, nullable,
+ columnType);
+ }
+
+ private static Map createColumn(String schema, String procedureName,
+ String columnName, int ordinalPosition, boolean nullable, int columnType) {
Map rules = getDefaultValueValidationRules();
+ rules.put(ProcedureColumnMetaData.PROCEDURE_SCHEM, schema);
rules.put(ProcedureColumnMetaData.PROCEDURE_NAME, procedureName);
- rules.put(ProcedureColumnMetaData.SPECIFIC_NAME, procedureName);
+ rules.put(ProcedureColumnMetaData.SPECIFIC_NAME, getProcedureSpecificName(schema, procedureName));
rules.put(ProcedureColumnMetaData.COLUMN_NAME, columnName);
rules.put(ProcedureColumnMetaData.ORDINAL_POSITION, ordinalPosition);
rules.put(ProcedureColumnMetaData.COLUMN_TYPE, columnType);
@@ -405,6 +414,16 @@ private static Map createColumn(String procedur
return rules;
}
+ private static String getProcedureSpecificName(String schema, String procedureName) {
+ if (schema == null || schema.isEmpty()) return procedureName;
+ var quote = QuoteStrategy.DIALECT_3;
+ // 5 = 4 quotes + 1 period
+ var sb = new StringBuilder(schema.length() + procedureName.length() + 5);
+ quote.appendQuoted(schema, sb).append('.');
+ quote.appendQuoted(procedureName, sb);
+ return sb.toString();
+ }
+
@SuppressWarnings("SameParameterValue")
private static Map createStringType(int jdbcType, String procedureName,
String columnName, int ordinalPosition, int length, boolean nullable, int columnType) {
@@ -554,7 +573,7 @@ private static List> withSpecificName(
static {
Map defaults = new EnumMap<>(ProcedureColumnMetaData.class);
defaults.put(ProcedureColumnMetaData.PROCEDURE_CAT, null);
- defaults.put(ProcedureColumnMetaData.PROCEDURE_SCHEM, null);
+ defaults.put(ProcedureColumnMetaData.PROCEDURE_SCHEM, ifSchemaElse("PUBLIC", null));
defaults.put(ProcedureColumnMetaData.SCALE, null);
defaults.put(ProcedureColumnMetaData.RADIX, FbMetadataConstants.RADIX_DECIMAL);
defaults.put(ProcedureColumnMetaData.NULLABLE, DatabaseMetaData.procedureNullable);
From 584e2cc6a09743556093cdb3e26831376de6c76a Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Sun, 22 Jun 2025 16:12:44 +0200
Subject: [PATCH 17/64] #882 Schema support for getPseudoColumns
---
.../firebirdsql/jdbc/FBDatabaseMetaData.java | 3 +-
.../jdbc/metadata/GetPseudoColumns.java | 136 +++++++++++++-----
.../FBDatabaseMetaDataPseudoColumnsTest.java | 31 +++-
3 files changed, 125 insertions(+), 45 deletions(-)
diff --git a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
index 4192cef86..d65133744 100644
--- a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
@@ -1817,7 +1817,8 @@ public static String escapeWildcards(String objectName) {
@Override
public ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern,
String columnNamePattern) throws SQLException {
- return GetPseudoColumns.create(getDbMetadataMediator()).getPseudoColumns(tableNamePattern, columnNamePattern);
+ return GetPseudoColumns.create(getDbMetadataMediator())
+ .getPseudoColumns(schemaPattern, tableNamePattern, columnNamePattern);
}
@Override
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetPseudoColumns.java b/src/main/org/firebirdsql/jdbc/metadata/GetPseudoColumns.java
index 0e1812c22..28ec287f9 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetPseudoColumns.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetPseudoColumns.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -32,6 +32,7 @@
public abstract class GetPseudoColumns {
private static final String PSEUDOCOLUMNS = "PSEUDOCOLUMNS";
+ public static final String COLUMN_SCHEMA_NAME = "RDB$SCHEMA_NAME";
public static final String COLUMN_RELATION_NAME = "RDB$RELATION_NAME";
private static final RowDescriptor ROW_DESCRIPTOR = DbMetadataMediator.newRowDescriptorBuilder(12)
@@ -62,7 +63,7 @@ private GetPseudoColumns(DbMetadataMediator mediator) {
this.mediator = mediator;
}
- public ResultSet getPseudoColumns(String tableNamePattern, String columnNamePattern) throws SQLException {
+ public ResultSet getPseudoColumns(String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException {
if ("".equals(tableNamePattern) || "".equals(columnNamePattern)) {
// Matching table and/or column not possible
return createEmpty();
@@ -77,19 +78,22 @@ public ResultSet getPseudoColumns(String tableNamePattern, String columnNamePatt
return createEmpty();
}
- try (ResultSet rs = mediator.performMetaDataQuery(createGetPseudoColumnsQuery(tableNamePattern))) {
+ MetadataQuery metadataQuery = createGetPseudoColumnsQuery(schemaPattern, tableNamePattern);
+ try (ResultSet rs = mediator.performMetaDataQuery(metadataQuery)) {
if (!rs.next()) {
return createEmpty();
}
- List rows = new ArrayList<>();
- RowValueBuilder valueBuilder = new RowValueBuilder(ROW_DESCRIPTOR);
+ var rows = new ArrayList();
+ var valueBuilder = new RowValueBuilder(ROW_DESCRIPTOR);
do {
+ String schema = rs.getString(COLUMN_SCHEMA_NAME);
String tableName = rs.getString(COLUMN_RELATION_NAME);
if (retrieveDbKey) {
int dbKeyLength = rs.getInt("RDB$DBKEY_LENGTH");
valueBuilder
+ .at(1).setString(schema)
.at(2).setString(tableName)
.at(3).setString("RDB$DB_KEY")
.at(4).setInt(Types.ROWID)
@@ -104,6 +108,7 @@ public ResultSet getPseudoColumns(String tableNamePattern, String columnNamePatt
if (retrieveRecordVersion && rs.getBoolean("HAS_RECORD_VERSION")) {
valueBuilder
+ .at(1).setString(schema)
.at(2).setString(tableName)
.at(3).setString("RDB$RECORD_VERSION")
.at(4).setInt(Types.BIGINT)
@@ -122,7 +127,7 @@ public ResultSet getPseudoColumns(String tableNamePattern, String columnNamePatt
abstract boolean supportsRecordVersion();
- abstract MetadataQuery createGetPseudoColumnsQuery(String tableNamePattern);
+ abstract MetadataQuery createGetPseudoColumnsQuery(String schemaPattern, String tableNamePattern);
private ResultSet createEmpty() throws SQLException {
return new FBResultSet(ROW_DESCRIPTOR, emptyList());
@@ -131,7 +136,9 @@ private ResultSet createEmpty() throws SQLException {
public static GetPseudoColumns create(DbMetadataMediator mediator) {
FirebirdSupportInfo firebirdSupportInfo = mediator.getFirebirdSupportInfo();
// NOTE: Indirection through static method prevents unnecessary classloading
- if (firebirdSupportInfo.isVersionEqualOrAbove(3, 0)) {
+ if (firebirdSupportInfo.isVersionEqualOrAbove(6)) {
+ return FB6.createInstance(mediator);
+ } else if (firebirdSupportInfo.isVersionEqualOrAbove(3)) {
return FB3.createInstance(mediator);
} else {
return FB2_5.createInstance(mediator);
@@ -141,17 +148,17 @@ public static GetPseudoColumns create(DbMetadataMediator mediator) {
@SuppressWarnings("java:S101")
private static final class FB2_5 extends GetPseudoColumns {
- //@formatter:off
- private static final String GET_PSEUDO_COLUMNS_FRAGMENT_2_5 =
- "select\n"
- + " RDB$RELATION_NAME,\n"
- + " RDB$DBKEY_LENGTH,\n"
- + " 'F' AS HAS_RECORD_VERSION,\n"
- + " '' AS RECORD_VERSION_NULLABLE\n" // unknown nullability (and doesn't matter, no RDB$RECORD_VERSION)
- + "from RDB$RELATIONS\n";
+ private static final String GET_PSEUDO_COLUMNS_FRAGMENT_2_5 = """
+ select
+ cast(null as char(1)) as RDB$SCHEMA_NAME,
+ RDB$RELATION_NAME,
+ RDB$DBKEY_LENGTH,
+ 'F' AS HAS_RECORD_VERSION,
+ -- unknown nullability (and doesn't matter, no RDB$RECORD_VERSION)
+ '' AS RECORD_VERSION_NULLABLE
+ from RDB$RELATIONS""";
- private static final String GET_PSEUDO_COLUMNS_END_2_5 = "order by RDB$RELATION_NAME";
- //@formatter:on
+ private static final String GET_PSEUDO_COLUMNS_END_2_5 = "\norder by RDB$RELATION_NAME";
private FB2_5(DbMetadataMediator mediator) {
super(mediator);
@@ -167,32 +174,35 @@ boolean supportsRecordVersion() {
}
@Override
- MetadataQuery createGetPseudoColumnsQuery(String tableNamePattern) {
- Clause tableNameClause = new Clause(COLUMN_RELATION_NAME, tableNamePattern);
+ MetadataQuery createGetPseudoColumnsQuery(String schemaPattern, String tableNamePattern) {
+ var tableNameClause = new Clause(COLUMN_RELATION_NAME, tableNamePattern);
String sql = GET_PSEUDO_COLUMNS_FRAGMENT_2_5
- + tableNameClause.getCondition("where ", "\n")
+ + tableNameClause.getCondition("\nwhere ", "")
+ GET_PSEUDO_COLUMNS_END_2_5;
return new MetadataQuery(sql, Clause.parameters(tableNameClause));
}
+
}
private static final class FB3 extends GetPseudoColumns {
- //@formatter:off
- private static final String GET_PSEUDO_COLUMNS_FRAGMENT_3 =
- "select\n"
- + " trim(trailing from RDB$RELATION_NAME) as RDB$RELATION_NAME,\n"
- + " RDB$DBKEY_LENGTH,\n"
- + " RDB$DBKEY_LENGTH = 8 as HAS_RECORD_VERSION,\n"
- + " case\n"
- + " when RDB$RELATION_TYPE in (0, 1, 4, 5) then 'NO'\n" // table, view, GTT preserve + delete: never null
- + " when RDB$RELATION_TYPE in (2, 3) then 'YES'\n" // external + virtual: always null
- + " else ''\n" // unknown or unsupported (by Jaybird) type: unknown nullability
- + " end as RECORD_VERSION_NULLABLE\n"
- + "from RDB$RELATIONS\n";
-
- private static final String GET_PSEUDO_COLUMNS_END_3 = "order by RDB$RELATION_NAME";
- //@formatter:on
+ private static final String GET_PSEUDO_COLUMNS_FRAGMENT_3 = """
+ select
+ null as RDB$SCHEMA_NAME,
+ trim(trailing from RDB$RELATION_NAME) as RDB$RELATION_NAME,
+ RDB$DBKEY_LENGTH,
+ RDB$DBKEY_LENGTH = 8 as HAS_RECORD_VERSION,
+ case
+ -- table, view, GTT preserve + delete: never null
+ when RDB$RELATION_TYPE in (0, 1, 4, 5) then 'NO'
+ -- external + virtual: always null
+ when RDB$RELATION_TYPE in (2, 3) then 'YES'
+ -- unknown or unsupported (by Jaybird) type: unknown nullability
+ else ''
+ end as RECORD_VERSION_NULLABLE
+ from RDB$RELATIONS""";
+
+ private static final String GET_PSEUDO_COLUMNS_END_3 = "\norder by RDB$RELATION_NAME";
private FB3(DbMetadataMediator mediator) {
super(mediator);
@@ -208,12 +218,60 @@ boolean supportsRecordVersion() {
}
@Override
- MetadataQuery createGetPseudoColumnsQuery(String tableNamePattern) {
- Clause tableNameClause = new Clause(COLUMN_RELATION_NAME, tableNamePattern);
+ MetadataQuery createGetPseudoColumnsQuery(String schemaPattern, String tableNamePattern) {
+ var tableNameClause = new Clause(COLUMN_RELATION_NAME, tableNamePattern);
String sql = GET_PSEUDO_COLUMNS_FRAGMENT_3
- + tableNameClause.getCondition("where ", "\n")
+ + tableNameClause.getCondition("\nwhere ", "")
+ GET_PSEUDO_COLUMNS_END_3;
return new MetadataQuery(sql, Clause.parameters(tableNameClause));
}
+
}
+
+ private static final class FB6 extends GetPseudoColumns {
+
+ private static final String GET_PSEUDO_COLUMNS_FRAGMENT_6 = """
+ select
+ trim(trailing from RDB$SCHEMA_NAME) as RDB$SCHEMA_NAME,
+ trim(trailing from RDB$RELATION_NAME) as RDB$RELATION_NAME,
+ RDB$DBKEY_LENGTH,
+ RDB$DBKEY_LENGTH = 8 as HAS_RECORD_VERSION,
+ case
+ -- table, view, GTT preserve + delete: never null
+ when RDB$RELATION_TYPE in (0, 1, 4, 5) then 'NO'
+ -- external + virtual: always null
+ when RDB$RELATION_TYPE in (2, 3) then 'YES'
+ -- unknown or unsupported (by Jaybird) type: unknown nullability
+ else ''
+ end as RECORD_VERSION_NULLABLE
+ from SYSTEM.RDB$RELATIONS""";
+
+ private static final String GET_PSEUDO_COLUMNS_END_6 = "\norder by RDB$SCHEMA_NAME, RDB$RELATION_NAME";
+
+ private FB6(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetPseudoColumns createInstance(DbMetadataMediator mediator) {
+ return new FB6(mediator);
+ }
+
+ @Override
+ boolean supportsRecordVersion() {
+ return true;
+ }
+
+ @Override
+ MetadataQuery createGetPseudoColumnsQuery(String schemaPattern, String tableNamePattern) {
+ var clauses = List.of(
+ new Clause(COLUMN_SCHEMA_NAME, schemaPattern),
+ new Clause(COLUMN_RELATION_NAME, tableNamePattern));
+ String sql = GET_PSEUDO_COLUMNS_FRAGMENT_6
+ + (Clause.anyCondition(clauses) ? "\nwhere " + Clause.conjunction(clauses) : "")
+ + GET_PSEUDO_COLUMNS_END_6;
+ return new MetadataQuery(sql, Clause.parameters(clauses));
+ }
+
+ }
+
}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataPseudoColumnsTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataPseudoColumnsTest.java
index 9e11dc329..0714e820b 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataPseudoColumnsTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataPseudoColumnsTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2018-2023 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2018-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -13,12 +13,15 @@
import static org.firebirdsql.common.FBTestProperties.getConnectionViaDriverManager;
import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
+import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
import static org.firebirdsql.common.JdbcResourceHelper.closeQuietly;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
class FBDatabaseMetaDataPseudoColumnsTest {
+ // TODO Add schema support: tests involving other schema
+
//@formatter:off
private static final String NORMAL_TABLE_NAME = "NORMAL_TABLE";
private static final String CREATE_NORMAL_TABLE = "create table " + NORMAL_TABLE_NAME + " ( "
@@ -136,7 +139,8 @@ void testExternalTable_allPseudoColumns() throws Exception {
@Test
void testMonitoringTable_allPseudoColumns() throws Exception {
assumeTrue(getDefaultSupportInfo().supportsMonitoringTables(), "Test requires monitoring tables");
- List> validationRules = createStandardValidationRules("MON$DATABASE", "YES");
+ List> validationRules =
+ createStandardValidationRules(ifSchemaElse("SYSTEM", null), "MON$DATABASE", "YES");
ResultSet pseudoColumns = dbmd.getPseudoColumns(null, null, "MON$DATABASE", "%");
validate(pseudoColumns, validationRules);
@@ -271,10 +275,15 @@ void testPattern_nullTable() throws Exception {
private List> createStandardValidationRules(String tableName,
String recordVersionNullable) {
+ return createStandardValidationRules(ifSchemaElse("PUBLIC", null), tableName, recordVersionNullable);
+ }
+
+ private List> createStandardValidationRules(String schema, String tableName,
+ String recordVersionNullable) {
List> validationRules = new ArrayList<>();
- validationRules.add(createDbkeyValidationRules(tableName, 8));
+ validationRules.add(createDbkeyValidationRules(schema, tableName, 8));
if (supportsRecordVersion) {
- validationRules.add(createRecordVersionValidationRules(tableName, recordVersionNullable));
+ validationRules.add(createRecordVersionValidationRules(schema, tableName, recordVersionNullable));
}
return validationRules;
}
@@ -301,7 +310,7 @@ private void validate(ResultSet pseudoColumns, List defaults = new EnumMap<>(PseudoColumnMetaData.class);
defaults.put(PseudoColumnMetaData.TABLE_CAT, null);
- defaults.put(PseudoColumnMetaData.TABLE_SCHEM, null);
+ defaults.put(PseudoColumnMetaData.TABLE_SCHEM, ifSchemaElse("PUBLIC", null));
defaults.put(PseudoColumnMetaData.DECIMAL_DIGITS, null);
defaults.put(PseudoColumnMetaData.NUM_PREC_RADIX, 10);
defaults.put(PseudoColumnMetaData.COLUMN_USAGE, PseudoColumnUsage.NO_USAGE_RESTRICTIONS.name());
@@ -317,7 +326,13 @@ private static Map getDefaultValueValidationRules(
}
private Map createDbkeyValidationRules(String tableName, int expectedDbKeyLength) {
+ return createDbkeyValidationRules(ifSchemaElse("PUBLIC", null), tableName, expectedDbKeyLength);
+ }
+
+ private Map createDbkeyValidationRules(String schema, String tableName,
+ int expectedDbKeyLength) {
Map rules = getDefaultValueValidationRules();
+ rules.put(PseudoColumnMetaData.TABLE_SCHEM, schema);
rules.put(PseudoColumnMetaData.TABLE_NAME, tableName);
rules.put(PseudoColumnMetaData.COLUMN_NAME, "RDB$DB_KEY");
rules.put(PseudoColumnMetaData.DATA_TYPE, Types.ROWID);
@@ -328,7 +343,13 @@ private Map createDbkeyValidationRules(String tabl
}
private Map createRecordVersionValidationRules(String tableName, String nullable) {
+ return createRecordVersionValidationRules(ifSchemaElse("PUBLIC", null), tableName, nullable);
+ }
+
+ private Map createRecordVersionValidationRules(String schema, String tableName,
+ String nullable) {
Map rules = getDefaultValueValidationRules();
+ rules.put(PseudoColumnMetaData.TABLE_SCHEM, schema);
rules.put(PseudoColumnMetaData.TABLE_NAME, tableName);
rules.put(PseudoColumnMetaData.COLUMN_NAME, "RDB$RECORD_VERSION");
rules.put(PseudoColumnMetaData.DATA_TYPE, Types.BIGINT);
From 6bdb4bca516423f14bb3748b05181b7a96bfc2bf Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Mon, 23 Jun 2025 11:36:16 +0200
Subject: [PATCH 18/64] #882 Schema support for getTablePrivileges
Add extra column JB_GRANTEE_SCHEMA to getTablePrivileges and getColumnPrivileges
---
src/docs/asciidoc/release_notes.adoc | 11 +-
.../firebirdsql/jdbc/FBDatabaseMetaData.java | 10 +-
.../jdbc/metadata/GetColumnPrivileges.java | 13 +-
.../jdbc/metadata/GetTablePrivileges.java | 148 +++++++++++++-----
...BDatabaseMetaDataColumnPrivilegesTest.java | 4 +-
...FBDatabaseMetaDataTablePrivilegesTest.java | 24 ++-
6 files changed, 156 insertions(+), 54 deletions(-)
diff --git a/src/docs/asciidoc/release_notes.adoc b/src/docs/asciidoc/release_notes.adoc
index f820b919b..a633fc778 100644
--- a/src/docs/asciidoc/release_notes.adoc
+++ b/src/docs/asciidoc/release_notes.adoc
@@ -516,16 +516,15 @@ Firebird 6.0 introduces schemas, and Jaybird 7 provides support for schemas as d
Changes include:
* `DatabaseMetaData`
-** `getProcedures` now uses the `schemaPattern` to filter by schema, with the following caveats
-*** `schemaPattern` empty will return no rows on Firebird 6.0 and higher (all procedures are in a schema);
-use `null` or `"%"` to match all schemas
-*** `catalog` is (still) ignored if `useCatalogAsPackage` is `false`
+** Methods accepting a `schema` (exact match if not `null`) or `schemaPattern` (`LIKE` match if not `null`) will return no rows for value empty (`""`) on Firebird 6.0 and higher;
+use `null` or -- `schemaPattern` only -- `"%"` to match all schemas
** `getSchemas()` returns all defined schemas
** `getSchemas(String catalog, String schemaPattern)` returns all schemas matching the `LIKE` pattern `schemaPattern`, with the following caveats
*** `catalog` non-empty will return no rows;
we recommend to always use `null` for `catalog`
-*** `schemaPattern` empty will return no rows (there are no schemas with an empty name);
-use `null` or `"%"` to match all schemas
+** `getColumnPrivileges` and `getTablePrivileges` received an additional column, `JB_GRANTEE_SCHEMA`, which is non-``null`` for grantees that are schema-bound (e.g. a procedure).
++
+As this is a non-standard column, we recommend to always retrieve it by name.
// TODO add major changes
diff --git a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
index d65133744..8be2d4a60 100644
--- a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
@@ -1327,10 +1327,12 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa
* {@inheritDoc}
*
*
- * Jaybird defines an additional column:
+ * Jaybird defines these additional columns:
*
* - JB_GRANTEE_TYPE String => Object type of {@code GRANTEE} (NOTE: Jaybird specific column;
* retrieve by name!).
+ * - JB_GRANTEE_SCHEMA String => Schema of {@code GRANTEE} if it's a schema-bound object (NOTE:
+ * Jaybird specific column; retrieve by name!).
*
*
*
@@ -1358,10 +1360,12 @@ public ResultSet getColumnPrivileges(String catalog, String schema, String table
* {@inheritDoc}
*
*
- * Jaybird defines an additional column:
+ * Jaybird defines these additional columns:
*
* - JB_GRANTEE_TYPE String => Object type of {@code GRANTEE} (NOTE: Jaybird specific column;
* retrieve by name!).
+ * - JB_GRANTEE_SCHEMA String => Schema of {@code GRANTEE} if it's a schema-bound object (NOTE:
+ * Jaybird specific column; retrieve by name!).
*
*
*
@@ -1373,7 +1377,7 @@ public ResultSet getColumnPrivileges(String catalog, String schema, String table
@Override
public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern)
throws SQLException {
- return GetTablePrivileges.create(getDbMetadataMediator()).getTablePrivileges(tableNamePattern);
+ return GetTablePrivileges.create(getDbMetadataMediator()).getTablePrivileges(schemaPattern, tableNamePattern);
}
/**
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java b/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java
index 1912e254e..ad6d20843 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java
@@ -33,7 +33,7 @@
public abstract class GetColumnPrivileges extends AbstractMetadataMethod {
private static final String COLUMNPRIV = "COLUMNPRIV";
- private static final RowDescriptor ROW_DESCRIPTOR = DbMetadataMediator.newRowDescriptorBuilder(9)
+ private static final RowDescriptor ROW_DESCRIPTOR = DbMetadataMediator.newRowDescriptorBuilder(10)
.at(0).simple(SQL_VARYING | 1, OBJECT_NAME_LENGTH, "TABLE_CAT", COLUMNPRIV).addField()
.at(1).simple(SQL_VARYING | 1, OBJECT_NAME_LENGTH, "TABLE_SCHEM", COLUMNPRIV).addField()
.at(2).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_NAME", COLUMNPRIV).addField()
@@ -43,6 +43,7 @@ public abstract class GetColumnPrivileges extends AbstractMetadataMethod {
.at(6).simple(SQL_VARYING, 31, "PRIVILEGE", COLUMNPRIV).addField()
.at(7).simple(SQL_VARYING, 3, "IS_GRANTABLE", COLUMNPRIV).addField()
.at(8).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "JB_GRANTEE_TYPE", COLUMNPRIV).addField()
+ .at(9).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "JB_GRANTEE_SCHEMA", COLUMNPRIV).addField()
.toRowDescriptor();
GetColumnPrivileges(DbMetadataMediator mediator) {
@@ -74,6 +75,7 @@ RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQ
.at(6).setString(mapPrivilege(rs.getString("PRIVILEGE")))
.at(7).setString(rs.getBoolean("IS_GRANTABLE") ? "YES" : "NO")
.at(8).setString(rs.getString("JB_GRANTEE_TYPE"))
+ .at(9).setString(rs.getString("JB_GRANTEE_SCHEMA"))
.toRowValue(false);
}
@@ -99,7 +101,8 @@ private static final class FB5 extends GetColumnPrivileges {
UP.RDB$USER as GRANTEE,
UP.RDB$PRIVILEGE as PRIVILEGE,
UP.RDB$GRANT_OPTION as IS_GRANTABLE,
- T.RDB$TYPE_NAME as JB_GRANTEE_TYPE
+ T.RDB$TYPE_NAME as JB_GRANTEE_TYPE,
+ cast(null as char(1)) as JB_GRANTEE_SCHEMA
from RDB$RELATION_FIELDS RF
inner join RDB$USER_PRIVILEGES UP
on UP.RDB$RELATION_NAME = RF.RDB$RELATION_NAME
@@ -149,7 +152,8 @@ private static final class FB6 extends GetColumnPrivileges {
trim(trailing from UP.RDB$USER) as GRANTEE,
UP.RDB$PRIVILEGE as PRIVILEGE,
UP.RDB$GRANT_OPTION as IS_GRANTABLE,
- T.RDB$TYPE_NAME as JB_GRANTEE_TYPE
+ trim(trailing from T.RDB$TYPE_NAME) as JB_GRANTEE_TYPE,
+ trim(trailing from UP.RDB$USER_SCHEMA_NAME) as JB_GRANTEE_SCHEMA
from SYSTEM.RDB$RELATION_FIELDS RF
inner join SYSTEM.RDB$USER_PRIVILEGES UP
on UP.RDB$RELATION_SCHEMA_NAME = RF.RDB$SCHEMA_NAME and UP.RDB$RELATION_NAME = RF.RDB$RELATION_NAME
@@ -162,7 +166,8 @@ private static final class FB6 extends GetColumnPrivileges {
// NOTE: Sort by user and schema is not defined in JDBC, but we do this to ensure a consistent order for tests
private static final String GET_COLUMN_PRIVILEGES_END_6 =
- "\norder by RF.RDB$FIELD_NAME, UP.RDB$PRIVILEGE, UP.RDB$USER, RF.RDB$SCHEMA_NAME";
+ "\norder by RF.RDB$FIELD_NAME, UP.RDB$PRIVILEGE, UP.RDB$USER_SCHEMA_NAME nulls first, UP.RDB$USER, "
+ + "RF.RDB$SCHEMA_NAME";
private FB6(DbMetadataMediator mediator) {
super(mediator);
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetTablePrivileges.java b/src/main/org/firebirdsql/jdbc/metadata/GetTablePrivileges.java
index fc6610b72..a784e62fd 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetTablePrivileges.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetTablePrivileges.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -10,6 +10,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.List;
import static org.firebirdsql.gds.ISCConstants.SQL_VARYING;
import static org.firebirdsql.jdbc.metadata.FbMetadataConstants.OBJECT_NAME_LENGTH;
@@ -27,11 +28,11 @@
* @author Mark Rotteveel
* @since 5
*/
-public final class GetTablePrivileges extends AbstractMetadataMethod {
+public abstract sealed class GetTablePrivileges extends AbstractMetadataMethod {
private static final String TABLEPRIV = "TABLEPRIV";
- private static final RowDescriptor ROW_DESCRIPTOR = DbMetadataMediator.newRowDescriptorBuilder(8)
+ private static final RowDescriptor ROW_DESCRIPTOR = DbMetadataMediator.newRowDescriptorBuilder(9)
.at(0).simple(SQL_VARYING | 1, OBJECT_NAME_LENGTH, "TABLE_CAT", TABLEPRIV).addField()
.at(1).simple(SQL_VARYING | 1, OBJECT_NAME_LENGTH, "TABLE_SCHEM", TABLEPRIV).addField()
.at(2).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_NAME", TABLEPRIV).addField()
@@ -40,63 +41,138 @@ public final class GetTablePrivileges extends AbstractMetadataMethod {
.at(5).simple(SQL_VARYING, 31, "PRIVILEGE", TABLEPRIV).addField()
.at(6).simple(SQL_VARYING, 3, "IS_GRANTABLE", TABLEPRIV).addField()
.at(7).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "JB_GRANTEE_TYPE", TABLEPRIV).addField()
+ .at(8).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "JB_GRANTEE_SCHEMA", TABLEPRIV).addField()
.toRowDescriptor();
- //@formatter:off
- private static final String GET_TABLE_PRIVILEGES_START =
- // Distinct is needed as we're selecting privileges for the table and columns of the table
- "select distinct\n"
- + " UP.RDB$RELATION_NAME as TABLE_NAME,\n"
- + " UP.RDB$GRANTOR as GRANTOR,\n"
- + " UP.RDB$USER as GRANTEE,\n"
- + " UP.RDB$PRIVILEGE as PRIVILEGE,\n"
- + " UP.RDB$GRANT_OPTION as IS_GRANTABLE,\n"
- + " T.RDB$TYPE_NAME as JB_GRANTEE_TYPE\n"
- + "from RDB$USER_PRIVILEGES UP\n"
- + "left join RDB$TYPES T\n"
- + " on T.RDB$FIELD_NAME = 'RDB$OBJECT_TYPE' and T.RDB$TYPE = UP.RDB$USER_TYPE \n"
- // Other privileges don't make sense for table privileges
- // TODO Consider including ALTER/DROP privileges
- + "where UP.RDB$PRIVILEGE in ('A', 'D', 'I', 'R', 'S', 'U')\n"
- // Only tables and views
- + "and UP.RDB$OBJECT_TYPE in (0, 1)\n";
-
- // NOTE: Sort by user is not defined in JDBC, but we do this to ensure a consistent order for tests
- private static final String GET_TABLE_PRIVILEGES_END = "order by RDB$RELATION_NAME, RDB$PRIVILEGE, RDB$USER";
- //@formatter:on
-
private GetTablePrivileges(DbMetadataMediator mediator) {
super(ROW_DESCRIPTOR, mediator);
}
- public ResultSet getTablePrivileges(String tableNamePattern) throws SQLException {
+ public final ResultSet getTablePrivileges(String schemaPattern, String tableNamePattern) throws SQLException {
if ("".equals(tableNamePattern)) {
return createEmpty();
}
- Clause tableClause = new Clause("RDB$RELATION_NAME", tableNamePattern);
-
- String sql = GET_TABLE_PRIVILEGES_START
- + tableClause.getCondition("and ", "\n")
- + GET_TABLE_PRIVILEGES_END;
- MetadataQuery metadataQuery = new MetadataQuery(sql, Clause.parameters(tableClause));
+ MetadataQuery metadataQuery = createGetTablePrivilegesQuery(schemaPattern, tableNamePattern);
return createMetaDataResultSet(metadataQuery);
}
+ abstract MetadataQuery createGetTablePrivilegesQuery(String schemaPattern, String tableNamePattern);
+
@Override
RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQLException {
return valueBuilder
.at(0).set(null)
- .at(1).set(null)
+ .at(1).setString(rs.getString("TABLE_SCHEM"))
.at(2).setString(rs.getString("TABLE_NAME"))
.at(3).setString(rs.getString("GRANTOR"))
.at(4).setString(rs.getString("GRANTEE"))
.at(5).setString(mapPrivilege(rs.getString("PRIVILEGE")))
.at(6).setString(rs.getBoolean("IS_GRANTABLE") ? "YES" : "NO")
.at(7).setString(rs.getString("JB_GRANTEE_TYPE"))
+ .at(8).setString(rs.getString("JB_GRANTEE_SCHEMA"))
.toRowValue(false);
}
public static GetTablePrivileges create(DbMetadataMediator mediator) {
- return new GetTablePrivileges(mediator);
+ // NOTE: Indirection through static method prevents unnecessary classloading
+ if (mediator.getFirebirdSupportInfo().isVersionEqualOrAbove(6)) {
+ return FB6.createInstance(mediator);
+ } else {
+ return FB5.createInstance(mediator);
+ }
+ }
+
+ /**
+ * Implementation for Firebird 5.0 and older.
+ */
+ private static final class FB5 extends GetTablePrivileges {
+
+ // Distinct is needed as we're selecting privileges for the table and columns of the table
+ private static final String GET_TABLE_PRIVILEGES_START_5 = """
+ select distinct
+ cast(null as char(1)) as TABLE_SCHEM,
+ UP.RDB$RELATION_NAME as TABLE_NAME,
+ UP.RDB$GRANTOR as GRANTOR,
+ UP.RDB$USER as GRANTEE,
+ UP.RDB$PRIVILEGE as PRIVILEGE,
+ UP.RDB$GRANT_OPTION as IS_GRANTABLE,
+ T.RDB$TYPE_NAME as JB_GRANTEE_TYPE,
+ cast(null as char(1)) as JB_GRANTEE_SCHEMA
+ from RDB$USER_PRIVILEGES UP
+ left join RDB$TYPES T
+ on T.RDB$FIELD_NAME = 'RDB$OBJECT_TYPE' and T.RDB$TYPE = UP.RDB$USER_TYPE
+ where UP.RDB$PRIVILEGE in ('A', 'D', 'I', 'R', 'S', 'U') -- privileges relevant for tables
+ and UP.RDB$OBJECT_TYPE in (0, 1) -- Only tables and views""";
+
+ // NOTE: Sort by user is not defined in JDBC, but we do this to ensure a consistent order for tests
+ private static final String GET_TABLE_PRIVILEGES_END_5 =
+ "\norder by UP.RDB$RELATION_NAME, UP.RDB$PRIVILEGE, UP.RDB$USER";
+
+ private FB5(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetTablePrivileges createInstance(DbMetadataMediator mediator) {
+ return new FB5(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetTablePrivilegesQuery(String schemaPattern, String tableNamePattern) {
+ Clause tableClause = new Clause("UP.RDB$RELATION_NAME", tableNamePattern);
+ String sql = GET_TABLE_PRIVILEGES_START_5
+ + tableClause.getCondition("\nand ", "")
+ + GET_TABLE_PRIVILEGES_END_5;
+ return new MetadataQuery(sql, Clause.parameters(tableClause));
+ }
+
+ }
+
+ /**
+ * Implementation for Firebird 6.0 and newer.
+ */
+ private static final class FB6 extends GetTablePrivileges {
+
+ // Distinct is needed as we're selecting privileges for the table and columns of the table
+ private static final String GET_TABLE_PRIVILEGES_START_6 = """
+ select distinct
+ trim(trailing from UP.RDB$RELATION_SCHEMA_NAME) as TABLE_SCHEM,
+ trim(trailing from UP.RDB$RELATION_NAME) as TABLE_NAME,
+ trim(trailing from UP.RDB$GRANTOR) as GRANTOR,
+ trim(trailing from UP.RDB$USER) as GRANTEE,
+ UP.RDB$PRIVILEGE as PRIVILEGE,
+ UP.RDB$GRANT_OPTION as IS_GRANTABLE,
+ trim(trailing from T.RDB$TYPE_NAME) as JB_GRANTEE_TYPE,
+ trim(trailing from UP.RDB$USER_SCHEMA_NAME) as JB_GRANTEE_SCHEMA
+ from SYSTEM.RDB$USER_PRIVILEGES UP
+ left join SYSTEM.RDB$TYPES T
+ on T.RDB$FIELD_NAME = 'RDB$OBJECT_TYPE' and T.RDB$TYPE = UP.RDB$USER_TYPE
+ where UP.RDB$PRIVILEGE in ('A', 'D', 'I', 'R', 'S', 'U') -- privileges relevant for tables
+ and UP.RDB$OBJECT_TYPE in (0, 1) -- Only tables and views""";
+
+ // NOTE: Sort by user schema and user is not defined in JDBC, but we do this to ensure a consistent order for tests
+ private static final String GET_TABLE_PRIVILEGES_END_6 =
+ "\norder by UP.RDB$RELATION_SCHEMA_NAME, UP.RDB$RELATION_NAME, UP.RDB$PRIVILEGE, "
+ + "UP.RDB$USER_SCHEMA_NAME nulls first, UP.RDB$USER";
+
+ private FB6(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetTablePrivileges createInstance(DbMetadataMediator mediator) {
+ return new FB6(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetTablePrivilegesQuery(String schemaPattern, String tableNamePattern) {
+ var clauses = List.of(
+ new Clause("UP.RDB$RELATION_SCHEMA_NAME", schemaPattern),
+ new Clause("UP.RDB$RELATION_NAME", tableNamePattern));
+ String sql = GET_TABLE_PRIVILEGES_START_6
+ + (Clause.anyCondition(clauses) ? "\nand " + Clause.conjunction(clauses) : "")
+ + GET_TABLE_PRIVILEGES_END_6;
+ return new MetadataQuery(sql, Clause.parameters(clauses));
+ }
+
}
+
}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnPrivilegesTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnPrivilegesTest.java
index 9fba1fa3b..33358b0d4 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnPrivilegesTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnPrivilegesTest.java
@@ -231,6 +231,7 @@ private void validateExpectedColumnPrivileges(String schema, String table, Strin
defaults.put(ColumnPrivilegesMetadata.TABLE_SCHEM, ifSchemaElse("PUBLIC", null));
defaults.put(ColumnPrivilegesMetadata.GRANTOR, SYSDBA);
defaults.put(ColumnPrivilegesMetadata.JB_GRANTEE_TYPE, "USER");
+ defaults.put(ColumnPrivilegesMetadata.JB_GRANTEE_SCHEMA, null);
DEFAULT_COLUMN_PRIVILEGES_VALUES = Collections.unmodifiableMap(defaults);
}
@@ -248,7 +249,8 @@ private enum ColumnPrivilegesMetadata implements MetaDataInfo {
GRANTEE(6),
PRIVILEGE(7),
IS_GRANTABLE(8),
- JB_GRANTEE_TYPE(9);
+ JB_GRANTEE_TYPE(9),
+ JB_GRANTEE_SCHEMA(10);
private final int position;
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataTablePrivilegesTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataTablePrivilegesTest.java
index 77c021aed..ebed9a646 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataTablePrivilegesTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataTablePrivilegesTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2023-2022 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2023-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -21,6 +21,7 @@
import static org.firebirdsql.common.FBTestProperties.getConnectionViaDriverManager;
import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
+import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
import static org.firebirdsql.common.matchers.MatcherAssume.assumeThat;
import static org.hamcrest.Matchers.equalToIgnoringCase;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -32,6 +33,8 @@
*/
class FBDatabaseMetaDataTablePrivilegesTest {
+ // TODO Add schema support: tests involving other schema
+
private static final String SYSDBA = "SYSDBA";
private static final String USER1 = "USER1";
private static final String user2 = getDefaultSupportInfo().supportsCaseSensitiveUserNames() ? "user2" : "USER2";
@@ -116,7 +119,13 @@ void testColumnPrivileges_tbl2_all() throws Exception {
private Map createRule(String tableName, String grantee, boolean grantable,
String privilege) {
+ return createRule(ifSchemaElse("PUBLIC", null), tableName, grantee, grantable, privilege);
+ }
+
+ private Map createRule(String schema, String tableName, String grantee,
+ boolean grantable, String privilege) {
Map rules = getDefaultValueValidationRules();
+ rules.put(TablePrivilegesMetadata.TABLE_SCHEM, schema);
rules.put(TablePrivilegesMetadata.TABLE_NAME, tableName);
rules.put(TablePrivilegesMetadata.GRANTEE, grantee);
rules.put(TablePrivilegesMetadata.PRIVILEGE, privilege);
@@ -126,7 +135,12 @@ private Map createRule(String tableName, String
private void validateExpectedColumnPrivileges(String tableNamePattern,
List> expectedTablePrivileges) throws SQLException {
- try (ResultSet tablePrivileges = dbmd.getTablePrivileges(null, null, tableNamePattern)) {
+ validateExpectedColumnPrivileges(null, tableNamePattern, expectedTablePrivileges);
+ }
+
+ private void validateExpectedColumnPrivileges(String schemaPattern, String tableNamePattern,
+ List> expectedTablePrivileges) throws SQLException {
+ try (ResultSet tablePrivileges = dbmd.getTablePrivileges(null, schemaPattern, tableNamePattern)) {
int privilegeCount = 0;
while (tablePrivileges.next()) {
if (privilegeCount < expectedTablePrivileges.size()) {
@@ -144,9 +158,10 @@ private void validateExpectedColumnPrivileges(String tableNamePattern,
static {
Map defaults = new EnumMap<>(TablePrivilegesMetadata.class);
defaults.put(TablePrivilegesMetadata.TABLE_CAT, null);
- defaults.put(TablePrivilegesMetadata.TABLE_SCHEM, null);
+ defaults.put(TablePrivilegesMetadata.TABLE_SCHEM, ifSchemaElse("PUBLIC", null));
defaults.put(TablePrivilegesMetadata.GRANTOR, SYSDBA);
defaults.put(TablePrivilegesMetadata.JB_GRANTEE_TYPE, "USER");
+ defaults.put(TablePrivilegesMetadata.JB_GRANTEE_SCHEMA, null);
DEFAULT_TABLE_PRIVILEGES_VALUES = Collections.unmodifiableMap(defaults);
}
@@ -163,7 +178,8 @@ private enum TablePrivilegesMetadata implements MetaDataInfo {
GRANTEE(5),
PRIVILEGE(6),
IS_GRANTABLE(7),
- JB_GRANTEE_TYPE(8);
+ JB_GRANTEE_TYPE(8),
+ JB_GRANTEE_SCHEMA(9);
private final int position;
From 73ebef59dde2832949f71f589a7fc5bdf409cf41 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Mon, 23 Jun 2025 14:14:16 +0200
Subject: [PATCH 19/64] #882 Schema support for getTables
---
.../firebirdsql/jdbc/FBDatabaseMetaData.java | 2 +-
.../firebirdsql/jdbc/metadata/GetTables.java | 210 ++++++++++++------
.../jdbc/FBDatabaseMetaDataTablesTest.java | 9 +-
3 files changed, 146 insertions(+), 75 deletions(-)
diff --git a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
index 8be2d4a60..9b8005469 100644
--- a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
@@ -1251,7 +1251,7 @@ public ResultSet getProcedureColumns(String catalog, String schemaPattern, Strin
@Override
public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types)
throws SQLException {
- return createGetTablesInstance().getTables(tableNamePattern, types);
+ return createGetTablesInstance().getTables(schemaPattern, tableNamePattern, types);
}
private GetTables createGetTablesInstance() {
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetTables.java b/src/main/org/firebirdsql/jdbc/metadata/GetTables.java
index 3c7cdfa98..e250be3ef 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetTables.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetTables.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -9,7 +9,6 @@
import org.firebirdsql.jdbc.DbMetadataMediator;
import org.firebirdsql.jdbc.DbMetadataMediator.MetadataQuery;
import org.firebirdsql.jdbc.FBResultSet;
-import org.firebirdsql.util.InternalApi;
import java.sql.ResultSet;
import java.sql.SQLException;
@@ -41,12 +40,19 @@
* @author Mark Rotteveel
* @since 5
*/
-@InternalApi
-public abstract class GetTables extends AbstractMetadataMethod {
+public abstract sealed class GetTables extends AbstractMetadataMethod {
+ private static final String LEGACY_IS_TABLE = "rdb$relation_type is null and rdb$view_blr is null";
+ private static final String LEGACY_IS_VIEW = "rdb$relation_type is null and rdb$view_blr is not null";
private static final String TABLES = "TABLES";
private static final String TABLE_TYPE = "TABLE_TYPE";
+ /**
+ * All table types supported for Firebird 2.5 and higher
+ */
+ private static final Set ALL_TYPES = unmodifiableSet(new LinkedHashSet<>(
+ Arrays.asList(GLOBAL_TEMPORARY, SYSTEM_TABLE, TABLE, VIEW)));
+
private static final RowDescriptor ROW_DESCRIPTOR = DbMetadataMediator.newRowDescriptorBuilder(12)
.at(0).simple(SQL_VARYING | 1, OBJECT_NAME_LENGTH, "TABLE_CAT", TABLES).addField()
.at(1).simple(SQL_VARYING | 1, OBJECT_NAME_LENGTH, "TABLE_SCHEM", TABLES).addField()
@@ -75,19 +81,19 @@ private GetTables(DbMetadataMediator mediator) {
/**
* @see java.sql.DatabaseMetaData#getTables(String, String, String, String[])
*/
- public final ResultSet getTables(String tableNamePattern, String[] types) throws SQLException {
+ public final ResultSet getTables(String schemaPattern, String tableNamePattern, String[] types) throws SQLException {
if ("".equals(tableNamePattern) || types != null && types.length == 0) {
// Matching table name not possible
return createEmpty();
}
-
- MetadataQuery metadataQuery = createGetTablesQuery(tableNamePattern, toTypesSet(types));
+ MetadataQuery metadataQuery = createGetTablesQuery(schemaPattern, tableNamePattern, toTypesSet(types));
return createMetaDataResultSet(metadataQuery);
}
@Override
final RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQLException {
return valueBuilder
+ .at(1).setString(rs.getString("TABLE_SCHEM"))
.at(2).setString(rs.getString("TABLE_NAME"))
.at(3).setString(rs.getString(TABLE_TYPE))
.at(4).setString(rs.getString("REMARKS"))
@@ -116,7 +122,7 @@ private Set toTypesSet(String[] types) {
return types != null ? new HashSet<>(Arrays.asList(types)) : allTableTypes();
}
- abstract MetadataQuery createGetTablesQuery(String tableNamePattern, Set types);
+ abstract MetadataQuery createGetTablesQuery(String schemaPattern, String tableNamePattern, Set types);
/**
* All supported table types.
@@ -126,7 +132,9 @@ private Set toTypesSet(String[] types) {
*
* @return supported table types
*/
- abstract Set allTableTypes();
+ Set allTableTypes() {
+ return ALL_TYPES;
+ }
/**
* The ODS of a Firebird 2.5 database.
@@ -135,13 +143,48 @@ private Set toTypesSet(String[] types) {
public static GetTables create(DbMetadataMediator mediator) {
// NOTE: Indirection through static method prevents unnecessary classloading
- if (mediator.getOdsVersion().compareTo(ODS_11_2) >= 0) {
+ if (mediator.getFirebirdSupportInfo().isVersionEqualOrAbove(6)) {
+ return FB6.createInstance(mediator);
+ } else if (mediator.getOdsVersion().compareTo(ODS_11_2) >= 0) {
return FB2_5.createInstance(mediator);
} else {
return FB2_1.createInstance(mediator);
}
}
+ void buildTypeCondition(StringBuilder sb, Set types) {
+ final int initialLength = sb.length();
+ if (types.contains(SYSTEM_TABLE) && types.contains(TABLE)) {
+ sb.append("(rdb$relation_type in (0, 2, 3) or " + LEGACY_IS_TABLE + ")");
+ } else if (types.contains(SYSTEM_TABLE)) {
+ // We assume that external tables are never system and that virtual tables are always system
+ sb.append("(rdb$relation_type in (0, 3) or " + LEGACY_IS_TABLE + ") and rdb$system_flag = 1");
+ } else if (types.contains(TABLE)) {
+ // We assume that external tables are never system and that virtual tables are always system
+ sb.append("(rdb$relation_type in (0, 2) or " + LEGACY_IS_TABLE + ") and rdb$system_flag = 0");
+ }
+
+ if (types.contains(VIEW)) {
+ if (sb.length() != initialLength) {
+ sb.append(" or ");
+ }
+ // We assume (but don't check) that views are never system
+ sb.append("(rdb$relation_type = 1 or " + LEGACY_IS_VIEW + ")");
+ }
+
+ if (types.contains(GLOBAL_TEMPORARY)) {
+ if (sb.length() != initialLength) {
+ sb.append(" or ");
+ }
+ sb.append("rdb$relation_type in (4, 5)");
+ }
+
+ if (sb.length() == initialLength) {
+ // Requested types are unknown, query nothing
+ sb.append("1 = 0");
+ }
+ }
+
@SuppressWarnings("java:S101")
private static final class FB2_1 extends GetTables {
@@ -150,7 +193,7 @@ private static final class FB2_1 extends GetTables {
private static final String TABLE_COLUMNS_NORMAL_2_1 =
formatTableQuery(TABLE, "RDB$SYSTEM_FLAG = 0 and rdb$view_blr is null");
private static final String TABLE_COLUMNS_VIEW_2_1 = formatTableQuery(VIEW, "rdb$view_blr is not null");
- private static final String GET_TABLE_ORDER_BY_2_1 = "\norder by 2, 1";
+ private static final String GET_TABLE_ORDER_BY_2_1 = "\norder by 3, 2";
private static final Map QUERY_PER_TYPE;
static {
@@ -176,7 +219,7 @@ private static GetTables createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createGetTablesQuery(String tableNamePattern, Set types) {
+ MetadataQuery createGetTablesQuery(String schemaPattern, String tableNamePattern, Set types) {
var tableNameClause = new Clause("RDB$RELATION_NAME", tableNamePattern);
var clauses = new ArrayList(types.size());
var queryBuilder = new StringBuilder(2000);
@@ -203,6 +246,7 @@ Set allTableTypes() {
private static String formatTableQuery(String tableType, String condition) {
return String.format("""
select
+ cast(null as char(1)) as TABLE_SCHEM,
RDB$RELATION_NAME as TABLE_NAME,
cast('%s' as varchar(31)) as TABLE_TYPE,
RDB$DESCRIPTION as REMARKS,
@@ -217,34 +261,27 @@ private static String formatTableQuery(String tableType, String condition) {
@SuppressWarnings("java:S101")
private static final class FB2_5 extends GetTables {
- private static final String GET_TABLE_ORDER_BY_2_5 = "\norder by 2, 1";
+ private static final String GET_TABLE_ORDER_BY_2_5 = "\norder by 3, 2";
//@formatter:off
- private static final String LEGACY_IS_TABLE = "rdb$relation_type is null and rdb$view_blr is null";
- private static final String LEGACY_IS_VIEW = "rdb$relation_type is null and rdb$view_blr is not null";
-
- private static final String TABLE_COLUMNS_2_5 =
- "select\n"
- + " trim(trailing from RDB$RELATION_NAME) as TABLE_NAME,\n"
- + " trim(trailing from case"
- + " when rdb$relation_type = 0 or " + LEGACY_IS_TABLE + " then case when RDB$SYSTEM_FLAG = 1 then '" + SYSTEM_TABLE + "' else '" + TABLE + "' end\n"
- + " when rdb$relation_type = 1 or " + LEGACY_IS_VIEW + " then '" + VIEW + "'\n"
- + " when rdb$relation_type = 2 then '" + TABLE + "'\n" // external table; assume as normal table
- + " when rdb$relation_type = 3 then '" + SYSTEM_TABLE + "'\n" // virtual (monitoring) table: assume system
- + " when rdb$relation_type in (4, 5) then '" + GLOBAL_TEMPORARY + "'\n"
- + " end) as TABLE_TYPE,\n"
- + " RDB$DESCRIPTION as REMARKS,\n"
- + " trim(trailing from RDB$OWNER_NAME) as OWNER_NAME,\n"
- + " RDB$RELATION_ID as JB_RELATION_ID\n"
- + "from RDB$RELATIONS";
+ private static final String TABLE_COLUMNS_2_5 = """
+ select
+ null as TABLE_SCHEM,
+ trim(trailing from RDB$RELATION_NAME) as TABLE_NAME,
+ trim(trailing from case
+ """ +
+ " when rdb$relation_type = 0 or " + LEGACY_IS_TABLE + " then case when RDB$SYSTEM_FLAG = 1 then '" + SYSTEM_TABLE + "' else '" + TABLE + "' end\n" +
+ " when rdb$relation_type = 1 or " + LEGACY_IS_VIEW + " then '" + VIEW + "'\n" +
+ " when rdb$relation_type = 2 then '" + TABLE + "' -- external table; assume as normal table\n" +
+ " when rdb$relation_type = 3 then '" + SYSTEM_TABLE + "' -- virtual (monitoring) table: assume system\n" +
+ " when rdb$relation_type in (4, 5) then '" + GLOBAL_TEMPORARY + "'\n" + """
+ end) as TABLE_TYPE,
+ RDB$DESCRIPTION as REMARKS,
+ trim(trailing from RDB$OWNER_NAME) as OWNER_NAME,
+ RDB$RELATION_ID as JB_RELATION_ID
+ from RDB$RELATIONS""";
//@formatter:on
- /**
- * All table types supported for Firebird 2.5 and higher
- */
- private static final Set ALL_TYPES_2_5 = unmodifiableSet(new LinkedHashSet<>(
- Arrays.asList(GLOBAL_TEMPORARY, SYSTEM_TABLE, TABLE, VIEW)));
-
private FB2_5(DbMetadataMediator mediator) {
super(mediator);
}
@@ -254,7 +291,7 @@ private static GetTables createInstance(DbMetadataMediator mediator) {
}
@Override
- MetadataQuery createGetTablesQuery(String tableNamePattern, Set types) {
+ MetadataQuery createGetTablesQuery(String schemaPattern, String tableNamePattern, Set types) {
var tableNameClause = new Clause("RDB$RELATION_NAME", tableNamePattern);
var queryBuilder = new StringBuilder(1000).append(TABLE_COLUMNS_2_5);
@@ -266,13 +303,15 @@ MetadataQuery createGetTablesQuery(String tableNamePattern, Set types) {
params = Collections.emptyList();
}
- if (!types.containsAll(ALL_TYPES_2_5)) {
+ if (!types.containsAll(ALL_TYPES)) {
// Only construct conditions when we don't query for all
- StringBuilder typeCondition = buildTypeCondition(types);
if (tableNameClause.hasCondition()) {
- queryBuilder.append("\nand (").append(typeCondition).append(")");
+ queryBuilder.append("\nand (");
+ buildTypeCondition(queryBuilder, types);
+ queryBuilder.append(")");
} else {
- queryBuilder.append("\nwhere ").append(typeCondition);
+ queryBuilder.append("\nwhere ");
+ buildTypeCondition(queryBuilder, types);
}
}
queryBuilder.append(GET_TABLE_ORDER_BY_2_5);
@@ -280,43 +319,70 @@ MetadataQuery createGetTablesQuery(String tableNamePattern, Set types) {
return new MetadataQuery(queryBuilder.toString(), params);
}
- private static StringBuilder buildTypeCondition(Set types) {
- var typeCondition = new StringBuilder(120);
- if (types.contains(SYSTEM_TABLE) && types.contains(TABLE)) {
- typeCondition.append("(rdb$relation_type in (0, 2, 3) or " + LEGACY_IS_TABLE + ")");
- } else if (types.contains(SYSTEM_TABLE)) {
- // We assume that external tables are never system and that virtual tables are always system
- typeCondition.append("(rdb$relation_type in (0, 3) or " + LEGACY_IS_TABLE + ") and rdb$system_flag = 1");
- } else if (types.contains(TABLE)) {
- // We assume that external tables are never system and that virtual tables are always system
- typeCondition.append("(rdb$relation_type in (0, 2) or " + LEGACY_IS_TABLE + ") and rdb$system_flag = 0");
- }
+ }
- if (types.contains(VIEW)) {
- if (!typeCondition.isEmpty()) {
- typeCondition.append(" or ");
- }
- // We assume (but don't check) that views are never system
- typeCondition.append("(rdb$relation_type = 1 or " + LEGACY_IS_VIEW + ")");
+ private static final class FB6 extends GetTables {
+
+ private static final String GET_TABLE_ORDER_BY_6 = "\norder by 3, 1, 2";
+
+ //@formatter:off
+ private static final String TABLE_COLUMNS_6 = """
+ select
+ trim(trailing from RDB$SCHEMA_NAME) as TABLE_SCHEM,
+ trim(trailing from RDB$RELATION_NAME) as TABLE_NAME,
+ trim(trailing from case
+ """ +
+ " when rdb$relation_type = 0 or " + LEGACY_IS_TABLE + " then case when RDB$SYSTEM_FLAG = 1 then '" + SYSTEM_TABLE + "' else '" + TABLE + "' end\n" +
+ " when rdb$relation_type = 1 or " + LEGACY_IS_VIEW + " then '" + VIEW + "'\n" +
+ " when rdb$relation_type = 2 then '" + TABLE + "' -- external table; assume as normal table\n" +
+ " when rdb$relation_type = 3 then '" + SYSTEM_TABLE + "' -- virtual (monitoring) table: assume system\n" +
+ " when rdb$relation_type in (4, 5) then '" + GLOBAL_TEMPORARY + "'\n" + """
+ end) as TABLE_TYPE,
+ RDB$DESCRIPTION as REMARKS,
+ trim(trailing from RDB$OWNER_NAME) as OWNER_NAME,
+ RDB$RELATION_ID as JB_RELATION_ID
+ from SYSTEM.RDB$RELATIONS""";
+ //@formatter:on
+
+ private FB6(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetTables createInstance(DbMetadataMediator mediator) {
+ return new FB6(mediator);
+ }
+
+ @Override
+ MetadataQuery createGetTablesQuery(String schemaPattern, String tableNamePattern, Set types) {
+ var clauses = List.of(
+ new Clause("RDB$SCHEMA_NAME", schemaPattern),
+ new Clause("RDB$RELATION_NAME", tableNamePattern));
+
+ var queryBuilder = new StringBuilder(1000).append(TABLE_COLUMNS_6);
+ List params;
+ if (Clause.anyCondition(clauses)) {
+ queryBuilder.append("\nwhere ").append(Clause.conjunction(clauses));
+ params = Clause.parameters(clauses);
+ } else {
+ params = Collections.emptyList();
}
- if (types.contains(GLOBAL_TEMPORARY)) {
- if (!typeCondition.isEmpty()) {
- typeCondition.append(" or ");
+ if (!types.containsAll(ALL_TYPES)) {
+ // Only construct conditions when we don't query for all
+ if (Clause.anyCondition(clauses)) {
+ queryBuilder.append("\nand (");
+ buildTypeCondition(queryBuilder, types);
+ queryBuilder.append(")");
+ } else {
+ queryBuilder.append("\nwhere ");
+ buildTypeCondition(queryBuilder, types);
}
- typeCondition.append("rdb$relation_type in (4, 5)");
}
+ queryBuilder.append(GET_TABLE_ORDER_BY_6);
- if (typeCondition.isEmpty()) {
- // Requested types are unknown, query nothing
- typeCondition.append("1 = 0");
- }
- return typeCondition;
+ return new MetadataQuery(queryBuilder.toString(), params);
}
- @Override
- Set allTableTypes() {
- return ALL_TYPES_2_5;
- }
}
+
}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataTablesTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataTablesTest.java
index dd0984f46..f4bf1dfcb 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataTablesTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataTablesTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2012-2023 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2012-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -17,6 +17,7 @@
import static java.lang.String.format;
import static org.firebirdsql.common.FBTestProperties.getConnectionViaDriverManager;
import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
+import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
@@ -35,6 +36,8 @@
*/
class FBDatabaseMetaDataTablesTest {
+ // TODO Add schema support: tests involving other schema
+
// Valid values for TABLE_TYPE (separate from those defined in FBDatabaseMetaData for testing)
private static final String VIEW = "VIEW";
private static final String TABLE = "TABLE";
@@ -233,6 +236,7 @@ private void validateTableMetaData_allSystemTables(String tableNamePattern) thro
Set expectedTables = new HashSet<>(Arrays.asList("RDB$FIELDS", "RDB$GENERATORS",
"RDB$ROLES", "RDB$DATABASE", "RDB$TRIGGERS"));
Map rules = getDefaultValueValidationRules();
+ rules.put(TableMetaData.TABLE_SCHEM, ifSchemaElse("SYSTEM", null));
rules.put(TableMetaData.TABLE_TYPE, SYSTEM_TABLE);
try (ResultSet tables = dbmd.getTables(null, null, tableNamePattern, new String[] { SYSTEM_TABLE })) {
while (tables.next()) {
@@ -550,6 +554,7 @@ private void validateTableMetaDataNoRow(String tableNamePattern, String[] types)
private void updateTableRules(String tableName, Map rules) {
rules.put(TableMetaData.TABLE_NAME, tableName);
if (tableName.startsWith("RDB$") || tableName.startsWith("MON$") || tableName.startsWith("SEC$")) {
+ rules.put(TableMetaData.TABLE_SCHEM, ifSchemaElse("SYSTEM", null));
rules.put(TableMetaData.TABLE_TYPE, SYSTEM_TABLE);
} else if (tableName.equals("TEST_NORMAL_TABLE") || tableName.equals("test_quoted_normal_table")
|| tableName.equals("testquotedwith\\table")) {
@@ -570,7 +575,7 @@ private void updateTableRules(String tableName, Map rules
static {
Map defaults = new EnumMap<>(TableMetaData.class);
defaults.put(TableMetaData.TABLE_CAT, null);
- defaults.put(TableMetaData.TABLE_SCHEM, null);
+ defaults.put(TableMetaData.TABLE_SCHEM, ifSchemaElse("PUBLIC", null));
defaults.put(TableMetaData.REMARKS, null);
defaults.put(TableMetaData.TYPE_CAT, null);
defaults.put(TableMetaData.TYPE_SCHEM, null);
From 90ce855d072ba5aeecb7073615d5890867c7c80c Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Mon, 23 Jun 2025 14:24:38 +0200
Subject: [PATCH 20/64] #882 Schema support for getCatalogs
---
devdoc/jdp/jdp-2025-06-schema-support.adoc | 1 +
src/docs/asciidoc/release_notes.adoc | 9 ++--
.../firebirdsql/jdbc/FBDatabaseMetaData.java | 3 +-
.../jdbc/metadata/GetCatalogs.java | 46 ++++++++++++++-----
4 files changed, 44 insertions(+), 15 deletions(-)
diff --git a/devdoc/jdp/jdp-2025-06-schema-support.adoc b/devdoc/jdp/jdp-2025-06-schema-support.adoc
index 467553bb6..960ac3bda 100644
--- a/devdoc/jdp/jdp-2025-06-schema-support.adoc
+++ b/devdoc/jdp/jdp-2025-06-schema-support.adoc
@@ -94,6 +94,7 @@ Jaybird cannot honour this requirement for plain `Statement`, as schema resoluti
*** Maybe make it search `PUBLIC`, or `PUBLIC` and `SYSTEM`, or those on the search path?
*** Or add a compatibility connection property to make it behave as `null` ("`__[...] means that the schema name should not be used to narrow the search__`")?
*** Or just accept it as a breaking change?
+** `getCatalogs`: TODO: Maybe add a custom column with a list of schema names for `useCatalogAsPackage=true`?
* TODO: Define effects for management API
Note to self: use `// TODO Add schema support` in places that you identify need to get/improve schema support, while working on schema support elsewhere
diff --git a/src/docs/asciidoc/release_notes.adoc b/src/docs/asciidoc/release_notes.adoc
index a633fc778..3a7fc7dc9 100644
--- a/src/docs/asciidoc/release_notes.adoc
+++ b/src/docs/asciidoc/release_notes.adoc
@@ -518,13 +518,16 @@ Changes include:
* `DatabaseMetaData`
** Methods accepting a `schema` (exact match if not `null`) or `schemaPattern` (`LIKE` match if not `null`) will return no rows for value empty (`""`) on Firebird 6.0 and higher;
use `null` or -- `schemaPattern` only -- `"%"` to match all schemas
+** `getCatalogs` -- when `useCatalogAsPackage=true` -- returns all (distinct) package names over all schemas.
+Within the limitations and specification of the JDBC API, this method cannot be used to find out which schema(s) contain a specific package name.
+// TODO Maybe add a custom column with a list of schema names?
+** `getColumnPrivileges` and `getTablePrivileges` received an additional column, `JB_GRANTEE_SCHEMA`, which is non-``null`` for grantees that are schema-bound (e.g. a procedure).
++
+As this is a non-standard column, we recommend to always retrieve it by name.
** `getSchemas()` returns all defined schemas
** `getSchemas(String catalog, String schemaPattern)` returns all schemas matching the `LIKE` pattern `schemaPattern`, with the following caveats
*** `catalog` non-empty will return no rows;
we recommend to always use `null` for `catalog`
-** `getColumnPrivileges` and `getTablePrivileges` received an additional column, `JB_GRANTEE_SCHEMA`, which is non-``null`` for grantees that are schema-bound (e.g. a procedure).
-+
-As this is a non-standard column, we recommend to always retrieve it by name.
// TODO add major changes
diff --git a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
index 9b8005469..57b203947 100644
--- a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
@@ -1272,7 +1272,8 @@ public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLExce
* {@inheritDoc}
*
* When {@code useCatalogAsPackage = true} and packages are supported, this method will return the package names in
- * column {@code TABLE_CAT}.
+ * column {@code TABLE_CAT}. In Firebird 6.0, packages are schema-bound, so finding procedures or functions in a
+ * package may require you to search in a specific schema, or in all schemas.
*
>
*/
@Override
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetCatalogs.java b/src/main/org/firebirdsql/jdbc/metadata/GetCatalogs.java
index c8473819a..314f05d84 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetCatalogs.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetCatalogs.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2023 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2022-2023 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -24,6 +24,8 @@
*/
public sealed class GetCatalogs extends AbstractMetadataMethod {
+ // TODO Add schema support: maybe add a custom column with a list of schema names for useCatalogsAsPackage?
+
private static final RowDescriptor ROW_DESCRIPTOR = DbMetadataMediator.newRowDescriptorBuilder(1)
.at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_CAT", "TABLECATALOGS").addField()
.toRowDescriptor();
@@ -38,7 +40,11 @@ public ResultSet getCatalogs() throws SQLException {
public static GetCatalogs create(DbMetadataMediator mediator) {
if (mediator.isUseCatalogAsPackage()) {
- return CatalogAsPackage.createInstance(mediator);
+ if (mediator.getFirebirdSupportInfo().isVersionEqualOrAbove(6)) {
+ return FB6CatalogAsPackage.createInstance(mediator);
+ } else {
+ return FB3CatalogAsPackage.createInstance(mediator);
+ }
} else {
return new GetCatalogs(mediator);
}
@@ -46,17 +52,19 @@ public static GetCatalogs create(DbMetadataMediator mediator) {
@Override
RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQLException {
- throw new AssertionError("should not get called");
+ return valueBuilder
+ .at(0).setString(rs.getString("PACKAGE_NAME"))
+ .toRowValue(false);
}
- private static final class CatalogAsPackage extends GetCatalogs {
+ private static final class FB3CatalogAsPackage extends GetCatalogs {
- private CatalogAsPackage(DbMetadataMediator mediator) {
+ private FB3CatalogAsPackage(DbMetadataMediator mediator) {
super(mediator);
}
private static GetCatalogs createInstance(DbMetadataMediator mediator) {
- return new CatalogAsPackage(mediator);
+ return new FB3CatalogAsPackage(mediator);
}
@Override
@@ -68,11 +76,27 @@ select trim(trailing from RDB$PACKAGE_NAME) as PACKAGE_NAME
return createMetaDataResultSet(metadataQuery);
}
+ }
+
+ private static final class FB6CatalogAsPackage extends GetCatalogs {
+
+ private FB6CatalogAsPackage(DbMetadataMediator mediator) {
+ super(mediator);
+ }
+
+ private static GetCatalogs createInstance(DbMetadataMediator mediator) {
+ return new FB6CatalogAsPackage(mediator);
+ }
+
@Override
- RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQLException {
- return valueBuilder
- .at(0).setString(rs.getString("PACKAGE_NAME"))
- .toRowValue(false);
+ public ResultSet getCatalogs() throws SQLException {
+ var metadataQuery = new MetadataQuery("""
+ select distinct trim(trailing from RDB$PACKAGE_NAME) as PACKAGE_NAME
+ from SYSTEM.RDB$PACKAGES
+ order by 1""", List.of());
+ return createMetaDataResultSet(metadataQuery);
}
+
}
+
}
From 11349a7c7d830916f8f48d85b3e63997d897eb8f Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Mon, 23 Jun 2025 14:41:44 +0200
Subject: [PATCH 21/64] Mark metadata classes sealed or final
---
.../firebirdsql/jdbc/metadata/GetBestRowIdentifier.java | 2 +-
.../firebirdsql/jdbc/metadata/GetColumnPrivileges.java | 2 +-
src/main/org/firebirdsql/jdbc/metadata/GetColumns.java | 8 ++++----
.../org/firebirdsql/jdbc/metadata/GetCrossReference.java | 2 +-
.../org/firebirdsql/jdbc/metadata/GetExportedKeys.java | 2 +-
.../org/firebirdsql/jdbc/metadata/GetFunctionColumns.java | 2 +-
src/main/org/firebirdsql/jdbc/metadata/GetFunctions.java | 2 +-
.../org/firebirdsql/jdbc/metadata/GetImportedKeys.java | 2 +-
.../org/firebirdsql/jdbc/metadata/GetPrimaryKeys.java | 2 +-
.../firebirdsql/jdbc/metadata/GetProcedureColumns.java | 8 ++++----
src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java | 2 +-
.../org/firebirdsql/jdbc/metadata/GetPseudoColumns.java | 2 +-
src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java | 2 +-
.../org/firebirdsql/jdbc/metadata/GetVersionColumns.java | 6 +++---
14 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java b/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java
index eed40154f..a2cf72228 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java
@@ -42,7 +42,7 @@
* @author Mark Rotteveel
* @since 5
*/
-public abstract class GetBestRowIdentifier extends AbstractMetadataMethod {
+public abstract sealed class GetBestRowIdentifier extends AbstractMetadataMethod {
private static final String ROWIDENTIFIER = "ROWIDENTIFIER";
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java b/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java
index ad6d20843..01858fc43 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java
@@ -30,7 +30,7 @@
* @author Mark Rotteveel
* @since 5
*/
-public abstract class GetColumnPrivileges extends AbstractMetadataMethod {
+public abstract sealed class GetColumnPrivileges extends AbstractMetadataMethod {
private static final String COLUMNPRIV = "COLUMNPRIV";
private static final RowDescriptor ROW_DESCRIPTOR = DbMetadataMediator.newRowDescriptorBuilder(10)
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetColumns.java b/src/main/org/firebirdsql/jdbc/metadata/GetColumns.java
index 4cb1c46f4..6afb758cc 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetColumns.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetColumns.java
@@ -36,7 +36,7 @@
* @author Mark Rotteveel
* @since 5
*/
-public abstract class GetColumns extends AbstractMetadataMethod {
+public abstract sealed class GetColumns extends AbstractMetadataMethod {
private static final String COLUMNINFO = "COLUMNINFO";
@@ -156,7 +156,7 @@ public static GetColumns create(DbMetadataMediator mediator) {
}
@SuppressWarnings("java:S101")
- private static class FB2_5 extends GetColumns {
+ private static final class FB2_5 extends GetColumns {
private static final String GET_COLUMNS_FRAGMENT_2_5 = """
select
@@ -203,7 +203,7 @@ MetadataQuery createGetColumnsQuery(String schemaPattern, String tableNamePatter
}
- private static class FB3 extends GetColumns {
+ private static final class FB3 extends GetColumns {
private static final String GET_COLUMNS_FRAGMENT_3 = """
select
@@ -250,7 +250,7 @@ MetadataQuery createGetColumnsQuery(String schemaPattern, String tableNamePatter
}
- private static class FB6 extends GetColumns {
+ private static final class FB6 extends GetColumns {
private static final String GET_COLUMNS_FRAGMENT_6 = """
select
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetCrossReference.java b/src/main/org/firebirdsql/jdbc/metadata/GetCrossReference.java
index 47369e451..36f9f0500 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetCrossReference.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetCrossReference.java
@@ -18,7 +18,7 @@
* @author Mark Rotteveel
* @since 5
*/
-public abstract class GetCrossReference extends AbstractKeysMethod {
+public abstract sealed class GetCrossReference extends AbstractKeysMethod {
private GetCrossReference(DbMetadataMediator mediator) {
super(mediator);
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetExportedKeys.java b/src/main/org/firebirdsql/jdbc/metadata/GetExportedKeys.java
index 0434e8853..2459ea103 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetExportedKeys.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetExportedKeys.java
@@ -18,7 +18,7 @@
* @author Mark Rotteveel
* @since 5
*/
-public abstract class GetExportedKeys extends AbstractKeysMethod {
+public abstract sealed class GetExportedKeys extends AbstractKeysMethod {
private GetExportedKeys(DbMetadataMediator mediator) {
super(mediator);
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetFunctionColumns.java b/src/main/org/firebirdsql/jdbc/metadata/GetFunctionColumns.java
index c7c464109..88ec1425e 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetFunctionColumns.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetFunctionColumns.java
@@ -41,7 +41,7 @@
*/
@InternalApi
@SuppressWarnings({ "java:S1192", "java:S5665" })
-public abstract class GetFunctionColumns extends AbstractMetadataMethod {
+public abstract sealed class GetFunctionColumns extends AbstractMetadataMethod {
private static final String FUNCTION_COLUMNS = "FUNCTION_COLUMNS";
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetFunctions.java b/src/main/org/firebirdsql/jdbc/metadata/GetFunctions.java
index 9f6c0dfba..0d14ef598 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetFunctions.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetFunctions.java
@@ -28,7 +28,7 @@
* @since 4.0
*/
@InternalApi
-public abstract class GetFunctions extends AbstractMetadataMethod {
+public abstract sealed class GetFunctions extends AbstractMetadataMethod {
private static final String FUNCTIONS = "FUNCTIONS";
private static final String COLUMN_CATALOG_NAME = "RDB$PACKAGE_NAME";
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetImportedKeys.java b/src/main/org/firebirdsql/jdbc/metadata/GetImportedKeys.java
index 8c264b6fb..ad0fc6bad 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetImportedKeys.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetImportedKeys.java
@@ -18,7 +18,7 @@
* @author Mark Rotteveel
* @since 5
*/
-public abstract class GetImportedKeys extends AbstractKeysMethod {
+public abstract sealed class GetImportedKeys extends AbstractKeysMethod {
private GetImportedKeys(DbMetadataMediator mediator) {
super(mediator);
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetPrimaryKeys.java b/src/main/org/firebirdsql/jdbc/metadata/GetPrimaryKeys.java
index 4908edb89..9ef6a16da 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetPrimaryKeys.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetPrimaryKeys.java
@@ -23,7 +23,7 @@
* @author Mark Rotteveel
* @since 5
*/
-public abstract class GetPrimaryKeys extends AbstractMetadataMethod {
+public abstract sealed class GetPrimaryKeys extends AbstractMetadataMethod {
private static final String COLUMNINFO = "COLUMNINFO";
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetProcedureColumns.java b/src/main/org/firebirdsql/jdbc/metadata/GetProcedureColumns.java
index 6d1fb9ca5..3567fbf4e 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetProcedureColumns.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetProcedureColumns.java
@@ -39,7 +39,7 @@
* @since 5
*/
@SuppressWarnings("java:S1192")
-public abstract class GetProcedureColumns extends AbstractMetadataMethod {
+public abstract sealed class GetProcedureColumns extends AbstractMetadataMethod {
private static final String COLUMNINFO = "COLUMNINFO";
private static final String COLUMN_SCHEMA_NAME = "PP.RDB$SCHEMA_NAME";
@@ -153,7 +153,7 @@ public static GetProcedureColumns create(DbMetadataMediator mediator) {
}
@SuppressWarnings("java:S101")
- private static class FB2_5 extends GetProcedureColumns {
+ private static final class FB2_5 extends GetProcedureColumns {
//@formatter:off
private static final String GET_PROCEDURE_COLUMNS_FRAGMENT_2_5 = """
@@ -203,7 +203,7 @@ MetadataQuery createGetProcedureColumnsQuery(String catalog, String schemaPatter
}
- private static class FB3 extends GetProcedureColumns {
+ private static final class FB3 extends GetProcedureColumns {
//@formatter:off
private static final String GET_PROCEDURE_COLUMNS_FRAGMENT_3 = """
@@ -318,7 +318,7 @@ MetadataQuery createGetProcedureColumnsQuery(String catalog, String schemaPatter
}
- private static class FB6 extends GetProcedureColumns {
+ private static final class FB6 extends GetProcedureColumns {
//@formatter:off
private static final String GET_PROCEDURE_COLUMNS_FRAGMENT_6 = """
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java b/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java
index 2d277313e..65a089d1a 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetProcedures.java
@@ -29,7 +29,7 @@
* @since 5
*/
@InternalApi
-public abstract class GetProcedures extends AbstractMetadataMethod {
+public abstract sealed class GetProcedures extends AbstractMetadataMethod {
private static final String PROCEDURES = "PROCEDURES";
private static final String COLUMN_PROCEDURE_NAME = "RDB$PROCEDURE_NAME";
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetPseudoColumns.java b/src/main/org/firebirdsql/jdbc/metadata/GetPseudoColumns.java
index 28ec287f9..af25c3620 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetPseudoColumns.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetPseudoColumns.java
@@ -29,7 +29,7 @@
* @author Mark Rotteveel
* @since 5
*/
-public abstract class GetPseudoColumns {
+public abstract sealed class GetPseudoColumns {
private static final String PSEUDOCOLUMNS = "PSEUDOCOLUMNS";
public static final String COLUMN_SCHEMA_NAME = "RDB$SCHEMA_NAME";
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java b/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java
index 514a924d0..fd9d152a3 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java
@@ -21,7 +21,7 @@
* @author Mark Rotteveel
* @since 5
*/
-public abstract class GetSchemas extends AbstractMetadataMethod {
+public abstract sealed class GetSchemas extends AbstractMetadataMethod {
private static final RowDescriptor ROW_DESCRIPTOR = DbMetadataMediator.newRowDescriptorBuilder(2)
.at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_SCHEM", "TABLESCHEMAS").addField()
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetVersionColumns.java b/src/main/org/firebirdsql/jdbc/metadata/GetVersionColumns.java
index 013fec716..8c1759e4c 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetVersionColumns.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetVersionColumns.java
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
-// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
+// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc.metadata;
@@ -25,7 +25,7 @@
/**
* @author Mark Rotteveel
*/
-public class GetVersionColumns {
+public final class GetVersionColumns {
private static final String VERSIONCOL = "VERSIONCOL";
From ea0a2738196f4d08542129a2d87b25695266c92e Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Mon, 23 Jun 2025 17:03:37 +0200
Subject: [PATCH 22/64] #882 Schema support for getXXXSourceCode
---
src/docs/asciidoc/release_notes.adoc | 3 +
.../firebirdsql/jdbc/FBDatabaseMetaData.java | 78 ++++++++++++------
.../jdbc/FirebirdDatabaseMetaData.java | 81 ++++++++++++++++---
.../org/firebirdsql/jdbc/metadata/Clause.java | 2 +-
4 files changed, 126 insertions(+), 38 deletions(-)
diff --git a/src/docs/asciidoc/release_notes.adoc b/src/docs/asciidoc/release_notes.adoc
index 3a7fc7dc9..cff02351d 100644
--- a/src/docs/asciidoc/release_notes.adoc
+++ b/src/docs/asciidoc/release_notes.adoc
@@ -524,6 +524,9 @@ Within the limitations and specification of the JDBC API, this method cannot be
** `getColumnPrivileges` and `getTablePrivileges` received an additional column, `JB_GRANTEE_SCHEMA`, which is non-``null`` for grantees that are schema-bound (e.g. a procedure).
+
As this is a non-standard column, we recommend to always retrieve it by name.
+** `getProcedureSourceCode`/`getTriggerSourceCode`/`getViewSourceCode` now also have an overload accepting the schema;
+the overloads without a `schema` parameter, or `schema` is `null` will return the source code of the first match found.
+The `schema` parameter is ignored on Firebird 5.0 and older.
** `getSchemas()` returns all defined schemas
** `getSchemas(String catalog, String schemaPattern)` returns all schemas matching the `LIKE` pattern `schemaPattern`, with the following caveats
*** `catalog` non-empty will return no rows;
diff --git a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
index 57b203947..c294702cd 100644
--- a/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
@@ -1834,42 +1834,72 @@ public boolean generatedKeyAlwaysReturned() throws SQLException {
@Override
public String getProcedureSourceCode(String procedureName) throws SQLException {
- String sResult = null;
- String sql = "Select RDB$PROCEDURE_SOURCE From RDB$PROCEDURES Where "
- + "RDB$PROCEDURE_NAME = ?";
- List params = new ArrayList<>();
- params.add(procedureName);
- try (ResultSet rs = doQuery(sql, params)) {
- if (rs.next()) sResult = rs.getString(1);
- }
+ return getProcedureSourceCode(null, procedureName);
+ }
- return sResult;
+ @Override
+ public String getProcedureSourceCode(String schema, String procedureName) throws SQLException {
+ return getSourceCode(schema, procedureName, SourceObjectType.PROCEDURE);
}
@Override
public String getTriggerSourceCode(String triggerName) throws SQLException {
- String sResult = null;
- String sql = "Select RDB$TRIGGER_SOURCE From RDB$TRIGGERS Where RDB$TRIGGER_NAME = ?";
- List params = new ArrayList<>();
- params.add(triggerName);
- try (ResultSet rs = doQuery(sql, params)) {
- if (rs.next()) sResult = rs.getString(1);
- }
+ return getTriggerSourceCode(null, triggerName);
+ }
- return sResult;
+ @Override
+ public String getTriggerSourceCode(String schema, String triggerName) throws SQLException {
+ return getSourceCode(schema, triggerName, SourceObjectType.TRIGGER);
}
@Override
public String getViewSourceCode(String viewName) throws SQLException {
- String sResult = null;
- String sql = "Select RDB$VIEW_SOURCE From RDB$RELATIONS Where RDB$RELATION_NAME = ?";
- List params = new ArrayList<>();
- params.add(viewName);
- try (ResultSet rs = doQuery(sql, params)) {
- if (rs.next()) sResult = rs.getString(1);
+ return getViewSourceCode(null, viewName);
+ }
+
+ @Override
+ public String getViewSourceCode(String schema, String viewName) throws SQLException {
+ return getSourceCode(schema, viewName, SourceObjectType.VIEW);
+ }
+
+ private enum SourceObjectType {
+ PROCEDURE("RDB$PROCEDURES", "RDB$PROCEDURE_SOURCE", "RDB$PROCEDURE_NAME"),
+ TRIGGER("RDB$TRIGGERS", "RDB$TRIGGER_SOUCE", "RDB$TRIGGER_NAME"),
+ VIEW("RDB$RELATIONS", "RDB$VIEW_SOURCE", "RDB$RELATION_NAME"),
+ ;
+
+ private final String tableName;
+ private final String objectSourceColumn;
+ private final String objectNameColumn;
+
+ SourceObjectType(String tableName, String objectSourceColumn, String objectNameColumn) {
+ this.tableName = tableName;
+ this.objectSourceColumn = objectSourceColumn;
+ this.objectNameColumn = objectNameColumn;
+ }
+
+ List toClauses(boolean supportsSchemas, String schema, String objectName) {
+ var objectNameClause = Clause.equalsClause(objectNameColumn, objectName);
+ return schema != null && supportsSchemas
+ ? List.of(Clause.equalsClause("RDB$SCHEMA_NAME", schema), objectNameClause)
+ : List.of(objectNameClause);
}
- return sResult;
+ String toQuery(boolean supportsSchemas, List clauses) {
+ return "select " + objectSourceColumn + " from " + (supportsSchemas ? "SYSTEM." : "") + tableName
+ + " where " + Clause.conjunction(clauses);
+ }
+
+ }
+
+ private String getSourceCode(String schema, String objectName, SourceObjectType objectType) throws SQLException {
+ final boolean supportsSchemas = firebirdSupportInfo.supportsSchemas();
+ var clauses = objectType.toClauses(supportsSchemas, schema, objectName);
+ String sql = objectType.toQuery(supportsSchemas, clauses);
+ try (ResultSet rs = doQuery(sql, Clause.parameters(clauses))) {
+ if (rs.next()) return rs.getString(1);
+ }
+ return null;
}
protected static byte[] getBytes(String value) {
diff --git a/src/main/org/firebirdsql/jdbc/FirebirdDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FirebirdDatabaseMetaData.java
index fb0c07381..3cf49ef2a 100644
--- a/src/main/org/firebirdsql/jdbc/FirebirdDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FirebirdDatabaseMetaData.java
@@ -16,39 +16,94 @@
*/
@SuppressWarnings("unused")
public interface FirebirdDatabaseMetaData extends DatabaseMetaData {
-
+
/**
* Get the source of a stored procedure.
- *
+ *
+ * On Firebird 6.0 and higher, it is recommended to use {@link #getProcedureSourceCode(String, String)} instead.
+ *
+ *
* @param procedureName
- * name of the stored procedure.
- * @return source of the stored procedure.
+ * name of the stored procedure
+ * @return source of the stored procedure
* @throws SQLException
- * if specified procedure cannot be found.
+ * if specified procedure cannot be found
+ * @see #getProcedureSourceCode(String, String)
*/
String getProcedureSourceCode(String procedureName) throws SQLException;
+ /**
+ * Get the source of a stored procedure.
+ *
+ * @param schema
+ * schema of the stored procedure ({@code null} drops the schema from the search; ignored on Firebird 5.0
+ * and older)
+ * @param procedureName
+ * name of the stored procedure
+ * @return source of the stored procedure
+ * @throws SQLException
+ * if specified procedure cannot be found
+ * @since 7
+ */
+ String getProcedureSourceCode(String schema, String procedureName) throws SQLException;
+
/**
* Get the source of a trigger.
- *
+ *
+ * On Firebird 6.0 and higher, it is recommended to use {@link #getTriggerSourceCode(String, String)} instead.
+ *
+ *
* @param triggerName
- * name of the trigger.
- * @return source of the trigger.
+ * name of the trigger
+ * @return source of the trigger
* @throws SQLException
- * if specified trigger cannot be found.
+ * if specified trigger cannot be found
+ * @see #getTriggerSourceCode(String, String)
*/
String getTriggerSourceCode(String triggerName) throws SQLException;
+ /**
+ * Get the source of a trigger.
+ *
+ * @param schema
+ * schema of the trigger ({@code null} drops the schema from the search; ignored on Firebird 5.0 and older)
+ * @param triggerName
+ * name of the trigger
+ * @return source of the trigger
+ * @throws SQLException
+ * if specified trigger cannot be found
+ * @since 7
+ */
+ String getTriggerSourceCode(String schema, String triggerName) throws SQLException;
+
/**
* Get the source of a view.
- *
+ *
+ * On Firebird 6.0 and higher, it is recommended to use {@link #getViewSourceCode(String, String)} instead.
+ *
+ *
* @param viewName
- * name of the view.
- * @return source of the view.
+ * name of the view
+ * @return source of the view
* @throws SQLException
- * if specified view cannot be found.
+ * if specified view cannot be found
+ * @see #getViewSourceCode(String, String)
*/
String getViewSourceCode(String viewName) throws SQLException;
+
+ /**
+ * Get the source of a view.
+ *
+ * @param schema
+ * schema of the trigger ({@code null} drops the schema from the search; ignored on Firebird 5.0 and older)
+ * @param viewName
+ * name of the view
+ * @return source of the view
+ * @throws SQLException
+ * if specified view cannot be found
+ * @since 7
+ */
+ String getViewSourceCode(String schema, String viewName) throws SQLException;
/**
* Get the major version of the ODS (On-Disk Structure) of the database.
diff --git a/src/main/org/firebirdsql/jdbc/metadata/Clause.java b/src/main/org/firebirdsql/jdbc/metadata/Clause.java
index 4d765aea9..3335abff7 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/Clause.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/Clause.java
@@ -51,7 +51,7 @@ private Clause(String columnName, MetadataPattern metadataPattern) {
* value for equals condition
* @return clause for a SQL equals ({@code =}) condition
*/
- static Clause equalsClause(String columnName, String value) {
+ public static Clause equalsClause(String columnName, String value) {
return new Clause(columnName, MetadataPattern.equalsCondition(value));
}
From b60c7d6006589bbec5de0e356535666e0d1c3059 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Mon, 23 Jun 2025 17:04:01 +0200
Subject: [PATCH 23/64] Add ifSchemaElse also to FirebirdSupportInfo
---
.../org/firebirdsql/util/FirebirdSupportInfo.java | 13 +++++++++++++
.../org/firebirdsql/common/FBTestProperties.java | 3 ++-
2 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/src/main/org/firebirdsql/util/FirebirdSupportInfo.java b/src/main/org/firebirdsql/util/FirebirdSupportInfo.java
index 1b566b40d..e62fcfac2 100644
--- a/src/main/org/firebirdsql/util/FirebirdSupportInfo.java
+++ b/src/main/org/firebirdsql/util/FirebirdSupportInfo.java
@@ -793,6 +793,19 @@ public boolean supportsSchemas() {
return isVersionEqualOrAbove(6);
}
+ /**
+ * If schema support is available, returns {@code forSchema}, otherwise returns {@code withoutSchema}.
+ *
+ * @param forSchema
+ * value to return when schema support is available
+ * @param withoutSchema
+ * value to return when schema support is not available
+ * @return {@code forSchema} if schema support is available, otherwise {@code withoutSchema}
+ */
+ public T ifSchemaElse(T forSchema, T withoutSchema) {
+ return supportsSchemas() ? forSchema : withoutSchema;
+ }
+
/**
* @return {@code true} when this Firebird version is considered a supported version
*/
diff --git a/src/test/org/firebirdsql/common/FBTestProperties.java b/src/test/org/firebirdsql/common/FBTestProperties.java
index d211e1d86..bf239013d 100644
--- a/src/test/org/firebirdsql/common/FBTestProperties.java
+++ b/src/test/org/firebirdsql/common/FBTestProperties.java
@@ -374,9 +374,10 @@ public static void defaultDatabaseTearDown(FBManager fbManager) throws Exception
* @param withoutSchema
* value to return when schema support is not available
* @return {@code forSchema} if schema support is available, otherwise {@code withoutSchema}
+ * @see FirebirdSupportInfo#ifSchemaElse(Object, Object)
*/
public static T ifSchemaElse(T forSchema, T withoutSchema) {
- return getDefaultSupportInfo().supportsSchemas() ? forSchema : withoutSchema;
+ return getDefaultSupportInfo().ifSchemaElse(forSchema, withoutSchema);
}
private FBTestProperties() {
From a1545519abd2e7db7cd1204297c5b11abe3176e3 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Tue, 24 Jun 2025 10:06:07 +0200
Subject: [PATCH 24/64] #882 Client props, xid detection
---
.../jaybird/xca/FBManagedConnection.java | 40 ++++++++++++++++++-
.../firebirdsql/jdbc/ClientInfoProvider.java | 8 +++-
2 files changed, 44 insertions(+), 4 deletions(-)
diff --git a/src/main/org/firebirdsql/jaybird/xca/FBManagedConnection.java b/src/main/org/firebirdsql/jaybird/xca/FBManagedConnection.java
index f5be226e3..a4b575666 100644
--- a/src/main/org/firebirdsql/jaybird/xca/FBManagedConnection.java
+++ b/src/main/org/firebirdsql/jaybird/xca/FBManagedConnection.java
@@ -6,7 +6,7 @@
SPDX-FileCopyrightText: Copyright 2003 Ryan Baldwin
SPDX-FileCopyrightText: Copyright 2005 Steven Jardine
SPDX-FileCopyrightText: Copyright 2006 Ludovic Orban
- SPDX-FileCopyrightText: Copyright 2011-2024 Mark Rotteveel
+ SPDX-FileCopyrightText: Copyright 2011-2025 Mark Rotteveel
SPDX-License-Identifier: LGPL-2.1-or-later
*/
package org.firebirdsql.jaybird.xca;
@@ -1383,7 +1383,9 @@ private interface XidQueries {
String recoveryQueryParameterized();
static XidQueries forVersion(GDSServerVersion version) {
- if (version.isEqualOrAbove(3, 0)) {
+ if (version.isEqualOrAbove(6)) {
+ return XidQueriesFB60.INSTANCE;
+ } else if (version.isEqualOrAbove(3)) {
return XidQueriesFB30.INSTANCE;
} else if (version.isEqualOrAbove(2, 5)) {
return XidQueriesFB25.INSTANCE;
@@ -1392,6 +1394,40 @@ static XidQueries forVersion(GDSServerVersion version) {
}
}
+ /**
+ * Relatively efficient XID queries that work with Firebird 6.0 and higher.
+ */
+ private static final class XidQueriesFB60 implements XidQueries {
+
+ static final XidQueriesFB60 INSTANCE = new XidQueriesFB60();
+ // We're no longer casting RDB$TRANSACTION_DESCRIPTION, as it will benefit from inline blobs
+ private static final String FIND_TRANSACTION_FRAGMENT =
+ "select RDB$TRANSACTION_ID, RDB$TRANSACTION_DESCRIPTION from SYSTEM.RDB$TRANSACTIONS\n";
+
+ @Override
+ public String forgetFindQuery() {
+ return FIND_TRANSACTION_FRAGMENT + """
+ where RDB$TRANSACTION_STATE in (2, 3)"
+ and RDB$TRANSACTION_DESCRIPTION starting with x'0105'""";
+ }
+
+ @Override
+ public String forgetDelete() {
+ return "delete from SYSTEM.RDB$TRANSACTIONS where RDB$TRANSACTION_ID = ";
+ }
+
+ @Override
+ public String recoveryQuery() {
+ return FIND_TRANSACTION_FRAGMENT + "where RDB$TRANSACTION_DESCRIPTION starting with x'0105'";
+ }
+
+ @Override
+ public String recoveryQueryParameterized() {
+ return FIND_TRANSACTION_FRAGMENT
+ + "where RDB$TRANSACTION_DESCRIPTION = cast(? AS varchar(32764) character set octets)";
+ }
+ }
+
/**
* Relatively efficient XID queries that work with Firebird 3.0 and higher.
*/
diff --git a/src/main/org/firebirdsql/jdbc/ClientInfoProvider.java b/src/main/org/firebirdsql/jdbc/ClientInfoProvider.java
index f5f2ad649..b6e04f73a 100644
--- a/src/main/org/firebirdsql/jdbc/ClientInfoProvider.java
+++ b/src/main/org/firebirdsql/jdbc/ClientInfoProvider.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2023-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2023-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -179,7 +179,7 @@ public String getClientInfo(String name) throws SQLException {
QuoteStrategy quoteStrategy = connection.getQuoteStrategy();
var sb = new StringBuilder("select ");
renderGetValue(sb, property, quoteStrategy);
- sb.append(" from RDB$DATABASE");
+ sb.append(" from ").append(hasSystemSchema() ? "SYSTEM.RDB$DATABASE" : "RDB$DATABASE");
try (var rs = getStatement().executeQuery(sb.toString())) {
if (rs.next()) {
registerKnownProperty(property);
@@ -195,6 +195,10 @@ public String getClientInfo(String name) throws SQLException {
}
}
+ private boolean hasSystemSchema() throws SQLException {
+ return connection.getMetaData().supportsSchemasInDataManipulation();
+ }
+
private void renderGetValue(StringBuilder sb, ClientInfoProperty property, QuoteStrategy quoteStrategy) {
// CLIENT_PROCESS@SYSTEM was introduced in Firebird 2.5.3, so don't fall back for earlier versions
if (APPLICATION_NAME_PROP.equals(property) && supportInfoFor(connection).isVersionEqualOrAbove(2, 5, 3)) {
From 18a52280c62102771069c2da51974caeedc95081 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Tue, 24 Jun 2025 10:25:30 +0200
Subject: [PATCH 25/64] #882 Retrieve schema name in column information
---
.../org/firebirdsql/gds/ISCConstants.java | 3 +
.../gds/ng/ServerVersionInformation.java | 59 ++++++++++++
.../gds/ng/StatementInfoProcessor.java | 6 +-
.../gds/ng/fields/FieldDescriptor.java | 42 ++++++++-
.../gds/ng/fields/RowDescriptorBuilder.java | 19 +++-
.../jdbc/AbstractFieldMetaData.java | 13 ++-
.../firebirdsql/jdbc/FBResultSetMetaData.java | 74 ++++++++++-----
.../gds/ng/AbstractStatementTest.java | 49 ++++------
.../gds/ng/fields/FieldDescriptorTest.java | 4 +-
.../ng/fields/RowDescriptorBuilderTest.java | 37 ++++----
.../gds/ng/fields/RowValueTest.java | 17 +---
.../FBResultSetMetaDataParametrizedTest.java | 92 +++++++++----------
12 files changed, 273 insertions(+), 142 deletions(-)
diff --git a/src/main/org/firebirdsql/gds/ISCConstants.java b/src/main/org/firebirdsql/gds/ISCConstants.java
index 1cdb58f7a..8fe688d50 100644
--- a/src/main/org/firebirdsql/gds/ISCConstants.java
+++ b/src/main/org/firebirdsql/gds/ISCConstants.java
@@ -378,6 +378,9 @@ public interface ISCConstants {
int isc_info_sql_stmt_timeout_user = 28;
int isc_info_sql_stmt_timeout_run = 29;
int isc_info_sql_stmt_blob_align = 30;
+ int isc_info_sql_exec_path_blr_bytes = 31;
+ int isc_info_sql_exec_path_blr_text = 32;
+ int isc_info_sql_relation_schema = 33;
// SQL information return values
diff --git a/src/main/org/firebirdsql/gds/ng/ServerVersionInformation.java b/src/main/org/firebirdsql/gds/ng/ServerVersionInformation.java
index a8fc5ff0e..85607565c 100644
--- a/src/main/org/firebirdsql/gds/ng/ServerVersionInformation.java
+++ b/src/main/org/firebirdsql/gds/ng/ServerVersionInformation.java
@@ -43,6 +43,22 @@ public byte[] getStatementInfoRequestItems() {
public byte[] getParameterDescriptionInfoRequestItems() {
return Constants.V_2_0_PARAMETER_INFO.clone();
}
+ },
+ /**
+ * Information for Version 6.0 and higher
+ *
+ * @since 7
+ */
+ VERSION_6_0(6, 0) {
+ @Override
+ public byte[] getStatementInfoRequestItems() {
+ return Constants.V_6_0_STATEMENT_INFO.clone();
+ }
+
+ @Override
+ public byte[] getParameterDescriptionInfoRequestItems() {
+ return Constants.V_6_0_PARAMETER_INFO.clone();
+ }
};
private final int majorVersion;
@@ -213,6 +229,49 @@ private static final class Constants {
isc_info_sql_describe_end
};
+ static final byte[] V_6_0_STATEMENT_INFO = new byte[] {
+ isc_info_sql_stmt_type,
+ isc_info_sql_select,
+ isc_info_sql_describe_vars,
+ isc_info_sql_sqlda_seq,
+ isc_info_sql_type, isc_info_sql_sub_type,
+ isc_info_sql_scale, isc_info_sql_length,
+ isc_info_sql_field,
+ isc_info_sql_alias,
+ isc_info_sql_relation_schema,
+ isc_info_sql_relation,
+ isc_info_sql_relation_alias,
+ isc_info_sql_owner,
+ isc_info_sql_describe_end,
+
+ isc_info_sql_bind,
+ isc_info_sql_describe_vars,
+ isc_info_sql_sqlda_seq,
+ isc_info_sql_type, isc_info_sql_sub_type,
+ isc_info_sql_scale, isc_info_sql_length,
+ // TODO: Information not available in normal queries, check for procedures, otherwise remove
+ //isc_info_sql_field,
+ //isc_info_sql_alias,
+ //isc_info_sql_relation_schema,
+ //isc_info_sql_relation,
+ //isc_info_sql_relation_alias,
+ //isc_info_sql_owner,
+ isc_info_sql_describe_end
+ };
+ static final byte[] V_6_0_PARAMETER_INFO = new byte[] {
+ isc_info_sql_describe_vars,
+ isc_info_sql_sqlda_seq,
+ isc_info_sql_type, isc_info_sql_sub_type,
+ isc_info_sql_scale, isc_info_sql_length,
+ isc_info_sql_field,
+ isc_info_sql_alias,
+ isc_info_sql_relation_schema,
+ isc_info_sql_relation,
+ isc_info_sql_relation_alias,
+ isc_info_sql_owner,
+ isc_info_sql_describe_end
+ };
+
private Constants() {
// no instances
}
diff --git a/src/main/org/firebirdsql/gds/ng/StatementInfoProcessor.java b/src/main/org/firebirdsql/gds/ng/StatementInfoProcessor.java
index 9e22649c9..ac38482ed 100644
--- a/src/main/org/firebirdsql/gds/ng/StatementInfoProcessor.java
+++ b/src/main/org/firebirdsql/gds/ng/StatementInfoProcessor.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2013-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2013-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.gds.ng;
@@ -166,6 +166,10 @@ private void processDescriptors(final StatementInfo info, final RowDescriptorBui
rdb.setFieldName(readStringValue(info));
break;
+ case ISCConstants.isc_info_sql_relation_schema:
+ rdb.setOriginalSchema(readStringValue(info));
+ break;
+
case ISCConstants.isc_info_sql_relation:
rdb.setOriginalTableName(readStringValue(info));
break;
diff --git a/src/main/org/firebirdsql/gds/ng/fields/FieldDescriptor.java b/src/main/org/firebirdsql/gds/ng/fields/FieldDescriptor.java
index f9c883537..fdbf213ec 100644
--- a/src/main/org/firebirdsql/gds/ng/fields/FieldDescriptor.java
+++ b/src/main/org/firebirdsql/gds/ng/fields/FieldDescriptor.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2013-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2013-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later OR BSD-3-Clause
package org.firebirdsql.gds.ng.fields;
@@ -33,6 +33,7 @@ public final class FieldDescriptor {
private final String fieldName;
private final String tableAlias;
private final String originalName;
+ private final String originalSchema;
private final String originalTableName;
private final String ownerName;
@@ -62,6 +63,8 @@ public final class FieldDescriptor {
* Column table alias
* @param originalName
* Column original name
+ * @param originalSchema
+ * Column original schema
* @param originalTableName
* Column original table
* @param ownerName
@@ -69,8 +72,8 @@ public final class FieldDescriptor {
*/
public FieldDescriptor(int position, DatatypeCoder datatypeCoder,
int type, int subType, int scale, int length,
- String fieldName, String tableAlias, String originalName, String originalTableName,
- String ownerName) {
+ String fieldName, String tableAlias,
+ String originalName, String originalSchema, String originalTableName, String ownerName) {
this.position = position;
this.datatypeCoder = datatypeCoderForType(datatypeCoder, type, subType, scale);
this.type = type;
@@ -82,10 +85,35 @@ public FieldDescriptor(int position, DatatypeCoder datatypeCoder,
// TODO May want to do the reverse, or handle this better; see FirebirdResultSetMetaData contract
this.tableAlias = trimToNull(tableAlias);
this.originalName = originalName;
+ this.originalSchema = originalSchema;
this.originalTableName = originalTableName;
this.ownerName = ownerName;
}
+ /**
+ * Constructor for metadata FieldDescriptor.
+ *
+ * This constructor sets all string-fields {@code null}, and is primarily intended for testing purposes.
+ *
+ *
+ * @param position
+ * Position of this field (0-based), or {@code -1} if position is not known (e.g. for test code)
+ * @param datatypeCoder
+ * Instance of DatatypeCoder to use when decoding column data (note that another instance may be derived
+ * internally, which then will be returned by {@link #getDatatypeCoder()})
+ * @param type
+ * Column SQL type
+ * @param subType
+ * Column subtype
+ * @param scale
+ * Column scale
+ * @param length
+ * Column defined length
+ */
+ public FieldDescriptor(int position, DatatypeCoder datatypeCoder, int type, int subType, int scale, int length) {
+ this(position, datatypeCoder, type, subType, scale, length, null, null, null, null, null, null);
+ }
+
/**
* The position of the field in the row or parameter set.
*
@@ -162,6 +190,14 @@ public String getOriginalName() {
return originalName;
}
+ /**
+ * @return The original schema ({@code null} if schemaless, e.g. Firebird 5.0 or older, or a column not backed by
+ * a table)
+ */
+ public String getOriginalSchema() {
+ return originalSchema;
+ }
+
/**
* @return The original table name
*/
diff --git a/src/main/org/firebirdsql/gds/ng/fields/RowDescriptorBuilder.java b/src/main/org/firebirdsql/gds/ng/fields/RowDescriptorBuilder.java
index 3076bd00b..2262039fe 100644
--- a/src/main/org/firebirdsql/gds/ng/fields/RowDescriptorBuilder.java
+++ b/src/main/org/firebirdsql/gds/ng/fields/RowDescriptorBuilder.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2013-2022 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2013-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.gds.ng.fields;
@@ -28,6 +28,7 @@ public final class RowDescriptorBuilder {
private String fieldName;
private String tableAlias;
private String originalName;
+ private String originalSchema;
private String originalTableName;
private String ownerName;
private final FieldDescriptor[] fieldDescriptors;
@@ -143,6 +144,18 @@ public RowDescriptorBuilder setOriginalName(final String originalName) {
return this;
}
+ /**
+ * Sets the original schema of the underlying table.
+ *
+ * @param originalSchema
+ * The schema of the table
+ * @return this builder
+ */
+ public RowDescriptorBuilder setOriginalSchema(final String originalSchema) {
+ this.originalSchema = originalSchema;
+ return this;
+ }
+
/**
* Sets the original name of the underlying table.
*
@@ -247,7 +260,7 @@ public RowDescriptorBuilder simple(final int type, final int length, final Strin
*/
public FieldDescriptor toFieldDescriptor() {
return new FieldDescriptor(currentFieldIndex, datatypeCoder, type, subType, scale, length, fieldName,
- tableAlias, originalName, originalTableName, ownerName);
+ tableAlias, originalName, originalSchema, originalTableName, ownerName);
}
/**
@@ -261,6 +274,7 @@ public RowDescriptorBuilder resetField() {
fieldName = null;
tableAlias = null;
originalName = null;
+ originalSchema = null;
originalTableName = null;
ownerName = null;
return this;
@@ -281,6 +295,7 @@ public RowDescriptorBuilder copyFieldFrom(final FieldDescriptor sourceFieldDescr
fieldName = sourceFieldDescriptor.getFieldName();
tableAlias = sourceFieldDescriptor.getTableAlias();
originalName = sourceFieldDescriptor.getOriginalName();
+ originalSchema = sourceFieldDescriptor.getOriginalSchema();
originalTableName = sourceFieldDescriptor.getOriginalTableName();
ownerName = sourceFieldDescriptor.getOwnerName();
return this;
diff --git a/src/main/org/firebirdsql/jdbc/AbstractFieldMetaData.java b/src/main/org/firebirdsql/jdbc/AbstractFieldMetaData.java
index 1aab3d20f..869be7781 100644
--- a/src/main/org/firebirdsql/jdbc/AbstractFieldMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/AbstractFieldMetaData.java
@@ -6,7 +6,7 @@
SPDX-FileCopyrightText: Copyright 2002-2003 Blas Rodriguez Somoza
SPDX-FileCopyrightText: Copyright 2003 Nikolay Samofatov
SPDX-FileCopyrightText: Copyright 2005-2006 Steven Jardine
- SPDX-FileCopyrightText: Copyright 2011-2024 Mark Rotteveel
+ SPDX-FileCopyrightText: Copyright 2011-2025 Mark Rotteveel
SPDX-License-Identifier: LGPL-2.1-or-later
*/
package org.firebirdsql.jdbc;
@@ -265,22 +265,25 @@ protected final ExtendedFieldInfo getExtFieldInfo(int columnIndex) throws SQLExc
* Stores additional information about fields in a database.
*/
protected record ExtendedFieldInfo(FieldKey fieldKey, int fieldPrecision, boolean autoIncrement) {
- public ExtendedFieldInfo(String relationName, String fieldName, int precision, boolean autoIncrement) {
- this(new FieldKey(relationName, fieldName), precision, autoIncrement);
+ public ExtendedFieldInfo(String schema, String relationName, String fieldName, int precision,
+ boolean autoIncrement) {
+ this(new FieldKey(schema, relationName, fieldName), precision, autoIncrement);
}
}
/**
* A composite key for internal field mapping structures.
*
+ * @param schema schema ({@code null} if schemaless, i.e. Firebird 5.0 and older)
* @param relationName
* relation name
* @param fieldName
* field name
*/
- protected record FieldKey(String relationName, String fieldName) {
+ protected record FieldKey(String schema, String relationName, String fieldName) {
public FieldKey(FieldDescriptor fieldDescriptor) {
- this(fieldDescriptor.getOriginalTableName(), fieldDescriptor.getOriginalName());
+ this(fieldDescriptor.getOriginalSchema(), fieldDescriptor.getOriginalTableName(),
+ fieldDescriptor.getOriginalName());
}
}
}
diff --git a/src/main/org/firebirdsql/jdbc/FBResultSetMetaData.java b/src/main/org/firebirdsql/jdbc/FBResultSetMetaData.java
index 4db7d9018..426bf5c0c 100644
--- a/src/main/org/firebirdsql/jdbc/FBResultSetMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FBResultSetMetaData.java
@@ -6,7 +6,7 @@
SPDX-FileCopyrightText: Copyright 2002-2003 Blas Rodriguez Somoza
SPDX-FileCopyrightText: Copyright 2003 Nikolay Samofatov
SPDX-FileCopyrightText: Copyright 2005-2006 Steven Jardine
- SPDX-FileCopyrightText: Copyright 2011-2024 Mark Rotteveel
+ SPDX-FileCopyrightText: Copyright 2011-2025 Mark Rotteveel
SPDX-License-Identifier: LGPL-2.1-or-later
*/
package org.firebirdsql.jdbc;
@@ -24,6 +24,7 @@
import java.sql.Types;
import java.util.*;
+import static java.util.Objects.requireNonNullElseGet;
import static org.firebirdsql.jaybird.util.StringUtils.isNullOrEmpty;
/**
@@ -160,11 +161,12 @@ public String getColumnName(int column) throws SQLException {
/**
* {@inheritDoc}
*
- * @return Always {@code ""} as schemas are not supported.
+ * @return Schema of table, empty string ({@code ""}) if schemaless, e.g. always on Firebird 5.0 and older, or if
+ * the column has no backing table
*/
@Override
public String getSchemaName(int column) throws SQLException {
- return "";
+ return Objects.toString(getFieldDescriptor(column).getOriginalSchema(), "");
}
/**
@@ -187,16 +189,17 @@ public int getScale(int column) throws SQLException {
@Override
public String getTableName(int column) throws SQLException {
- String result = getFieldDescriptor(column).getOriginalTableName();
- if (result == null) result = "";
- return result;
+ return getTableName(getFieldDescriptor(column));
+ }
+
+ private static String getTableName(FieldDescriptor fieldDescriptor) {
+ return Objects.toString(fieldDescriptor.getOriginalTableName(), "");
}
@Override
public String getTableAlias(int column) throws SQLException {
- String result = getFieldDescriptor(column).getTableAlias();
- if (result == null) result = getTableName(column);
- return result;
+ FieldDescriptor fieldDescriptor = getFieldDescriptor(column);
+ return requireNonNullElseGet(fieldDescriptor.getTableAlias(), () -> getTableName(fieldDescriptor));
}
/**
@@ -253,13 +256,15 @@ public String getColumnClassName(int column) throws SQLException {
return getFieldClassName(column);
}
- private static final int FIELD_INFO_RELATION_NAME = 1;
- private static final int FIELD_INFO_FIELD_NAME = 2;
- private static final int FIELD_INFO_FIELD_PRECISION = 3;
- private static final int FIELD_INFO_FIELD_AUTO_INC = 4;
+ private static final int FIELD_INFO_SCHEMA_NAME = 1;
+ private static final int FIELD_INFO_RELATION_NAME = 2;
+ private static final int FIELD_INFO_FIELD_NAME = 3;
+ private static final int FIELD_INFO_FIELD_PRECISION = 4;
+ private static final int FIELD_INFO_FIELD_AUTO_INC = 5;
private static final String GET_FIELD_INFO_25 = """
select
+ cast(null as char(1)) as SCHEMA_NAME,
RF.RDB$RELATION_NAME as RELATION_NAME,
RF.RDB$FIELD_NAME as FIELD_NAME,
F.RDB$FIELD_PRECISION as FIELD_PRECISION,
@@ -270,14 +275,27 @@ public String getColumnClassName(int column) throws SQLException {
private static final String GET_FIELD_INFO_30 = """
select
- RF.RDB$RELATION_NAME as RELATION_NAME,
- RF.RDB$FIELD_NAME as FIELD_NAME,
+ null as SCHEMA_NAME,
+ trim(trailing from RF.RDB$RELATION_NAME) as RELATION_NAME,
+ trim(trailing from RF.RDB$FIELD_NAME) as FIELD_NAME,
F.RDB$FIELD_PRECISION as FIELD_PRECISION,
RF.RDB$IDENTITY_TYPE is not null as FIELD_AUTO_INC
from RDB$RELATION_FIELDS RF inner join RDB$FIELDS F
on RF.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME
where RF.RDB$FIELD_NAME = ? and RF.RDB$RELATION_NAME = ?""";
+ private static final String GET_FIELD_INFO_60 = """
+ select
+ trim(trailing from RF.RDB$SCHEMA_NAME) as SCHEMA_NAME,
+ trim(trailing from RF.RDB$RELATION_NAME) as RELATION_NAME,
+ trim(trailing from RF.RDB$FIELD_NAME) as FIELD_NAME,
+ F.RDB$FIELD_PRECISION as FIELD_PRECISION,
+ RF.RDB$IDENTITY_TYPE is not null as FIELD_AUTO_INC
+ from SYSTEM.RDB$RELATION_FIELDS RF
+ inner join SYSTEM.RDB$FIELDS F
+ on RF.RDB$FIELD_SOURCE_SCHEMA_NAME = F.RDB$SCHEMA_NAME and RF.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME
+ where RF.RDB$FIELD_NAME = ? and RF.RDB$SCHEMA_NAME = ? and RF.RDB$RELATION_NAME = ?""";
+
// Apparently there is a limit in the UNION. It is necessary to split in several queries. Although the problem
// reported with 93 UNION, use only 70.
private static final int MAX_FIELD_INFO_UNIONS = 70;
@@ -292,9 +310,12 @@ protected Map getExtendedFieldInfo(FBConnection con
var result = new HashMap();
FBDatabaseMetaData metaData = (FBDatabaseMetaData) connection.getMetaData();
var params = new ArrayList();
+ final boolean fb3OrHigher = metaData.getDatabaseMajorVersion() >= 3;
+ final boolean supportsSchemas = metaData.supportsSchemasInDataManipulation();
+ String getFieldInfoQuery = fb3OrHigher
+ ? (supportsSchemas ? GET_FIELD_INFO_60 : GET_FIELD_INFO_30)
+ : GET_FIELD_INFO_25;
var sb = new StringBuilder();
- boolean fb3OrHigher = metaData.getDatabaseMajorVersion() >= 3;
- String getFieldInfoQuery = fb3OrHigher ? GET_FIELD_INFO_30 : GET_FIELD_INFO_25;
while (currentColumn <= fieldCount) {
params.clear();
sb.setLength(0);
@@ -303,10 +324,15 @@ protected Map getExtendedFieldInfo(FBConnection con
FieldDescriptor fieldDescriptor = getFieldDescriptor(currentColumn);
if (!needsExtendedFieldInfo(fieldDescriptor, fb3OrHigher)) continue;
+ String schemaName = fieldDescriptor.getOriginalSchema();
String relationName = fieldDescriptor.getOriginalTableName();
String fieldName = fieldDescriptor.getOriginalName();
- if (isNullOrEmpty(relationName) || isNullOrEmpty(fieldName)) continue;
+ if (isNullOrEmpty(relationName)
+ || isNullOrEmpty(fieldName)
+ || supportsSchemas && isNullOrEmpty(schemaName)) {
+ continue;
+ }
if (unionCount != 0) {
sb.append("\nunion all\n");
@@ -314,6 +340,9 @@ protected Map getExtendedFieldInfo(FBConnection con
sb.append(getFieldInfoQuery);
params.add(fieldName);
+ if (supportsSchemas) {
+ params.add(schemaName);
+ }
params.add(relationName);
unionCount++;
@@ -332,8 +361,9 @@ protected Map getExtendedFieldInfo(FBConnection con
}
private static ExtendedFieldInfo extractExtendedFieldInfo(ResultSet rs) throws SQLException {
- return new ExtendedFieldInfo(rs.getString(FIELD_INFO_RELATION_NAME), rs.getString(FIELD_INFO_FIELD_NAME),
- rs.getInt(FIELD_INFO_FIELD_PRECISION), rs.getBoolean(FIELD_INFO_FIELD_AUTO_INC));
+ return new ExtendedFieldInfo(rs.getString(FIELD_INFO_SCHEMA_NAME), rs.getString(FIELD_INFO_RELATION_NAME),
+ rs.getString(FIELD_INFO_FIELD_NAME), rs.getInt(FIELD_INFO_FIELD_PRECISION),
+ rs.getBoolean(FIELD_INFO_FIELD_AUTO_INC));
}
/**
@@ -365,9 +395,7 @@ private enum ColumnStrategy {
DEFAULT {
@Override
String getColumnName(FieldDescriptor fieldDescriptor) {
- return fieldDescriptor.getOriginalName() != null
- ? fieldDescriptor.getOriginalName()
- : getColumnLabel(fieldDescriptor);
+ return requireNonNullElseGet(fieldDescriptor.getOriginalName(), () -> getColumnLabel(fieldDescriptor));
}
},
/**
diff --git a/src/test/org/firebirdsql/gds/ng/AbstractStatementTest.java b/src/test/org/firebirdsql/gds/ng/AbstractStatementTest.java
index be44ce567..5974bc062 100644
--- a/src/test/org/firebirdsql/gds/ng/AbstractStatementTest.java
+++ b/src/test/org/firebirdsql/gds/ng/AbstractStatementTest.java
@@ -164,18 +164,19 @@ public void testSelect_NoParameters_Describe() throws Exception {
assertNotNull(fields, "Fields");
final FirebirdSupportInfo supportInfo = supportInfoFor(db);
final int metadataCharSetId = supportInfo.reportedMetadataCharacterSetId();
+ final String schema = ifSchemaElse("SYSTEM", null);
var expectedFields = List.of(
new FieldDescriptor(0, db.getDatatypeCoder(), ISCConstants.SQL_BLOB | 1, 1,
supportInfo.reportsBlobCharSetInDescriptor() ? metadataCharSetId : 0, 8, "Description", null,
- "RDB$DESCRIPTION", "RDB$DATABASE", "SYSDBA"),
+ "RDB$DESCRIPTION", schema, "RDB$DATABASE", "SYSDBA"),
new FieldDescriptor(1, db.getDatatypeCoder(), ISCConstants.SQL_SHORT | 1, 0, 0, 2,
- "RDB$RELATION_ID", null, "RDB$RELATION_ID", "RDB$DATABASE", "SYSDBA"),
+ "RDB$RELATION_ID", null, "RDB$RELATION_ID", schema, "RDB$DATABASE", "SYSDBA"),
new FieldDescriptor(2, db.getDatatypeCoder(), ISCConstants.SQL_TEXT | 1, metadataCharSetId, 0,
supportInfo.maxReportedIdentifierLengthBytes(), "RDB$SECURITY_CLASS", null,
- "RDB$SECURITY_CLASS", "RDB$DATABASE", "SYSDBA"),
+ "RDB$SECURITY_CLASS", schema, "RDB$DATABASE", "SYSDBA"),
new FieldDescriptor(3, db.getDatatypeCoder(), ISCConstants.SQL_TEXT | 1, metadataCharSetId, 0,
supportInfo.maxReportedIdentifierLengthBytes(), "RDB$CHARACTER_SET_NAME", null,
- "RDB$CHARACTER_SET_NAME", "RDB$DATABASE", "SYSDBA")
+ "RDB$CHARACTER_SET_NAME", schema, "RDB$DATABASE", "SYSDBA")
);
assertEquals(expectedFields, fields.getFieldDescriptors(), "Unexpected values for fields");
assertNotNull(statement.getParameterDescriptor(), "Parameters");
@@ -230,18 +231,16 @@ public void testSelect_WithParameters_Describe() throws Exception {
var expectedFields = List.of(
new FieldDescriptor(0, db.getDatatypeCoder(), ISCConstants.SQL_TEXT | 1, metadataCharSetId, 0,
supportInfo.maxReportedIdentifierLengthBytes(), "RDB$CHARACTER_SET_NAME",
- supportsTableAlias ? "A" : null, "RDB$CHARACTER_SET_NAME", "RDB$CHARACTER_SETS", "SYSDBA")
+ supportsTableAlias ? "A" : null, "RDB$CHARACTER_SET_NAME", ifSchemaElse("SYSTEM", null),
+ "RDB$CHARACTER_SETS", "SYSDBA")
);
assertEquals(expectedFields, fields.getFieldDescriptors(), "Unexpected values for fields");
final RowDescriptor parameters = statement.getParameterDescriptor();
assertNotNull(parameters, "Parameters");
var expectedParameters = List.of(
- new FieldDescriptor(0, db.getDatatypeCoder(), ISCConstants.SQL_SHORT | 1, 0, 0, 2,
- null, null, null, null, null),
- new FieldDescriptor(1, db.getDatatypeCoder(), ISCConstants.SQL_SHORT | 1, 0, 0, 2,
- null, null, null, null, null)
- );
+ new FieldDescriptor(0, db.getDatatypeCoder(), ISCConstants.SQL_SHORT | 1, 0, 0, 2),
+ new FieldDescriptor(1, db.getDatatypeCoder(), ISCConstants.SQL_SHORT | 1, 0, 0, 2));
assertEquals(expectedParameters, parameters.getFieldDescriptors(), "Unexpected values for parameters");
}
@@ -291,16 +290,14 @@ public void test_PrepareExecutableStoredProcedure() throws Exception {
assertNotNull(fields, "Fields");
var expectedFields = List.of(
new FieldDescriptor(0, db.getDatatypeCoder(), ISCConstants.SQL_LONG | 1, 0, 0, 4, "OUTVALUE", null,
- "OUTVALUE", "INCREMENT", "SYSDBA")
+ "OUTVALUE", ifSchemaElse("PUBLIC", null), "INCREMENT", "SYSDBA")
);
assertEquals(expectedFields, fields.getFieldDescriptors(), "Unexpected values for fields");
final RowDescriptor parameters = statement.getParameterDescriptor();
assertNotNull(parameters, "Parameters");
var expectedParameters = List.of(
- new FieldDescriptor(0, db.getDatatypeCoder(), ISCConstants.SQL_LONG | 1, 0, 0, 4, null, null, null,
- null, null)
- );
+ new FieldDescriptor(0, db.getDatatypeCoder(), ISCConstants.SQL_LONG | 1, 0, 0, 4));
assertEquals(expectedParameters, parameters.getFieldDescriptors(), "Unexpected values for parameters");
}
@@ -336,18 +333,15 @@ public void test_PrepareSelectableStoredProcedure() throws Exception {
assertNotNull(fields, "Fields");
var expectedFields = List.of(
new FieldDescriptor(0, db.getDatatypeCoder(), ISCConstants.SQL_LONG | 1, 0, 0, 4, "OUTVALUE", null,
- "OUTVALUE", "RANGE", "SYSDBA")
+ "OUTVALUE", ifSchemaElse("PUBLIC", null), "RANGE", "SYSDBA")
);
assertEquals(expectedFields, fields.getFieldDescriptors(), "Unexpected values for fields");
final RowDescriptor parameters = statement.getParameterDescriptor();
assertNotNull(parameters, "Parameters");
var expectedParameters = List.of(
- new FieldDescriptor(0, db.getDatatypeCoder(), ISCConstants.SQL_LONG | 1, 0, 0, 4, null, null, null,
- null, null),
- new FieldDescriptor(1, db.getDatatypeCoder(), ISCConstants.SQL_LONG | 1, 0, 0, 4, null, null, null,
- null, null)
- );
+ new FieldDescriptor(0, db.getDatatypeCoder(), ISCConstants.SQL_LONG | 1, 0, 0, 4),
+ new FieldDescriptor(1, db.getDatatypeCoder(), ISCConstants.SQL_LONG | 1, 0, 0, 4));
assertEquals(expectedParameters, parameters.getFieldDescriptors(), "Unexpected values for parameters");
}
@@ -365,16 +359,14 @@ public void test_PrepareInsertReturning() throws Exception {
assertNotNull(fields, "Fields");
var expectedFields = List.of(
new FieldDescriptor(0, db.getDatatypeCoder(), ISCConstants.SQL_LONG | 1, 0, 0, 4, "THEKEY", null,
- "THEKEY", "KEYVALUE", "SYSDBA")
+ "THEKEY", ifSchemaElse("PUBLIC", null), "KEYVALUE", "SYSDBA")
);
assertEquals(expectedFields, fields.getFieldDescriptors(), "Unexpected values for fields");
final RowDescriptor parameters = statement.getParameterDescriptor();
assertNotNull(parameters, "Parameters");
var expectedParameters = List.of(
- new FieldDescriptor(0, db.getDatatypeCoder(), ISCConstants.SQL_VARYING | 1, 0, 0, 5, null, null,
- null, null, null)
- );
+ new FieldDescriptor(0, db.getDatatypeCoder(), ISCConstants.SQL_VARYING | 1, 0, 0, 5));
assertEquals(expectedParameters, parameters.getFieldDescriptors(), "Unexpected values for parameters");
}
@@ -754,20 +746,19 @@ public void testStatementPrepareLongObjectNames() throws Exception {
final RowDescriptor fields = statement.getRowDescriptor();
assertNotNull(fields, "Fields");
+ final String schema = ifSchemaElse("PUBLIC", null);
var expectedFields = List.of(
new FieldDescriptor(0, db.getDatatypeCoder(), ISCConstants.SQL_VARYING | 1, 4,
- 0, 40, column1, null, column1, tableName, "SYSDBA"),
+ 0, 40, column1, null, column1, schema, tableName, "SYSDBA"),
new FieldDescriptor(1, db.getDatatypeCoder(), ISCConstants.SQL_VARYING | 1, 4,
- 0, 80, column2, null, column2, tableName, "SYSDBA")
+ 0, 80, column2, null, column2, schema, tableName, "SYSDBA")
);
assertEquals(expectedFields, fields.getFieldDescriptors(), "Unexpected values for fields");
RowDescriptor parameters = statement.getParameterDescriptor();
assertNotNull(parameters, "Parameters");
var expectedParameters = List.of(
- new FieldDescriptor(0, db.getDatatypeCoder(), ISCConstants.SQL_VARYING | 1, 4, 0, 40, null, null, null,
- null, null)
- );
+ new FieldDescriptor(0, db.getDatatypeCoder(), ISCConstants.SQL_VARYING | 1, 4, 0, 40));
assertEquals(expectedParameters, parameters.getFieldDescriptors(), "Unexpected values for parameters");
}
diff --git a/src/test/org/firebirdsql/gds/ng/fields/FieldDescriptorTest.java b/src/test/org/firebirdsql/gds/ng/fields/FieldDescriptorTest.java
index a712b69d5..86277e682 100644
--- a/src/test/org/firebirdsql/gds/ng/fields/FieldDescriptorTest.java
+++ b/src/test/org/firebirdsql/gds/ng/fields/FieldDescriptorTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2017-2022 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2017-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.gds.ng.fields;
@@ -92,6 +92,6 @@ void shouldUseEncodingSpecificDatatypeCoder_blobTextType_notDefaultCharset() {
}
private FieldDescriptor createFieldDescriptor(int type, int subType, int scale) {
- return new FieldDescriptor(1, defaultDatatypeCoder, type, subType, scale, 8, "x", "t", "x", "t", "");
+ return new FieldDescriptor(1, defaultDatatypeCoder, type, subType, scale, 8, "x", "t", "x", "s", "t", "");
}
}
\ No newline at end of file
diff --git a/src/test/org/firebirdsql/gds/ng/fields/RowDescriptorBuilderTest.java b/src/test/org/firebirdsql/gds/ng/fields/RowDescriptorBuilderTest.java
index 68b1a48a9..cae611954 100644
--- a/src/test/org/firebirdsql/gds/ng/fields/RowDescriptorBuilderTest.java
+++ b/src/test/org/firebirdsql/gds/ng/fields/RowDescriptorBuilderTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2013-2022 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2013-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.gds.ng.fields;
@@ -9,8 +9,6 @@
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@@ -24,17 +22,13 @@ class RowDescriptorBuilderTest {
private static final DatatypeCoder datatypeCoder =
DefaultDatatypeCoder.forEncodingFactory(EncodingFactory.createInstance(StandardCharsets.UTF_8));
- private static final List TEST_FIELD_DESCRIPTORS;
- static {
- List fields = new ArrayList<>();
- fields.add(new FieldDescriptor(0, datatypeCoder, 1, 1, 1, 1, "1", "1", "1", "1", "1"));
- fields.add(new FieldDescriptor(1, datatypeCoder, 2, 2, 2, 2, "2", "2", "2", "2", "2"));
- fields.add(new FieldDescriptor(2, datatypeCoder, 3, 3, 3, 3, "3", "3", "3", "3", "3"));
+ private static final List TEST_FIELD_DESCRIPTORS = List.of(
+ new FieldDescriptor(0, datatypeCoder, 1, 1, 1, 1, "1", "1", "1", "1", "1", "1"),
+ new FieldDescriptor(1, datatypeCoder, 2, 2, 2, 2, "2", "2", "2", "2", "2", "2"),
+ new FieldDescriptor(2, datatypeCoder, 3, 3, 3, 3, "3", "3", "3", "3", "3", "3"));
- TEST_FIELD_DESCRIPTORS = Collections.unmodifiableList(fields);
- }
-
- private static final FieldDescriptor SOURCE = new FieldDescriptor(-1, datatypeCoder, 1, 2, 3, 4, "5", "6", "7", "8", "9");
+ private static final FieldDescriptor SOURCE =
+ new FieldDescriptor(-1, datatypeCoder, 1, 2, 3, 4, "5", "6", "7", "8", "9", "10");
@Test
void testEmptyField() {
@@ -49,6 +43,7 @@ void testEmptyField() {
assertNull(descriptor.getFieldName(), "Unexpected FieldName");
assertNull(descriptor.getTableAlias(), "Unexpected TableAlias");
assertNull(descriptor.getOriginalName(), "Unexpected OriginalName");
+ assertNull(descriptor.getOriginalSchema(), "Unexpected OriginalSchema");
assertNull(descriptor.getOriginalTableName(), "Unexpected OriginalTableName");
assertNull(descriptor.getOwnerName(), "Unexpected OwnerName");
}
@@ -64,8 +59,9 @@ void testBasicFieldInitialization() {
.setFieldName("5")
.setTableAlias("6")
.setOriginalName("7")
- .setOriginalTableName("8")
- .setOwnerName("9")
+ .setOriginalSchema("8")
+ .setOriginalTableName("9")
+ .setOwnerName("10")
.toFieldDescriptor();
assertEquals(0, descriptor.getPosition(), "Unexpected Position");
@@ -76,8 +72,9 @@ void testBasicFieldInitialization() {
assertEquals("5", descriptor.getFieldName(), "Unexpected FieldName");
assertEquals("6", descriptor.getTableAlias(), "Unexpected TableAlias");
assertEquals("7", descriptor.getOriginalName(), "Unexpected OriginalName");
- assertEquals("8", descriptor.getOriginalTableName(), "Unexpected OriginalTableName");
- assertEquals("9", descriptor.getOwnerName(), "Unexpected OwnerName");
+ assertEquals("8", descriptor.getOriginalSchema(), "Unexpected OriginalSchema");
+ assertEquals("9", descriptor.getOriginalTableName(), "Unexpected OriginalTableName");
+ assertEquals("10", descriptor.getOwnerName(), "Unexpected OwnerName");
}
@Test
@@ -94,8 +91,9 @@ void testCopyFrom() {
assertEquals("5", fieldDescriptor.getFieldName(), "Unexpected FieldName");
assertEquals("6", fieldDescriptor.getTableAlias(), "Unexpected TableAlias");
assertEquals("7", fieldDescriptor.getOriginalName(), "Unexpected OriginalName");
- assertEquals("8", fieldDescriptor.getOriginalTableName(), "Unexpected OriginalTableName");
- assertEquals("9", fieldDescriptor.getOwnerName(), "Unexpected OwnerName");
+ assertEquals("8", fieldDescriptor.getOriginalSchema(), "Unexpected OriginalSchema");
+ assertEquals("9", fieldDescriptor.getOriginalTableName(), "Unexpected OriginalTableName");
+ assertEquals("10", fieldDescriptor.getOwnerName(), "Unexpected OwnerName");
}
@Test
@@ -113,6 +111,7 @@ void testResetField() {
assertNull(fieldDescriptor.getFieldName(), "Unexpected FieldName");
assertNull(fieldDescriptor.getTableAlias(), "Unexpected TableAlias");
assertNull(fieldDescriptor.getOriginalName(), "Unexpected OriginalName");
+ assertNull(fieldDescriptor.getOriginalSchema(), "Unexpected OriginalSchema");
assertNull(fieldDescriptor.getOriginalTableName(), "Unexpected OriginalTableName");
assertNull(fieldDescriptor.getOwnerName(), "Unexpected OwnerName");
}
diff --git a/src/test/org/firebirdsql/gds/ng/fields/RowValueTest.java b/src/test/org/firebirdsql/gds/ng/fields/RowValueTest.java
index 7cd5b756f..b43e003de 100644
--- a/src/test/org/firebirdsql/gds/ng/fields/RowValueTest.java
+++ b/src/test/org/firebirdsql/gds/ng/fields/RowValueTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2018-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2018-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.gds.ng.fields;
@@ -8,8 +8,6 @@
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@@ -20,15 +18,10 @@ class RowValueTest {
private static final DatatypeCoder datatypeCoder =
DefaultDatatypeCoder.forEncodingFactory(EncodingFactory.createInstance(StandardCharsets.UTF_8));
private static final RowDescriptor EMPTY_ROW_DESCRIPTOR = RowDescriptor.empty(datatypeCoder);
- private static final List TEST_FIELD_DESCRIPTORS;
- static {
- List fields = new ArrayList<>();
- fields.add(new FieldDescriptor(0, datatypeCoder, 1, 1, 1, 1, "A", "1", "1", "1", "1"));
- fields.add(new FieldDescriptor(1, datatypeCoder, 2, 2, 2, 2, "B", "2", "2", "2", "2"));
- fields.add(new FieldDescriptor(2, datatypeCoder, 3, 3, 3, 3, "C", "3", "3", "3", "3"));
-
- TEST_FIELD_DESCRIPTORS = Collections.unmodifiableList(fields);
- }
+ private static final List TEST_FIELD_DESCRIPTORS = List.of(
+ new FieldDescriptor(0, datatypeCoder, 1, 1, 1, 1, "A", "1", "1", "1", "1", "1"),
+ new FieldDescriptor(1, datatypeCoder, 2, 2, 2, 2, "B", "2", "2", "2", "2", "2"),
+ new FieldDescriptor(2, datatypeCoder, 3, 3, 3, 3, "C", "3", "3", "3", "3", "3"));
@Test
void testDefaultFor_emptyRowDescriptor_returns_EMPTY_ROW_VALUE() {
diff --git a/src/test/org/firebirdsql/jdbc/FBResultSetMetaDataParametrizedTest.java b/src/test/org/firebirdsql/jdbc/FBResultSetMetaDataParametrizedTest.java
index beaa7f5ef..26cb7cc6c 100644
--- a/src/test/org/firebirdsql/jdbc/FBResultSetMetaDataParametrizedTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBResultSetMetaDataParametrizedTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2014-2023 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2014-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -41,51 +41,51 @@
*/
class FBResultSetMetaDataParametrizedTest {
+ // TODO Add schema support: tests involving other schema
+
private static final String TABLE_NAME = "TEST_P_METADATA";
- //@formatter:off
- private static final String CREATE_TABLE =
- "CREATE TABLE test_p_metadata (" +
- " id INTEGER, " +
- " simple_field VARCHAR(60) CHARACTER SET WIN1251 COLLATE PXW_CYRL, " +
- " two_byte_field VARCHAR(60) CHARACTER SET BIG_5, " +
- " three_byte_field VARCHAR(60) CHARACTER SET UNICODE_FSS, " +
- " long_field BIGINT, " +
- " int_field INTEGER, " +
- " short_field SMALLINT, " +
- " float_field FLOAT, " +
- " double_field DOUBLE PRECISION, " +
- " smallint_numeric NUMERIC(3,1), " +
- " integer_decimal_1 DECIMAL(3,1), " +
- " integer_numeric NUMERIC(5,2), " +
- " integer_decimal_2 DECIMAL(9,3), " +
- " bigint_numeric NUMERIC(10,4), " +
- " bigint_decimal DECIMAL(18,9), " +
- " date_field DATE, " +
- " time_field TIME, " +
- " timestamp_field TIMESTAMP, " +
- " blob_field BLOB, " +
- " blob_text_field BLOB SUB_TYPE TEXT, " +
- " blob_minus_one BLOB SUB_TYPE -1 " +
- " /* boolean */ " +
- " /* decfloat */ " +
- " /* extended numerics */ " +
- " /* time zone */ " +
- " /* int128 */ " +
- ")";
-
- private static final String TEST_QUERY =
- "SELECT " +
- "simple_field, two_byte_field, three_byte_field, long_field, int_field, short_field," +
- "float_field, double_field, smallint_numeric, integer_decimal_1, integer_numeric," +
- "integer_decimal_2, bigint_numeric, bigint_decimal, date_field, time_field," +
- "timestamp_field, blob_field, blob_text_field, blob_minus_one " +
- "/* boolean */ " +
- "/* decfloat */ " +
- "/* extended numerics */ " +
- "/* time zone */ " +
- "/* int128 */ " +
- "FROM test_p_metadata";
- //@formatter:on
+ private static final String CREATE_TABLE = """
+ CREATE TABLE test_p_metadata (
+ id INTEGER,
+ simple_field VARCHAR(60) CHARACTER SET WIN1251 COLLATE PXW_CYRL,
+ two_byte_field VARCHAR(60) CHARACTER SET BIG_5,
+ three_byte_field VARCHAR(60) CHARACTER SET UNICODE_FSS,
+ long_field BIGINT,
+ int_field INTEGER,
+ short_field SMALLINT,
+ float_field FLOAT,
+ double_field DOUBLE PRECISION,
+ smallint_numeric NUMERIC(3,1),
+ integer_decimal_1 DECIMAL(3,1),
+ integer_numeric NUMERIC(5,2),
+ integer_decimal_2 DECIMAL(9,3),
+ bigint_numeric NUMERIC(10,4),
+ bigint_decimal DECIMAL(18,9),
+ date_field DATE,
+ time_field TIME,
+ timestamp_field TIMESTAMP,
+ blob_field BLOB,
+ blob_text_field BLOB SUB_TYPE TEXT,
+ blob_minus_one BLOB SUB_TYPE -1
+ /* boolean */
+ /* decfloat */
+ /* extended numerics */
+ /* time zone */
+ /* int128 */
+ )""";
+
+ private static final String TEST_QUERY = """
+ SELECT
+ simple_field, two_byte_field, three_byte_field, long_field, int_field, short_field,
+ float_field, double_field, smallint_numeric, integer_decimal_1, integer_numeric,
+ integer_decimal_2, bigint_numeric, bigint_decimal, date_field, time_field,
+ timestamp_field, blob_field, blob_text_field, blob_minus_one
+ /* boolean */
+ /* decfloat */
+ /* extended numerics */
+ /* time zone */
+ /* int128 */
+ FROM test_p_metadata""";
@RegisterExtension
static final UsesDatabaseExtension.UsesDatabaseForAll usesDatabase = UsesDatabaseExtension.usesDatabaseForAll();
@@ -259,7 +259,7 @@ void testGetScale(Integer columnIndex, ResultSetMetaDataInfo expectedMetaData, S
@ParameterizedTest(name = "Index {0} ({2})")
@MethodSource("testData")
void testGetSchemaName(Integer columnIndex, ResultSetMetaDataInfo ignored1, String ignored2) throws Exception {
- assertEquals("", rsmd.getSchemaName(columnIndex), "getSchemaName");
+ assertEquals(supportInfo.ifSchemaElse("PUBLIC", ""), rsmd.getSchemaName(columnIndex), "getSchemaName");
}
@ParameterizedTest(name = "Index {0} ({2})")
From bed0828c5585d12f1b4afec78f465f714a717da6 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Tue, 24 Jun 2025 10:51:22 +0200
Subject: [PATCH 26/64] #882 Incomplete change to selectable procedure
detection
---
devdoc/jdp/jdp-2025-06-schema-support.adoc | 1 +
.../jdbc/StoredProcedureMetaDataFactory.java | 10 ++++++++--
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/devdoc/jdp/jdp-2025-06-schema-support.adoc b/devdoc/jdp/jdp-2025-06-schema-support.adoc
index 960ac3bda..d5376ee61 100644
--- a/devdoc/jdp/jdp-2025-06-schema-support.adoc
+++ b/devdoc/jdp/jdp-2025-06-schema-support.adoc
@@ -96,6 +96,7 @@ Jaybird cannot honour this requirement for plain `Statement`, as schema resoluti
*** Or just accept it as a breaking change?
** `getCatalogs`: TODO: Maybe add a custom column with a list of schema names for `useCatalogAsPackage=true`?
* TODO: Define effects for management API
+* TODO: Redesign retrieval of selectable procedure information (`StoredProcedureMetaDataFactory`) to be able to find stored procedures by schema
Note to self: use `// TODO Add schema support` in places that you identify need to get/improve schema support, while working on schema support elsewhere
diff --git a/src/main/org/firebirdsql/jdbc/StoredProcedureMetaDataFactory.java b/src/main/org/firebirdsql/jdbc/StoredProcedureMetaDataFactory.java
index 633ef17f8..d229f64a8 100644
--- a/src/main/org/firebirdsql/jdbc/StoredProcedureMetaDataFactory.java
+++ b/src/main/org/firebirdsql/jdbc/StoredProcedureMetaDataFactory.java
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2007 Gabriel Reid
-// SPDX-FileCopyrightText: Copyright 2012-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2012-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -61,6 +61,10 @@ private static boolean versionEqualOrAboveFB21(int majorVersion, int minorVersio
*/
final class DefaultCallableStatementMetaData implements StoredProcedureMetaData {
+ // TODO Add schema support: solution needs to be reworked to support schemas, which will cascade into
+ // callable statement parsing. This needs further investigation. In addition, the current solution doesn't handle
+ // case-sensitivity
+
final Set selectableProcedureNames = new HashSet<>();
public DefaultCallableStatementMetaData(Connection connection)
@@ -71,7 +75,9 @@ public DefaultCallableStatementMetaData(Connection connection)
private void loadSelectableProcedureNames(Connection connection) throws SQLException {
try (Statement stmt = connection.createStatement()) {
// TODO Replace with looking for specific procedure
- String sql = "SELECT RDB$PROCEDURE_NAME FROM RDB$PROCEDURES WHERE RDB$PROCEDURE_TYPE = 1";
+ String sql = connection.getMetaData().supportsSchemasInDataManipulation()
+ ? "SELECT RDB$PROCEDURE_NAME FROM SYSTEM.RDB$PROCEDURES WHERE RDB$PROCEDURE_TYPE = 1"
+ : "SELECT RDB$PROCEDURE_NAME FROM RDB$PROCEDURES WHERE RDB$PROCEDURE_TYPE = 1";
try (ResultSet resultSet = stmt.executeQuery(sql)) {
while (resultSet.next()) {
selectableProcedureNames.add(resultSet.getString(1).trim().toUpperCase(Locale.ROOT));
From ce3abcc05fd8d6a283dae0d6e5279bcd466b00e5 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Tue, 24 Jun 2025 10:59:56 +0200
Subject: [PATCH 27/64] #882 Misc wording of jdp-2025-06
---
devdoc/jdp/jdp-2025-06-schema-support.adoc | 17 +++++------------
1 file changed, 5 insertions(+), 12 deletions(-)
diff --git a/devdoc/jdp/jdp-2025-06-schema-support.adoc b/devdoc/jdp/jdp-2025-06-schema-support.adoc
index d5376ee61..f5586dff8 100644
--- a/devdoc/jdp/jdp-2025-06-schema-support.adoc
+++ b/devdoc/jdp/jdp-2025-06-schema-support.adoc
@@ -31,6 +31,7 @@ Important details related to schemas:
The session default can be configured with `isc_dpb_search_path` (string DPB item).
The current search path can be altered with `SET SEARCH_PATH TO ...`.
`ALTER SESSION RESET` reverts to the session default.
+* If `SYSTEM` is not on the search path, it is automatically searched last
* The "`current`" schema cannot be set separately;
the first valid schema listed in the search path is considered the current schema.
* `CURRENT_SCHEMA` and `RDB$GET_CONTEXT('SYSTEM', 'CURRENT_SCHEMA')` return the first valid schema from the search path
@@ -38,16 +39,12 @@ the first valid schema listed in the search path is considered the current schem
* Objects not qualified with a schema name will be resolved using the current search path.
This is done -- with some exceptions -- at prepare time.
* TBP has new item `isc_tpb_lock_table_schema` to specify the schema of a table to be locked (1 byte length + string data)
-* Gbak has additional options to include/exclude (skip) schema datas in backup or restore, similar to existing options to include/exclude tables
+* Gbak has additional options to include/exclude (skip) schema data in backup or restore, similar to existing options to include/exclude tables
* Gstat has additional options to specify a schema for operations involving tables
* For validation, `val_sch_incl` and `val_sch_excl` (I don't think we use the equivalent,`val_tab_incl`/`val_tab_excl` in Jaybird, so might not be relevant)
JDBC defines various methods, parameters, and return values or result set columns that are or are related to schemas.
-These are:
-
-* ...
-
Jaybird 5 is the "`long-term support`" version for Java 8.
[NOTE]
@@ -62,7 +59,7 @@ When Jaybird 7 is used on Firebird 5.0 or older, it will behave as before (no sc
Further details can be found in <>.
-Decision on backport to Jaybird 6 and/or Jaybird 5 is pending, and may be subject of a separate JDP.
+Decision on backport to Jaybird 6 and/or Jaybird 5 is pending, and may be the subject of a separate JDP.
[#consequences]
== Consequences
@@ -84,16 +81,12 @@ The schema name is stored _only_ for this replacement operation (i.e. it will no
** JDBC specifies that "`__Calling ``setSchema`` has no effect on previously created or prepared Statement objects.__`";
Jaybird cannot honour this requirement for plain `Statement`, as schema resolution is on prepare time (which for plain `Statement` is on execute), and not always for `CallableStatement` (as the implementation may delay actual prepare until execution).
* Request `isc_info_sql_relation_schema` after preparing a query, record it in `FieldDescriptor`, and return it were relevant for JDBC (e.g. `ResultSetMetaData.getSchemaName(int)`)
-** For Firebird 5.0 and older, we need to ensure that JDBC methods continue to report the correct value (i.e. ``""`` for schema-less objects)
+** For Firebird 5.0 and older, we need to ensure that JDBC methods continue to report the correct value (i.e. `""` for schema-less objects)
* A Firebird 6.0 variant of the `DatabaseMetaData` and other internal metadata queries needs to be written to address at least the following things:
-** Explicitly qualify metadata tables with `"SYSTEM"`, so the queries will work even if `SYSTEM` is not on the search path.
+** Explicitly qualify metadata tables with `SYSTEM`, so the queries will work even if `SYSTEM` is not on the search path.
** Returning schema names, and qualified object names where relevant (e.g. in `DatabaseMetaData` result sets)
** Include schema names in joins to ensure matching the right objects
** Allow searching for schema or schema pattern as specified in JDBC, or were needed for internal metadata queries
-** TODO: investigate need for backwards compatible behaviour for `DatabaseMetaData` parameter `schema` with value `""` ("`__[...] retrieves those without a schema__`").
-*** Maybe make it search `PUBLIC`, or `PUBLIC` and `SYSTEM`, or those on the search path?
-*** Or add a compatibility connection property to make it behave as `null` ("`__[...] means that the schema name should not be used to narrow the search__`")?
-*** Or just accept it as a breaking change?
** `getCatalogs`: TODO: Maybe add a custom column with a list of schema names for `useCatalogAsPackage=true`?
* TODO: Define effects for management API
* TODO: Redesign retrieval of selectable procedure information (`StoredProcedureMetaDataFactory`) to be able to find stored procedures by schema
From d365d92e97e2b6b3a1581f89fe2b7a0cc0de9de5 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Tue, 24 Jun 2025 12:03:47 +0200
Subject: [PATCH 28/64] #882 Define connection property searchPath
---
devdoc/jdp/jdp-2025-06-schema-support.adoc | 1 +
src/docs/asciidoc/release_notes.adoc | 10 +++
...bstractConnectionPropertiesDataSource.java | 10 +++
.../jaybird/fb/constants/DpbItems.java | 3 +
.../props/DatabaseConnectionProperties.java | 26 ++++++
.../jaybird/props/PropertyNames.java | 1 +
.../StandardConnectionPropertyDefiner.java | 1 +
.../firebirdsql/common/FBTestProperties.java | 4 +-
.../jdbc/ConnectionPropertiesTest.java | 79 ++++++++++++++++++-
9 files changed, 132 insertions(+), 3 deletions(-)
diff --git a/devdoc/jdp/jdp-2025-06-schema-support.adoc b/devdoc/jdp/jdp-2025-06-schema-support.adoc
index f5586dff8..0b4645ff5 100644
--- a/devdoc/jdp/jdp-2025-06-schema-support.adoc
+++ b/devdoc/jdp/jdp-2025-06-schema-support.adoc
@@ -90,6 +90,7 @@ Jaybird cannot honour this requirement for plain `Statement`, as schema resoluti
** `getCatalogs`: TODO: Maybe add a custom column with a list of schema names for `useCatalogAsPackage=true`?
* TODO: Define effects for management API
* TODO: Redesign retrieval of selectable procedure information (`StoredProcedureMetaDataFactory`) to be able to find stored procedures by schema
+* TODO: Add information to Jaybird manual
Note to self: use `// TODO Add schema support` in places that you identify need to get/improve schema support, while working on schema support elsewhere
diff --git a/src/docs/asciidoc/release_notes.adoc b/src/docs/asciidoc/release_notes.adoc
index cff02351d..290bf07bb 100644
--- a/src/docs/asciidoc/release_notes.adoc
+++ b/src/docs/asciidoc/release_notes.adoc
@@ -515,6 +515,14 @@ Firebird 6.0 introduces schemas, and Jaybird 7 provides support for schemas as d
Changes include:
+* Connection property `searchPath` sets the initial search path of the connection.
+The search path is the list of schemas that will be searched for schema-bound objects if they are not explicitly qualified with a schema name.
+The first _valid_ schema is the current schema of the connection.
++
+The value of `searchPath` is a comma-separated list of schema names.
+Schema names that are case-sensitive or otherwise non-regular identifiers, must be quoted.
+Unknown schema names are ignored.
+If `SYSTEM` is not included, the server will automatically add it as the last schema.
* `DatabaseMetaData`
** Methods accepting a `schema` (exact match if not `null`) or `schemaPattern` (`LIKE` match if not `null`) will return no rows for value empty (`""`) on Firebird 6.0 and higher;
use `null` or -- `schemaPattern` only -- `"%"` to match all schemas
@@ -531,6 +539,8 @@ The `schema` parameter is ignored on Firebird 5.0 and older.
** `getSchemas(String catalog, String schemaPattern)` returns all schemas matching the `LIKE` pattern `schemaPattern`, with the following caveats
*** `catalog` non-empty will return no rows;
we recommend to always use `null` for `catalog`
+* `ResultSetMetaData`
+** `getSchemaName` reports the schema if the column is backed by a table, otherwise empty string (`""`)
// TODO add major changes
diff --git a/src/main/org/firebirdsql/ds/AbstractConnectionPropertiesDataSource.java b/src/main/org/firebirdsql/ds/AbstractConnectionPropertiesDataSource.java
index e2225a143..99dea317a 100644
--- a/src/main/org/firebirdsql/ds/AbstractConnectionPropertiesDataSource.java
+++ b/src/main/org/firebirdsql/ds/AbstractConnectionPropertiesDataSource.java
@@ -518,6 +518,16 @@ public void setMaxBlobCacheSize(int maxBlobCacheSize) {
FirebirdConnectionProperties.super.setMaxBlobCacheSize(maxBlobCacheSize);
}
+ @Override
+ public String getSearchPath() {
+ return FirebirdConnectionProperties.super.getSearchPath();
+ }
+
+ @Override
+ public void setSearchPath(String searchPath) {
+ FirebirdConnectionProperties.super.setSearchPath(searchPath);
+ }
+
@SuppressWarnings("deprecation")
@Deprecated(since = "5")
@Override
diff --git a/src/main/org/firebirdsql/jaybird/fb/constants/DpbItems.java b/src/main/org/firebirdsql/jaybird/fb/constants/DpbItems.java
index 41f4c0728..6cfa83678 100644
--- a/src/main/org/firebirdsql/jaybird/fb/constants/DpbItems.java
+++ b/src/main/org/firebirdsql/jaybird/fb/constants/DpbItems.java
@@ -128,6 +128,9 @@ public final class DpbItems {
// Firebird 6 constants
public static final int isc_dpb_owner = 102;
+ public static final int isc_dpb_search_path = 105;
+ public static final int isc_dpb_blr_request_search_path = 106;
+ public static final int isc_dpb_gbak_restore_has_schema = 107;
private DpbItems() {
// no instances
diff --git a/src/main/org/firebirdsql/jaybird/props/DatabaseConnectionProperties.java b/src/main/org/firebirdsql/jaybird/props/DatabaseConnectionProperties.java
index fc09fdbe8..2ec37f9e5 100644
--- a/src/main/org/firebirdsql/jaybird/props/DatabaseConnectionProperties.java
+++ b/src/main/org/firebirdsql/jaybird/props/DatabaseConnectionProperties.java
@@ -824,4 +824,30 @@ default void setMaxBlobCacheSize(int maxBlobCacheSize) {
setIntProperty(PropertyNames.maxBlobCacheSize, Math.max(0, maxBlobCacheSize));
}
+ /**
+ * @return the initial search path of the connection, {@code null} if the server default search path is used
+ * @see #setSearchPath(String)
+ */
+ default String getSearchPath() {
+ return getProperty(PropertyNames.searchPath);
+ }
+
+ /**
+ * Sets the initial search path of the connection. The search path is a list of schemas that will be searched for
+ * unqualified objects (i.e. without an explicit schema).
+ *
+ * This only applies to Firebird 6.0 and higher.
+ *
+ *
+ * The default value is {@code null}, which uses the server default (on Firebird 6.0, `PUBLIC, SYSTEM`).
+ * Case-sensitive, or otherwise non-regular identifiers need to be explicitly quoted.
+ *
+ *
+ * @param searchPath
+ * list of comma-separated schema names
+ */
+ default void setSearchPath(String searchPath) {
+ setProperty(PropertyNames.searchPath, searchPath);
+ }
+
}
diff --git a/src/main/org/firebirdsql/jaybird/props/PropertyNames.java b/src/main/org/firebirdsql/jaybird/props/PropertyNames.java
index 857f6f8d4..2bc43b8c9 100644
--- a/src/main/org/firebirdsql/jaybird/props/PropertyNames.java
+++ b/src/main/org/firebirdsql/jaybird/props/PropertyNames.java
@@ -69,6 +69,7 @@ public final class PropertyNames {
public static final String asyncFetch = "asyncFetch";
public static final String maxInlineBlobSize = "maxInlineBlobSize";
public static final String maxBlobCacheSize = "maxBlobCacheSize";
+ public static final String searchPath = "searchPath";
// service connection
public static final String expectedDb = "expectedDb";
diff --git a/src/main/org/firebirdsql/jaybird/props/internal/StandardConnectionPropertyDefiner.java b/src/main/org/firebirdsql/jaybird/props/internal/StandardConnectionPropertyDefiner.java
index bc84e971a..41203451e 100644
--- a/src/main/org/firebirdsql/jaybird/props/internal/StandardConnectionPropertyDefiner.java
+++ b/src/main/org/firebirdsql/jaybird/props/internal/StandardConnectionPropertyDefiner.java
@@ -105,6 +105,7 @@ public Stream defineProperties() {
.dpbItem(isc_dpb_max_inline_blob_size),
builder(maxBlobCacheSize).type(INT).aliases("max_blob_cache_size", "isc_dpb_max_blob_cache_size")
.dpbItem(isc_dpb_max_blob_cache_size),
+ builder(searchPath).aliases("search_path", "isc_dpb_search_path").dpbItem(isc_dpb_search_path),
// TODO Consider removing this property, otherwise formally add it to PropertyNames
builder("filename_charset"),
diff --git a/src/test/org/firebirdsql/common/FBTestProperties.java b/src/test/org/firebirdsql/common/FBTestProperties.java
index bf239013d..3a8c05c41 100644
--- a/src/test/org/firebirdsql/common/FBTestProperties.java
+++ b/src/test/org/firebirdsql/common/FBTestProperties.java
@@ -135,7 +135,9 @@ public static Properties getDefaultPropertiesForConnection() {
*/
public static Properties getPropertiesForConnection(String k1, String v1) {
Properties props = getDefaultPropertiesForConnection();
- props.setProperty(k1, v1);
+ if (v1 != null) {
+ props.setProperty(k1, v1);
+ }
return props;
}
diff --git a/src/test/org/firebirdsql/jdbc/ConnectionPropertiesTest.java b/src/test/org/firebirdsql/jdbc/ConnectionPropertiesTest.java
index 8253311d8..0df91a59d 100644
--- a/src/test/org/firebirdsql/jdbc/ConnectionPropertiesTest.java
+++ b/src/test/org/firebirdsql/jdbc/ConnectionPropertiesTest.java
@@ -1,17 +1,27 @@
-// SPDX-FileCopyrightText: Copyright 2019-2023 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2019-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
import org.firebirdsql.common.extension.UsesDatabaseExtension;
import org.firebirdsql.ds.FBSimpleDataSource;
+import org.firebirdsql.jaybird.props.PropertyNames;
+import org.firebirdsql.util.FirebirdSupportInfo;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import java.sql.Connection;
import java.sql.DriverManager;
+import java.sql.ResultSetMetaData;
+import java.util.List;
import java.util.Properties;
+import java.util.stream.Stream;
import static org.firebirdsql.common.FBTestProperties.*;
+import static org.firebirdsql.common.FbAssumptions.assumeFeature;
+import static org.firebirdsql.common.assertions.ResultSetAssertions.assertNextRow;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
@@ -22,7 +32,19 @@
class ConnectionPropertiesTest {
@RegisterExtension
- static final UsesDatabaseExtension.UsesDatabaseForAll usesDatabase = UsesDatabaseExtension.usesDatabaseForAll();
+ static final UsesDatabaseExtension.UsesDatabaseForAll usesDatabase =
+ UsesDatabaseExtension.usesDatabaseForAll(getDbInitStatements());
+
+ private static List getDbInitStatements() {
+ FirebirdSupportInfo supportInfo = getDefaultSupportInfo();
+ if (supportInfo.supportsSchemas()) {
+ return List.of(
+ "create schema OTHER_SCHEMA",
+ "create table PUBLIC.TEST_TABLE (ID integer)",
+ "create table OTHER_SCHEMA.TEST_TABLE (OTHER_ID bigint)");
+ }
+ return List.of();
+ }
@Test
void testProperty_defaultIsolation_onDataSource() throws Exception {
@@ -59,6 +81,59 @@ void testProperty_isolation_onDriverManager() throws Exception {
}
}
+ @ParameterizedTest
+ @MethodSource("searchPathTestCases")
+ void testProperty_searchPath_onDriverManager(String searchPath, String expectedSearchPath, String expectedSchema,
+ String expectedColumn) throws Exception {
+ assumeFeature(FirebirdSupportInfo::supportsSchemas, "Test requires schema support");
+ try (Connection connection = getConnectionViaDriverManager(PropertyNames.searchPath, searchPath)) {
+ verifySearchPath(connection, expectedSearchPath, expectedSchema, expectedColumn);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("searchPathTestCases")
+ void testProperty_searchPath_onataSource(String searchPath, String expectedSearchPath, String expectedSchema,
+ String expectedColumn) throws Exception {
+ assumeFeature(FirebirdSupportInfo::supportsSchemas, "Test requires schema support");
+ FBSimpleDataSource ds = createDataSource();
+
+ ds.setSearchPath(searchPath);
+
+ try (var connection = ds.getConnection()) {
+ verifySearchPath(connection, expectedSearchPath, expectedSchema, expectedColumn);
+ }
+ }
+
+ private static void verifySearchPath(Connection connection, String expectedSearchPath, String expectedSchema,
+ String expectedColumn) throws Exception {
+ connection.setAutoCommit(false);
+ try (var stmt = connection.createStatement()) {
+ try (var rs = stmt.executeQuery(
+ "select rdb$get_context('SYSTEM', 'SEARCH_PATH') from SYSTEM.RDB$DATABASE")) {
+ assertNextRow(rs);
+ assertEquals(expectedSearchPath, rs.getString(1), "unexpected search path");
+ }
+ }
+ try (var pstmt = connection.prepareStatement("select * from TEST_TABLE")) {
+ ResultSetMetaData metaData = pstmt.getMetaData();
+ assertEquals("TEST_TABLE", metaData.getTableName(1), "tableName");
+ assertEquals(expectedSchema, metaData.getSchemaName(1), "schemaName");
+ assertEquals(expectedColumn, metaData.getColumnName(1), "columnName");
+ }
+ }
+
+ static Stream searchPathTestCases() {
+ return Stream.of(
+ Arguments.arguments(null, "\"PUBLIC\", \"SYSTEM\"", "PUBLIC", "ID"),
+ Arguments.arguments("PUBLIC", "\"PUBLIC\", \"SYSTEM\"", "PUBLIC", "ID"),
+ Arguments.arguments("OTHER_SCHEMA", "\"OTHER_SCHEMA\", \"SYSTEM\"", "OTHER_SCHEMA", "OTHER_ID"),
+ Arguments.arguments("PUBLIC, OTHER_SCHEMA", "\"PUBLIC\", \"OTHER_SCHEMA\", \"SYSTEM\"", "PUBLIC", "ID"),
+ Arguments.arguments("OTHER_SCHEMA, PUBLIC", "\"OTHER_SCHEMA\", \"PUBLIC\", \"SYSTEM\"", "OTHER_SCHEMA",
+ "OTHER_ID"));
+ }
+
+
private FBSimpleDataSource createDataSource() {
return configureDefaultDbProperties(new FBSimpleDataSource());
}
From 276d8bc1b9730ad65ee450554e488e92080235c3 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Wed, 25 Jun 2025 09:30:25 +0200
Subject: [PATCH 29/64] Add supportInfoFor(FirebirdConnection) to avoid wrapper
check
---
.../firebirdsql/util/FirebirdSupportInfo.java | 27 +++++++++++++++----
1 file changed, 22 insertions(+), 5 deletions(-)
diff --git a/src/main/org/firebirdsql/util/FirebirdSupportInfo.java b/src/main/org/firebirdsql/util/FirebirdSupportInfo.java
index e62fcfac2..446428e09 100644
--- a/src/main/org/firebirdsql/util/FirebirdSupportInfo.java
+++ b/src/main/org/firebirdsql/util/FirebirdSupportInfo.java
@@ -37,6 +37,8 @@
@SuppressWarnings("unused")
public final class FirebirdSupportInfo {
+ private static final int SUPPORTED_MIN_VERSION = 3;
+
private final GDSServerVersion serverVersion;
private FirebirdSupportInfo(GDSServerVersion serverVersion) {
@@ -810,7 +812,7 @@ public T ifSchemaElse(T forSchema, T withoutSchema) {
* @return {@code true} when this Firebird version is considered a supported version
*/
public boolean isSupportedVersion() {
- return isVersionEqualOrAbove(3);
+ return isVersionEqualOrAbove(SUPPORTED_MIN_VERSION);
}
/**
@@ -844,15 +846,15 @@ public static FirebirdSupportInfo supportInfoFor(FbAttachment attachment) {
/**
* @param connection
- * A database connection (NOTE: {@link java.sql.Connection} is used, but it must be or unwrap to a
- * {@link org.firebirdsql.jdbc.FirebirdConnection}).
+ * a database connection (NOTE: it must be or unwrap to a {@link org.firebirdsql.jdbc.FirebirdConnection})
* @return FirebirdVersionSupport instance
* @throws java.lang.IllegalArgumentException
- * When the provided connection is not an instance of or wrapper for
+ * when the provided connection is not an instance of or wrapper for
* {@link org.firebirdsql.jdbc.FirebirdConnection}
* @throws java.lang.IllegalStateException
- * When an SQLException occurs unwrapping the connection, or creating
+ * when an SQLException occurs unwrapping the connection, or creating
* the {@link org.firebirdsql.util.FirebirdSupportInfo} instance
+ * @see #supportInfoFor(FirebirdConnection)
*/
public static FirebirdSupportInfo supportInfoFor(java.sql.Connection connection) {
try {
@@ -867,4 +869,19 @@ public static FirebirdSupportInfo supportInfoFor(java.sql.Connection connection)
}
}
+ /**
+ * @param connection
+ * a database connection
+ * @return FirebirdVersionSupport instance
+ * @throws java.lang.IllegalStateException
+ * when an SQLException occurs creating the {@link org.firebirdsql.util.FirebirdSupportInfo} instance
+ */
+ public static FirebirdSupportInfo supportInfoFor(FirebirdConnection connection) {
+ try {
+ return supportInfoFor(connection.getFbDatabase());
+ } catch (SQLException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
}
From 501eb78caa693fd9ab6b307415bcff1820be111f Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Wed, 25 Jun 2025 09:58:34 +0200
Subject: [PATCH 30/64] Tighten up StoredProcedureMetaData, use support info
---
src/docs/asciidoc/release_notes.adoc | 3 ++-
.../firebirdsql/jdbc/ClientInfoProvider.java | 4 ++--
.../jdbc/StoredProcedureMetaData.java | 5 +++-
.../jdbc/StoredProcedureMetaDataFactory.java | 23 ++++++++++++-------
4 files changed, 23 insertions(+), 12 deletions(-)
diff --git a/src/docs/asciidoc/release_notes.adoc b/src/docs/asciidoc/release_notes.adoc
index 290bf07bb..8d6db8db8 100644
--- a/src/docs/asciidoc/release_notes.adoc
+++ b/src/docs/asciidoc/release_notes.adoc
@@ -682,13 +682,14 @@ If you are confronted with such a change, let us know on {firebird-java}[firebir
* `FbWireOperations`
** 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
** Interface `ProcessAttachCallback` was removed
+* Interface `StoredProcedureMetaData` was made package-private
[#breaking-changes-unlikely]
=== Unlikely breaking changes
The following changes might cause issues, though we think this is unlikely:
-// TODO Document unlikely breaking changes, or remove section
+// TODO Document unlikely breaking changes, or comment out this section
[#breaking-changes-for-jaybird-8]
=== Breaking changes for Jaybird 8
diff --git a/src/main/org/firebirdsql/jdbc/ClientInfoProvider.java b/src/main/org/firebirdsql/jdbc/ClientInfoProvider.java
index b6e04f73a..4ba27b864 100644
--- a/src/main/org/firebirdsql/jdbc/ClientInfoProvider.java
+++ b/src/main/org/firebirdsql/jdbc/ClientInfoProvider.java
@@ -195,8 +195,8 @@ public String getClientInfo(String name) throws SQLException {
}
}
- private boolean hasSystemSchema() throws SQLException {
- return connection.getMetaData().supportsSchemasInDataManipulation();
+ private boolean hasSystemSchema() {
+ return supportInfoFor(connection).supportsSchemas();
}
private void renderGetValue(StringBuilder sb, ClientInfoProperty property, QuoteStrategy quoteStrategy) {
diff --git a/src/main/org/firebirdsql/jdbc/StoredProcedureMetaData.java b/src/main/org/firebirdsql/jdbc/StoredProcedureMetaData.java
index 4d8aeb1c7..7c4e8599f 100644
--- a/src/main/org/firebirdsql/jdbc/StoredProcedureMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/StoredProcedureMetaData.java
@@ -4,6 +4,7 @@
package org.firebirdsql.jdbc;
import org.firebirdsql.util.InternalApi;
+import org.jspecify.annotations.NullMarked;
import java.sql.SQLException;
@@ -15,7 +16,9 @@
*
*/
@InternalApi
-public interface StoredProcedureMetaData {
+@NullMarked
+sealed interface StoredProcedureMetaData
+ permits DefaultCallableStatementMetaData, DummyCallableStatementMetaData {
/**
* Determine if the "selectability" of procedures is available.
diff --git a/src/main/org/firebirdsql/jdbc/StoredProcedureMetaDataFactory.java b/src/main/org/firebirdsql/jdbc/StoredProcedureMetaDataFactory.java
index d229f64a8..b6eeec797 100644
--- a/src/main/org/firebirdsql/jdbc/StoredProcedureMetaDataFactory.java
+++ b/src/main/org/firebirdsql/jdbc/StoredProcedureMetaDataFactory.java
@@ -3,20 +3,21 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
-import java.sql.Connection;
-import java.sql.ResultSet;
+import org.jspecify.annotations.NullMarked;
+
import java.sql.SQLException;
import java.sql.SQLNonTransientException;
-import java.sql.Statement;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import static org.firebirdsql.jdbc.SQLStateConstants.SQL_STATE_GENERAL_ERROR;
+import static org.firebirdsql.util.FirebirdSupportInfo.supportInfoFor;
/**
* Factory to retrieve meta-data on stored procedures in a Firebird database.
*/
+@NullMarked
final class StoredProcedureMetaDataFactory {
private StoredProcedureMetaDataFactory() {
@@ -59,6 +60,7 @@ private static boolean versionEqualOrAboveFB21(int majorVersion, int minorVersio
/**
* A fully-functional implementation of {@link StoredProcedureMetaData}.
*/
+@NullMarked
final class DefaultCallableStatementMetaData implements StoredProcedureMetaData {
// TODO Add schema support: solution needs to be reworked to support schemas, which will cascade into
@@ -67,18 +69,18 @@ final class DefaultCallableStatementMetaData implements StoredProcedureMetaData
final Set selectableProcedureNames = new HashSet<>();
- public DefaultCallableStatementMetaData(Connection connection)
+ DefaultCallableStatementMetaData(FBConnection connection)
throws SQLException {
loadSelectableProcedureNames(connection);
}
- private void loadSelectableProcedureNames(Connection connection) throws SQLException {
- try (Statement stmt = connection.createStatement()) {
+ private void loadSelectableProcedureNames(FBConnection connection) throws SQLException {
+ try (var stmt = connection.createStatement()) {
// TODO Replace with looking for specific procedure
- String sql = connection.getMetaData().supportsSchemasInDataManipulation()
+ String sql = supportInfoFor(connection).supportsSchemas()
? "SELECT RDB$PROCEDURE_NAME FROM SYSTEM.RDB$PROCEDURES WHERE RDB$PROCEDURE_TYPE = 1"
: "SELECT RDB$PROCEDURE_NAME FROM RDB$PROCEDURES WHERE RDB$PROCEDURE_TYPE = 1";
- try (ResultSet resultSet = stmt.executeQuery(sql)) {
+ try (var resultSet = stmt.executeQuery(sql)) {
while (resultSet.next()) {
selectableProcedureNames.add(resultSet.getString(1).trim().toUpperCase(Locale.ROOT));
}
@@ -86,10 +88,12 @@ private void loadSelectableProcedureNames(Connection connection) throws SQLExcep
}
}
+ @Override
public boolean canGetSelectableInformation() {
return true;
}
+ @Override
public boolean isSelectable(String procedureName) {
return selectableProcedureNames.contains(procedureName.toUpperCase(Locale.ROOT));
}
@@ -98,12 +102,15 @@ public boolean isSelectable(String procedureName) {
/**
* A non-functional implementation of {@link StoredProcedureMetaData} for databases that don't have this capability.
*/
+@NullMarked
final class DummyCallableStatementMetaData implements StoredProcedureMetaData {
+ @Override
public boolean canGetSelectableInformation() {
return false;
}
+ @Override
public boolean isSelectable(String procedureName) throws SQLException {
throw new SQLNonTransientException("A DummyCallableStatementMetaData can't retrieve selectable settings",
SQL_STATE_GENERAL_ERROR);
From ca102d27a17bcbe6fec9676c0ef22a0994a4fbf7 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Wed, 9 Jul 2025 18:14:10 +0200
Subject: [PATCH 31/64] #882 Implement Connection.get/setSchema
---
devdoc/jdp/jdp-2025-06-schema-support.adoc | 13 +-
src/docs/asciidoc/release_notes.adoc | 9 +-
.../jaybird/parser/SearchPathExtractor.java | 79 +++++++
.../jaybird/parser/SqlTokenizer.java | 6 +-
.../jaybird/util/SearchPathHelper.java | 60 +++++
.../firebirdsql/jdbc/ClientInfoProvider.java | 17 +-
.../org/firebirdsql/jdbc/FBConnection.java | 67 +++++-
.../firebirdsql/jdbc/FirebirdConnection.java | 29 ++-
.../jdbc/MetadataStatementHolder.java | 63 +++++
.../org/firebirdsql/jdbc/SchemaChanger.java | 202 ++++++++++++++++
.../jdbc/StoredProcedureMetaDataFactory.java | 2 +-
.../parser/SearchPathExtractorTest.java | 47 ++++
.../jaybird/parser/SqlTokenizerTest.java | 33 ++-
.../jaybird/util/SearchPathHelperTest.java | 57 +++++
.../jdbc/FBConnectionSchemaTest.java | 223 ++++++++++++++++++
15 files changed, 869 insertions(+), 38 deletions(-)
create mode 100644 src/main/org/firebirdsql/jaybird/parser/SearchPathExtractor.java
create mode 100644 src/main/org/firebirdsql/jaybird/util/SearchPathHelper.java
create mode 100644 src/main/org/firebirdsql/jdbc/MetadataStatementHolder.java
create mode 100644 src/main/org/firebirdsql/jdbc/SchemaChanger.java
create mode 100644 src/test/org/firebirdsql/jaybird/parser/SearchPathExtractorTest.java
create mode 100644 src/test/org/firebirdsql/jaybird/util/SearchPathHelperTest.java
create mode 100644 src/test/org/firebirdsql/jdbc/FBConnectionSchemaTest.java
diff --git a/devdoc/jdp/jdp-2025-06-schema-support.adoc b/devdoc/jdp/jdp-2025-06-schema-support.adoc
index 0b4645ff5..9c42211d5 100644
--- a/devdoc/jdp/jdp-2025-06-schema-support.adoc
+++ b/devdoc/jdp/jdp-2025-06-schema-support.adoc
@@ -33,17 +33,17 @@ The current search path can be altered with `SET SEARCH_PATH TO ...`.
`ALTER SESSION RESET` reverts to the session default.
* If `SYSTEM` is not on the search path, it is automatically searched last
* The "`current`" schema cannot be set separately;
-the first valid schema listed in the search path is considered the current schema.
+the first valid (i.e. existing) schema listed in the search path is considered the current schema.
* `CURRENT_SCHEMA` and `RDB$GET_CONTEXT('SYSTEM', 'CURRENT_SCHEMA')` return the first valid schema from the search path
* `RDB$GET_CONTEXT('SYSTEM', 'SEARCH_PATH')` returns the current search path
* Objects not qualified with a schema name will be resolved using the current search path.
This is done -- with some exceptions -- at prepare time.
-* TBP has new item `isc_tpb_lock_table_schema` to specify the schema of a table to be locked (1 byte length + string data)
+* TPB has new item `isc_tpb_lock_table_schema` to specify the schema of a table to be locked (1 byte length + string data)
* Gbak has additional options to include/exclude (skip) schema data in backup or restore, similar to existing options to include/exclude tables
* Gstat has additional options to specify a schema for operations involving tables
* For validation, `val_sch_incl` and `val_sch_excl` (I don't think we use the equivalent,`val_tab_incl`/`val_tab_excl` in Jaybird, so might not be relevant)
-JDBC defines various methods, parameters, and return values or result set columns that are or are related to schemas.
+JDBC defines various methods, parameters, and return values or result set columns that are related to schemas.
Jaybird 5 is the "`long-term support`" version for Java 8.
@@ -76,18 +76,21 @@ the connection will not store this value
The schema name is stored _only_ for this replacement operation (i.e. it will not be returned by `getSchema`!)
+
** The name must match exactly as is stored in the metadata (it is always case-sensitive!)
-** Jaybird will take care of quoting, and will always quote
+** Jaybird will take care of quoting, and will always quote on dialect 3
** Existence of the schema is **not** checked, so it is possible the current schema does not change with this operation, as `CURRENT_SCHEMA` reports the first _valid_ schema
** JDBC specifies that "`__Calling ``setSchema`` has no effect on previously created or prepared Statement objects.__`";
Jaybird cannot honour this requirement for plain `Statement`, as schema resolution is on prepare time (which for plain `Statement` is on execute), and not always for `CallableStatement` (as the implementation may delay actual prepare until execution).
* Request `isc_info_sql_relation_schema` after preparing a query, record it in `FieldDescriptor`, and return it were relevant for JDBC (e.g. `ResultSetMetaData.getSchemaName(int)`)
** For Firebird 5.0 and older, we need to ensure that JDBC methods continue to report the correct value (i.e. `""` for schema-less objects)
* A Firebird 6.0 variant of the `DatabaseMetaData` and other internal metadata queries needs to be written to address at least the following things:
-** Explicitly qualify metadata tables with `SYSTEM`, so the queries will work even if `SYSTEM` is not on the search path.
+** Explicitly qualify metadata tables with `SYSTEM`, so the queries will work even if another schema on the search path contains tables with the same names as the system tables.
** Returning schema names, and qualified object names where relevant (e.g. in `DatabaseMetaData` result sets)
** Include schema names in joins to ensure matching the right objects
** Allow searching for schema or schema pattern as specified in JDBC, or were needed for internal metadata queries
** `getCatalogs`: TODO: Maybe add a custom column with a list of schema names for `useCatalogAsPackage=true`?
+* `FirebirdConnection`
+** Added method `String getSearchPath()` to obtain the search path as reported by `RBB$GET_CONTEXT('SYSTEM', 'SEARCH_PATH')`, or `null` if schemas are not supported
+** Added method `List getSearchPatList()` to obtain the search path as a list of unquoted object names, or empty list if schemas are not supported
* TODO: Define effects for management API
* TODO: Redesign retrieval of selectable procedure information (`StoredProcedureMetaDataFactory`) to be able to find stored procedures by schema
* TODO: Add information to Jaybird manual
diff --git a/src/docs/asciidoc/release_notes.adoc b/src/docs/asciidoc/release_notes.adoc
index 8d6db8db8..773f78b4f 100644
--- a/src/docs/asciidoc/release_notes.adoc
+++ b/src/docs/asciidoc/release_notes.adoc
@@ -524,12 +524,12 @@ Schema names that are case-sensitive or otherwise non-regular identifiers, must
Unknown schema names are ignored.
If `SYSTEM` is not included, the server will automatically add it as the last schema.
* `DatabaseMetaData`
-** Methods accepting a `schema` (exact match if not `null`) or `schemaPattern` (`LIKE` match if not `null`) will return no rows for value empty (`""`) on Firebird 6.0 and higher;
+** Methods accepting a `schema` (exact match if not `null`) or `schemaPattern` (`LIKE` match if not `null`) will return no rows for value empty (`++""++`) on Firebird 6.0 and higher;
use `null` or -- `schemaPattern` only -- `"%"` to match all schemas
** `getCatalogs` -- when `useCatalogAsPackage=true` -- returns all (distinct) package names over all schemas.
Within the limitations and specification of the JDBC API, this method cannot be used to find out which schema(s) contain a specific package name.
// TODO Maybe add a custom column with a list of schema names?
-** `getColumnPrivileges` and `getTablePrivileges` received an additional column, `JB_GRANTEE_SCHEMA`, which is non-``null`` for grantees that are schema-bound (e.g. a procedure).
+** `getColumnPrivileges` and `getTablePrivileges` received an additional column, `JB_GRANTEE_SCHEMA`, which is non-``null`` for grantees that are schema-bound (e.g. procedures).
+
As this is a non-standard column, we recommend to always retrieve it by name.
** `getProcedureSourceCode`/`getTriggerSourceCode`/`getViewSourceCode` now also have an overload accepting the schema;
@@ -537,10 +537,13 @@ the overloads without a `schema` parameter, or `schema` is `null` will return th
The `schema` parameter is ignored on Firebird 5.0 and older.
** `getSchemas()` returns all defined schemas
** `getSchemas(String catalog, String schemaPattern)` returns all schemas matching the `LIKE` pattern `schemaPattern`, with the following caveats
-*** `catalog` non-empty will return no rows;
+*** `catalog` non-empty will return no rows -- even if `useCatalogAsPackage` is `true`;
we recommend to always use `null` for `catalog`
* `ResultSetMetaData`
** `getSchemaName` reports the schema if the column is backed by a table, otherwise empty string (`""`)
+* `FirebirdConnection`/`FBConnection`
+** Added method `String getSearchPath()` to obtain the search path as reported by `RBB$GET_CONTEXT('SYSTEM', 'SEARCH_PATH')`, or `null` if schemas are not supported
+** Added method `List getSearchPatList()` to obtain the search path as a list of unquoted object names, or empty list if schemas are not supported
// TODO add major changes
diff --git a/src/main/org/firebirdsql/jaybird/parser/SearchPathExtractor.java b/src/main/org/firebirdsql/jaybird/parser/SearchPathExtractor.java
new file mode 100644
index 000000000..093f44529
--- /dev/null
+++ b/src/main/org/firebirdsql/jaybird/parser/SearchPathExtractor.java
@@ -0,0 +1,79 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jaybird.parser;
+
+import org.firebirdsql.jaybird.util.SearchPathHelper;
+import org.jspecify.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Token visitor to extract a search path to a list of unquoted schema names.
+ *
+ * This visitor is written for the needs of {@link SearchPathHelper#parseSearchPath(String)}, and
+ * may not be generally usable.
+ *
+ */
+public final class SearchPathExtractor implements TokenVisitor {
+
+ private final List identifiers = new ArrayList<>();
+ @Nullable
+ private Token previousToken;
+
+ @Override
+ public void visitToken(Token token, VisitorRegistrar visitorRegistrar) {
+ if (token.isWhitespaceOrComment()) return;
+ try {
+ extractIdentifier(token, visitorRegistrar);
+ } finally {
+ previousToken = token;
+ }
+ }
+
+ private void extractIdentifier(Token token, VisitorRegistrar visitorRegistrar) {
+ if (isPreviousTokenSeparator()) {
+ if (token instanceof QuotedIdentifierToken quotedIdentifier) {
+ identifiers.add(quotedIdentifier.name());
+ } else if (token instanceof GenericToken identifier) {
+ // Firebird returns the search path with quoted identifiers, but this offers extra flexibility if needed
+ identifiers.add(identifier.text().toUpperCase(Locale.ROOT));
+ } else {
+ // Unexpected token, end parsing
+ visitorRegistrar.removeVisitor(this);
+ identifiers.clear();
+ }
+ } else if (!(token instanceof CommaToken && isPreviousTokenIdentifier())) {
+ // Unexpected token, end parsing
+ visitorRegistrar.removeVisitor(this);
+ identifiers.clear();
+ }
+ }
+
+ private boolean isPreviousTokenIdentifier() {
+ return previousToken instanceof QuotedIdentifierToken || previousToken instanceof GenericToken;
+ }
+
+ private boolean isPreviousTokenSeparator() {
+ return previousToken instanceof CommaToken || previousToken == null;
+ }
+
+ @Override
+ public void complete(VisitorRegistrar visitorRegistrar) {
+ if (!isPreviousTokenIdentifier()) {
+ // Unexpected token, clear list; for most cases, we already cleared the list, except if last was CommaToken
+ identifiers.clear();
+ }
+ }
+
+ /**
+ * The extract search path list, or empty if not parsed or if the parsed text was not a valid search path list.
+ *
+ * @return immutable list of unquoted search path entries
+ */
+ public List getSearchPathList() {
+ return List.copyOf(identifiers);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/org/firebirdsql/jaybird/parser/SqlTokenizer.java b/src/main/org/firebirdsql/jaybird/parser/SqlTokenizer.java
index 41148acfd..6a4744084 100644
--- a/src/main/org/firebirdsql/jaybird/parser/SqlTokenizer.java
+++ b/src/main/org/firebirdsql/jaybird/parser/SqlTokenizer.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2021-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2021-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jaybird.parser;
@@ -513,8 +513,8 @@ private boolean detectToken(char[][] expectedChars) {
private static boolean isNormalTokenBoundary(int c) {
return switch (c) {
- case EOF, '\t', '\n', '\r', ' ', '(', ')', '{', '}', '[', ']', '\'', '"', ':', ';', '.', '+', '-', '/', '*',
- '=', '>', '<', '~', '^', '!', '?' -> true;
+ case EOF, '\t', '\n', '\r', ' ', '(', ')', '{', '}', '[', ']', '\'', '"', ':', ';', '.', ',', '+', '-', '/',
+ '*', '=', '>', '<', '~', '^', '!', '?' -> true;
default -> false;
};
}
diff --git a/src/main/org/firebirdsql/jaybird/util/SearchPathHelper.java b/src/main/org/firebirdsql/jaybird/util/SearchPathHelper.java
new file mode 100644
index 000000000..a323c454b
--- /dev/null
+++ b/src/main/org/firebirdsql/jaybird/util/SearchPathHelper.java
@@ -0,0 +1,60 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jaybird.util;
+
+import org.firebirdsql.jaybird.parser.FirebirdReservedWords;
+import org.firebirdsql.jaybird.parser.SearchPathExtractor;
+import org.firebirdsql.jaybird.parser.SqlParser;
+import org.firebirdsql.jdbc.QuoteStrategy;
+import org.jspecify.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * Helpers for working with identifiers.
+ *
+ * @since 7
+ */
+public final class SearchPathHelper {
+
+ /**
+ * Parses the elements of the search path to a list of unquoted schema names.
+ *
+ * @param searchPath
+ * comma-separated search path, with — optionally — quoted schema names
+ * @return list of unquoted schema names, or empty if {@code searchPath} is {@code null}, blank or an invalid search
+ * path (e.g. not a comma-separated list of potential schema names, or unquoted schema names are reserved words)
+ */
+ public static List parseSearchPath(@Nullable String searchPath) {
+ if (searchPath == null || searchPath.isBlank()) return List.of();
+ var extractor = new SearchPathExtractor();
+ SqlParser.withReservedWords(FirebirdReservedWords.latest())
+ .withVisitor(extractor)
+ .of(searchPath)
+ .parse();
+ return extractor.getSearchPathList();
+ }
+
+ /**
+ * Creates a search path from {@code searchPathList}.
+ *
+ * @param searchPathList
+ * list of unquoted schema names, blank values are ignored
+ * @param quoteStrategy
+ * quote strategy
+ * @return comma and space separated search path, quoted according to {@code quoteStrategy}
+ */
+ public static String toSearchPath(List searchPathList, QuoteStrategy quoteStrategy) {
+ if (searchPathList.isEmpty()) return "";
+ // Assume each entry takes 15 characters, including quotes and separators
+ var sb = new StringBuilder(searchPathList.size() * 15);
+ for (String schema : searchPathList) {
+ if (schema.isBlank()) continue;
+ quoteStrategy.appendQuoted(schema, sb).append(", ");
+ }
+ // Remove last separator
+ sb.setLength(sb.length() - 2);
+ return sb.toString();
+ }
+
+}
diff --git a/src/main/org/firebirdsql/jdbc/ClientInfoProvider.java b/src/main/org/firebirdsql/jdbc/ClientInfoProvider.java
index 4ba27b864..7b3ae5df9 100644
--- a/src/main/org/firebirdsql/jdbc/ClientInfoProvider.java
+++ b/src/main/org/firebirdsql/jdbc/ClientInfoProvider.java
@@ -4,10 +4,8 @@
import org.firebirdsql.gds.ISCConstants;
import org.firebirdsql.gds.ng.FbExceptionBuilder;
-import org.firebirdsql.jdbc.InternalTransactionCoordinator.MetaDataTransactionCoordinator;
import java.sql.ClientInfoStatus;
-import java.sql.ResultSet;
import java.sql.SQLClientInfoException;
import java.sql.SQLException;
import java.sql.Statement;
@@ -54,11 +52,10 @@ final class ClientInfoProvider {
.collect(toUnmodifiableSet());
private final FBConnection connection;
+ // Holds statement used for setting or retrieving client info properties.
+ private final MetadataStatementHolder statementHolder;
// if null, use DEFAULT_CLIENT_INFO_PROPERTIES
private Set knownProperties;
- // Statement used for setting or retrieving client info properties.
- // We don't try to close this statement, and rely on it getting closed by connection close
- private Statement statement;
ClientInfoProvider(FBConnection connection) throws SQLException {
connection.checkValidity();
@@ -67,17 +64,11 @@ final class ClientInfoProvider {
"Required functionality (RDB$SET_CONTEXT()) only available in Firebird 2.0 or higher");
}
this.connection = connection;
+ statementHolder = new MetadataStatementHolder(connection);
}
private Statement getStatement() throws SQLException {
- Statement statement = this.statement;
- if (statement != null && !statement.isClosed()) return statement;
- var metaDataTransactionCoordinator = new MetaDataTransactionCoordinator(connection.txCoordinator);
- // Create statement which piggybacks on active transaction, starts one when needed, but does not commit (not
- // even in auto-commit)
- var rsBehavior = ResultSetBehavior.of(
- ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT);
- return this.statement = new FBStatement(connection, rsBehavior, metaDataTransactionCoordinator);
+ return statementHolder.getStatement();
}
/**
diff --git a/src/main/org/firebirdsql/jdbc/FBConnection.java b/src/main/org/firebirdsql/jdbc/FBConnection.java
index 06142604e..6853e49d2 100644
--- a/src/main/org/firebirdsql/jdbc/FBConnection.java
+++ b/src/main/org/firebirdsql/jdbc/FBConnection.java
@@ -4,7 +4,7 @@
SPDX-FileCopyrightText: Copyright 2002-2003 Blas Rodriguez Somoza
SPDX-FileCopyrightText: Copyright 2003 Nikolay Samofatov
SPDX-FileCopyrightText: Copyright 2007 Gabriel Reid
- SPDX-FileCopyrightText: Copyright 2011-2024 Mark Rotteveel
+ SPDX-FileCopyrightText: Copyright 2011-2025 Mark Rotteveel
SPDX-FileCopyrightText: Copyright 2016 Adriano dos Santos Fernandes
SPDX-License-Identifier: LGPL-2.1-or-later
*/
@@ -86,6 +86,7 @@ public class FBConnection implements FirebirdConnection {
private StoredProcedureMetaData storedProcedureMetaData;
private GeneratedKeysSupport generatedKeysSupport;
private ClientInfoProvider clientInfoProvider;
+ private SchemaChanger schemaChanger;
private boolean readOnly;
/**
@@ -577,7 +578,7 @@ private boolean isValidImpl(int timeout) {
}
@Override
- public DatabaseMetaData getMetaData() throws SQLException {
+ public FirebirdDatabaseMetaData getMetaData() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkValidity();
if (metaData == null)
@@ -1058,25 +1059,69 @@ public T unwrap(Class iface) throws SQLException {
/**
* {@inheritDoc}
*
- * Implementation ignores calls to this method as schemas are not supported.
+ * Schemas are supported on Firebird 6.0 and higher. On older Firebird versions, this method is silently ignored,
+ * except for a connection validity check. Contrary to specified in the JDBC API, calling this method
+ * will affect previously created {@link Statement} objects, and may affect
+ * previously created {@link CallableStatement} objects. That is because the current search path is applied when
+ * preparing a statement (which for {@code Statement} happens on execute, and for {@code CallableStatement} may be
+ * delayed until execute).
*
+ *
+ * This method modifies the search path of the connection by adding the specified {@code schema} as the first
+ * schema. This method does not check if the specified schema exists, so it may not actually change the current
+ * schema. Interleaving calls to this method with explicit execution of {@code SET SEARCH_PATH TO ...} may lead to
+ * undefined behaviour.
+ *
+ *
+ * @param schema
+ * correctly capitalized schema name, without quotes, {@code null} or blank is not allowed if
+ * schemas are supported
*/
@Override
public void setSchema(String schema) throws SQLException {
- // Ignore: no schema support
- checkValidity();
+ try (var ignored = withLock()) {
+ checkValidity();
+ getSchemaChanger().setSchema(schema);
+ }
}
/**
* {@inheritDoc}
+ *
+ * Schemas are supported on Firebird 6.0 and higher. The current schema is the value of {@code CURRENT_SCHEMA},
+ * which reports the first valid schema of the search path.
+ *
*
- * @return Always {@code null} as schemas ar not supported
+ * @return the current schema, on Firebird 5.0 and older always {@code null} as schemas ar not supported
*/
@Override
- @SuppressWarnings("java:S4144")
- public String getSchema() throws SQLException {
- checkValidity();
- return null;
+ public final String getSchema() throws SQLException {
+ return getSchemaInfo().schema();
+ }
+
+ @Override
+ public final String getSearchPath() throws SQLException {
+ return getSchemaInfo().searchPath();
+ }
+
+ @Override
+ public final List getSearchPathList() throws SQLException {
+ return getSchemaInfo().toSearchPathList();
+ }
+
+ private SchemaChanger.SchemaInfo getSchemaInfo() throws SQLException {
+ try (var ignored = withLock()) {
+ return getSchemaChanger().getCurrentSchemaInfo();
+ }
+ }
+
+ private SchemaChanger getSchemaChanger() throws SQLException {
+ try (var ignored = withLock()) {
+ checkValidity();
+ SchemaChanger schemaChanger = this.schemaChanger;
+ if (schemaChanger != null) return schemaChanger;
+ return this.schemaChanger = SchemaChanger.createInstance(this);
+ }
}
public void addWarning(SQLWarning warning) {
@@ -1380,7 +1425,7 @@ QuoteStrategy getQuoteStrategy() throws SQLException {
GeneratedKeysSupport getGeneratedKeysSupport() throws SQLException {
if (generatedKeysSupport == null) {
generatedKeysSupport = GeneratedKeysSupportFactory
- .createFor(getGeneratedKeysEnabled(), (FirebirdDatabaseMetaData) getMetaData());
+ .createFor(getGeneratedKeysEnabled(), getMetaData());
}
return generatedKeysSupport;
}
diff --git a/src/main/org/firebirdsql/jdbc/FirebirdConnection.java b/src/main/org/firebirdsql/jdbc/FirebirdConnection.java
index d6743c887..0af165010 100644
--- a/src/main/org/firebirdsql/jdbc/FirebirdConnection.java
+++ b/src/main/org/firebirdsql/jdbc/FirebirdConnection.java
@@ -1,15 +1,17 @@
// SPDX-FileCopyrightText: Copyright 2003-2005 Roman Rokytskyy
-// SPDX-FileCopyrightText: Copyright 2011-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2011-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later OR BSD-3-Clause
package org.firebirdsql.jdbc;
import org.firebirdsql.gds.TransactionParameterBuffer;
import org.firebirdsql.gds.ng.FbDatabase;
+import org.firebirdsql.jaybird.util.SearchPathHelper;
import org.firebirdsql.util.InternalApi;
import java.sql.Blob;
import java.sql.Connection;
import java.sql.SQLException;
+import java.util.List;
/**
* Extension of {@link Connection} interface providing access to Firebird specific features.
@@ -126,4 +128,29 @@ public interface FirebirdConnection extends Connection {
*/
void resetKnownClientInfoProperties();
+ /**
+ * Returns the schema search path.
+ *
+ * @return comma-separated list of quoted schema names of the search path, or {@code null} if schemas are not
+ * supported
+ * @throws SQLException
+ * if the connections is closed, or for database access errors
+ * @see #getSearchPathList()
+ * @since 7
+ */
+ String getSearchPath() throws SQLException;
+
+ /**
+ * Returns the schema search path as a list of unquoted schema names.
+ *
+ * @return list of unquoted schema names, or an empty list if schemas are not supported
+ * @throws SQLException
+ * if the connection is closed, or for database access errors
+ * @see #getSearchPath()
+ * @since 7
+ */
+ default List getSearchPathList() throws SQLException {
+ return SearchPathHelper.parseSearchPath(getSearchPath());
+ }
+
}
\ No newline at end of file
diff --git a/src/main/org/firebirdsql/jdbc/MetadataStatementHolder.java b/src/main/org/firebirdsql/jdbc/MetadataStatementHolder.java
new file mode 100644
index 000000000..670675225
--- /dev/null
+++ b/src/main/org/firebirdsql/jdbc/MetadataStatementHolder.java
@@ -0,0 +1,63 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jdbc;
+
+import org.firebirdsql.util.InternalApi;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A class that holds a single {@link Statement} for executing metadata queries.
+ *
+ * The statement returned by {@link #getStatement()} will piggyback on the active transaction, or start one when needed,
+ * but does not commit (not even in auto-commit).
+ *
+ *
+ * @since 7
+ */
+@InternalApi
+@NullMarked
+final class MetadataStatementHolder {
+
+ private final FBConnection connection;
+ private @Nullable FBStatement statement;
+
+ MetadataStatementHolder(FBConnection connection) {
+ this.connection = requireNonNull(connection, "connection");
+ }
+
+ /**
+ * Returns an {@link FBStatement} suitable for executing metadata statements.
+ *
+ * For efficiency reasons, it is recommended that callers do not close the returned statement. If this holder has no
+ * statement, or if it has been closed, a new statement will be created.
+ *
+ *
+ * @return statement
+ * @throws SQLException
+ * if the connection is closed or the statement could not be allocated
+ * @see MetadataStatementHolder
+ */
+ FBStatement getStatement() throws SQLException {
+ try (var ignored = connection.withLock()) {
+ FBStatement statement = this.statement;
+ if (statement != null && !statement.isClosed()) return statement;
+ return this.statement = createStatement();
+ }
+ }
+
+ private FBStatement createStatement() throws SQLException {
+ var metaDataTransactionCoordinator =
+ new InternalTransactionCoordinator.MetaDataTransactionCoordinator(connection.txCoordinator);
+ var rsBehavior = ResultSetBehavior.of(
+ ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT);
+ return new FBStatement(connection, rsBehavior, metaDataTransactionCoordinator);
+ }
+
+}
diff --git a/src/main/org/firebirdsql/jdbc/SchemaChanger.java b/src/main/org/firebirdsql/jdbc/SchemaChanger.java
new file mode 100644
index 000000000..2466b2b07
--- /dev/null
+++ b/src/main/org/firebirdsql/jdbc/SchemaChanger.java
@@ -0,0 +1,202 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jdbc;
+
+import org.firebirdsql.jaybird.util.SearchPathHelper;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+import java.sql.SQLDataException;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import static org.firebirdsql.util.FirebirdSupportInfo.supportInfoFor;
+
+/**
+ * Changes the current schema of a connection, reports on the current schema, and tracks the information necessary to
+ * correctly perform subsequent modifications.
+ *
+ * @author Mark Rotteveel
+ * @since 7
+ */
+@NullMarked
+sealed abstract class SchemaChanger {
+
+ /**
+ * If schemas are supported, attempts to change the current schema to {@code schema}.
+ *
+ * If {@code schema} is not an existing schema, the search path may be modified, but will not actually change
+ * the current schema. If schemas are not supported, this method is a no-op.
+ *
+ *
+ * The implementation tries to handle external search path changes, but correct functioning is
+ * not guaranteed if it is.
+ *
+ *
+ * @param schema
+ * new schema to set (non-{@code null} and not blank)
+ * @throws SQLException
+ * for database access errors, or if {@code schema} is {@code null} or blank if schemas are
+ * supported
+ */
+ abstract void setSchema(String schema) throws SQLException;
+
+ /**
+ * Current schema and search path.
+ *
+ * If schemas are not supported, an instance is returned with {@code schema} and {@code searchPath} {@code null}.
+ *
+ *
+ * @return current schema and search path
+ * @throws SQLException
+ * for database access errors
+ */
+ abstract SchemaInfo getCurrentSchemaInfo() throws SQLException;
+
+ /**
+ * Creates a schema changer.
+ *
+ * Depending on the Firebird version, the returned instance may ignore attempts to change the schema.
+ *
+ *
+ * @param connection
+ * connection
+ * @return a schema change (never {@code null})
+ * @throws SQLException
+ * for database access errors
+ */
+ static SchemaChanger createInstance(FBConnection connection) throws SQLException {
+ if (supportInfoFor(connection).supportsSchemas()) {
+ return new SchemaSupport(connection);
+ }
+ return NoSchemaSupport.getInstance();
+ }
+
+ /**
+ * Schema and search path.
+ *
+ * @param schema
+ * schema
+ * @param searchPath
+ * search path string
+ */
+ record SchemaInfo(@Nullable String schema, @Nullable String searchPath) {
+ static final SchemaInfo NULL_INSTANCE = new SchemaInfo(null, null);
+
+ List toSearchPathList() {
+ return SearchPathHelper.parseSearchPath(searchPath);
+ }
+
+ boolean searchPathEquals(SchemaInfo other) {
+ return Objects.equals(this.searchPath, other.searchPath);
+ }
+ }
+
+ /**
+ * Implementation for Firebird 6.0 and higher, which support schemas.
+ */
+ private static final class SchemaSupport extends SchemaChanger {
+
+ private final FBConnection connection;
+ // Holds statement used for querying and changing the schema
+ private final MetadataStatementHolder statementHolder;
+ private SchemaInfo schemaInfoAfterLastChange = SchemaInfo.NULL_INSTANCE;
+ /**
+ * {@code null} signifies no change recorded
+ */
+ private @Nullable String lastSchemaChange;
+ private List lastSearchPath = List.of();
+
+ SchemaSupport(FBConnection connection) throws SQLException {
+ connection.checkValidity();
+ if (!supportInfoFor(connection).supportsSchemas()) {
+ throw new FBDriverNotCapableException("Schema support is only available in Firebird 6.0 and higher");
+ }
+ this.connection = connection;
+ statementHolder = new MetadataStatementHolder(connection);
+ }
+
+ private Statement getStatement() throws SQLException {
+ return statementHolder.getStatement();
+ }
+
+ @Override
+ SchemaInfo getCurrentSchemaInfo() throws SQLException {
+ try (var rs = getStatement().executeQuery(
+ "select CURRENT_SCHEMA, RDB$GET_CONTEXT('SYSTEM', 'SEARCH_PATH') from SYSTEM.RDB$DATABASE")) {
+ rs.next();
+ return new SchemaInfo(rs.getString(1), rs.getString(2));
+ }
+ }
+
+ @Override
+ void setSchema(String schema) throws SQLException {
+ if (schema == null || schema.isBlank()) {
+ // TODO externalize?
+ throw new SQLDataException("schema must be non-null and not blank",
+ SQLStateConstants.SQL_STATE_INVALID_USE_NULL);
+ }
+ try (var ignored = connection.withLock()) {
+ SchemaInfo currentSchemaInfo = getCurrentSchemaInfo();
+ final List newSearchPath;
+ if (currentSchemaInfo.searchPathEquals(schemaInfoAfterLastChange)) {
+ // assume no changes
+ if (schema.equals(lastSchemaChange)) return;
+
+ // modify schema by replacing previous first schema with new first schema
+ newSearchPath = new ArrayList<>(lastSearchPath);
+ if (!newSearchPath.set(0, schema).equals(lastSchemaChange)) {
+ // TODO SQLstate, externalize?
+ throw new SQLException(("Expected first item in lastSearchPath to be '%s', but "
+ + "lastSearchPath was '%s'; this is probably a bug in Jaybird")
+ .formatted(lastSchemaChange, lastSearchPath));
+ }
+ } else {
+ List originalSearchPath = currentSchemaInfo.toSearchPathList();
+ if (lastSchemaChange == null
+ && !originalSearchPath.isEmpty() && schema.equals(originalSearchPath.get(0))) {
+ // Initial search path already has the specified schema first, don't change anything
+ return;
+ }
+ newSearchPath = new ArrayList<>(originalSearchPath.size() + 1);
+ newSearchPath.add(schema);
+ newSearchPath.addAll(originalSearchPath);
+ }
+
+ //noinspection SqlSourceToSinkFlow
+ getStatement().execute("set search_path to "
+ + SearchPathHelper.toSearchPath(newSearchPath, connection.getQuoteStrategy()));
+ schemaInfoAfterLastChange = getCurrentSchemaInfo();
+ lastSearchPath = List.copyOf(newSearchPath);
+ lastSchemaChange = schema;
+ }
+ }
+ }
+
+ /**
+ * Implementation for Firebird 5.0 and older, which do not support schemas.
+ */
+ static final class NoSchemaSupport extends SchemaChanger {
+
+ private static final SchemaChanger INSTANCE = new NoSchemaSupport();
+
+ @Override
+ void setSchema(String schema) {
+ // do nothing (not even validate the name)
+ }
+
+ @Override
+ SchemaInfo getCurrentSchemaInfo() {
+ return SchemaInfo.NULL_INSTANCE;
+ }
+
+ static SchemaChanger getInstance() {
+ return INSTANCE;
+ }
+
+ }
+
+}
diff --git a/src/main/org/firebirdsql/jdbc/StoredProcedureMetaDataFactory.java b/src/main/org/firebirdsql/jdbc/StoredProcedureMetaDataFactory.java
index b6eeec797..eab78c4c8 100644
--- a/src/main/org/firebirdsql/jdbc/StoredProcedureMetaDataFactory.java
+++ b/src/main/org/firebirdsql/jdbc/StoredProcedureMetaDataFactory.java
@@ -45,7 +45,7 @@ private static boolean connectionHasProcedureMetadata(FBConnection connection) t
if (connection.isIgnoreProcedureType()) {
return false;
}
- FirebirdDatabaseMetaData metaData = (FirebirdDatabaseMetaData) connection.getMetaData();
+ FirebirdDatabaseMetaData metaData = connection.getMetaData();
return versionEqualOrAboveFB21(metaData.getDatabaseMajorVersion(), metaData.getDatabaseMinorVersion())
&& versionEqualOrAboveFB21(metaData.getOdsMajorVersion(), metaData.getOdsMinorVersion());
diff --git a/src/test/org/firebirdsql/jaybird/parser/SearchPathExtractorTest.java b/src/test/org/firebirdsql/jaybird/parser/SearchPathExtractorTest.java
new file mode 100644
index 000000000..ef34da4a7
--- /dev/null
+++ b/src/test/org/firebirdsql/jaybird/parser/SearchPathExtractorTest.java
@@ -0,0 +1,47 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jaybird.parser;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class SearchPathExtractorTest {
+
+ private final SearchPathExtractor extractor = new SearchPathExtractor();
+
+ @Test
+ void initialSearchPathListIsEmpty() {
+ assertThat(extractor.getSearchPathList(), is(empty()));
+ }
+
+ @ParameterizedTest
+ @MethodSource("extractionTestCases")
+ void testSearchPathListExtraction(String searchPath, List expectedSearchPathList) {
+ SqlParser.withReservedWords(FirebirdReservedWords.latest())
+ .withVisitor(extractor)
+ .of(searchPath)
+ .parse();
+ assertEquals(expectedSearchPathList, extractor.getSearchPathList());
+ }
+
+ static Stream extractionTestCases() {
+ return Stream.of(
+ Arguments.of("", List.of()),
+ Arguments.of("\"PUBLIC\"", List.of("PUBLIC")),
+ Arguments.of("\"PUBLIC\",\"SYSTEM\"", List.of("PUBLIC", "SYSTEM")),
+ Arguments.of("UNQUOTED_SCHEMA,\"QUOTED_SCHEMA\"", List.of("UNQUOTED_SCHEMA", "QUOTED_SCHEMA")),
+ Arguments.of("INVALID,,TWO_SEPARATORS", List.of()),
+ Arguments.of("INVALID,ENDS_IN_SEPARATOR,", List.of()));
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/org/firebirdsql/jaybird/parser/SqlTokenizerTest.java b/src/test/org/firebirdsql/jaybird/parser/SqlTokenizerTest.java
index f74637cbe..4607c36d3 100644
--- a/src/test/org/firebirdsql/jaybird/parser/SqlTokenizerTest.java
+++ b/src/test/org/firebirdsql/jaybird/parser/SqlTokenizerTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2021-2022 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2021-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jaybird.parser;
@@ -223,6 +223,37 @@ void simpleSelectStatement() {
);
}
+ @Test
+ void simpleColumnList() {
+ String statementText = "select a,b, c, d , e from some_table";
+
+ var tokenizer = SqlTokenizer.withReservedWords(FirebirdReservedWords.latest())
+ .of(statementText);
+
+ assertThat(tokenizer).toIterable().containsExactly(
+ new ReservedToken(0, "select"),
+ new WhitespaceToken(6, " "),
+ new GenericToken(7, "a"),
+ new CommaToken(8),
+ new GenericToken(9, "b"),
+ new CommaToken(10),
+ new WhitespaceToken(11, " "),
+ new GenericToken(12, "c"),
+ new CommaToken(13),
+ new WhitespaceToken(14, " "),
+ new GenericToken(15, "d"),
+ new WhitespaceToken(16, " "),
+ new CommaToken(17),
+ new WhitespaceToken(18, " "),
+ new GenericToken(19, "e"),
+ new WhitespaceToken(20, " "),
+ new ReservedToken(21, "from"),
+ new WhitespaceToken(25, " "),
+ new GenericToken(26, "some_table")
+ );
+
+ }
+
private static void expectSingleToken(String input, Token expectedToken) {
SqlTokenizer tokenizer = SqlTokenizer.withReservedWords(FirebirdReservedWords.latest()).of(input);
diff --git a/src/test/org/firebirdsql/jaybird/util/SearchPathHelperTest.java b/src/test/org/firebirdsql/jaybird/util/SearchPathHelperTest.java
new file mode 100644
index 000000000..7cf894934
--- /dev/null
+++ b/src/test/org/firebirdsql/jaybird/util/SearchPathHelperTest.java
@@ -0,0 +1,57 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jaybird.util;
+
+import org.firebirdsql.jdbc.QuoteStrategy;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class SearchPathHelperTest {
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @ValueSource(strings = { " ", " "})
+ void parseSearchPath_emptyList_forNullOrEmptyOrBlank(String searchPath) {
+ assertThat(SearchPathHelper.parseSearchPath(searchPath), is(empty()));
+ }
+
+ @ParameterizedTest
+ @MethodSource("searchPathCases")
+ void parseSearchPath(String inputSearchPath, List expectedSearchPathList, QuoteStrategy ignored1,
+ String ignored2) {
+ assertEquals(expectedSearchPathList, SearchPathHelper.parseSearchPath(inputSearchPath));
+ }
+
+ @ParameterizedTest
+ @MethodSource("searchPathCases")
+ void toSearchPath(String ignored, List inputSearchPathList, QuoteStrategy quoteStrategy,
+ String expectedSearchPath) {
+ assertEquals(expectedSearchPath, SearchPathHelper.toSearchPath(inputSearchPathList, quoteStrategy));
+ }
+
+ static Stream searchPathCases() {
+ return Stream.of(
+ Arguments.of("", List.of(), QuoteStrategy.DIALECT_3, ""),
+ Arguments.of("", List.of(), QuoteStrategy.DIALECT_1, ""),
+ Arguments.of("PUBLIC, SYSTEM", List.of("PUBLIC", "SYSTEM"), QuoteStrategy.DIALECT_3,
+ "\"PUBLIC\", \"SYSTEM\""),
+ Arguments.of("PUBLIC, SYSTEM", List.of("PUBLIC", "SYSTEM"), QuoteStrategy.DIALECT_1, "PUBLIC, SYSTEM"),
+ Arguments.of("\"PUBLIC\", \"SYSTEM\"", List.of("PUBLIC", "SYSTEM"), QuoteStrategy.DIALECT_3,
+ "\"PUBLIC\", \"SYSTEM\""),
+ Arguments.of("\"PUBLIC\", \"SYSTEM\"", List.of("PUBLIC", "SYSTEM"), QuoteStrategy.DIALECT_1,
+ "PUBLIC, SYSTEM")
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/org/firebirdsql/jdbc/FBConnectionSchemaTest.java b/src/test/org/firebirdsql/jdbc/FBConnectionSchemaTest.java
new file mode 100644
index 000000000..2354f7181
--- /dev/null
+++ b/src/test/org/firebirdsql/jdbc/FBConnectionSchemaTest.java
@@ -0,0 +1,223 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jdbc;
+
+import org.firebirdsql.common.extension.UsesDatabaseExtension;
+import org.firebirdsql.common.extension.UsesDatabaseExtension.UsesDatabaseForAll;
+import org.firebirdsql.jaybird.props.PropertyNames;
+import org.firebirdsql.jaybird.util.SearchPathHelper;
+import org.firebirdsql.util.FirebirdSupportInfo;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.sql.Connection;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLDataException;
+import java.sql.SQLException;
+import java.util.List;
+
+import static org.firebirdsql.common.FBTestProperties.getConnectionViaDriverManager;
+import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
+import static org.firebirdsql.common.FbAssumptions.assumeFeature;
+import static org.firebirdsql.common.FbAssumptions.assumeFeatureMissing;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Tests for {@link FBConnection#setSchema(String)} and {@link FBConnection#getSchema()}, and other schema-specific
+ * tests of {@link FBConnection}.
+ */
+class FBConnectionSchemaTest {
+
+ @RegisterExtension
+ static UsesDatabaseForAll usesDatabaseForAll = UsesDatabaseExtension.usesDatabaseForAll(dbInitStatements());
+
+ private static List dbInitStatements() {
+ if (getDefaultSupportInfo().supportsSchemas()) {
+ return List.of(
+ "create schema SCHEMA_1",
+ "create schema \"case_sensitive\"",
+ "create table PUBLIC.TABLE_ONE (IN_PUBLIC integer)",
+ "create table SCHEMA_1.TABLE_ONE (IN_SCHEMA_1 integer)",
+ "create table \"case_sensitive\".TABLE_ONE (\"IN_case_sensitive\" integer)");
+ }
+ return List.of(
+ "create table TABLE_ONE (IN_ integer)");
+ }
+
+ @Test
+ void getSchema_noSupport_returnsNull() throws Exception {
+ assumeNoSchemaSupport();
+ try (var connection = getConnectionViaDriverManager()) {
+ assertNull(connection.getSchema(), "schema");
+ checkSchemaResolution(connection, "");
+ assertNull(connection.getSearchPath(), "searchPath");
+ assertThat("searchPathList", connection.getSearchPathList(), is(empty()));
+ }
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @ValueSource(strings = { " ", "PUBLIC", "SYSTEM" })
+ void setSchema_noSupport_ignoresValue(String schemaName) throws Exception {
+ assumeNoSchemaSupport();
+ try (var connection = getConnectionViaDriverManager()) {
+ assertDoesNotThrow(() -> connection.setSchema(schemaName));
+ assertNull(connection.getSchema(), "schema always null");
+ checkSchemaResolution(connection, "");
+ assertNull(connection.getSearchPath(), "searchPath");
+ assertThat("searchPathList", connection.getSearchPathList(), is(empty()));
+ }
+ }
+
+ @Test
+ void getSchema_default_PUBLIC() throws Exception {
+ assumeSchemaSupport();
+ try (var connection = getConnectionViaDriverManager()) {
+ assertEquals("PUBLIC", connection.getSchema(), "schema");
+ checkSchemaResolution(connection, "PUBLIC");
+ assertEquals(List.of("PUBLIC", "SYSTEM"), connection.getSearchPathList(), "searchPathList");
+ assertEquals("\"PUBLIC\", \"SYSTEM\"", connection.getSearchPath(), "searchPath");
+ }
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @ValueSource(strings = " ")
+ void setSchema_nullOrBlank_notAccepted(String schemaName) throws Exception {
+ assumeSchemaSupport();
+ try (var connection = getConnectionViaDriverManager()) {
+ var exception = assertThrows(SQLDataException.class, () -> connection.setSchema(schemaName));
+ assertEquals("schema must be non-null and not blank", exception.getMessage(), "message");
+ assertEquals(SQLStateConstants.SQL_STATE_INVALID_USE_NULL, exception.getSQLState(), "SQLState");
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "PUBLIC", "SCHEMA_1", "case_sensitive" })
+ void setSchema_exists(String schemaName) throws Exception {
+ assumeSchemaSupport();
+ try (var connection = getConnectionViaDriverManager()) {
+ connection.setSchema(schemaName);
+ assertEquals(schemaName, connection.getSchema(), "schema after schema change");
+ checkSchemaResolution(connection, schemaName);
+ // We leave the search path unmodified if the schema is already the first
+ final var expectedSearchPathList = "PUBLIC".equals(schemaName)
+ ? List.of("PUBLIC", "SYSTEM")
+ : List.of(schemaName, "PUBLIC", "SYSTEM");
+ assertEquals(expectedSearchPathList, connection.getSearchPathList(), "searchPathList");
+ final String expectedSearchPath =
+ SearchPathHelper.toSearchPath(expectedSearchPathList, QuoteStrategy.DIALECT_3);
+ assertEquals(expectedSearchPath, connection.getSearchPath(), "searchPath");
+ }
+ }
+
+ @Test
+ void setSchema_SYSTEM_first() throws Exception {
+ assumeSchemaSupport();
+ try (var connection = getConnectionViaDriverManager()) {
+ connection.setSchema("SYSTEM");
+ assertEquals("SYSTEM", connection.getSchema(), "schema after schema change");
+ // We're prepending the schema, leaving schemas later in the list untouched
+ final var expectedSearchPathList = List.of("SYSTEM", "PUBLIC", "SYSTEM");
+ assertEquals(expectedSearchPathList, connection.getSearchPathList(), "searchPathList");
+ final String expectedSearchPath = "\"SYSTEM\", \"PUBLIC\", \"SYSTEM\"";
+ assertEquals(expectedSearchPath, connection.getSearchPath(), "searchPath");
+ }
+ }
+
+ @Test
+ void setSchema_doesNotExist() throws Exception {
+ assumeSchemaSupport();
+ try (var connection = getConnectionViaDriverManager()) {
+ assertDoesNotThrow(() -> connection.setSchema("DOES_NOT_EXIST"));
+ assertEquals("PUBLIC", connection.getSchema(),
+ "current schema not changed after setting non-existent schema");
+ // non-existent schema is included in the search path
+ final var expectedSearchPathList = List.of("DOES_NOT_EXIST", "PUBLIC", "SYSTEM");
+ assertEquals(expectedSearchPathList, connection.getSearchPathList(), "searchPathList");
+ final String expectedSearchPath = "\"DOES_NOT_EXIST\", \"PUBLIC\", \"SYSTEM\"";
+ assertEquals(expectedSearchPath, connection.getSearchPath(), "searchPath");
+ }
+ }
+
+ @Test
+ void setSchema_sequenceOfInvocations() throws Exception {
+ assumeSchemaSupport();
+ try (var connection = getConnectionViaDriverManager()) {
+ connection.setSchema("SCHEMA_1");
+ assertEquals("SCHEMA_1", connection.getSchema(), "schema after SCHEMA_1");
+ assertEquals(List.of("SCHEMA_1", "PUBLIC", "SYSTEM"), connection.getSearchPathList(),
+ "searchPathList after SCHEMA_1");
+ checkSchemaResolution(connection, "SCHEMA_1");
+
+ connection.setSchema("DOES_NOT_EXIST");
+ assertEquals("PUBLIC", connection.getSchema(), "schema after DOES_NOT_EXIST");
+ assertEquals(List.of("DOES_NOT_EXIST", "PUBLIC", "SYSTEM"), connection.getSearchPathList(),
+ "searchPathList after DOES_NOT_EXIST");
+ checkSchemaResolution(connection, "PUBLIC");
+
+ connection.setSchema("case_sensitive");
+ assertEquals("case_sensitive", connection.getSchema(), "schema after case_sensitive");
+ assertEquals(List.of("case_sensitive", "PUBLIC", "SYSTEM"), connection.getSearchPathList(),
+ "searchPathList after case_sensitive");
+ checkSchemaResolution(connection, "case_sensitive");
+
+ connection.setSchema("PUBLIC");
+ assertEquals("PUBLIC", connection.getSchema(), "schema after PUBLIC");
+ assertEquals(List.of("PUBLIC", "PUBLIC", "SYSTEM"), connection.getSearchPathList(),
+ "searchPathList after PUBLIC");
+ checkSchemaResolution(connection, "PUBLIC");
+ }
+ }
+
+ // There is some overlap with the searchPath tests in ConnectionPropertiesTest
+ @ParameterizedTest
+ @CsvSource(useHeadersInDisplayName = true, textBlock = """
+ searchPath, expectedSchema, expectedSearchPath
+ PUBLIC, PUBLIC, '"PUBLIC", "SYSTEM"'
+ 'PUBLIC, SYSTEM', PUBLIC, '"PUBLIC", "SYSTEM"'
+ public, PUBLIC, '"PUBLIC", "SYSTEM"'
+ "public", SYSTEM, '"public", "SYSTEM"'
+ SCHEMA_1, SCHEMA_1, '"SCHEMA_1", "SYSTEM"'
+ "case_sensitive", case_sensitive, '"case_sensitive", "SYSTEM"'
+ # NOTE Unquoted!
+ case_sensitive, SYSTEM, '"CASE_SENSITIVE", "SYSTEM"'
+ 'SCHEMA_1, "case_sensitive", SYSTEM, PUBLIC', SCHEMA_1, '"SCHEMA_1", "case_sensitive", "SYSTEM", "PUBLIC"'
+ """)
+ void connectionSearchPath(String searchPath, String expectedSchema, String expectedSearchPath) throws SQLException {
+ assumeSchemaSupport();
+ try (var connection = getConnectionViaDriverManager(PropertyNames.searchPath, searchPath)) {
+ assertEquals(expectedSchema, connection.getSchema(), "schema");
+ assertEquals(expectedSearchPath, connection.getSearchPath(), "searchPath");
+ assertEquals(SearchPathHelper.parseSearchPath(expectedSearchPath), connection.getSearchPathList(),
+ "searchPathList");
+ }
+ }
+
+ private static void checkSchemaResolution(Connection connection, String expectedSchema) throws SQLException {
+ try (var pstmt = connection.prepareStatement("select * from TABLE_ONE")) {
+ ResultSetMetaData rsmd = pstmt.getMetaData();
+ assertEquals(expectedSchema, rsmd.getSchemaName(1), "column schemaName");
+ assertEquals("TABLE_ONE", rsmd.getTableName(1), "column tableName");
+ assertEquals("IN_" + expectedSchema, rsmd.getColumnName(1), "column name");
+ }
+ }
+
+ private static void assumeSchemaSupport() {
+ assumeFeature(FirebirdSupportInfo::supportsSchemas, "Test expects schema support");
+ }
+
+ private static void assumeNoSchemaSupport() {
+ assumeFeatureMissing(FirebirdSupportInfo::supportsSchemas, "Test expects no schema support");
+ }
+}
From 6a7b308f62fb56edf8f8af839d356c1a4ba6f0be Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Sat, 12 Jul 2025 12:16:23 +0200
Subject: [PATCH 32/64] #882 Additional schema tests for FBResultSetMetaData
---
.../FBResultSetMetaDataParametrizedTest.java | 97 +++++++++++--------
.../jdbc/FBResultSetMetaDataTest.java | 30 +++++-
2 files changed, 84 insertions(+), 43 deletions(-)
diff --git a/src/test/org/firebirdsql/jdbc/FBResultSetMetaDataParametrizedTest.java b/src/test/org/firebirdsql/jdbc/FBResultSetMetaDataParametrizedTest.java
index 26cb7cc6c..ae65179d3 100644
--- a/src/test/org/firebirdsql/jdbc/FBResultSetMetaDataParametrizedTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBResultSetMetaDataParametrizedTest.java
@@ -38,11 +38,10 @@
*
* @author Mark Rotteveel
* @since 3.0
+ * @see FBResultSetMetaDataTest
*/
class FBResultSetMetaDataParametrizedTest {
- // TODO Add schema support: tests involving other schema
-
private static final String TABLE_NAME = "TEST_P_METADATA";
private static final String CREATE_TABLE = """
CREATE TABLE test_p_metadata (
@@ -85,7 +84,16 @@ bigint_decimal DECIMAL(18,9),
/* extended numerics */
/* time zone */
/* int128 */
- FROM test_p_metadata""";
+ , column_from_secondary as secondary_aliased
+ FROM test_p_metadata cross join SECONDARY_TABLE""";
+
+ private static final String OTHER_SCHEMA = "OTHER_SCHEMA";
+ private static final String SECONDARY_TABLE_NAME = "SECONDARY_TABLE";
+
+ private static final String CREATE_SECONDARY_TABLE = """
+ create table SECONDARY_TABLE (
+ column_from_secondary integer
+ )""";
@RegisterExtension
static final UsesDatabaseExtension.UsesDatabaseForAll usesDatabase = UsesDatabaseExtension.usesDatabaseForAll();
@@ -101,6 +109,7 @@ static void setupAll() throws Exception {
supportInfo = supportInfoFor(connection);
String createTable = CREATE_TABLE;
+ String createSecondaryTable = CREATE_SECONDARY_TABLE;
String testQuery = TEST_QUERY;
if (!supportInfo.supportsBigint()) {
// No BIGINT support, replacing type so number of columns remain the same
@@ -129,8 +138,19 @@ static void setupAll() throws Exception {
createTable =createTable.replace("/* int128 */", ", col_int128 INT128");
testQuery = testQuery.replace("/* int128 */", ", col_int128");
}
+ if (supportInfo.supportsSchemas()) {
+ createSecondaryTable = createSecondaryTable.replace(
+ SECONDARY_TABLE_NAME, OTHER_SCHEMA + "." + SECONDARY_TABLE_NAME);
+ testQuery = testQuery.replace(SECONDARY_TABLE_NAME, OTHER_SCHEMA + "." + SECONDARY_TABLE_NAME);
+ }
+ connection.setAutoCommit(false);
DdlHelper.executeCreateTable(connection, createTable);
+ if (supportInfo.supportsSchemas()) {
+ DdlHelper.executeDDL(connection, "create schema " + OTHER_SCHEMA);
+ }
+ DdlHelper.executeCreateTable(connection, createSecondaryTable);
+ connection.setAutoCommit(true);
pstmt = connection.prepareStatement(testQuery);
rsmd = pstmt.getMetaData();
@@ -150,47 +170,49 @@ static void tearDownAll() throws Exception {
static Stream testData() {
final boolean supportsFloatBinaryPrecision = getDefaultSupportInfo().supportsFloatBinaryPrecision();
+ final String defaultSchema = ifSchemaElse("PUBLIC", "");
List testData = new ArrayList<>(Arrays.asList(
- create(1, "java.lang.String", 60, "SIMPLE_FIELD", "SIMPLE_FIELD", VARCHAR, "VARCHAR", 60, 0, TABLE_NAME, columnNullable, true, false),
- create(2, "java.lang.String", 60, "TWO_BYTE_FIELD", "TWO_BYTE_FIELD", VARCHAR, "VARCHAR", 60, 0, TABLE_NAME, columnNullable, true, false),
- create(3, "java.lang.String", 60, "THREE_BYTE_FIELD", "THREE_BYTE_FIELD", VARCHAR, "VARCHAR", 60, 0, TABLE_NAME, columnNullable, true, false),
- create(4, "java.lang.Long", 20, "LONG_FIELD", "LONG_FIELD", BIGINT, "BIGINT", 19, 0, TABLE_NAME, columnNullable, true, true),
- create(5, "java.lang.Integer", 11, "INT_FIELD", "INT_FIELD", INTEGER, "INTEGER", 10, 0, TABLE_NAME, columnNullable, true, true),
- create(6, "java.lang.Integer", 6, "SHORT_FIELD", "SHORT_FIELD", SMALLINT, "SMALLINT", 5, 0, TABLE_NAME, columnNullable, true, true),
- create(7, "java.lang.Double", 13, "FLOAT_FIELD", "FLOAT_FIELD", FLOAT, "FLOAT", supportsFloatBinaryPrecision ? 24 : 7, 0, TABLE_NAME, columnNullable, true, true),
- create(8, "java.lang.Double", 22, "DOUBLE_FIELD", "DOUBLE_FIELD", DOUBLE, "DOUBLE PRECISION", supportsFloatBinaryPrecision ? 53 : 15, 0, TABLE_NAME, columnNullable, true, true),
- create(9, "java.math.BigDecimal", 5, "SMALLINT_NUMERIC", "SMALLINT_NUMERIC", NUMERIC, "NUMERIC", 3, 1, TABLE_NAME, columnNullable, true, true),
- create(10, "java.math.BigDecimal", 5, "INTEGER_DECIMAL_1", "INTEGER_DECIMAL_1", DECIMAL, "DECIMAL", 3, 1, TABLE_NAME, columnNullable, true, true),
- create(11, "java.math.BigDecimal", 7, "INTEGER_NUMERIC", "INTEGER_NUMERIC", NUMERIC, "NUMERIC", 5, 2, TABLE_NAME, columnNullable, true, true),
- create(12, "java.math.BigDecimal", 11, "INTEGER_DECIMAL_2", "INTEGER_DECIMAL_2", DECIMAL, "DECIMAL", 9, 3, TABLE_NAME, columnNullable, true, true),
- create(13, "java.math.BigDecimal", 12, "BIGINT_NUMERIC", "BIGINT_NUMERIC", NUMERIC, "NUMERIC", 10, 4, TABLE_NAME, columnNullable, true, true),
- create(14, "java.math.BigDecimal", 20, "BIGINT_DECIMAL", "BIGINT_DECIMAL", DECIMAL, "DECIMAL", 18, 9, TABLE_NAME, columnNullable, true, true),
- create(15, "java.sql.Date", 10, "DATE_FIELD", "DATE_FIELD", DATE, "DATE", 10, 0, TABLE_NAME, columnNullable, true, false),
- create(16, "java.sql.Time", 8, "TIME_FIELD", "TIME_FIELD", TIME, "TIME", 8, 0, TABLE_NAME, columnNullable, true, false),
- create(17, "java.sql.Timestamp", 19, "TIMESTAMP_FIELD", "TIMESTAMP_FIELD", TIMESTAMP, "TIMESTAMP", 19, 0, TABLE_NAME, columnNullable, true, false),
- create(18, "[B", 0, "BLOB_FIELD", "BLOB_FIELD", LONGVARBINARY, "BLOB SUB_TYPE BINARY", 0, 0, TABLE_NAME, columnNullable, false, false),
- create(19, "java.lang.String", 0, "BLOB_TEXT_FIELD", "BLOB_TEXT_FIELD", LONGVARCHAR, "BLOB SUB_TYPE TEXT", 0, 0, TABLE_NAME, columnNullable, false, false),
- create(20, "java.sql.Blob", 0, "BLOB_MINUS_ONE", "BLOB_MINUS_ONE", BLOB, "BLOB SUB_TYPE -1", 0, 0, TABLE_NAME, columnNullable, false, false)
+ create(1, "java.lang.String", 60, "SIMPLE_FIELD", "SIMPLE_FIELD", VARCHAR, "VARCHAR", 60, 0, defaultSchema, TABLE_NAME, columnNullable, true, false),
+ create(2, "java.lang.String", 60, "TWO_BYTE_FIELD", "TWO_BYTE_FIELD", VARCHAR, "VARCHAR", 60, 0, defaultSchema, TABLE_NAME, columnNullable, true, false),
+ create(3, "java.lang.String", 60, "THREE_BYTE_FIELD", "THREE_BYTE_FIELD", VARCHAR, "VARCHAR", 60, 0, defaultSchema, TABLE_NAME, columnNullable, true, false),
+ create(4, "java.lang.Long", 20, "LONG_FIELD", "LONG_FIELD", BIGINT, "BIGINT", 19, 0, defaultSchema, TABLE_NAME, columnNullable, true, true),
+ create(5, "java.lang.Integer", 11, "INT_FIELD", "INT_FIELD", INTEGER, "INTEGER", 10, 0, defaultSchema, TABLE_NAME, columnNullable, true, true),
+ create(6, "java.lang.Integer", 6, "SHORT_FIELD", "SHORT_FIELD", SMALLINT, "SMALLINT", 5, 0, defaultSchema, TABLE_NAME, columnNullable, true, true),
+ create(7, "java.lang.Double", 13, "FLOAT_FIELD", "FLOAT_FIELD", FLOAT, "FLOAT", supportsFloatBinaryPrecision ? 24 : 7, 0, defaultSchema, TABLE_NAME, columnNullable, true, true),
+ create(8, "java.lang.Double", 22, "DOUBLE_FIELD", "DOUBLE_FIELD", DOUBLE, "DOUBLE PRECISION", supportsFloatBinaryPrecision ? 53 : 15, 0, defaultSchema, TABLE_NAME, columnNullable, true, true),
+ create(9, "java.math.BigDecimal", 5, "SMALLINT_NUMERIC", "SMALLINT_NUMERIC", NUMERIC, "NUMERIC", 3, 1, defaultSchema, TABLE_NAME, columnNullable, true, true),
+ create(10, "java.math.BigDecimal", 5, "INTEGER_DECIMAL_1", "INTEGER_DECIMAL_1", DECIMAL, "DECIMAL", 3, 1, defaultSchema, TABLE_NAME, columnNullable, true, true),
+ create(11, "java.math.BigDecimal", 7, "INTEGER_NUMERIC", "INTEGER_NUMERIC", NUMERIC, "NUMERIC", 5, 2, defaultSchema, TABLE_NAME, columnNullable, true, true),
+ create(12, "java.math.BigDecimal", 11, "INTEGER_DECIMAL_2", "INTEGER_DECIMAL_2", DECIMAL, "DECIMAL", 9, 3, defaultSchema, TABLE_NAME, columnNullable, true, true),
+ create(13, "java.math.BigDecimal", 12, "BIGINT_NUMERIC", "BIGINT_NUMERIC", NUMERIC, "NUMERIC", 10, 4, defaultSchema, TABLE_NAME, columnNullable, true, true),
+ create(14, "java.math.BigDecimal", 20, "BIGINT_DECIMAL", "BIGINT_DECIMAL", DECIMAL, "DECIMAL", 18, 9, defaultSchema, TABLE_NAME, columnNullable, true, true),
+ create(15, "java.sql.Date", 10, "DATE_FIELD", "DATE_FIELD", DATE, "DATE", 10, 0, defaultSchema, TABLE_NAME, columnNullable, true, false),
+ create(16, "java.sql.Time", 8, "TIME_FIELD", "TIME_FIELD", TIME, "TIME", 8, 0, defaultSchema, TABLE_NAME, columnNullable, true, false),
+ create(17, "java.sql.Timestamp", 19, "TIMESTAMP_FIELD", "TIMESTAMP_FIELD", TIMESTAMP, "TIMESTAMP", 19, 0, defaultSchema, TABLE_NAME, columnNullable, true, false),
+ create(18, "[B", 0, "BLOB_FIELD", "BLOB_FIELD", LONGVARBINARY, "BLOB SUB_TYPE BINARY", 0, 0, defaultSchema, TABLE_NAME, columnNullable, false, false),
+ create(19, "java.lang.String", 0, "BLOB_TEXT_FIELD", "BLOB_TEXT_FIELD", LONGVARCHAR, "BLOB SUB_TYPE TEXT", 0, 0, defaultSchema, TABLE_NAME, columnNullable, false, false),
+ create(20, "java.sql.Blob", 0, "BLOB_MINUS_ONE", "BLOB_MINUS_ONE", BLOB, "BLOB SUB_TYPE -1", 0, 0, defaultSchema, TABLE_NAME, columnNullable, false, false)
));
final FirebirdSupportInfo supportInfo = getDefaultSupportInfo();
if (supportInfo.supportsBoolean()) {
- testData.add(create(testData.size() + 1, "java.lang.Boolean", 5, "BOOLEAN_FIELD", "BOOLEAN_FIELD", BOOLEAN, "BOOLEAN", 1, 0, TABLE_NAME, columnNullable, true, false));
+ testData.add(create(testData.size() + 1, "java.lang.Boolean", 5, "BOOLEAN_FIELD", "BOOLEAN_FIELD", BOOLEAN, "BOOLEAN", 1, 0, defaultSchema, TABLE_NAME, columnNullable, true, false));
}
if (supportInfo.supportsDecfloat()) {
- testData.add(create(testData.size() + 1, "java.math.BigDecimal", 23, "DECFLOAT16_FIELD", "DECFLOAT16_FIELD", JaybirdTypeCodes.DECFLOAT, "DECFLOAT", 16, 0, TABLE_NAME, columnNullable, true, true));
- testData.add(create(testData.size() + 1, "java.math.BigDecimal", 42, "DECFLOAT34_FIELD", "DECFLOAT34_FIELD", JaybirdTypeCodes.DECFLOAT, "DECFLOAT", 34, 0, TABLE_NAME, columnNullable, true, true));
+ testData.add(create(testData.size() + 1, "java.math.BigDecimal", 23, "DECFLOAT16_FIELD", "DECFLOAT16_FIELD", JaybirdTypeCodes.DECFLOAT, "DECFLOAT", 16, 0, defaultSchema, TABLE_NAME, columnNullable, true, true));
+ testData.add(create(testData.size() + 1, "java.math.BigDecimal", 42, "DECFLOAT34_FIELD", "DECFLOAT34_FIELD", JaybirdTypeCodes.DECFLOAT, "DECFLOAT", 34, 0, defaultSchema, TABLE_NAME, columnNullable, true, true));
}
if (supportInfo.supportsDecimalPrecision(38)) {
- testData.add(create(testData.size() + 1, "java.math.BigDecimal", 27, "COL_NUMERIC25_20", "COL_NUMERIC25_20", NUMERIC, "NUMERIC", 25, 20, TABLE_NAME, columnNullable, true, true));
- testData.add(create(testData.size() + 1, "java.math.BigDecimal", 32, "COL_DECIMAL30_5", "COL_DECIMAL30_5", DECIMAL, "DECIMAL", 30, 5, TABLE_NAME, columnNullable, true, true));
+ testData.add(create(testData.size() + 1, "java.math.BigDecimal", 27, "COL_NUMERIC25_20", "COL_NUMERIC25_20", NUMERIC, "NUMERIC", 25, 20, defaultSchema, TABLE_NAME, columnNullable, true, true));
+ testData.add(create(testData.size() + 1, "java.math.BigDecimal", 32, "COL_DECIMAL30_5", "COL_DECIMAL30_5", DECIMAL, "DECIMAL", 30, 5, defaultSchema, TABLE_NAME, columnNullable, true, true));
}
if (supportInfo.supportsTimeZones()) {
- testData.add(create(testData.size() + 1, "java.time.OffsetTime", 19, "COL_TIMETZ", "COL_TIMETZ", TIME_WITH_TIMEZONE, "TIME WITH TIME ZONE", 19, 0, TABLE_NAME, columnNullable, true, false));
- testData.add(create(testData.size() + 1, "java.time.OffsetDateTime", 30, "COL_TIMESTAMPTZ", "COL_TIMESTAMPTZ", TIMESTAMP_WITH_TIMEZONE, "TIMESTAMP WITH TIME ZONE", 30, 0, TABLE_NAME, columnNullable, true, false));
+ testData.add(create(testData.size() + 1, "java.time.OffsetTime", 19, "COL_TIMETZ", "COL_TIMETZ", TIME_WITH_TIMEZONE, "TIME WITH TIME ZONE", 19, 0, defaultSchema, TABLE_NAME, columnNullable, true, false));
+ testData.add(create(testData.size() + 1, "java.time.OffsetDateTime", 30, "COL_TIMESTAMPTZ", "COL_TIMESTAMPTZ", TIMESTAMP_WITH_TIMEZONE, "TIMESTAMP WITH TIME ZONE", 30, 0, defaultSchema, TABLE_NAME, columnNullable, true, false));
}
if (supportInfo.supportsInt128()) {
- testData.add(create(testData.size() + 1, "java.math.BigDecimal", 40, "COL_INT128", "COL_INT128", NUMERIC, "INT128", 38, 0, TABLE_NAME, columnNullable, true, true));
+ testData.add(create(testData.size() + 1, "java.math.BigDecimal", 40, "COL_INT128", "COL_INT128", NUMERIC, "INT128", 38, 0, defaultSchema, TABLE_NAME, columnNullable, true, true));
}
+ testData.add(create(testData.size() + 1, "java.lang.Integer", 11, "SECONDARY_ALIASED", "COLUMN_FROM_SECONDARY", INTEGER, "INTEGER", 10, 0, ifSchemaElse(OTHER_SCHEMA, ""), SECONDARY_TABLE_NAME, columnNullable, true, true));
return testData.stream();
}
@@ -258,8 +280,8 @@ void testGetScale(Integer columnIndex, ResultSetMetaDataInfo expectedMetaData, S
@ParameterizedTest(name = "Index {0} ({2})")
@MethodSource("testData")
- void testGetSchemaName(Integer columnIndex, ResultSetMetaDataInfo ignored1, String ignored2) throws Exception {
- assertEquals(supportInfo.ifSchemaElse("PUBLIC", ""), rsmd.getSchemaName(columnIndex), "getSchemaName");
+ void testGetSchemaName(Integer columnIndex, ResultSetMetaDataInfo expectedMetaData, String ignored2) throws Exception {
+ assertEquals(expectedMetaData.schemaName, rsmd.getSchemaName(columnIndex), "getSchemaName");
}
@ParameterizedTest(name = "Index {0} ({2})")
@@ -327,16 +349,15 @@ void testIsWritable(Integer columnIndex, ResultSetMetaDataInfo ignored1, String
@SuppressWarnings("SameParameterValue")
private static Arguments create(int index, String className, int displaySize, String label, String name, int type,
- String typeName, int precision, int scale, String tableName, int nullable, boolean searchable,
- boolean signed) {
+ String typeName, int precision, int scale, String schemaName, String tableName, int nullable,
+ boolean searchable, boolean signed) {
return Arguments.of(index,
new ResultSetMetaDataInfo(className, displaySize, label, name, type, typeName, precision, scale,
- tableName, nullable, searchable, signed),
- label);
+ schemaName, tableName, nullable, searchable, signed), label);
}
private record ResultSetMetaDataInfo(
String className, int displaySize, String label, String name, int type, String typeName, int precision,
- int scale, String tableName, int nullable, boolean searchable, boolean signed) {
+ int scale, String schemaName, String tableName, int nullable, boolean searchable, boolean signed) {
}
}
diff --git a/src/test/org/firebirdsql/jdbc/FBResultSetMetaDataTest.java b/src/test/org/firebirdsql/jdbc/FBResultSetMetaDataTest.java
index 4af6ac0f0..9702029b4 100644
--- a/src/test/org/firebirdsql/jdbc/FBResultSetMetaDataTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBResultSetMetaDataTest.java
@@ -4,7 +4,7 @@
SPDX-FileCopyrightText: Copyright 2003 Ryan Baldwin
SPDX-FileCopyrightText: Copyright 2003 Nikolay Samofatov
SPDX-FileCopyrightText: Copyright 2005 Steven Jardine
- SPDX-FileCopyrightText: Copyright 2012-2024 Mark Rotteveel
+ SPDX-FileCopyrightText: Copyright 2012-2025 Mark Rotteveel
SPDX-License-Identifier: LGPL-2.1-or-later
*/
package org.firebirdsql.jdbc;
@@ -45,6 +45,7 @@
*
* @author Roman Rokytskyy
* @author Mark Rotteveel
+ * @see FBResultSetMetaDataParametrizedTest
*/
class FBResultSetMetaDataTest {
@@ -324,16 +325,12 @@ void getTableAliasCTE() throws Exception {
"select * from (select column1 from tablea b) a"
}) {
try (ResultSet rs = stmt.executeQuery(query)) {
-// System.out.println(query);
FirebirdResultSetMetaData rsmd = rs.getMetaData().unwrap(FirebirdResultSetMetaData.class);
final String columnLabel = rsmd.getColumnLabel(1);
final String tableAlias = rsmd.getTableAlias(1);
-// System.out.println("'" + columnLabel + "'");
-// System.out.println("'" + tableAlias + "'");
assertEquals("COLUMN1", columnLabel, "columnLabel");
assertEquals("A", tableAlias, "tableAlias");
}
-// System.out.println("---------");
}
}
}
@@ -382,6 +379,7 @@ void getPrecision_connectionLessResultSet_shouldSucceedWithoutException_730() th
.at(0).simple(SQL_FLOAT, 4, "TEST", "FLOAT").addField()
.at(1).simple(SQL_DOUBLE, 8, "TEST", "DOUBLE").addField()
.toRowDescriptor();
+ //noinspection resource
var rs = new FBResultSet(rowDescriptor, List.of());
ResultSetMetaData rsmd = rs.getMetaData();
@@ -487,4 +485,26 @@ void extendedInfoQueryDoesNotCloseResultSet() throws Exception {
}
}
+ @Test
+ void expressionInSelect() throws Exception {
+ try (var connection = getConnectionViaDriverManager();
+ var stmt = connection.createStatement()) {
+ try (var rs = stmt.executeQuery("select id, long_field + 1 as EXPRESSION from test_rs_metadata")) {
+ ResultSetMetaData rsmd = rs.getMetaData();
+ assertColumnMetadata(rsmd, 1, ifSchemaElse("PUBLIC", ""), "TEST_RS_METADATA", "ID", "ID",
+ Types.INTEGER);
+ assertColumnMetadata(rsmd, 2, "", "", "EXPRESSION", "ADD", Types.NUMERIC);
+ }
+ }
+ }
+
+ static void assertColumnMetadata(ResultSetMetaData rsmd, int idx, String schema, String table, String label,
+ String name, int type) throws SQLException {
+ assertEquals(schema, rsmd.getSchemaName(idx), "schemaName");
+ assertEquals(table, rsmd.getTableName(idx), "tableName");
+ assertEquals(label, rsmd.getColumnLabel(idx), "columnLabel");
+ assertEquals(name, rsmd.getColumnName(idx), "columnName");
+ assertEquals(type, rsmd.getColumnType(idx), "columnType");
+ }
+
}
\ No newline at end of file
From aa3d585c21e908f62c060acf5adb2305e34e0099 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Sun, 13 Jul 2025 13:26:36 +0200
Subject: [PATCH 33/64] #882 Modify FBRowUpdater to search for schema
---
.../jaybird/util/QualifiedName.java | 122 ++++++++++++++++++
.../jdbc/AbstractFieldMetaData.java | 10 +-
.../org/firebirdsql/jdbc/FBRowUpdater.java | 68 +++++-----
.../jaybird/util/QualifiedNameTest.java | 103 +++++++++++++++
.../org/firebirdsql/jdbc/FBResultSetTest.java | 66 +++++++---
5 files changed, 314 insertions(+), 55 deletions(-)
create mode 100644 src/main/org/firebirdsql/jaybird/util/QualifiedName.java
create mode 100644 src/test/org/firebirdsql/jaybird/util/QualifiedNameTest.java
diff --git a/src/main/org/firebirdsql/jaybird/util/QualifiedName.java b/src/main/org/firebirdsql/jaybird/util/QualifiedName.java
new file mode 100644
index 000000000..cd63cd119
--- /dev/null
+++ b/src/main/org/firebirdsql/jaybird/util/QualifiedName.java
@@ -0,0 +1,122 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jaybird.util;
+
+import org.firebirdsql.gds.ng.fields.FieldDescriptor;
+import org.firebirdsql.jdbc.QuoteStrategy;
+import org.jspecify.annotations.Nullable;
+
+import java.util.Optional;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.Objects.requireNonNullElse;
+
+/**
+ * An object name qualified by a schema.
+ *
+ * NOTE: This class is not usable for packaged objects, as it has only one level of qualification.
+ *
+ *
+ * @param schema
+ * unquoted, case-sensitive schema name, {@code null} is returned as empty string ({@code ""})
+ * @param object
+ * unquoted, case-sensitive object name (e.g. a table name)
+ * @since 7
+ */
+public record QualifiedName(String schema, String object) {
+
+ /**
+ * Canonical constructor.
+ *
+ * @param schema
+ * unquoted, case-sensitive schema name, {@code null} is returned as empty string ({@code ""})
+ * @param object
+ * unquoted, case-sensitive object name (e.g. a table name)
+ * @throws IllegalArgumentException if {@code object} is blank
+ */
+ public QualifiedName(@Nullable String schema, String object) {
+ // defined explicitly to annotate @Nullable on schema for constructor only
+ this.schema = requireNonNullElse(schema, "");
+ if (object.isBlank()) {
+ throw new IllegalArgumentException("object cannot be blank");
+ }
+ this.object = requireNonNull(object, "object");
+ }
+
+ @Override
+ public String schema() {
+ return schema;
+ }
+
+ /**
+ * Estimated length of the quoted identifier.
+ *
+ * The estimate might be of if {@link #schema()} or {@link #object()} contains double quotes, or if
+ * {@link QuoteStrategy#DIALECT_1} is used.
+ *
+ *
+ * This can be used for pre-sizing a string builder for {@link #append(StringBuilder, QuoteStrategy)}.
+ *
+ *
+ * @return estimated length of the quoted identifier
+ */
+ public int estimatedLength() {
+ // 2: double quotes, 1: separator
+ return (schema.isEmpty() ? 0 : 2 + schema.length() + 1) + 2 + object.length();
+ }
+
+ /**
+ * Produces the string of the identifier chain.
+ *
+ * @param quoteStrategy
+ * quote strategy to apply on {@code schema} and {@code object}
+ * @return identifier chain
+ */
+ public String toString(QuoteStrategy quoteStrategy) {
+ if (!schema.isEmpty()) {
+ var sb = new StringBuilder(estimatedLength());
+ quoteStrategy.appendQuoted(schema, sb).append('.');
+ quoteStrategy.appendQuoted(object, sb);
+ return sb.toString();
+ }
+ return quoteStrategy.quoteObjectName(object);
+ }
+
+ /**
+ * Appends the identifier chain to {@code sb}, using {@code quoteStrategy}.
+ *
+ * @param sb
+ * StringBuilder for appending
+ * @param quoteStrategy
+ * quote strategy to apply on {@code schema} and {@code object}
+ * @return the StringBuilder for method chaining
+ * @see #estimatedLength()
+ */
+ public StringBuilder append(StringBuilder sb, QuoteStrategy quoteStrategy) {
+ if (!schema.isEmpty()) {
+ quoteStrategy.appendQuoted(schema, sb).append('.');
+ }
+ quoteStrategy.appendQuoted(object, sb);
+ return sb;
+ }
+
+ /**
+ * Creates a qualified name from {@code originalSchema} and {@code originalTableName} of
+ * {@code fieldDescriptor}.
+ *
+ * @param fieldDescriptor
+ * field descriptor
+ * @return a schema-qualified name of the original table from {@code fieldDescriptor} or empty if its
+ * {@code originalTableName} is empty string or {@code null}
+ */
+ public static Optional of(FieldDescriptor fieldDescriptor) {
+ String tableName = fieldDescriptor.getOriginalTableName();
+ if (StringUtils.isNullOrEmpty(tableName)) {
+ return Optional.empty();
+ }
+ // NOTE: This will produce an exception if tableName is blank and not empty, we accept that as that shouldn't
+ // happen in normal use
+ return Optional.of(new QualifiedName(fieldDescriptor.getOriginalSchema(), tableName));
+ }
+
+}
diff --git a/src/main/org/firebirdsql/jdbc/AbstractFieldMetaData.java b/src/main/org/firebirdsql/jdbc/AbstractFieldMetaData.java
index 869be7781..14661dd26 100644
--- a/src/main/org/firebirdsql/jdbc/AbstractFieldMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/AbstractFieldMetaData.java
@@ -274,13 +274,21 @@ public ExtendedFieldInfo(String schema, String relationName, String fieldName, i
/**
* A composite key for internal field mapping structures.
*
- * @param schema schema ({@code null} if schemaless, i.e. Firebird 5.0 and older)
+ * @param schema
+ * schema ({@code ""} if schemaless, i.e. Firebird 5.0 and older; {@code null} is converted to empty string)
* @param relationName
* relation name
* @param fieldName
* field name
*/
protected record FieldKey(String schema, String relationName, String fieldName) {
+
+ protected FieldKey {
+ if (schema == null) {
+ schema = "";
+ }
+ }
+
public FieldKey(FieldDescriptor fieldDescriptor) {
this(fieldDescriptor.getOriginalSchema(), fieldDescriptor.getOriginalTableName(),
fieldDescriptor.getOriginalName());
diff --git a/src/main/org/firebirdsql/jdbc/FBRowUpdater.java b/src/main/org/firebirdsql/jdbc/FBRowUpdater.java
index 5a55715c8..bbe8923c4 100644
--- a/src/main/org/firebirdsql/jdbc/FBRowUpdater.java
+++ b/src/main/org/firebirdsql/jdbc/FBRowUpdater.java
@@ -11,6 +11,7 @@
import org.firebirdsql.gds.ng.fields.RowValue;
import org.firebirdsql.gds.ng.listeners.StatementListener;
import org.firebirdsql.jaybird.util.SQLExceptionChainBuilder;
+import org.firebirdsql.jaybird.util.QualifiedName;
import org.firebirdsql.jaybird.util.UncheckedSQLException;
import org.firebirdsql.jdbc.field.FBField;
import org.firebirdsql.jdbc.field.FBFlushableField;
@@ -21,10 +22,8 @@
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
-import java.util.Objects;
import java.util.stream.StreamSupport;
-import static org.firebirdsql.jaybird.util.StringUtils.isNullOrEmpty;
import static org.firebirdsql.jdbc.SQLStateConstants.SQL_STATE_INVALID_CURSOR_STATE;
/**
@@ -69,7 +68,7 @@ final class FBRowUpdater implements FirebirdRowUpdater {
private static final byte[][] EMPTY_2D_BYTES = new byte[0][];
- private final String tableName;
+ private final QualifiedName tableName;
private final FBObjectListener.ResultSetListener rsListener;
private final GDSHelper gdsHelper;
private final RowDescriptor rowDescriptor;
@@ -87,12 +86,12 @@ final class FBRowUpdater implements FirebirdRowUpdater {
FBRowUpdater(FBConnection connection, RowDescriptor rowDescriptor, boolean cached,
FBObjectListener.ResultSetListener rsListener) throws SQLException {
- tableName = requireSingleTableName(rowDescriptor);
+ quoteStrategy = connection.getQuoteStrategy();
+ tableName = requireSingleTable(rowDescriptor, quoteStrategy);
keyColumns = deriveKeyColumns(tableName, rowDescriptor, connection.getMetaData());
this.rsListener = rsListener;
gdsHelper = connection.getGDSHelper();
- quoteStrategy = connection.getQuoteStrategy();
fields = createFields(rowDescriptor, cached);
newRow = rowDescriptor.createDefaultFieldValues();
@@ -119,31 +118,35 @@ private FBField createFieldUnchecked(FieldDescriptor fieldDescriptor, boolean ca
}
/**
- * Returns the single table name referenced by {@code rowDescriptor}, or throws an exception if there are no or
- * multiple table names.
+ * Returns the single table name (including schema if supported) referenced by {@code rowDescriptor}, or throws
+ * an exception if there are multiple tables, or derived columns (columns without a relation).
*
* @param rowDescriptor
* row descriptor
- * @return non-null table name
+ * @return non-null table identifier chain
* @throws SQLException
- * if {@code rowDescriptor} references multiple table names or no table names at all
+ * if {@code rowDescriptor} references multiple tables or has derived columns
*/
- private static String requireSingleTableName(RowDescriptor rowDescriptor) throws SQLException {
- // find the table name (there can be only one table per updatable result set)
- String tableName = null;
+ private static QualifiedName requireSingleTable(RowDescriptor rowDescriptor, QuoteStrategy quoteStrategy)
+ throws SQLException {
+ // find the tableName (there can be only one tableName per updatable result set)
+ QualifiedName tableName = null;
for (FieldDescriptor fieldDescriptor : rowDescriptor) {
- // TODO This will not detect derived columns in the prefix of the select list
+ var currentTable = QualifiedName.of(fieldDescriptor).orElse(null);
+ if (currentTable == null) {
+ // No table => derived column => not updatable
+ throw new FBResultSetNotUpdatableException(
+ "Underlying result set has derived columns (without a relation)");
+ }
if (tableName == null) {
- tableName = fieldDescriptor.getOriginalTableName();
- } else if (!Objects.equals(tableName, fieldDescriptor.getOriginalTableName())) {
+ tableName = currentTable;
+ } else if (!tableName.equals(currentTable)) {
+ // Different table => not updatable
throw new FBResultSetNotUpdatableException(
"Underlying result set references at least two relations: %s and %s."
- .formatted(tableName, fieldDescriptor.getOriginalTableName()));
+ .formatted(tableName.toString(quoteStrategy), currentTable.toString(quoteStrategy)));
}
}
- if (isNullOrEmpty(tableName)) {
- throw new FBResultSetNotUpdatableException("Underlying result set references no relations");
- }
return tableName;
}
@@ -217,10 +220,10 @@ public FBField getField(int fieldPosition) {
* @throws SQLException
* for errors looking up the best row identifier
*/
- private static List deriveKeyColumns(String tableName, RowDescriptor rowDescriptor,
+ private static List deriveKeyColumns(QualifiedName table, RowDescriptor rowDescriptor,
DatabaseMetaData dbmd) throws SQLException {
// first try best row identifier
- List keyColumns = keyColumnsOfBestRowIdentifier( tableName, rowDescriptor, dbmd);
+ List keyColumns = keyColumnsOfBestRowIdentifier(table, rowDescriptor, dbmd);
if (keyColumns.isEmpty()) {
// best row identifier not available or not fully matched, fallback to RDB$DB_KEY
// NOTE: fallback is updatable, but may not be insertable (e.g. if missing PK column(s) are not generated)!
@@ -248,11 +251,10 @@ private static List deriveKeyColumns(String tableName, RowDescr
* @throws SQLException
* for errors looking up the best row identifier
*/
- private static List keyColumnsOfBestRowIdentifier(String tableName, RowDescriptor rowDescriptor,
- DatabaseMetaData dbmd) throws SQLException {
- // TODO Add schema support
- try (ResultSet bestRowIdentifier = dbmd
- .getBestRowIdentifier("", null, tableName, DatabaseMetaData.bestRowTransaction, true)) {
+ private static List keyColumnsOfBestRowIdentifier(QualifiedName table,
+ RowDescriptor rowDescriptor, DatabaseMetaData dbmd) throws SQLException {
+ try (ResultSet bestRowIdentifier = dbmd.getBestRowIdentifier(
+ "", table.schema(), table.object(), DatabaseMetaData.bestRowTransaction, true)) {
int bestRowIdentifierColumnCount = 0;
List keyColumns = new ArrayList<>();
while (bestRowIdentifier.next()) {
@@ -326,7 +328,7 @@ private String buildUpdateStatement() {
// TODO raise exception if there are no updated columns, or do nothing?
var sb = new StringBuilder(EST_STATEMENT_SIZE + newRow.initializedCount() * EST_COLUMN_SIZE)
.append("update ");
- quoteStrategy.appendQuoted(tableName, sb).append(" set ");
+ tableName.append(sb, quoteStrategy).append(" set ");
boolean first = true;
for (FieldDescriptor fieldDescriptor : rowDescriptor) {
@@ -349,7 +351,7 @@ private String buildUpdateStatement() {
private String buildDeleteStatement() {
var sb = new StringBuilder(EST_STATEMENT_SIZE).append("delete from ");
- quoteStrategy.appendQuoted(tableName, sb).append('\n');
+ tableName.append(sb, quoteStrategy).append('\n');
appendWhereClause(sb);
return sb.toString();
@@ -377,9 +379,9 @@ private String buildInsertStatement() {
}
// 27 = length of appended literals + 2 quote characters
- var sb = new StringBuilder(27 + tableName.length() + columns.length() + params.length()).append("insert into ");
- quoteStrategy.appendQuoted(tableName, sb)
- .append(" (").append(columns).append(") values (").append(params).append(')');
+ var sb = new StringBuilder(27 + tableName.estimatedLength() + columns.length() + params.length())
+ .append("insert into ");
+ tableName.append(sb,quoteStrategy).append(" (").append(columns).append(") values (").append(params).append(')');
return sb.toString();
}
@@ -403,9 +405,9 @@ private String buildSelectStatement() {
}
}
- var sb = new StringBuilder(EST_STATEMENT_SIZE + columns.length())
+ var sb = new StringBuilder(EST_STATEMENT_SIZE + columns.length() + tableName.estimatedLength())
.append("select ").append(columns).append("\nfrom ");
- quoteStrategy.appendQuoted(tableName, sb).append('\n');
+ tableName.append(sb, quoteStrategy).append('\n');
appendWhereClause(sb);
return sb.toString();
}
diff --git a/src/test/org/firebirdsql/jaybird/util/QualifiedNameTest.java b/src/test/org/firebirdsql/jaybird/util/QualifiedNameTest.java
new file mode 100644
index 000000000..5b4e5630a
--- /dev/null
+++ b/src/test/org/firebirdsql/jaybird/util/QualifiedNameTest.java
@@ -0,0 +1,103 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jaybird.util;
+
+import org.firebirdsql.encodings.EncodingFactory;
+import org.firebirdsql.gds.ng.DatatypeCoder;
+import org.firebirdsql.gds.ng.DefaultDatatypeCoder;
+import org.firebirdsql.gds.ng.fields.FieldDescriptor;
+import org.firebirdsql.gds.ng.fields.RowDescriptorBuilder;
+import org.firebirdsql.jdbc.QuoteStrategy;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.EmptySource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class QualifiedNameTest {
+
+ private static final DatatypeCoder datatypeCoder =
+ DefaultDatatypeCoder.forEncodingFactory(EncodingFactory.createInstance(StandardCharsets.UTF_8));
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ void nullOrEmptySchemaReportedAsEmpty(@Nullable String schema) {
+ var name = new QualifiedName(schema, "COLUMN");
+ assertEquals("COLUMN", name.object());
+ assertEquals("", name.schema());
+ }
+
+ @ParameterizedTest
+ @EmptySource
+ @ValueSource(strings = " ")
+ void blankObject_throwsException(String object) {
+ assertThrows(IllegalArgumentException.class, () -> new QualifiedName("SCHEMA_NAME", object));
+ }
+
+ @Test
+ @SuppressWarnings("DataFlowIssue")
+ void nullObject_throwsException() {
+ assertThrows(NullPointerException.class, () -> new QualifiedName("SCHEMA_NAME", null));
+ }
+
+ @ParameterizedTest
+ @CsvSource(useHeadersInDisplayName = true, nullValues = "", textBlock = """
+ schema, object, quoteStrategy, expectedLength, expectedIdentifierChain
+ , OBJECT, DIALECT_3, 8, "OBJECT"
+ '', OBJECT, DIALECT_1, 8, OBJECT
+ SCHEMA, OBJECT, DIALECT_3, 17, "SCHEMA"."OBJECT"
+ SCHEMA, OBJECT, DIALECT_1, 17, SCHEMA.OBJECT
+ # Weird case
+ SCHEM"QUOTE, OBJ"QUOTE, DIALECT_3, 25, "SCHEM""QUOTE"."OBJ""QUOTE"
+ """)
+ void identifierChainProduction(@Nullable String schema, String object, QuoteStrategy quoteStrategy,
+ int expectedLength, String expectedIdentifierChain) {
+ var name = new QualifiedName(schema, object);
+ assertAll(
+ () -> assertEquals(expectedLength, name.estimatedLength(), "estimatedLength"),
+ () -> assertEquals(expectedIdentifierChain, name.toString(quoteStrategy), "toString(QuoteStrategy)"),
+ () -> {
+ var sb = new StringBuilder(name.estimatedLength());
+ name.append(sb, quoteStrategy);
+ assertEquals(expectedIdentifierChain, sb.toString(), "append(StringBuilder, QuoteStrategy)");
+ }
+ );
+ }
+
+ @SuppressWarnings("OptionalGetWithoutIsPresent")
+ @ParameterizedTest
+ @CsvSource(useHeadersInDisplayName = true, nullValues = "", textBlock = """
+ schema, object, expectedEmpty
+ , TABLE, false
+ '', TABLE, false
+ SCHEMA, TABLE, false
+ SCHEMA, , true
+ SCHEMA, '', true
+ , , true
+ '', '', true
+ """)
+ void testOf(@Nullable String schema, String object, boolean expectedEmpty) {
+ FieldDescriptor fieldDescriptor = new RowDescriptorBuilder(1, datatypeCoder)
+ .setOriginalSchema(schema)
+ .setOriginalTableName(object)
+ .toFieldDescriptor();
+ Optional optName = QualifiedName.of(fieldDescriptor);
+ assertEquals(expectedEmpty, optName.isEmpty(), "empty");
+ if (!expectedEmpty) {
+ QualifiedName name = optName.get();
+ String expectedSchema = schema == null || schema.isBlank() ? "" : schema;
+ assertEquals(expectedSchema, name.schema(), "schema");
+ assertEquals(object, name.object(), "object");
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/org/firebirdsql/jdbc/FBResultSetTest.java b/src/test/org/firebirdsql/jdbc/FBResultSetTest.java
index bf9dc174c..ad6960a4b 100644
--- a/src/test/org/firebirdsql/jdbc/FBResultSetTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBResultSetTest.java
@@ -630,14 +630,20 @@ void testUpdatableStatementResultSetDowngradeToReadOnlyWhenQueryNotUpdatable(
executeCreateTable(connection, CREATE_TABLE_STATEMENT);
executeCreateTable(connection, CREATE_TABLE_STATEMENT2);
- try (var stmt = connection.createStatement(TYPE_SCROLL_INSENSITIVE, CONCUR_UPDATABLE);
- var rs = stmt.executeQuery("select * from test_table t1 left join test_table2 t2 on t1.id = t2.id")) {
- assertThat(stmt.getWarnings(), allOf(
- notNullValue(),
- fbMessageStartsWith(JaybirdErrorCodes.jb_concurrencyResetReadOnlyReasonNotUpdatable)));
+ assertDowngradeToReadOnly(connection,
+ "select * from test_table t1 left join test_table2 t2 on t1.id = t2.id");
+ }
+ }
- assertEquals(CONCUR_READ_ONLY, rs.getConcurrency(), "Expected downgrade to CONCUR_READ_ONLY");
- }
+ private static void assertDowngradeToReadOnly(Connection connection, String statementText)
+ throws SQLException {
+ try (var stmt = connection.createStatement(TYPE_SCROLL_INSENSITIVE, CONCUR_UPDATABLE);
+ var rs = stmt.executeQuery(statementText)) {
+ assertThat(stmt.getWarnings(), allOf(
+ notNullValue(),
+ fbMessageStartsWith(JaybirdErrorCodes.jb_concurrencyResetReadOnlyReasonNotUpdatable)));
+
+ assertEquals(CONCUR_READ_ONLY, rs.getConcurrency(), "Expected downgrade to CONCUR_READ_ONLY");
}
}
@@ -652,14 +658,8 @@ void testUpdatableStatementResultSetDowngradeToReadOnlyWhenQueryNotUpdatable(
void testUpdatableStatementPrefixPK_downgradeToReadOnly(String scrollableCursorPropertyValue) throws Exception {
try (Connection connection = createConnection(scrollableCursorPropertyValue)) {
executeCreateTable(connection, CREATE_WITH_COMPOSITE_PK);
- try (var stmt = connection.createStatement(TYPE_SCROLL_INSENSITIVE, CONCUR_UPDATABLE);
- var rs = stmt.executeQuery("select id1, val from WITH_COMPOSITE_PK")) {
- assertThat(stmt.getWarnings(), allOf(
- notNullValue(),
- fbMessageStartsWith(JaybirdErrorCodes.jb_concurrencyResetReadOnlyReasonNotUpdatable)));
- assertEquals(CONCUR_READ_ONLY, rs.getConcurrency(), "Expected downgrade to CONCUR_READ_ONLY");
- }
+ assertDowngradeToReadOnly(connection, "select id1, val from WITH_COMPOSITE_PK");
}
}
@@ -674,14 +674,38 @@ void testUpdatableStatementPrefixPK_downgradeToReadOnly(String scrollableCursorP
void testUpdatableStatementSuffixPK_downgradeToReadOnly(String scrollableCursorPropertyValue) throws Exception {
try (Connection connection = createConnection(scrollableCursorPropertyValue)) {
executeCreateTable(connection, CREATE_WITH_COMPOSITE_PK);
- try (var stmt = connection.createStatement(TYPE_SCROLL_INSENSITIVE, CONCUR_UPDATABLE);
- var rs = stmt.executeQuery("select id2, val from WITH_COMPOSITE_PK")) {
- assertThat(stmt.getWarnings(), allOf(
- notNullValue(),
- fbMessageStartsWith(JaybirdErrorCodes.jb_concurrencyResetReadOnlyReasonNotUpdatable)));
- assertEquals(CONCUR_READ_ONLY, rs.getConcurrency(), "Expected downgrade to CONCUR_READ_ONLY");
- }
+ assertDowngradeToReadOnly(connection, "select id2, val from WITH_COMPOSITE_PK");
+ }
+ }
+
+ /**
+ * Tests if a statement that contains columns not directly referencing table columns before those that do,
+ * will be downgraded to read-only.
+ */
+ @ParameterizedTest
+ @MethodSource("scrollableCursorPropertyValues")
+ void testUpdatableStatementSuffixExpression_downgradeToReadOnly(String scrollableCursorPropertyValue)
+ throws Exception {
+ try (var connection = createConnection(scrollableCursorPropertyValue)) {
+ executeCreateTable(connection, CREATE_TABLE_STATEMENT);
+
+ assertDowngradeToReadOnly(connection, "select id + 1, id, str from test_table");
+ }
+ }
+
+ /**
+ * Tests if a statement that contains columns not directly referencing table columns after at least one
+ * that does, will be downgraded to read-only.
+ */
+ @ParameterizedTest
+ @MethodSource("scrollableCursorPropertyValues")
+ void testUpdatableStatementPrefixExpression_downgradeToReadOnly(String scrollableCursorPropertyValue)
+ throws Exception {
+ try (var connection = createConnection(scrollableCursorPropertyValue)) {
+ executeCreateTable(connection, CREATE_TABLE_STATEMENT);
+
+ assertDowngradeToReadOnly(connection, "select id, str, id + 1 from test_table");
}
}
From 1f839f0c43851a99e1a9806a1f7a388744d556bd Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Sun, 13 Jul 2025 15:39:39 +0200
Subject: [PATCH 34/64] Deduplicate strings in StatementInfoProcessor
---
.../gds/ng/StatementInfoProcessor.java | 11 +-
.../jaybird/util/StringDeduplicator.java | 129 ++++++++++++++++++
.../jaybird/util/StringDeduplicatorTest.java | 113 +++++++++++++++
3 files changed, 249 insertions(+), 4 deletions(-)
create mode 100644 src/main/org/firebirdsql/jaybird/util/StringDeduplicator.java
create mode 100644 src/test/org/firebirdsql/jaybird/util/StringDeduplicatorTest.java
diff --git a/src/main/org/firebirdsql/gds/ng/StatementInfoProcessor.java b/src/main/org/firebirdsql/gds/ng/StatementInfoProcessor.java
index ac38482ed..b06d66952 100644
--- a/src/main/org/firebirdsql/gds/ng/StatementInfoProcessor.java
+++ b/src/main/org/firebirdsql/gds/ng/StatementInfoProcessor.java
@@ -5,8 +5,10 @@
import org.firebirdsql.gds.ISCConstants;
import org.firebirdsql.gds.ng.fields.RowDescriptor;
import org.firebirdsql.gds.ng.fields.RowDescriptorBuilder;
+import org.firebirdsql.jaybird.util.StringDeduplicator;
import java.sql.SQLException;
+import java.util.List;
import static org.firebirdsql.gds.VaxEncoding.iscVaxInteger;
import static org.firebirdsql.gds.VaxEncoding.iscVaxInteger2;
@@ -21,9 +23,11 @@
public final class StatementInfoProcessor implements InfoProcessor {
private static final System.Logger log = System.getLogger(StatementInfoProcessor.class.getName());
+ private static final List DEDUPLICATOR_PRESET = List.of("SYSTEM", "PUBLIC", "SYSDBA");
private final AbstractFbStatement statement;
private final FbDatabase database;
+ private final StringDeduplicator stringDeduplicator = StringDeduplicator.of(DEDUPLICATOR_PRESET);
/**
* Creates an instance of this class.
@@ -85,10 +89,8 @@ private void handleTruncatedInfo(final StatementInfo info) throws SQLException {
newInfoItems[newIndex++] = 2; // size of short
newInfoItems[newIndex++] = (byte) (descriptorIndex & 0xFF);
newInfoItems[newIndex++] = (byte) (descriptorIndex >> 8);
- newInfoItems[newIndex++] = infoItem;
- } else {
- newInfoItems[newIndex++] = infoItem;
}
+ newInfoItems[newIndex++] = infoItem;
}
assert newIndex == newInfoItems.length : "newInfoItems size too long";
// Doubling request buffer up to the maximum
@@ -212,10 +214,11 @@ private int readIntValue(StatementInfo info) {
private String readStringValue(StatementInfo info) {
int len = iscVaxInteger2(info.buffer, info.currentIndex);
info.currentIndex += 2;
+ if (len == 0) return "";
// TODO Is it correct to use the connection encoding here, or should we use UTF-8 always?
String value = database.getEncoding().decodeFromCharset(info.buffer, info.currentIndex, len);
info.currentIndex += len;
- return value;
+ return stringDeduplicator.get(value);
}
/**
diff --git a/src/main/org/firebirdsql/jaybird/util/StringDeduplicator.java b/src/main/org/firebirdsql/jaybird/util/StringDeduplicator.java
new file mode 100644
index 000000000..6294d10a3
--- /dev/null
+++ b/src/main/org/firebirdsql/jaybird/util/StringDeduplicator.java
@@ -0,0 +1,129 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jaybird.util;
+
+import org.jspecify.annotations.Nullable;
+
+import java.io.Serial;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * Best-effort string deduplicator.
+ *
+ * Given this class uses an LRU-evicted map with a maximum capacity internally, 100% deduplication of strings is
+ * not guaranteed.
+ *
+ *
+ * This class is not thread-safe.
+ *
+ *
+ * @author Mark Rotteveel
+ * @since 7
+ */
+public final class StringDeduplicator {
+
+ private static final int DEFAULT_MAX_CAPACITY = 50;
+
+ private final Map cache;
+
+ private StringDeduplicator(int maxCapacity, Collection preset) {
+ cache = new StringCache(maxCapacity);
+ // NOTE: if preset.size() is greater than maxCapacity, prefix will be immediately evicted
+ preset.forEach(v -> cache.put(v, v));
+ }
+
+ /**
+ * Deduplicates this value if already cached, otherwise caches and returns {@code value}.
+ *
+ * @param value
+ * value to deduplicate (can be {@code null})
+ * @return previous cached value equal to {@code value}, or {@code value} itself
+ */
+ public @Nullable String get(@Nullable String value) {
+ if (value == null) return null;
+ if (value.isEmpty()) return "";
+ return cache.computeIfAbsent(value, Function.identity());
+ }
+
+ /**
+ * @return a string deduplicator with a default max capacity
+ */
+ public static StringDeduplicator of() {
+ return of(DEFAULT_MAX_CAPACITY, List.of());
+ }
+
+ /**
+ * @param preset
+ * values to initially add to the cache
+ * @return a string deduplicator with a default max capacity, initialized with {@code preset}
+ */
+ public static StringDeduplicator of(String... preset) {
+ return of(Arrays.asList(preset));
+ }
+
+ /**
+ * @param preset
+ * values to initially add to the cache
+ * @return a string deduplicator with a default max capacity, initialized with {@code preset}
+ */
+ public static StringDeduplicator of(Collection preset) {
+ return of(DEFAULT_MAX_CAPACITY, preset);
+ }
+
+ /**
+ * @param maxCapacity
+ * maximum capacity
+ * @param preset
+ * values to initially add to the cache
+ * @return a string deduplicator with max capacity {@code maxCapacity}, initialized with {@code preset}
+ */
+ public static StringDeduplicator of(int maxCapacity, String... preset) {
+ return of(maxCapacity, Arrays.asList(preset));
+ }
+
+ /**
+ * @param maxCapacity
+ * maximum capacity
+ * @param preset
+ * values to initially add to the cache
+ * @return a string deduplicator with max capacity {@code maxCapacity}, initialized with {@code preset}
+ */
+ public static StringDeduplicator of(int maxCapacity, Collection preset) {
+ return new StringDeduplicator(maxCapacity, preset);
+ }
+
+ /**
+ * String cache with LRU (Least-Recently Used) eviction order.
+ */
+ private static final class StringCache extends LinkedHashMap {
+
+ @Serial
+ private static final long serialVersionUID = -201579959301651333L;
+
+ private final int maxCapacity;
+
+ private StringCache(int maxCapacity) {
+ super(initialCapacity(maxCapacity), 0.75f, true);
+ if (maxCapacity <= 0) {
+ throw new IllegalArgumentException("maxCapacity must be greater than 0, was: " + maxCapacity);
+ }
+ this.maxCapacity = maxCapacity;
+ }
+
+ private static int initialCapacity(int maxCapacity) {
+ return Math.max(8, maxCapacity / 2);
+ }
+
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ return size() > maxCapacity;
+ }
+
+ }
+
+}
diff --git a/src/test/org/firebirdsql/jaybird/util/StringDeduplicatorTest.java b/src/test/org/firebirdsql/jaybird/util/StringDeduplicatorTest.java
new file mode 100644
index 000000000..75b6ab2c5
--- /dev/null
+++ b/src/test/org/firebirdsql/jaybird/util/StringDeduplicatorTest.java
@@ -0,0 +1,113 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jaybird.util;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.List;
+import java.util.stream.IntStream;
+
+import static org.firebirdsql.common.matchers.MatcherAssume.assumeThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Tests for {@link StringDeduplicator}.
+ */
+class StringDeduplicatorTest {
+
+ @Test
+ void deduplicate() {
+ var deduplicator = StringDeduplicator.of();
+
+ final String value1 = "value1";
+ final String value1Copy = copyOf(value1);
+
+ // Check our assumptions (indirectly test copyOf(...))
+ assumeEqualNotSameInstance(value1, value1Copy, "value1Copy should be a distinct instance");
+
+ assertSame(value1, deduplicator.get(value1));
+ assertSame(value1, deduplicator.get(value1Copy));
+ }
+
+ @Test
+ void deduplicateToPreset() {
+ var deduplicator = StringDeduplicator.of("PRESET_1", "PRESET_2");
+
+ assertSame("PRESET_1", deduplicator.get(copyOf("PRESET_1")));
+ assertSame("PRESET_2", deduplicator.get(copyOf("PRESET_2")));
+ }
+
+ @Test
+ void eviction() {
+ final int maxCapacity = 5;
+ var deduplicator = StringDeduplicator.of(maxCapacity, List.of());
+
+ final String value1 = "value1";
+ final String value1Copy = copyOf(value1);
+
+ assertSame(value1, deduplicator.get(value1), value1);
+ // Deduplicate other values to evict value1
+ IntStream.rangeClosed(2, maxCapacity + 1).forEach(i -> {
+ String value = "value" + i;
+ assertSame(value, deduplicator.get(value), value);
+ });
+
+ assertSame(value1Copy, deduplicator.get(value1Copy), "expected value1Copy as value1 has been evicted");
+ }
+
+ @Test
+ void deduplicateNull() {
+ var deduplicator = StringDeduplicator.of();
+
+ assertNull(deduplicator.get(null));
+ }
+
+ @Test
+ void deduplicateEmptyString_toLiteralEmpty() {
+ var deduplicator = StringDeduplicator.of();
+
+ final String emptyCopy = copyOf("");
+ assumeEqualNotSameInstance("", emptyCopy, "emptyCopy should be a distinct instance");
+
+ assertSame("", deduplicator.get(emptyCopy), "should deduplicate to empty literal instance, not copy");
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = { -2, -1, 0 })
+ void cannotCreateWithMaxCapacityZeroOrSmaller(int maxCapacity) {
+ assertThrows(IllegalArgumentException.class, () -> StringDeduplicator.of(maxCapacity, List.of()));
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = { 1, 2, 10 })
+ void canCreateWithMaxCapacityOneOrGreater(int maxCapacity) {
+ assertDoesNotThrow(() -> StringDeduplicator.of(maxCapacity, List.of()));
+ }
+
+ /**
+ * Creates a distinct copy of {@code value}.
+ *
+ * @param value
+ * value to copy
+ * @return distinct copy of {@code value} (i.e. {@code new String(value)})
+ */
+ @SuppressWarnings("StringOperationCanBeSimplified")
+ private static String copyOf(String value) {
+ return new String(value);
+ }
+
+ private void assumeEqualNotSameInstance(String v1, String v2, String message) {
+ assumeThat(message, v2,
+ allOf(equalTo(v1), not(sameInstance(v1))));
+ }
+
+}
\ No newline at end of file
From fbff0a60cdaa81b1049dfb129f416dfebae75ad0 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Mon, 14 Jul 2025 10:01:46 +0200
Subject: [PATCH 35/64] Add some todo for later
---
src/main/org/firebirdsql/jaybird/parser/StatementDetector.java | 2 ++
src/main/org/firebirdsql/jdbc/GeneratedKeysQueryBuilder.java | 2 ++
2 files changed, 4 insertions(+)
diff --git a/src/main/org/firebirdsql/jaybird/parser/StatementDetector.java b/src/main/org/firebirdsql/jaybird/parser/StatementDetector.java
index 8bf545596..649577137 100644
--- a/src/main/org/firebirdsql/jaybird/parser/StatementDetector.java
+++ b/src/main/org/firebirdsql/jaybird/parser/StatementDetector.java
@@ -27,6 +27,8 @@
@InternalApi
public final class StatementDetector implements TokenVisitor {
+ // TODO Add schema support: identify (optional) schema
+
private static final StateAfterStart INITIAL_OTHER =
new StateAfterStart(ParserState.OTHER, LocalStatementType.OTHER);
private static final Map NEXT_AFTER_START;
diff --git a/src/main/org/firebirdsql/jdbc/GeneratedKeysQueryBuilder.java b/src/main/org/firebirdsql/jdbc/GeneratedKeysQueryBuilder.java
index 370a643d0..35b7d1ea8 100644
--- a/src/main/org/firebirdsql/jdbc/GeneratedKeysQueryBuilder.java
+++ b/src/main/org/firebirdsql/jdbc/GeneratedKeysQueryBuilder.java
@@ -23,6 +23,8 @@
*/
final class GeneratedKeysQueryBuilder {
+ // TODO Add schema support: parse schema from query, and find a way to identify table if unqualified (needed for
+ // mapColumnNamesByIndex)
// TODO Add caching for column info
private static final System.Logger logger = System.getLogger(GeneratedKeysQueryBuilder.class.getName());
From 10ed6548f6d72dfdd044530435ff11f7653c8940 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Thu, 17 Jul 2025 10:59:05 +0200
Subject: [PATCH 36/64] #882 Improve schema test coverage
---
.../jaybird/util/CollectionUtils.java | 20 +-
.../jaybird/util/CollectionUtilsTest.java | 10 +-
.../FBDatabaseMetaDataAbstractKeysTest.java | 139 +++++----
...BDatabaseMetaDataColumnPrivilegesTest.java | 109 +++++--
.../jdbc/FBDatabaseMetaDataColumnsTest.java | 227 +++++++++-----
.../FBDatabaseMetaDataCrossReferenceTest.java | 40 ++-
.../FBDatabaseMetaDataExportedKeysTest.java | 32 +-
...FBDatabaseMetaDataFunctionColumnsTest.java | 290 +++++++++++-------
.../FBDatabaseMetaDataImportedKeysTest.java | 31 +-
9 files changed, 595 insertions(+), 303 deletions(-)
diff --git a/src/main/org/firebirdsql/jaybird/util/CollectionUtils.java b/src/main/org/firebirdsql/jaybird/util/CollectionUtils.java
index 115a9f760..45db08795 100644
--- a/src/main/org/firebirdsql/jaybird/util/CollectionUtils.java
+++ b/src/main/org/firebirdsql/jaybird/util/CollectionUtils.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2023-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2023-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jaybird.util;
@@ -69,4 +69,22 @@ public static void growToSize(final List> list, final int size) {
return size > 0 ? list.get(size - 1) : null;
}
+ /**
+ * Concatenates two lists to a new modifiable list.
+ *
+ * @param list1
+ * list 1
+ * @param list2
+ * list 2
+ * @param
+ * type parameter of {@code list1}, and parent type parameter of {@code list2}
+ * @return concatenation of {@code list1} and {@code list2}
+ */
+ public static List concat(List list1, List extends T> list2) {
+ var newList = new ArrayList(list1.size() + list2.size());
+ newList.addAll(list1);
+ newList.addAll(list2);
+ return newList;
+ }
+
}
diff --git a/src/test/org/firebirdsql/jaybird/util/CollectionUtilsTest.java b/src/test/org/firebirdsql/jaybird/util/CollectionUtilsTest.java
index 28ff16654..c3e5a00c0 100644
--- a/src/test/org/firebirdsql/jaybird/util/CollectionUtilsTest.java
+++ b/src/test/org/firebirdsql/jaybird/util/CollectionUtilsTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2023-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2023-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jaybird.util;
@@ -89,6 +89,14 @@ void getLast_multipleItems() {
assertEquals(item2, CollectionUtils.getLast(List.of(item1, item2)));
}
+ @Test
+ void concat() {
+ var list1 = List.of("item1", "item2");
+ var list2 = List.of("item3", "item4");
+
+ assertEquals(List.of("item1", "item2", "item3", "item4"), CollectionUtils.concat(list1, list2));
+ }
+
static Stream listFactories() {
return Stream.of(
Arguments.of(factory(ArrayList::new)),
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataAbstractKeysTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataAbstractKeysTest.java
index c25a1d6ab..c265878d2 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataAbstractKeysTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataAbstractKeysTest.java
@@ -12,6 +12,7 @@
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.List;
@@ -19,6 +20,7 @@
import static java.util.Collections.unmodifiableMap;
import static org.firebirdsql.common.FBTestProperties.getConnectionViaDriverManager;
+import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
import static org.firebirdsql.common.assertions.ResultSetAssertions.assertNextRow;
import static org.firebirdsql.common.assertions.ResultSetAssertions.assertNoNextRow;
@@ -35,59 +37,9 @@ abstract class FBDatabaseMetaDataAbstractKeysTest {
private static final String UNNAMED_PK_INDEX_PREFIX = "RDB$PRIMARY";
private static final String UNNAMED_FK_INDEX_PREFIX = "RDB$FOREIGN";
- //@formatter:off
@RegisterExtension
static final UsesDatabaseExtension.UsesDatabaseForAll usesDatabase = UsesDatabaseExtension.usesDatabaseForAll(
- """
- create table TABLE_1 (
- ID integer constraint PK_TABLE_1 primary key
- )""",
- """
- create table TABLE_2 (
- ID1 integer not null,
- ID2 integer not null,
- TABLE_1_ID integer constraint FK_TABLE_2_TO_1 references TABLE_1 (ID),
- constraint PK_TABLE_2 unique (ID1, ID2) using index ALT_INDEX_NAME_2
- )""",
- """
- create table TABLE_3 (
- ID integer constraint PK_TABLE_3 primary key using index ALT_INDEX_NAME_3,
- TABLE_2_ID1 integer,
- TABLE_2_ID2 integer,
- constraint FK_TABLE_3_TO_2 foreign key (TABLE_2_ID1, TABLE_2_ID2) references TABLE_2 (ID1, ID2)
- on delete cascade on update set default
- )""",
- """
- create table TABLE_4 (
- ID integer primary key using index ALT_INDEX_NAME_4,
- TABLE_2_ID1 integer,
- TABLE_2_ID2 integer,
- constraint FK_TABLE_4_TO_2 foreign key (TABLE_2_ID1, TABLE_2_ID2) references TABLE_2 (ID1, ID2)
- on delete set default on update set null
- )""",
- """
- create table TABLE_5 (
- ID integer primary key,
- TABLE_2_ID1 integer,
- TABLE_2_ID2 integer,
- foreign key (TABLE_2_ID1, TABLE_2_ID2) references TABLE_2 (ID1, ID2)
- on delete set null on update no action using index ALT_INDEX_NAME_5
- )""",
- """
- create table TABLE_6 (
- ID integer primary key,
- TABLE_2_ID1 integer,
- TABLE_2_ID2 integer,
- foreign key (TABLE_2_ID1, TABLE_2_ID2) references TABLE_2 (ID1, ID2)
- on delete no action on update cascade
- )""",
- """
- create table TABLE_7 (
- ID integer primary key,
- TABLE_6_ID integer constraint FK_TABLE_7_TO_6 references TABLE_6 (ID) on update cascade
- )"""
- );
- //@formatter:on
+ dbInitStatements());
protected static final MetadataResultSetDefinition keysDefinition =
new MetadataResultSetDefinition(KeysMetaData.class);
@@ -95,6 +47,76 @@ TABLE_6_ID integer constraint FK_TABLE_7_TO_6 references TABLE_6 (ID) on update
protected static Connection con;
protected static DatabaseMetaData dbmd;
+ private static List dbInitStatements() {
+ var statements = new ArrayList<>(Arrays.asList(
+ """
+ create table TABLE_1 (
+ ID integer constraint PK_TABLE_1 primary key
+ )""",
+ """
+ create table TABLE_2 (
+ ID1 integer not null,
+ ID2 integer not null,
+ TABLE_1_ID integer constraint FK_TABLE_2_TO_1 references TABLE_1 (ID),
+ constraint PK_TABLE_2 unique (ID1, ID2) using index ALT_INDEX_NAME_2
+ )""",
+ """
+ create table TABLE_3 (
+ ID integer constraint PK_TABLE_3 primary key using index ALT_INDEX_NAME_3,
+ TABLE_2_ID1 integer,
+ TABLE_2_ID2 integer,
+ constraint FK_TABLE_3_TO_2 foreign key (TABLE_2_ID1, TABLE_2_ID2) references TABLE_2 (ID1, ID2)
+ on delete cascade on update set default
+ )""",
+ """
+ create table TABLE_4 (
+ ID integer primary key using index ALT_INDEX_NAME_4,
+ TABLE_2_ID1 integer,
+ TABLE_2_ID2 integer,
+ constraint FK_TABLE_4_TO_2 foreign key (TABLE_2_ID1, TABLE_2_ID2) references TABLE_2 (ID1, ID2)
+ on delete set default on update set null
+ )""",
+ """
+ create table TABLE_5 (
+ ID integer primary key,
+ TABLE_2_ID1 integer,
+ TABLE_2_ID2 integer,
+ foreign key (TABLE_2_ID1, TABLE_2_ID2) references TABLE_2 (ID1, ID2)
+ on delete set null on update no action using index ALT_INDEX_NAME_5
+ )""",
+ """
+ create table TABLE_6 (
+ ID integer primary key,
+ TABLE_2_ID1 integer,
+ TABLE_2_ID2 integer,
+ foreign key (TABLE_2_ID1, TABLE_2_ID2) references TABLE_2 (ID1, ID2)
+ on delete no action on update cascade
+ )"""
+ ));
+ if (!getDefaultSupportInfo().supportsSchemas()) {
+ statements.add("""
+ create table TABLE_7 (
+ ID integer primary key,
+ TABLE_6_ID integer constraint FK_TABLE_7_TO_6 references TABLE_6 (ID) on update cascade
+ )""");
+ } else {
+ statements.add("create schema OTHER_SCHEMA");
+ statements.add("""
+ create table OTHER_SCHEMA.TABLE_8 (
+ ID integer primary key,
+ TABLE_1_ID integer constraint FK_TABLE_8_TO_1 references PUBLIC.TABLE_1 (ID)
+ )""");
+ statements.add("""
+ create table TABLE_7 (
+ ID integer primary key,
+ TABLE_6_ID integer constraint FK_TABLE_7_TO_6 references TABLE_6 (ID) on update cascade,
+ TABLE_8_ID integer constraint FK_TABLE_7_TO_8 references OTHER_SCHEMA.TABLE_8 (ID) on delete cascade
+ )""");
+ }
+
+ return statements;
+ }
+
@BeforeAll
static void setupAll() throws SQLException {
con = getConnectionViaDriverManager();
@@ -163,13 +185,26 @@ protected static List> table6Fks() {
UNNAMED_CONSTRAINT_PREFIX, "ALT_INDEX_NAME_2", UNNAMED_FK_INDEX_PREFIX));
}
- protected static List> table7Fks() {
+ protected static List> table7to6Fks() {
return List.of(
createKeysTestData("TABLE_6", "ID", "TABLE_7", "TABLE_6_ID", 1, DatabaseMetaData.importedKeyCascade,
DatabaseMetaData.importedKeyNoAction, UNNAMED_CONSTRAINT_PREFIX, "FK_TABLE_7_TO_6",
UNNAMED_PK_INDEX_PREFIX, "FK_TABLE_7_TO_6"));
}
+ protected static List> table7to8Fks() {
+ return List.of(createKeysTestData("OTHER_SCHEMA", "TABLE_8", "ID", "PUBLIC", "TABLE_7", "TABLE_8_ID", 1,
+ DatabaseMetaData.importedKeyNoAction, DatabaseMetaData.importedKeyCascade,
+ UNNAMED_CONSTRAINT_PREFIX, "FK_TABLE_7_TO_8", UNNAMED_PK_INDEX_PREFIX, "FK_TABLE_7_TO_8"));
+ }
+
+ protected static List> table8Fks() {
+ return List.of(
+ createKeysTestData("PUBLIC", "TABLE_1", "ID", "OTHER_SCHEMA", "TABLE_8", "TABLE_1_ID", 1,
+ DatabaseMetaData.importedKeyNoAction, DatabaseMetaData.importedKeyNoAction,
+ "PK_TABLE_1", "FK_TABLE_8_TO_1", "PK_TABLE_1", "FK_TABLE_8_TO_1"));
+ }
+
protected void validateExpectedKeys(ResultSet keys, List> expectedKeys)
throws Exception {
for (Map expectedColumn : expectedKeys) {
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnPrivilegesTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnPrivilegesTest.java
index 33358b0d4..b5039e1e0 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnPrivilegesTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnPrivilegesTest.java
@@ -4,18 +4,20 @@
import org.firebirdsql.common.FBTestProperties;
import org.firebirdsql.common.extension.UsesDatabaseExtension;
+import org.firebirdsql.util.FirebirdSupportInfo;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.NullSource;
+import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
@@ -25,6 +27,7 @@
import static org.firebirdsql.common.FBTestProperties.getConnectionViaDriverManager;
import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
+import static org.firebirdsql.common.FbAssumptions.assumeFeature;
import static org.firebirdsql.common.matchers.MatcherAssume.assumeThat;
import static org.hamcrest.Matchers.equalToIgnoringCase;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -36,22 +39,14 @@
*/
class FBDatabaseMetaDataColumnPrivilegesTest {
- // TODO Add schema support: tests involving other schema
-
private static final String SYSDBA = "SYSDBA";
private static final String USER1 = "USER1";
private static final String user2 = getDefaultSupportInfo().supportsCaseSensitiveUserNames() ? "user2" : "USER2";
private static final String PUBLIC = "PUBLIC";
@RegisterExtension
- static final UsesDatabaseExtension.UsesDatabaseForAll usesDatabase = UsesDatabaseExtension.usesDatabaseForAll(
- "create table TBL1 (COL1 integer, COL2 varchar(50), \"val3\" varchar(50))",
- "create table \"tbl2\" (COL1 integer, COL2 varchar(50), \"val3\" varchar(50))",
- "grant all on TBL1 to USER1",
- "grant select on TBL1 to PUBLIC",
- "grant update (COL1, \"val3\") on TBL1 to \"user2\"",
- "grant select on \"tbl2\" to \"user2\" with grant option",
- "grant references (COL1) on \"tbl2\" to USER1");
+ static final UsesDatabaseExtension.UsesDatabaseForAll usesDatabase =
+ UsesDatabaseExtension.usesDatabaseForAll(dbInitStatements());
private static final MetadataResultSetDefinition getColumnPrivilegesDefinition =
new MetadataResultSetDefinition(ColumnPrivilegesMetadata.class);
@@ -59,6 +54,28 @@ class FBDatabaseMetaDataColumnPrivilegesTest {
private static Connection con;
private static DatabaseMetaData dbmd;
+ private static List dbInitStatements() {
+ var statements = new ArrayList<>(Arrays.asList(
+ "create table TBL1 (COL1 integer, COL2 varchar(50), \"val3\" varchar(50))",
+ "create table \"tbl2\" (COL1 integer, COL2 varchar(50), \"val3\" varchar(50))",
+ "grant all on TBL1 to USER1",
+ "grant select on TBL1 to PUBLIC",
+ "grant update (COL1, \"val3\") on TBL1 to \"user2\"",
+ "grant select on \"tbl2\" to \"user2\" with grant option",
+ "grant references (COL1) on \"tbl2\" to USER1"
+ ));
+ if (getDefaultSupportInfo().supportsSchemas()) {
+ statements.addAll(Arrays.asList(
+ "create schema OTHER_SCHEMA",
+ "create table OTHER_SCHEMA.TBL3 (COL1 integer, COL2 varchar(50), \"val3\" varchar(50))",
+ "grant select on OTHER_SCHEMA.TBL3 to PUBLIC",
+ "grant update on OTHER_SCHEMA.TBL3 to USER1"
+ ));
+ }
+
+ return statements;
+ }
+
@BeforeAll
static void setupAll() throws SQLException {
// Otherwise we need to take into account additional rules
@@ -88,9 +105,14 @@ void testColumnPrivilegesMetaDataColumns() throws Exception {
}
@ParameterizedTest
- @ValueSource(strings = "%")
- @NullSource
- void testColumnPrivileges_TBL1_all(String allPattern) throws Exception {
+ @CsvSource(useHeadersInDisplayName = true, nullValues = { "" }, textBlock = """
+ schemaNull, columNameAllPattern
+ false, %
+ false,
+ true, %
+ true,
+ """)
+ void testColumnPrivileges_TBL1_all(boolean schemaNull, String columnNameAllPattern) throws Exception {
List> rules = Arrays.asList(
createRule("TBL1", "COL1", SYSDBA, true, "DELETE"),
createRule("TBL1", "COL1", USER1, false, "DELETE"),
@@ -128,13 +150,13 @@ void testColumnPrivileges_TBL1_all(String allPattern) throws Exception {
createRule("TBL1", "val3", USER1, false, "UPDATE"),
createRule("TBL1", "val3", user2, false, "UPDATE"));
- validateExpectedColumnPrivileges(ifSchemaElse("PUBLIC", ""), "TBL1", allPattern, rules);
- // schema = null should also find it:
- validateExpectedColumnPrivileges(null, "TBL1", allPattern, rules);
+ validateExpectedColumnPrivileges(schemaNull ? null : ifSchemaElse("PUBLIC", ""), "TBL1", columnNameAllPattern,
+ rules);
}
- @Test
- void testColumnPrivileges_TBL1_COL_wildcard() throws Exception {
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void testColumnPrivileges_TBL1_COL_wildcard(boolean schemaNull) throws Exception {
List> rules = Arrays.asList(
createRule("TBL1", "COL1", SYSDBA, true, "DELETE"),
createRule("TBL1", "COL1", USER1, false, "DELETE"),
@@ -160,13 +182,12 @@ void testColumnPrivileges_TBL1_COL_wildcard() throws Exception {
createRule("TBL1", "COL2", SYSDBA, true, "UPDATE"),
createRule("TBL1", "COL2", USER1, false, "UPDATE"));
- validateExpectedColumnPrivileges(ifSchemaElse("PUBLIC", ""), "TBL1", "COL%", rules);
- // schema = null should also find it:
- validateExpectedColumnPrivileges(null, "TBL1", "COL%", rules);
+ validateExpectedColumnPrivileges(schemaNull ? null : ifSchemaElse("PUBLIC", ""), "TBL1", "COL%", rules);
}
- @Test
- void testColumnPrivileges_tbl2_all() throws Exception {
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void testColumnPrivileges_tbl2_all(boolean schemaNull) throws Exception {
List> rules = Arrays.asList(
createRule("tbl2", "COL1", SYSDBA, true, "DELETE"),
createRule("tbl2", "COL1", SYSDBA, true, "INSERT"),
@@ -188,7 +209,45 @@ void testColumnPrivileges_tbl2_all() throws Exception {
createRule("tbl2", "val3", user2, true, "SELECT"),
createRule("tbl2", "val3", SYSDBA, true, "UPDATE"));
- validateExpectedColumnPrivileges(ifSchemaElse("PUBLIC", ""), "tbl2", "%", rules);
+ validateExpectedColumnPrivileges(schemaNull ? null : ifSchemaElse("PUBLIC", ""), "tbl2", "%", rules);
+ }
+
+ @ParameterizedTest
+ @CsvSource(useHeadersInDisplayName = true, nullValues = { "" }, textBlock = """
+ schemaNull, columNameAllPattern
+ false, %
+ false,
+ true, %
+ true,
+ """)
+ void testColumnPrivileges_other_schema_tbl2_all(boolean schemaNull, String columnNameAllPattern) throws Exception {
+ assumeFeature(FirebirdSupportInfo::supportsSchemas, "Test requires schema support");
+ var rules = Arrays.asList(
+ createRule("OTHER_SCHEMA", "TBL3", "COL1", SYSDBA, true, "DELETE"),
+ createRule("OTHER_SCHEMA", "TBL3", "COL1", SYSDBA, true, "INSERT"),
+ createRule("OTHER_SCHEMA", "TBL3", "COL1", SYSDBA, true, "REFERENCES"),
+ createRule("OTHER_SCHEMA", "TBL3", "COL1", PUBLIC, false, "SELECT"),
+ createRule("OTHER_SCHEMA", "TBL3", "COL1", SYSDBA, true, "SELECT"),
+ createRule("OTHER_SCHEMA", "TBL3", "COL1", SYSDBA, true, "UPDATE"),
+ createRule("OTHER_SCHEMA", "TBL3", "COL1", USER1, false, "UPDATE"),
+ createRule("OTHER_SCHEMA", "TBL3", "COL2", SYSDBA, true, "DELETE"),
+ createRule("OTHER_SCHEMA", "TBL3", "COL2", SYSDBA, true, "INSERT"),
+ createRule("OTHER_SCHEMA", "TBL3", "COL2", SYSDBA, true, "REFERENCES"),
+ createRule("OTHER_SCHEMA", "TBL3", "COL2", PUBLIC, false, "SELECT"),
+ createRule("OTHER_SCHEMA", "TBL3", "COL2", SYSDBA, true, "SELECT"),
+ createRule("OTHER_SCHEMA", "TBL3", "COL2", SYSDBA, true, "UPDATE"),
+ createRule("OTHER_SCHEMA", "TBL3", "COL2", USER1, false, "UPDATE"),
+ createRule("OTHER_SCHEMA", "TBL3", "val3", SYSDBA, true, "DELETE"),
+ createRule("OTHER_SCHEMA", "TBL3", "val3", SYSDBA, true, "INSERT"),
+ createRule("OTHER_SCHEMA", "TBL3", "val3", SYSDBA, true, "REFERENCES"),
+ createRule("OTHER_SCHEMA", "TBL3", "val3", PUBLIC, false, "SELECT"),
+ createRule("OTHER_SCHEMA", "TBL3", "val3", SYSDBA, true, "SELECT"),
+ createRule("OTHER_SCHEMA", "TBL3", "val3", SYSDBA, true, "UPDATE"),
+ createRule("OTHER_SCHEMA", "TBL3", "val3", USER1, false, "UPDATE")
+ );
+
+ validateExpectedColumnPrivileges(schemaNull ? null : "OTHER_SCHEMA", "TBL3", columnNameAllPattern,
+ rules);
}
private Map createRule(String table, String columnName, String grantee,
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnsTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnsTest.java
index 01f384573..9117372f0 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnsTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataColumnsTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2012-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2012-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
@@ -18,78 +18,86 @@
import static org.firebirdsql.common.FBTestProperties.getConnectionViaDriverManager;
import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
+import static org.firebirdsql.common.FbAssumptions.assumeFeature;
import static org.firebirdsql.jaybird.util.StringUtils.trimToNull;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
/**
* Tests for {@link FBDatabaseMetaData} for column related metadata.
- *
+ *
* @author Mark Rotteveel
*/
class FBDatabaseMetaDataColumnsTest {
- // TODO Add schema support: tests involving other schema
-
private static final String TEST_TABLE = "TEST_COLUMN_METADATA";
private static final String CREATE_DOMAIN_WITH_DEFAULT =
"CREATE DOMAIN DOMAIN_WITH_DEFAULT AS VARCHAR(100) DEFAULT 'this is a default'";
- //@formatter:off
- private static final String CREATE_COLUMN_METADATA_TEST_TABLE =
- "CREATE TABLE " + TEST_TABLE + " (" +
- " col_integer INTEGER," +
- " col_bigint BIGINT," +
- " col_smallint SMALLINT," +
- " col_double DOUBLE PRECISION," +
- " col_float FLOAT," +
- " col_dec18_2 DECIMAL(18,2)," +
- " col_dec18_0 DECIMAL(18,0)," +
- " col_dec7_3 DECIMAL(7,3)," +
- " col_dec7_0 DECIMAL(7,0)," +
- " col_dec4_3 DECIMAL(4,3), " +
- " col_dec4_0 DECIMAL(4,0), " +
- " col_num18_2 NUMERIC(18,2)," +
- " col_num18_0 NUMERIC(18,0)," +
- " col_num7_3 NUMERIC(7,3)," +
- " col_num7_0 NUMERIC(7,0)," +
- " col_num4_3 NUMERIC(4,3), " +
- " col_num4_0 NUMERIC(4,0), " +
- " col_date DATE," +
- " col_time TIME," +
- " col_timestamp TIMESTAMP," +
- " col_char_10_utf8 CHAR(10) CHARACTER SET UTF8," +
- " col_char_10_iso8859_1 CHAR(10) CHARACTER SET ISO8859_1," +
- " col_char_10_octets CHAR(10) CHARACTER SET OCTETS," +
- " col_varchar_10_utf8 VARCHAR(10) CHARACTER SET UTF8," +
- " col_varchar_10_iso8859_1 VARCHAR(10) CHARACTER SET ISO8859_1," +
- " col_varchar_10_octets VARCHAR(10) CHARACTER SET OCTETS," +
- " col_blob_text_utf8 BLOB SUB_TYPE TEXT CHARACTER SET UTF8," +
- " col_blob_text_iso8859_1 BLOB SUB_TYPE TEXT CHARACTER SET ISO8859_1," +
- " col_blob_binary BLOB SUB_TYPE BINARY," +
- " col_integer_not_null INTEGER NOT NULL," +
- " col_varchar_not_null VARCHAR(100) NOT NULL," +
- " col_integer_default_null INTEGER DEFAULT NULL," +
- " col_integer_default_999 INTEGER DEFAULT 999," +
- " col_varchar_default_null VARCHAR(100) DEFAULT NULL," +
- " col_varchar_default_user VARCHAR(100) DEFAULT USER," +
- " col_varchar_default_literal VARCHAR(100) DEFAULT 'literal'," +
- " col_varchar_generated VARCHAR(200) COMPUTED BY (col_varchar_default_user || ' ' || col_varchar_default_literal)," +
- " col_domain_with_default DOMAIN_WITH_DEFAULT," +
- " col_domain_w_default_overridden DOMAIN_WITH_DEFAULT DEFAULT 'overridden default' " +
- " /* boolean */ " +
- " /* decfloat */ " +
- " /* extended numerics */ " +
- " /* time zone */ " +
- " /* int128 */ " +
- ")";
- //@formatter:on
+ private static final String CREATE_COLUMN_METADATA_TEST_TABLE = """
+ CREATE TABLE TEST_COLUMN_METADATA (
+ col_integer INTEGER,
+ col_bigint BIGINT,
+ col_smallint SMALLINT,
+ col_double DOUBLE PRECISION,
+ col_float FLOAT,
+ col_dec18_2 DECIMAL(18,2),
+ col_dec18_0 DECIMAL(18,0),
+ col_dec7_3 DECIMAL(7,3),
+ col_dec7_0 DECIMAL(7,0),
+ col_dec4_3 DECIMAL(4,3),
+ col_dec4_0 DECIMAL(4,0),
+ col_num18_2 NUMERIC(18,2),
+ col_num18_0 NUMERIC(18,0),
+ col_num7_3 NUMERIC(7,3),
+ col_num7_0 NUMERIC(7,0),
+ col_num4_3 NUMERIC(4,3),
+ col_num4_0 NUMERIC(4,0),
+ col_date DATE,
+ col_time TIME,
+ col_timestamp TIMESTAMP,
+ col_char_10_utf8 CHAR(10) CHARACTER SET UTF8,
+ col_char_10_iso8859_1 CHAR(10) CHARACTER SET ISO8859_1,
+ col_char_10_octets CHAR(10) CHARACTER SET OCTETS,
+ col_varchar_10_utf8 VARCHAR(10) CHARACTER SET UTF8,
+ col_varchar_10_iso8859_1 VARCHAR(10) CHARACTER SET ISO8859_1,
+ col_varchar_10_octets VARCHAR(10) CHARACTER SET OCTETS,
+ col_blob_text_utf8 BLOB SUB_TYPE TEXT CHARACTER SET UTF8,
+ col_blob_text_iso8859_1 BLOB SUB_TYPE TEXT CHARACTER SET ISO8859_1,
+ col_blob_binary BLOB SUB_TYPE BINARY,
+ col_integer_not_null INTEGER NOT NULL,
+ col_varchar_not_null VARCHAR(100) NOT NULL,
+ col_integer_default_null INTEGER DEFAULT NULL,
+ col_integer_default_999 INTEGER DEFAULT 999,
+ col_varchar_default_null VARCHAR(100) DEFAULT NULL,
+ col_varchar_default_user VARCHAR(100) DEFAULT USER,
+ col_varchar_default_literal VARCHAR(100) DEFAULT 'literal',
+ col_varchar_generated VARCHAR(200) COMPUTED BY (col_varchar_default_user || ' ' || col_varchar_default_literal),
+ col_domain_with_default DOMAIN_WITH_DEFAULT,
+ col_domain_w_default_overridden DOMAIN_WITH_DEFAULT DEFAULT 'overridden default'
+ /* boolean */
+ /* decfloat */
+ /* extended numerics */
+ /* time zone */
+ /* int128 */
+ )""";
private static final String ADD_COMMENT_ON_COLUMN =
"COMMENT ON COLUMN test_column_metadata.col_integer IS 'Some comment'";
+ private static final String OTHER_SCHEMA = "OTHER_SCHEMA";
+
+ private static final String CREATE_OTHER_SCHEMA = "create schema OTHER_SCHEMA";
+
+ private static final String OTHER_SCHEMA_TABLE = "OTHER_TABLE";
+
+ private static final String CREATE_OTHER_SCHEMA_TABLE = """
+ create table OTHER_SCHEMA.OTHER_TABLE (
+ ID integer constraint PK_OTHER_TABLE primary key,
+ COL2 varchar(50)
+ )""";
+
private static final MetadataResultSetDefinition getColumnsDefinition =
new MetadataResultSetDefinition(ColumnMetaData.class);
@@ -145,14 +153,19 @@ private static List getCreateStatements() {
createTable = createTable.replace("/* int128 */",
", col_int128 INT128");
}
-
statements.add(createTable);
+
if (supportInfo.supportsComment()) {
statements.add(ADD_COMMENT_ON_COLUMN);
}
+ if (supportInfo.supportsSchemas()) {
+ statements.add(CREATE_OTHER_SCHEMA);
+ statements.add(CREATE_OTHER_SCHEMA_TABLE);
+ }
+
return statements;
}
-
+
/**
* Tests the ordinal positions and types for the metadata columns of getColumns().
*/
@@ -750,7 +763,39 @@ void testInt128Column() throws Exception {
validate(ifSchemaElse("PUBLIC", ""), TEST_TABLE, "COL_INT128", validationRules);
}
-
+
+ @Test
+ void testOtherSchemaTable() throws Exception {
+ assumeFeature(FirebirdSupportInfo::supportsSchemas, "Test requires schema support");
+
+ List> validationRules = new ArrayList<>();
+ Map idRules = getDefaultValueValidationRules();
+ idRules.put(ColumnMetaData.TABLE_SCHEM, OTHER_SCHEMA);
+ idRules.put(ColumnMetaData.TABLE_NAME, OTHER_SCHEMA_TABLE);
+ idRules.put(ColumnMetaData.COLUMN_NAME, "ID");
+ idRules.put(ColumnMetaData.DATA_TYPE, Types.INTEGER);
+ idRules.put(ColumnMetaData.TYPE_NAME, "INTEGER");
+ idRules.put(ColumnMetaData.IS_NULLABLE, "NO");
+ idRules.put(ColumnMetaData.NULLABLE, DatabaseMetaData.columnNoNulls);
+ idRules.put(ColumnMetaData.COLUMN_SIZE, 10);
+ idRules.put(ColumnMetaData.DECIMAL_DIGITS, 0);
+ idRules.put(ColumnMetaData.ORDINAL_POSITION, 1);
+ idRules.put(ColumnMetaData.IS_AUTOINCREMENT, "");
+ validationRules.add(idRules);
+ Map col2Rules = getDefaultValueValidationRules();
+ col2Rules.put(ColumnMetaData.TABLE_SCHEM, OTHER_SCHEMA);
+ col2Rules.put(ColumnMetaData.TABLE_NAME, OTHER_SCHEMA_TABLE);
+ col2Rules.put(ColumnMetaData.COLUMN_NAME, "COL2");
+ col2Rules.put(ColumnMetaData.DATA_TYPE, Types.VARCHAR);
+ col2Rules.put(ColumnMetaData.TYPE_NAME, "VARCHAR");
+ col2Rules.put(ColumnMetaData.COLUMN_SIZE, 50);
+ col2Rules.put(ColumnMetaData.CHAR_OCTET_LENGTH, 50);
+ col2Rules.put(ColumnMetaData.ORDINAL_POSITION, 2);
+ validationRules.add(col2Rules);
+
+ validate(OTHER_SCHEMA, OTHER_SCHEMA_TABLE, "%", validationRules);
+ }
+
// TODO: Add more extensive tests of patterns
/**
@@ -773,10 +818,22 @@ private void validate(String schema, String tableName, String columnName,
validationRules.put(ColumnMetaData.COLUMN_NAME, columnName);
getColumnsDefinition.checkValidationRulesComplete(validationRules);
- try (ResultSet columns = dbmd.getColumns(null, schema, tableName, columnName)) {
- assertTrue(columns.next(), "Expected row in column metadata");
- getColumnsDefinition.validateRowValues(columns, validationRules);
- assertFalse(columns.next(), "Expected only one row in resultset");
+ validate(schema, tableName, columnName, List.of(validationRules));
+ }
+
+ private void validate(String schema, String tableName, String columnNamePattern,
+ List> expectedColumns) throws Exception {
+ try (ResultSet columns = dbmd.getColumns(null, schema, tableName, columnNamePattern)) {
+ int columnCount = 0;
+ while (columns.next()) {
+ if (columnCount < expectedColumns.size()) {
+ Map rules = expectedColumns.get(columnCount);
+ getColumnsDefinition.checkValidationRulesComplete(rules);
+ getColumnsDefinition.validateRowValues(columns, rules);
+ }
+ columnCount++;
+ }
+ assertEquals(expectedColumns.size(), columnCount, "Unexpected number of columns");
}
}
@@ -810,34 +867,34 @@ private void validate(String schema, String tableName, String columnName,
private static Map getDefaultValueValidationRules() {
return new EnumMap<>(DEFAULT_COLUMN_VALUES);
}
-
+
/**
* Columns defined for the getColumns() metadata.
*/
private enum ColumnMetaData implements MetaDataInfo {
- TABLE_CAT(1, String.class),
- TABLE_SCHEM(2, String.class),
- TABLE_NAME(3, String.class),
- COLUMN_NAME(4, String.class),
- DATA_TYPE(5, Integer.class),
- TYPE_NAME(6, String.class),
- COLUMN_SIZE(7, Integer.class),
- BUFFER_LENGTH(8, Integer.class),
- DECIMAL_DIGITS(9, Integer.class),
- NUM_PREC_RADIX(10, Integer.class),
- NULLABLE(11, Integer.class),
- REMARKS(12, String.class),
- COLUMN_DEF(13, String.class),
- SQL_DATA_TYPE(14, Integer.class),
- SQL_DATETIME_SUB(15, Integer.class),
- CHAR_OCTET_LENGTH(16, Integer.class),
- ORDINAL_POSITION(17, Integer.class),
- IS_NULLABLE(18, String.class),
+ TABLE_CAT(1, String.class),
+ TABLE_SCHEM(2, String.class),
+ TABLE_NAME(3, String.class),
+ COLUMN_NAME(4, String.class),
+ DATA_TYPE(5, Integer.class),
+ TYPE_NAME(6, String.class),
+ COLUMN_SIZE(7, Integer.class),
+ BUFFER_LENGTH(8, Integer.class),
+ DECIMAL_DIGITS(9, Integer.class),
+ NUM_PREC_RADIX(10, Integer.class),
+ NULLABLE(11, Integer.class),
+ REMARKS(12, String.class),
+ COLUMN_DEF(13, String.class),
+ SQL_DATA_TYPE(14, Integer.class),
+ SQL_DATETIME_SUB(15, Integer.class),
+ CHAR_OCTET_LENGTH(16, Integer.class),
+ ORDINAL_POSITION(17, Integer.class),
+ IS_NULLABLE(18, String.class),
SCOPE_CATALOG(19, String.class),
- SCOPE_SCHEMA(20, String.class),
- SCOPE_TABLE(21, String.class),
- SOURCE_DATA_TYPE(22, Short.class),
- IS_AUTOINCREMENT(23, String.class),
+ SCOPE_SCHEMA(20, String.class),
+ SCOPE_TABLE(21, String.class),
+ SOURCE_DATA_TYPE(22, Short.class),
+ IS_AUTOINCREMENT(23, String.class),
IS_GENERATEDCOLUMN(24,String.class),
JB_IS_IDENTITY(25,String.class),
JB_IDENTITY_TYPE(26,String.class);
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataCrossReferenceTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataCrossReferenceTest.java
index 041bc72a6..4e015b49b 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataCrossReferenceTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataCrossReferenceTest.java
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jdbc;
+import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@@ -12,6 +13,7 @@
import java.util.Map;
import java.util.stream.Stream;
+import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
/**
@@ -21,8 +23,6 @@
*/
class FBDatabaseMetaDataCrossReferenceTest extends FBDatabaseMetaDataAbstractKeysTest {
- // TODO Add schema support: tests involving other schema
-
@Test
void testCrossReferenceMetaDataColumns() throws Exception {
try (ResultSet crossReference = dbmd.getCrossReference(
@@ -31,33 +31,55 @@ void testCrossReferenceMetaDataColumns() throws Exception {
}
}
- @ParameterizedTest(name = "{0} - {1}")
+ @ParameterizedTest(name = "({0}, {1}) - ({2}, {3})")
@MethodSource
- void testCrossReference(String parentTable, String foreignTable, List> expectedKeys)
+ void testCrossReference(@Nullable String parentSchema, String parentTable,
+ @Nullable String foreignSchema, String foreignTable,
+ List> expectedKeys)
throws Exception {
- try (ResultSet crossReference = dbmd.getCrossReference(null, ifSchemaElse("PUBLIC", ""), parentTable, null,
- ifSchemaElse("PUBLIC", ""), foreignTable)) {
+ try (ResultSet crossReference = dbmd.getCrossReference(null, parentSchema, parentTable,
+ null, foreignSchema, foreignTable)) {
validateExpectedKeys(crossReference, expectedKeys);
}
}
static Stream testCrossReference() {
- return Stream.of(
+ var generalArguments = Stream.of(
crossRefTestCase("TABLE_1", "TABLE_2", table2Fks()),
+ crossRefTestCase(null, "TABLE_1", null, "TABLE_2", table2Fks()),
+ crossRefTestCase(ifSchemaElse("PUBLIC", ""), "TABLE_1", null, "TABLE_2", table2Fks()),
+ crossRefTestCase(null, "TABLE_1", ifSchemaElse("PUBLIC", ""), "TABLE_2", table2Fks()),
crossRefTestCase("TABLE_2", "TABLE_1", List.of()),
crossRefTestCase("TABLE_1", "TABLE_3", List.of()),
crossRefTestCase("TABLE_2", "TABLE_3", table3Fks()),
crossRefTestCase("TABLE_2", "TABLE_4", table4Fks()),
crossRefTestCase("TABLE_2", "TABLE_5", table5Fks()),
crossRefTestCase("TABLE_2", "TABLE_6", table6Fks()),
- crossRefTestCase("TABLE_6", "TABLE_7", table7Fks()),
+ crossRefTestCase("TABLE_6", "TABLE_7", table7to6Fks()),
crossRefTestCase("TABLE_1", "doesnotexist", List.of()),
crossRefTestCase("doesnotexist", "TABLE_2", List.of()));
+ if (!getDefaultSupportInfo().supportsSchemas()) {
+ return generalArguments;
+ }
+ return Stream.concat(generalArguments, Stream.of(
+ crossRefTestCase("PUBLIC", "TABLE_1", "OTHER_SCHEMA", "TABLE_8", table8Fks()),
+ crossRefTestCase(null, "TABLE_1", null, "TABLE_8", table8Fks()),
+ crossRefTestCase("PUBLIC", "TABLE_1", null, "TABLE_8", table8Fks()),
+ crossRefTestCase(null, "TABLE_1", "OTHER_SCHEMA", "TABLE_8", table8Fks()),
+ crossRefTestCase("OTHER_SCHEMA", "TABLE_8", "PUBLIC", "TABLE_7", table7to8Fks())
+ ));
}
private static Arguments crossRefTestCase(String parentTable, String foreignTable,
List> expectedKeys) {
- return Arguments.of(parentTable, foreignTable, expectedKeys);
+ String defaultSchema = ifSchemaElse("PUBLIC", "");
+ return crossRefTestCase(defaultSchema, parentTable, defaultSchema, foreignTable, expectedKeys);
+ }
+
+ private static Arguments crossRefTestCase(@Nullable String parentSchema, String parentTable,
+ @Nullable String foreignSchema, String foreignTable,
+ List> expectedKeys) {
+ return Arguments.of(parentSchema, parentTable, foreignSchema, foreignTable, expectedKeys);
}
}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataExportedKeysTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataExportedKeysTest.java
index 3b4217ebe..16632bcbf 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataExportedKeysTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataExportedKeysTest.java
@@ -8,12 +8,13 @@
import org.junit.jupiter.params.provider.MethodSource;
import java.sql.ResultSet;
-import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
+import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
/**
@@ -23,8 +24,6 @@
*/
class FBDatabaseMetaDataExportedKeysTest extends FBDatabaseMetaDataAbstractKeysTest {
- // TODO Add schema support: tests involving other schema
-
@Test
void testExportedKeysMetaDataColumns() throws Exception {
try (ResultSet exportedKeys = dbmd.getExportedKeys(null, null, "doesnotexit")) {
@@ -32,7 +31,7 @@ void testExportedKeysMetaDataColumns() throws Exception {
}
}
- @ParameterizedTest
+ @ParameterizedTest(name = "({0}, {1})")
@MethodSource
void testExportedKeys(String schema, String table, List> expectedKeys) throws Exception {
try (ResultSet exportedKeys = dbmd.getExportedKeys(null, schema, table)) {
@@ -41,12 +40,19 @@ void testExportedKeys(String schema, String table, List testExportedKeys() {
- return Stream.of(
- exportedKeysTestCase("TABLE_1", table2Fks()),
+ var generalArguments = Stream.of(
+ exportedKeysTestCase("TABLE_1", ifSchemaElse(table8Fks(), List.of()), table2Fks()),
+ exportedKeysTestCase(null, "TABLE_1", ifSchemaElse(table8Fks(), List.of()), table2Fks()),
exportedKeysTestCase("doesnotexist", List.of()),
exportedKeysTestCase("TABLE_2", table3Fks(), table4Fks(), table5Fks(), table6Fks()),
exportedKeysTestCase("TABLE_3", List.of()),
- exportedKeysTestCase("TABLE_6", table7Fks()));
+ exportedKeysTestCase("TABLE_6", table7to6Fks()));
+ if (!getDefaultSupportInfo().supportsSchemas()) {
+ return generalArguments;
+ }
+ return Stream.concat(generalArguments, Stream.of(
+ exportedKeysTestCase("OTHER_SCHEMA", "TABLE_8", table7to8Fks()),
+ exportedKeysTestCase(null, "TABLE_8", table7to8Fks())));
}
private static Arguments exportedKeysTestCase(String table, List> expectedKeys) {
@@ -58,12 +64,16 @@ private static Arguments exportedKeysTestCase(String schema, String table,
return Arguments.of(schema, table, expectedKeys);
}
- @SuppressWarnings("SameParameterValue")
@SafeVarargs
private static Arguments exportedKeysTestCase(String table, List>... expectedKeys) {
- var combinedExpectedKeys = new ArrayList>();
- Arrays.stream(expectedKeys).forEach(combinedExpectedKeys::addAll);
- return exportedKeysTestCase(table, combinedExpectedKeys);
+ return exportedKeysTestCase(ifSchemaElse("PUBLIC", ""), table, expectedKeys);
+ }
+
+ @SafeVarargs
+ private static Arguments exportedKeysTestCase(String schema, String table,
+ List>... expectedKeys) {
+ var combinedExpectedKeys = Arrays.stream(expectedKeys).flatMap(Collection::stream).toList();
+ return exportedKeysTestCase(schema, table, combinedExpectedKeys);
}
}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionColumnsTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionColumnsTest.java
index 9d20e0a40..b7a6ef492 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionColumnsTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionColumnsTest.java
@@ -4,6 +4,8 @@
import org.firebirdsql.common.extension.UsesDatabaseExtension;
import org.firebirdsql.jaybird.props.PropertyNames;
+import org.firebirdsql.jaybird.util.CollectionUtils;
+import org.firebirdsql.jaybird.util.QualifiedName;
import org.firebirdsql.util.FirebirdSupportInfo;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
@@ -23,6 +25,7 @@
import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
import static org.firebirdsql.common.FBTestProperties.getUrl;
import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
+import static org.firebirdsql.common.FbAssumptions.assumeFeature;
import static org.firebirdsql.common.assertions.ResultSetAssertions.assertNextRow;
import static org.firebirdsql.jdbc.FBDatabaseMetaDataFunctionsTest.isIgnoredFunction;
import static org.firebirdsql.jdbc.metadata.FbMetadataConstants.*;
@@ -36,10 +39,9 @@
*/
class FBDatabaseMetaDataFunctionColumnsTest {
- // TODO Add schema support: tests involving other schema
-
private static final String PSQL_EXAMPLE_1 = "PSQL$EXAMPLE$1";
private static final String PSQL_EXAMPLE_2 = "PSQL$EXAMPLE$2";
+ private static final String PSQL_EXAMPLE_3 = "PSQL$EXAMPLE$3";
private static final String UDF_EXAMPLE_1 = "UDF$EXAMPLE$1";
private static final String UDF_EXAMPLE_2 = "UDF$EXAMPLE$2";
private static final String UDF_EXAMPLE_3 = "UDF$EXAMPLE$3";
@@ -50,77 +52,93 @@ class FBDatabaseMetaDataFunctionColumnsTest {
private static final String CREATE_DOMAIN_D_INTEGER =
"create domain D_INTEGER as integer";
- private static final String CREATE_PSQL_EXAMPLE_1 = "create function " + PSQL_EXAMPLE_1 + "("
- + "C$01$FLOAT float not null,"
- + "C$02$DOUBLE double precision,"
- + "C$03$CHAR10 char(10),"
- + "C$04$VARCHAR15 varchar(15) not null,"
- + "C$05$BINARY20 char(20) character set octets,"
- + "C$06$VARBINARY25 varchar(25) character set octets,"
- + "C$07$BIGINT bigint,"
- + "C$08$INTEGER integer,"
- + "C$09$SMALLINT smallint,"
- + "C$10$NUMERIC18$3 numeric(18,3),"
- + "C$11$NUMERIC9$3 numeric(9,3),"
- + "C$12$NUMERIC4$3 numeric(4,3),"
- + "C$13$DECIMAL18$2 decimal(18,2),"
- + "C$14$DECIMAL9$2 decimal(9,2),"
- + "C$15$DECIMAL4$2 decimal(4,2),"
- + "C$16$DATE date,"
- + "C$17$TIME time,"
- + "C$18$TIMESTAMP timestamp,"
- + "C$19$BOOLEAN boolean,"
- + "C$20$D_INTEGER_NOT_NULL D_INTEGER_NOT_NULL,"
- + "C$21$D_INTEGER_WITH_NOT_NULL D_INTEGER NOT NULL) "
- + "returns varchar(100) "
- + "as "
- + "begin"
- + " return 'a';"
- + "end";
-
- private static final String CREATE_PSQL_EXAMPLE_2 = "create function " + PSQL_EXAMPLE_2 + "("
- + "C$01$TIME_WITH_TIME_ZONE time with time zone,"
- + "C$02$TIMESTAMP_WITH_TIME_ZONE timestamp with time zone,"
- + "C$03$DECFLOAT decfloat,"
- + "C$04$DECFLOAT16 decfloat(16),"
- + "C$05$DECFLOAT34 decfloat(34),"
- + "C$06$NUMERIC21$5 numeric(21,5),"
- + "C$07$DECIMAL34$19 decimal(34,19)) "
- + "returns varchar(100) not null "
- + "as "
- + "begin"
- + " return 'a';"
- + "end";
-
- private static final String CREATE_UDF_EXAMPLE_1 = "declare external function " + UDF_EXAMPLE_1
- + "/* 1*/ float by descriptor,"
- + "/* 2*/ double precision,"
- + "/* 3*/ char(10),"
- + "/* 4*/ varchar(15) by descriptor,"
- + "/* 5*/ char(20) character set octets,"
- + "/* 6*/ varchar(25) character set octets,"
- + "/* 7*/ bigint,"
- + "/* 8*/ integer,"
- + "/* 9*/ smallint,"
- + "/*10*/ numeric(18,3) "
- + "returns varchar(100) "
- + "entry_point 'UDF$EXAMPLE$1' module_name 'module_1'";
-
- private static final String CREATE_UDF_EXAMPLE_2 = "declare external function " + UDF_EXAMPLE_2
- + "/* 1*/ numeric(9,3),"
- + "/* 2*/ numeric(4,3),"
- + "/* 3*/ decimal(18,2),"
- + "/* 4*/ decimal(9,2),"
- + "/* 5*/ decimal(4,2),"
- + "/* 6*/ date,"
- + "/* 7*/ time,"
- + "/* 8*/ timestamp "
- + "returns varchar(100) by descriptor "
- + "entry_point 'UDF$EXAMPLE$2' module_name 'module_1'";
-
- private static final String CREATE_UDF_EXAMPLE_3 = "declare external function " + UDF_EXAMPLE_3
- + " returns cstring(100)"
- + "entry_point 'UDF$EXAMPLE$3' module_name 'module_1'";
+ private static final String CREATE_PSQL_EXAMPLE_1 = """
+ create function PSQL$EXAMPLE$1(
+ C$01$FLOAT float not null,
+ C$02$DOUBLE double precision,
+ C$03$CHAR10 char(10),
+ C$04$VARCHAR15 varchar(15) not null,
+ C$05$BINARY20 char(20) character set octets,
+ C$06$VARBINARY25 varchar(25) character set octets,
+ C$07$BIGINT bigint,
+ C$08$INTEGER integer,
+ C$09$SMALLINT smallint,
+ C$10$NUMERIC18$3 numeric(18,3),
+ C$11$NUMERIC9$3 numeric(9,3),
+ C$12$NUMERIC4$3 numeric(4,3),
+ C$13$DECIMAL18$2 decimal(18,2),
+ C$14$DECIMAL9$2 decimal(9,2),
+ C$15$DECIMAL4$2 decimal(4,2),
+ C$16$DATE date,
+ C$17$TIME time,
+ C$18$TIMESTAMP timestamp,
+ C$19$BOOLEAN boolean,
+ C$20$D_INTEGER_NOT_NULL D_INTEGER_NOT_NULL,
+ C$21$D_INTEGER_WITH_NOT_NULL D_INTEGER NOT NULL)
+ returns varchar(100)
+ as
+ begin
+ return 'a';
+ end""";
+
+ private static final String CREATE_PSQL_EXAMPLE_2 = """
+ create function PSQL$EXAMPLE$2(
+ C$01$TIME_WITH_TIME_ZONE time with time zone,
+ C$02$TIMESTAMP_WITH_TIME_ZONE timestamp with time zone,
+ C$03$DECFLOAT decfloat,
+ C$04$DECFLOAT16 decfloat(16),
+ C$05$DECFLOAT34 decfloat(34),
+ C$06$NUMERIC21$5 numeric(21,5),
+ C$07$DECIMAL34$19 decimal(34,19))
+ returns varchar(100) not null
+ as
+ begin
+ return 'a';
+ end""";
+
+ private static final String CREATE_OTHER_SCHEMA = "create schema OTHER_SCHEMA";
+
+ private static final String CREATE_PSQL_EXAMPLE_3 = """
+ create function OTHER_SCHEMA.PSQL$EXAMPLE$3(
+ C$01$TIME_WITH_TIME_ZONE time with time zone)
+ returns varchar(100) not null
+ as
+ begin
+ return 'a';
+ end""";
+
+ private static final String CREATE_UDF_EXAMPLE_1 = """
+ declare external function UDF$EXAMPLE$1
+ /* 1*/ float by descriptor,
+ /* 2*/ double precision,
+ /* 3*/ char(10),
+ /* 4*/ varchar(15) by descriptor,
+ /* 5*/ char(20) character set octets,
+ /* 6*/ varchar(25) character set octets,
+ /* 7*/ bigint,
+ /* 8*/ integer,
+ /* 9*/ smallint,
+ /*10*/ numeric(18,3)
+ returns varchar(100)
+ entry_point 'UDF$EXAMPLE$1' module_name 'module_1'""";
+
+ private static final String CREATE_UDF_EXAMPLE_2 = """
+ declare external function UDF$EXAMPLE$2
+ /* 1*/ numeric(9,3),
+ /* 2*/ numeric(4,3),
+ /* 3*/ decimal(18,2),
+ /* 4*/ decimal(9,2),
+ /* 5*/ decimal(4,2),
+ /* 6*/ date,
+ /* 7*/ time,
+ /* 8*/ timestamp
+ returns varchar(100) by descriptor
+ entry_point 'UDF$EXAMPLE$2' module_name 'module_1'""";
+
+ private static final String CREATE_UDF_EXAMPLE_3 = """
+ declare external function UDF$EXAMPLE$3
+ returns cstring(100)
+ entry_point 'UDF$EXAMPLE$3' module_name 'module_1'""";
private static final String CREATE_PACKAGE_WITH_FUNCTION = """
create package WITH$FUNCTION
@@ -177,7 +195,7 @@ private static List getCreateStatements() {
if (supportInfo.supportsPsqlFunctions()) {
statements.add(CREATE_PSQL_EXAMPLE_1);
- if (supportInfo.isVersionEqualOrAbove(4, 0)) {
+ if (supportInfo.isVersionEqualOrAbove(4)) {
statements.add(CREATE_PSQL_EXAMPLE_2);
}
@@ -191,6 +209,11 @@ private static List getCreateStatements() {
statements.add(CREATE_UDF_EXAMPLE_2);
statements.add(CREATE_UDF_EXAMPLE_3);
+ if (supportInfo.supportsSchemas()) {
+ statements.add(CREATE_OTHER_SCHEMA);
+ statements.add(CREATE_PSQL_EXAMPLE_3);
+ }
+
return statements;
}
@@ -204,30 +227,12 @@ void testFunctionColumnMetaDataColumns() throws Exception {
}
}
- @Test
- void testFunctionColumnMetaData_everything_functionNamePattern_null() throws Exception {
- validateFunctionColumnMetaData_everything(null);
- }
-
- @Test
- void testFunctionColumnMetaData_everything_functionNamePattern_allPattern() throws Exception {
- validateFunctionColumnMetaData_everything("%");
- }
-
- private void validateFunctionColumnMetaData_everything(String functionNamePattern)
+ @ParameterizedTest
+ @NullSource
+ @ValueSource(strings = "%")
+ void testFunctionColumnMetaData_everything_functionNamePattern_all(String functionNamePattern)
throws Exception {
- FirebirdSupportInfo defaultSupportInfo = getDefaultSupportInfo();
- List> expectedColumns = new ArrayList<>();
- if (defaultSupportInfo.supportsPsqlFunctions()) {
- expectedColumns.addAll(getPsqlExample1Columns());
- if (defaultSupportInfo.isVersionEqualOrAbove(4, 0)) {
- expectedColumns.addAll(getPsqlExample2Columns());
- }
- }
- expectedColumns.addAll(getUdfExample1Columns());
- expectedColumns.addAll(getUdfExample2Columns());
- expectedColumns.add(createStringType(Types.VARCHAR, UDF_EXAMPLE_3, "PARAM_0", 0, 100, false));
- validateExpectedFunctionColumns(functionNamePattern, null, expectedColumns);
+ validateExpectedFunctionColumns(functionNamePattern, null, getAllNonPackagedFunctionColumns());
}
@Test
@@ -244,6 +249,25 @@ private void validateNoRows(String functionNamePattern, String columnNamePattern
validateExpectedFunctionColumns(functionNamePattern, columnNamePattern, Collections.emptyList());
}
+ @ParameterizedTest
+ @NullSource
+ @ValueSource(strings = "%")
+ void testFunctionColumnMetaData_defaultSchema_functionNamePatternAll(String functionNamePattern) throws Exception {
+ validateExpectedFunctionColumns("", ifSchemaElse("PUBLIC", ""), functionNamePattern, "%",
+ getDefaultSchemaAllNonPackagedFunctionColumns());
+ }
+
+ @ParameterizedTest
+ @NullSource
+ @ValueSource(strings = "%")
+ void testFunctionColumnMetaData_otherSchema_functionNamePatternAll(String functionNamePattern) throws Exception {
+ // For 4.0 and older, we ignore the schemaPattern, so we need to skip this test, as the query would return
+ // functions (the "default schema" functions) instead of no functions
+ assumeFeature(FirebirdSupportInfo::supportsSchemas, "Test requires schema support");
+ validateExpectedFunctionColumns("", "OTHER_SCHEMA", functionNamePattern, "%",
+ getOtherSchemaAllNonPackagedFunctionColumns());
+ }
+
@Test
void testFunctionColumnMetaData_PsqlExample1() throws Exception {
assumeTrue(getDefaultSupportInfo().supportsPsqlFunctions(), "Requires PSQL function support");
@@ -303,14 +327,8 @@ void testFunctionColumnMetaData_useCatalogAsPackage_everything() throws Exceptio
props.setProperty(PropertyNames.useCatalogAsPackage, "true");
try (var connection = DriverManager.getConnection(getUrl(), props)) {
dbmd = connection.getMetaData();
- var expectedColumns = new ArrayList<>(getPsqlExample1Columns());
- if (supportInfo.isVersionEqualOrAbove(4)) {
- expectedColumns.addAll(getPsqlExample2Columns());
- }
- expectedColumns.addAll(getUdfExample1Columns());
- expectedColumns.addAll(getUdfExample2Columns());
- expectedColumns.add(createStringType(Types.VARCHAR, UDF_EXAMPLE_3, "PARAM_0", 0, 100, false));
- withCatalog("", expectedColumns);
+ List> expectedColumns =
+ withCatalog("", getAllNonPackagedFunctionColumns());
expectedColumns.addAll(getWithFunctionInPackageColumns());
validateExpectedFunctionColumns(null, null, null, expectedColumns);
}
@@ -370,14 +388,8 @@ void testFunctionColumnMetaData_useCatalogAsPackage_nonPackagedOnly() throws Exc
props.setProperty(PropertyNames.useCatalogAsPackage, "true");
try (var connection = DriverManager.getConnection(getUrl(), props)) {
dbmd = connection.getMetaData();
- var expectedColumns = new ArrayList<>(getPsqlExample1Columns());
- if (supportInfo.isVersionEqualOrAbove(4)) {
- expectedColumns.addAll(getPsqlExample2Columns());
- }
- expectedColumns.addAll(getUdfExample1Columns());
- expectedColumns.addAll(getUdfExample2Columns());
- expectedColumns.add(createStringType(Types.VARCHAR, UDF_EXAMPLE_3, "PARAM_0", 0, 100, false));
- withCatalog("", expectedColumns);
+ List> expectedColumns =
+ withCatalog("", getAllNonPackagedFunctionColumns());
validateExpectedFunctionColumns("", null, null, expectedColumns);
}
}
@@ -418,6 +430,38 @@ private static void expectNoMoreRows(ResultSet rs) throws SQLException {
}
}
+ private static List> getAllNonPackagedFunctionColumns() {
+ return CollectionUtils.concat(
+ getOtherSchemaAllNonPackagedFunctionColumns(), getDefaultSchemaAllNonPackagedFunctionColumns());
+ }
+
+ /**
+ * NOTE: returns an empty list when schemas are not supported.
+ */
+ private static List> getOtherSchemaAllNonPackagedFunctionColumns() {
+ FirebirdSupportInfo supportInfo = getDefaultSupportInfo();
+ List> expectedColumns = new ArrayList<>();
+ if (supportInfo.supportsSchemas()) {
+ expectedColumns.addAll(getPsqlExample3Columns());
+ }
+ return expectedColumns;
+ }
+
+ private static List> getDefaultSchemaAllNonPackagedFunctionColumns() {
+ FirebirdSupportInfo supportInfo = getDefaultSupportInfo();
+ List> expectedColumns = new ArrayList<>();
+ if (supportInfo.supportsPsqlFunctions()) {
+ expectedColumns.addAll(getPsqlExample1Columns());
+ if (supportInfo.isVersionEqualOrAbove(4)) {
+ expectedColumns.addAll(getPsqlExample2Columns());
+ }
+ }
+ expectedColumns.addAll(getUdfExample1Columns());
+ expectedColumns.addAll(getUdfExample2Columns());
+ expectedColumns.add(createStringType(Types.VARCHAR, UDF_EXAMPLE_3, "PARAM_0", 0, 100, false));
+ return expectedColumns;
+ }
+
private static List> getPsqlExample1Columns() {
return List.of(
withColumnTypeFunctionReturn(createStringType(Types.VARCHAR, PSQL_EXAMPLE_1, "PARAM_0", 0, 100, true)),
@@ -456,6 +500,12 @@ private static List> getPsqlExample2Columns(
createNumericalType(Types.DECIMAL, PSQL_EXAMPLE_2, "C$07$DECIMAL34$19", 7, 34, 19, true));
}
+ private static List> getPsqlExample3Columns() {
+ return withSchema("OTHER_SCHEMA", List.of(
+ withColumnTypeFunctionReturn(createStringType(Types.VARCHAR, PSQL_EXAMPLE_3, "PARAM_0", 0, 100, false)),
+ createDateTime(Types.TIME_WITH_TIMEZONE, PSQL_EXAMPLE_3, "C$01$TIME_WITH_TIME_ZONE", 1, true)));
+ }
+
private static List> getUdfExample1Columns() {
return List.of(
withColumnTypeFunctionReturn(createStringType(Types.VARCHAR, UDF_EXAMPLE_1, "PARAM_0", 0, 100, false)),
@@ -515,12 +565,24 @@ private static List> withSpecificName(String
return rules;
}
+ @SuppressWarnings("SameParameterValue")
+ private static List> withSchema(String schema,
+ List> rules) {
+ for (Map rowRule : rules) {
+ String functionName = (String) rowRule.get(FunctionColumnMetaData.FUNCTION_NAME);
+ rowRule.put(FunctionColumnMetaData.SPECIFIC_NAME,
+ new QualifiedName(schema, functionName).toString(QuoteStrategy.DIALECT_3));
+ rowRule.put(FunctionColumnMetaData.FUNCTION_SCHEM, schema);
+ }
+ return rules;
+ }
+
private static Map createColumn(String functionName, String columnName,
int ordinalPosition, boolean nullable) {
Map rules = getDefaultValidationRules();
rules.put(FunctionColumnMetaData.FUNCTION_NAME, functionName);
rules.put(FunctionColumnMetaData.SPECIFIC_NAME, ifSchemaElse(
- "\"PUBLIC\"." + QuoteStrategy.DIALECT_3.quoteObjectName(functionName), functionName));
+ new QualifiedName("PUBLIC", functionName).toString(QuoteStrategy.DIALECT_3), functionName));
rules.put(FunctionColumnMetaData.COLUMN_NAME, columnName);
rules.put(FunctionColumnMetaData.ORDINAL_POSITION, ordinalPosition);
if (nullable) {
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataImportedKeysTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataImportedKeysTest.java
index 296cd3b42..cafe2fa7e 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataImportedKeysTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataImportedKeysTest.java
@@ -8,10 +8,12 @@
import org.junit.jupiter.params.provider.MethodSource;
import java.sql.ResultSet;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
+import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
/**
@@ -21,8 +23,6 @@
*/
class FBDatabaseMetaDataImportedKeysTest extends FBDatabaseMetaDataAbstractKeysTest {
- // TODO Add schema support: tests involving other schema
-
@Test
void testExportedKeysMetaDataColumns() throws Exception {
try (ResultSet importedKeys = dbmd.getImportedKeys(null, null, "doesnotexit")) {
@@ -30,7 +30,7 @@ void testExportedKeysMetaDataColumns() throws Exception {
}
}
- @ParameterizedTest
+ @ParameterizedTest(name = "({0}, {1})")
@MethodSource
void testImportedKeys(String schema, String table, List> expectedKeys) throws Exception {
try (ResultSet importedKeys = dbmd.getImportedKeys(null, schema, table)) {
@@ -39,15 +39,23 @@ void testImportedKeys(String schema, String table, List testImportedKeys() {
- return Stream.of(
+ var generalArguments = Stream.of(
importedKeysTestCase("TABLE_1", table1Fks()),
+ importedKeysTestCase(null, "TABLE_1", table1Fks()),
importedKeysTestCase("doesnotexist", List.of()),
importedKeysTestCase("TABLE_2", table2Fks()),
+ importedKeysTestCase(null, "TABLE_2", table2Fks()),
importedKeysTestCase("TABLE_3", table3Fks()),
importedKeysTestCase("TABLE_4", table4Fks()),
importedKeysTestCase("TABLE_5", table5Fks()),
importedKeysTestCase("TABLE_6", table6Fks()),
- importedKeysTestCase("TABLE_7", table7Fks()));
+ importedKeysTestCase("TABLE_7", ifSchemaElse(table7to8Fks(), List.of()), table7to6Fks()));
+ if (!getDefaultSupportInfo().supportsSchemas()) {
+ return generalArguments;
+ }
+ return Stream.concat(generalArguments, Stream.of(
+ importedKeysTestCase("OTHER_SCHEMA", "TABLE_8", table8Fks()),
+ importedKeysTestCase(null, "TABLE_8", table8Fks())));
}
private static Arguments importedKeysTestCase(String table, List> expectedKeys) {
@@ -59,4 +67,17 @@ private static Arguments importedKeysTestCase(String schema, String table,
return Arguments.of(schema, table, expectedKeys);
}
+ @SuppressWarnings("SameParameterValue")
+ @SafeVarargs
+ private static Arguments importedKeysTestCase(String table, List>... expectedKeys) {
+ return importedKeysTestCase(ifSchemaElse("PUBLIC", ""), table, expectedKeys);
+ }
+
+ @SafeVarargs
+ private static Arguments importedKeysTestCase(String schema, String table,
+ List>... expectedKeys) {
+ var combinedExpectedKeys = Stream.of(expectedKeys).flatMap(Collection::stream).toList();
+ return importedKeysTestCase(schema, table, combinedExpectedKeys);
+ }
+
}
From 3662432c5455a513124e9e4bca45ce01cb7652bc Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Thu, 17 Jul 2025 13:10:43 +0200
Subject: [PATCH 37/64] #882 Improve schema test coverage (getFunctions)
---
.../jdbc/FBDatabaseMetaDataFunctionsTest.java | 281 ++++++++++++------
1 file changed, 193 insertions(+), 88 deletions(-)
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionsTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionsTest.java
index ccd821777..5a05d2d70 100644
--- a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionsTest.java
+++ b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataFunctionsTest.java
@@ -4,26 +4,31 @@
import org.firebirdsql.common.extension.UsesDatabaseExtension;
import org.firebirdsql.jaybird.props.PropertyNames;
+import org.firebirdsql.jaybird.util.QualifiedName;
import org.firebirdsql.util.FirebirdSupportInfo;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
-import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
import static org.firebirdsql.common.FBTestProperties.getConnectionViaDriverManager;
-import static org.firebirdsql.common.FBTestProperties.getDefaultPropertiesForConnection;
import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
-import static org.firebirdsql.common.FBTestProperties.getUrl;
import static org.firebirdsql.common.FBTestProperties.ifSchemaElse;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -37,8 +42,6 @@
*/
class FBDatabaseMetaDataFunctionsTest {
- // TODO Add schema support: tests involving other schema
-
private static final String CREATE_UDF_EXAMPLE = """
declare external function UDF$EXAMPLE
int by descriptor, int by descriptor
@@ -55,6 +58,21 @@ class FBDatabaseMetaDataFunctionsTest {
return X+1;
end""";
+ // Same name, different schema as CREATE_PSQL_EXAMPLE
+ private static final String CREATE_OTHER_SCHEMA_PSQL_EXAMPLE = """
+ create function OTHER_SCHEMA.PSQL$EXAMPLE(X double precision) returns varchar(50)
+ as
+ begin
+ return cast(x as varchar(50));
+ end""";
+
+ private static final String CREATE_OTHER_SCHEMA_PSQL_EXAMPLE2 = """
+ create function OTHER_SCHEMA.PSQL$EXAMPLE2(X int) returns int
+ as
+ begin
+ return X+1;
+ end""";
+
private static final String ADD_COMMENT_ON_PSQL_EXAMPLE =
"comment on function PSQL$EXAMPLE is 'Comment on PSQL$EXAMPLE'";
@@ -76,6 +94,8 @@ class FBDatabaseMetaDataFunctionsTest {
end
end""";
+ private static final String CREATE_OTHER_SCHEMA = "create schema OTHER_SCHEMA";
+
@RegisterExtension
static final UsesDatabaseExtension.UsesDatabaseForAll usesDatabase = UsesDatabaseExtension.usesDatabaseForAll(
getCreateStatements());
@@ -84,12 +104,19 @@ class FBDatabaseMetaDataFunctionsTest {
new MetadataResultSetDefinition(FunctionMetaData.class);
private static Connection con;
- private static DatabaseMetaData dbmd;
+ private static DatabaseMetaData originalDbmd;
+ // may get replaced during a test
+ private DatabaseMetaData dbmd;
@BeforeAll
static void setupAll() throws SQLException {
con = getConnectionViaDriverManager();
- dbmd = con.getMetaData();
+ originalDbmd = con.getMetaData();
+ }
+
+ @BeforeEach
+ void setup() {
+ dbmd = originalDbmd;
}
@AfterAll
@@ -98,7 +125,7 @@ static void tearDownAll() throws SQLException {
con.close();
} finally {
con = null;
- dbmd = null;
+ originalDbmd = null;
}
}
@@ -120,6 +147,12 @@ private static List getCreateStatements() {
statements.add(CREATE_PACKAGE_BODY_WITH_FUNCTION);
}
}
+ if (supportInfo.supportsSchemas()) {
+ statements.add(CREATE_OTHER_SCHEMA);
+ statements.add(CREATE_OTHER_SCHEMA_PSQL_EXAMPLE);
+ statements.add(CREATE_OTHER_SCHEMA_PSQL_EXAMPLE2);
+ }
+
// TODO See if we can add a UDR example as well.
return statements;
}
@@ -135,40 +168,63 @@ void testFunctionMetaDataColumns() throws Exception {
}
@ParameterizedTest
- @ValueSource(strings = "%")
- @NullSource
- void testFunctionMetadata_everything_functionNamePattern(String functionNamePattern) throws Exception {
- try (ResultSet functions = dbmd.getFunctions(null, null, functionNamePattern)) {
- if (getDefaultSupportInfo().supportsPsqlFunctions()) {
- expectNextFunction(functions);
- validatePsqlExample(functions);
- }
-
- // Verify UDF$EXAMPLE
- expectNextFunction(functions);
- validateUdfExample(functions);
+ @CsvSource(useHeadersInDisplayName = true, nullValues = { "" }, textBlock = """
+ schemaPattern, functionNamePattern
+ ,
+ , %
+ %,
+ %, %
+ """)
+ void testFunctionMetadata_everything_functionNamePattern(String schemaPattern, String functionNamePattern)
+ throws Exception {
+ validateExpectedFunctions(null, schemaPattern, functionNamePattern, getAllFunctionsNonPackaged());
+ }
- expectNoMoreRows(functions);
- }
+ @ParameterizedTest
+ @CsvSource(useHeadersInDisplayName = true, nullValues = { "" }, textBlock = """
+ schemaPattern, functionNamePattern
+ OTHER_SCHEMA,
+ OTHER_SCHEMA, %
+ OTHER_SCHEMA, PSQL$%
+ OTHER_%,
+ OTHER_%, %
+ OTHER_%, PSQL$%
+ """)
+ void testFunctionMetadata_everything_ofOtherSchema(String schemaPattern, String functionNamePattern)
+ throws Exception {
+ var expectedFunctions = List.of(getOtherSchemaPsqlExample(), getOtherSchemaPsqlExample2());
+ validateExpectedFunctions(null, schemaPattern, functionNamePattern, expectedFunctions);
}
@Test
void testFunctionMetaData_udfExampleOnly() throws Exception {
- try (ResultSet functions = dbmd.getFunctions(null, null, "UDF$EXAMPLE")) {
- assertTrue(functions.next(), "Expected a row");
- validateUdfExample(functions);
- assertFalse(functions.next(), "Expected no more rows");
- }
+ validateExpectedFunctions(null, null, "UDF$EXAMPLE", List.of(getUdfExample()));
}
@Test
- void testFunctionMetaData_psqlExampleOnly() throws Exception {
+ void testFunctionMetaData_defaultSchema_psqlExampleOnly() throws Exception {
assumeTrue(getDefaultSupportInfo().supportsPsqlFunctions(), "Requires PSQL function support");
- try (ResultSet functions = dbmd.getFunctions(null, null, "PSQL$EXAMPLE")) {
- assertTrue(functions.next(), "Expected a row");
- validatePsqlExample(functions);
- assertFalse(functions.next(), "Expected no more rows");
+ validateExpectedFunctions(null, ifSchemaElse("PUBLIC", ""), "PSQL$EXAMPLE", List.of(getPsqlExample()));
+ }
+
+ @ParameterizedTest
+ @NullSource
+ @ValueSource(strings = "%")
+ void testFunctionMetaData_allSchema_psqlExampleOnly(String schemaPattern) throws Exception {
+ FirebirdSupportInfo supportInfo = getDefaultSupportInfo();
+ assumeTrue(supportInfo.supportsPsqlFunctions(), "Requires PSQL function support");
+ var expectedFunctions = new ArrayList>();
+ if (supportInfo.supportsSchemas()) {
+ expectedFunctions.add(getOtherSchemaPsqlExample());
}
+ expectedFunctions.add(getPsqlExample());
+ validateExpectedFunctions(null, schemaPattern, "PSQL$EXAMPLE", expectedFunctions);
+ }
+
+ @Test
+ void testFunctionMetaData_otherSchema_psqlExampleOnly() throws Exception {
+ assumeTrue(getDefaultSupportInfo().supportsSchemas(), "Requires schema support");
+ validateExpectedFunctions(null, "OTHER_SCHEMA", "PSQL$EXAMPLE", List.of(getOtherSchemaPsqlExample()));
}
@Test
@@ -191,37 +247,21 @@ void testFunctionMetaData_emptyString_noResults() throws Exception {
@Test
void testFunctionMetadata_useCatalogAsPackage_everything() throws Exception {
assumeTrue(getDefaultSupportInfo().supportsPackages(), "Test requires package support");
- Properties props = getDefaultPropertiesForConnection();
- props.setProperty(PropertyNames.useCatalogAsPackage, "true");
- try (var connection = DriverManager.getConnection(getUrl(), props);
- var functions = connection.getMetaData().getFunctions(null, null, "%")) {
- expectNextFunction(functions);
- validatePsqlExample(functions, true);
-
- // Verify UDF$EXAMPLE
- expectNextFunction(functions);
- validateUdfExample(functions, true);
-
- // Verify packaged function WITH$FUNCTION.IN$PACKAGE
- expectNextFunction(functions);
- validatePackageFunctionExample(functions);
+ List> expectedFunctions = getAllFunctionsNonPackaged(true);
+ expectedFunctions.add(getPackageFunctionExample());
- expectNoMoreRows(functions);
+ try (var connection = getConnectionViaDriverManager(PropertyNames.useCatalogAsPackage, "true")) {
+ dbmd = connection.getMetaData();
+ validateExpectedFunctions(null, null, "%", expectedFunctions);
}
}
@Test
void testFunctionMetadata_useCatalogAsPackage_specificPackage() throws Exception {
assumeTrue(getDefaultSupportInfo().supportsPackages(), "Test requires package support");
- Properties props = getDefaultPropertiesForConnection();
- props.setProperty(PropertyNames.useCatalogAsPackage, "true");
- try (var connection = DriverManager.getConnection(getUrl(), props);
- var functions = connection.getMetaData().getFunctions("WITH$FUNCTION", null, "%")) {
- // Verify packaged function WITH$FUNCTION.IN$PACKAGE
- expectNextFunction(functions);
- validatePackageFunctionExample(functions);
-
- expectNoMoreRows(functions);
+ try (var connection = getConnectionViaDriverManager(PropertyNames.useCatalogAsPackage, "true")) {
+ dbmd = connection.getMetaData();
+ validateExpectedFunctions("WITH$FUNCTION", null, "%", List.of(getPackageFunctionExample()));
}
}
@@ -230,34 +270,35 @@ void testFunctionMetadata_useCatalogAsPackage_specificPackage() throws Exception
@ValueSource(strings = "WITH$FUNCTION")
void testFunctionMetadata_useCatalogAsPackage_specificPackageFunction(String catalog) throws Exception {
assumeTrue(getDefaultSupportInfo().supportsPackages(), "Test requires package support");
- Properties props = getDefaultPropertiesForConnection();
- props.setProperty(PropertyNames.useCatalogAsPackage, "true");
- try (var connection = DriverManager.getConnection(getUrl(), props);
- var functions = connection.getMetaData().getFunctions(catalog, null, "IN$PACKAGE")) {
- // Verify packaged function WITH$FUNCTION.IN$PACKAGE
- expectNextFunction(functions);
- validatePackageFunctionExample(functions);
-
- expectNoMoreRows(functions);
+ try (var connection = getConnectionViaDriverManager(PropertyNames.useCatalogAsPackage, "true")) {
+ dbmd = connection.getMetaData();
+ validateExpectedFunctions(catalog, null, "IN$PACKAGE", List.of(getPackageFunctionExample()));
}
}
@Test
void testFunctionMetadata_useCatalogAsPackage_nonPackagedOnly() throws Exception {
assumeTrue(getDefaultSupportInfo().supportsPackages(), "Test requires package support");
- Properties props = getDefaultPropertiesForConnection();
- props.setProperty(PropertyNames.useCatalogAsPackage, "true");
- try (var connection = DriverManager.getConnection(getUrl(), props);
- var functions = connection.getMetaData().getFunctions("", null, "%")) {
- expectNextFunction(functions);
- validatePsqlExample(functions, true);
+ try (var connection = getConnectionViaDriverManager(PropertyNames.useCatalogAsPackage, "true")) {
+ dbmd = connection.getMetaData();
+ validateExpectedFunctions("", null, "%", getAllFunctionsNonPackaged(true));
+ }
+ }
- // Verify UDF$EXAMPLE
- expectNextFunction(functions);
- validateUdfExample(functions, true);
+ private void validateExpectedFunctions(String catalog, String schemaPattern, String functionNamePattern,
+ List> expectedColumns) throws Exception {
+ try (ResultSet functions = dbmd.getFunctions(catalog, schemaPattern, functionNamePattern)) {
+ validateFunctions(functions, expectedColumns);
+ }
+ }
- expectNoMoreRows(functions);
+ private static void validateFunctions(ResultSet functions,
+ List> expectedColumns) throws SQLException {
+ for (Map expectedColumn : expectedColumns) {
+ expectNextFunction(functions);
+ getFunctionsDefinition.validateRowValues(functions, expectedColumn);
}
+ expectNoMoreRows(functions);
}
private void validateNoRows(String functionNamePattern) throws Exception {
@@ -266,18 +307,39 @@ private void validateNoRows(String functionNamePattern) throws Exception {
}
}
- private void validatePsqlExample(ResultSet functions) throws SQLException {
- validatePsqlExample(functions, false);
+ private static List> getAllFunctionsNonPackaged() {
+ return getAllFunctionsNonPackaged(false);
+ }
+
+ private static List> getAllFunctionsNonPackaged(boolean useCatalogAsPackage) {
+ FirebirdSupportInfo supportInfo = getDefaultSupportInfo();
+ var expectedFunctions = new ArrayList>();
+ if (supportInfo.supportsSchemas()) {
+ expectedFunctions.add(getOtherSchemaPsqlExample(useCatalogAsPackage));
+ expectedFunctions.add(getOtherSchemaPsqlExample2(useCatalogAsPackage));
+ }
+ if (supportInfo.supportsPsqlFunctions()) {
+ expectedFunctions.add(getPsqlExample(useCatalogAsPackage));
+ }
+ expectedFunctions.add(getUdfExample(useCatalogAsPackage));
+ return expectedFunctions;
+ }
+
+
+
+ private static Map getPsqlExample() {
+ return getPsqlExample(false);
}
- private void validatePsqlExample(ResultSet functions, boolean useCatalogAsPackage) throws SQLException {
+ private static Map getPsqlExample(boolean useCatalogAsPackage) {
final boolean supportsComments = getDefaultSupportInfo().supportsComment();
Map rules = getDefaultValidationRules();
if (useCatalogAsPackage) {
rules.put(FunctionMetaData.FUNCTION_CAT, "");
}
rules.put(FunctionMetaData.FUNCTION_NAME, "PSQL$EXAMPLE");
- rules.put(FunctionMetaData.SPECIFIC_NAME, ifSchemaElse("\"PUBLIC\".\"PSQL$EXAMPLE\"", "PSQL$EXAMPLE"));
+ rules.put(FunctionMetaData.SPECIFIC_NAME, ifSchemaElse(
+ new QualifiedName("PUBLIC", "PSQL$EXAMPLE").toString(QuoteStrategy.DIALECT_3), "PSQL$EXAMPLE"));
if (supportsComments) {
rules.put(FunctionMetaData.REMARKS, "Comment on PSQL$EXAMPLE");
}
@@ -286,32 +348,74 @@ private void validatePsqlExample(ResultSet functions, boolean useCatalogAsPackag
return X+1;
end""");
rules.put(FunctionMetaData.JB_FUNCTION_KIND, "PSQL");
+ return rules;
+ }
- getFunctionsDefinition.validateRowValues(functions, rules);
+ private static Map getOtherSchemaPsqlExample() {
+ return getOtherSchemaPsqlExample(false);
}
- private void validateUdfExample(ResultSet functions) throws SQLException {
- validateUdfExample(functions, false);
+ private static Map getOtherSchemaPsqlExample(boolean useCatalogAsPackage) {
+ Map rules = getDefaultValidationRules();
+ if (useCatalogAsPackage) {
+ rules.put(FunctionMetaData.FUNCTION_CAT, "");
+ }
+ rules.put(FunctionMetaData.FUNCTION_SCHEM, "OTHER_SCHEMA");
+ rules.put(FunctionMetaData.FUNCTION_NAME, "PSQL$EXAMPLE");
+ rules.put(FunctionMetaData.SPECIFIC_NAME,
+ new QualifiedName("OTHER_SCHEMA", "PSQL$EXAMPLE").toString(QuoteStrategy.DIALECT_3));
+ rules.put(FunctionMetaData.JB_FUNCTION_SOURCE, """
+ begin
+ return cast(x as varchar(50));
+ end""");
+ rules.put(FunctionMetaData.JB_FUNCTION_KIND, "PSQL");
+ return rules;
}
- private void validateUdfExample(ResultSet functions, boolean useCatalogAsPackage) throws SQLException {
+ private static Map getOtherSchemaPsqlExample2() {
+ return getOtherSchemaPsqlExample2(false);
+ }
+
+ private static Map getOtherSchemaPsqlExample2(boolean useCatalogAsPackage) {
+ Map rules = getDefaultValidationRules();
+ if (useCatalogAsPackage) {
+ rules.put(FunctionMetaData.FUNCTION_CAT, "");
+ }
+ rules.put(FunctionMetaData.FUNCTION_SCHEM, "OTHER_SCHEMA");
+ rules.put(FunctionMetaData.FUNCTION_NAME, "PSQL$EXAMPLE2");
+ rules.put(FunctionMetaData.SPECIFIC_NAME,
+ new QualifiedName("OTHER_SCHEMA", "PSQL$EXAMPLE2").toString(QuoteStrategy.DIALECT_3));
+ rules.put(FunctionMetaData.JB_FUNCTION_SOURCE, """
+ begin
+ return X+1;
+ end""");
+ rules.put(FunctionMetaData.JB_FUNCTION_KIND, "PSQL");
+ return rules;
+ }
+
+ private static Map getUdfExample() {
+ return getUdfExample(false);
+ }
+
+ private static Map getUdfExample(boolean useCatalogAsPackage) {
final boolean supportsComments = getDefaultSupportInfo().supportsComment();
Map rules = getDefaultValidationRules();
if (useCatalogAsPackage) {
rules.put(FunctionMetaData.FUNCTION_CAT, "");
}
rules.put(FunctionMetaData.FUNCTION_NAME, "UDF$EXAMPLE");
- rules.put(FunctionMetaData.SPECIFIC_NAME, ifSchemaElse("\"PUBLIC\".\"UDF$EXAMPLE\"", "UDF$EXAMPLE"));
+ rules.put(FunctionMetaData.SPECIFIC_NAME, ifSchemaElse(
+ new QualifiedName("PUBLIC", "UDF$EXAMPLE").toString(QuoteStrategy.DIALECT_3), "UDF$EXAMPLE"));
if (supportsComments) {
rules.put(FunctionMetaData.REMARKS, "Comment on UDF$EXAMPLE");
}
rules.put(FunctionMetaData.JB_FUNCTION_KIND, "UDF");
rules.put(FunctionMetaData.JB_MODULE_NAME, "fbudf");
rules.put(FunctionMetaData.JB_ENTRYPOINT, "idNvl");
- getFunctionsDefinition.validateRowValues(functions, rules);
+ return rules;
}
- private void validatePackageFunctionExample(ResultSet functions) throws SQLException {
+ private static Map getPackageFunctionExample() {
Map rules = getDefaultValidationRules();
rules.put(FunctionMetaData.FUNCTION_CAT, "WITH$FUNCTION");
rules.put(FunctionMetaData.FUNCTION_NAME, "IN$PACKAGE");
@@ -319,7 +423,8 @@ private void validatePackageFunctionExample(ResultSet functions) throws SQLExcep
// Stored with package
rules.put(FunctionMetaData.JB_FUNCTION_SOURCE, null);
rules.put(FunctionMetaData.JB_FUNCTION_KIND, "PSQL");
- getFunctionsDefinition.validateRowValues(functions, rules);
+
+ return rules;
}
private static void expectNextFunction(ResultSet rs) throws SQLException {
From b83f2758475816747c152404006d923777f6b471 Mon Sep 17 00:00:00 2001
From: Mark Rotteveel
Date: Fri, 18 Jul 2025 16:58:10 +0200
Subject: [PATCH 38/64] Replace QualifiedName with ObjectReference
---
.../jaybird/util/CollectionUtils.java | 27 +++
.../firebirdsql/jaybird/util/Identifier.java | 110 ++++++++++
.../jaybird/util/IdentifierChain.java | 88 ++++++++
.../jaybird/util/ObjectReference.java | 202 ++++++++++++++++++
.../jaybird/util/QualifiedName.java | 122 -----------
.../firebirdsql/jaybird/util/StringUtils.java | 16 +-
.../org/firebirdsql/jdbc/FBRowUpdater.java | 38 ++--
.../jaybird/util/CollectionUtilsTest.java | 22 +-
.../jaybird/util/IdentifierChainTest.java | 169 +++++++++++++++
.../jaybird/util/IdentifierTest.java | 150 +++++++++++++
.../jaybird/util/ObjectReferenceTest.java | 170 +++++++++++++++
.../jaybird/util/QualifiedNameTest.java | 103 ---------
.../jaybird/util/StringUtilsTest.java | 23 +-
...FBDatabaseMetaDataFunctionColumnsTest.java | 13 +-
.../jdbc/FBDatabaseMetaDataFunctionsTest.java | 15 +-
15 files changed, 1009 insertions(+), 259 deletions(-)
create mode 100644 src/main/org/firebirdsql/jaybird/util/Identifier.java
create mode 100644 src/main/org/firebirdsql/jaybird/util/IdentifierChain.java
create mode 100644 src/main/org/firebirdsql/jaybird/util/ObjectReference.java
delete mode 100644 src/main/org/firebirdsql/jaybird/util/QualifiedName.java
create mode 100644 src/test/org/firebirdsql/jaybird/util/IdentifierChainTest.java
create mode 100644 src/test/org/firebirdsql/jaybird/util/IdentifierTest.java
create mode 100644 src/test/org/firebirdsql/jaybird/util/ObjectReferenceTest.java
delete mode 100644 src/test/org/firebirdsql/jaybird/util/QualifiedNameTest.java
diff --git a/src/main/org/firebirdsql/jaybird/util/CollectionUtils.java b/src/main/org/firebirdsql/jaybird/util/CollectionUtils.java
index 45db08795..e45a1ef48 100644
--- a/src/main/org/firebirdsql/jaybird/util/CollectionUtils.java
+++ b/src/main/org/firebirdsql/jaybird/util/CollectionUtils.java
@@ -8,6 +8,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
+import java.util.stream.Stream;
/**
* Helper class for collections
@@ -87,4 +88,30 @@ public static List concat(List list1, List extends T> list2) {
return newList;
}
+ /**
+ * Concatenates two or more lists to a new modifiable list.
+ *
+ * If there are no lists in {@code otherLists}, it will return a new list, with the contents of {@code list1}.
+ *
+ *
+ * @param list1
+ * list 1
+ * @param otherLists
+ * other lists
+ * @param
+ * type parameter of {@code list1}, and parent type parameter of lists in {@code otherLists}
+ * @return concatenation of {@code list1} and {@code otherLists}
+ * @see #concat(List, List)
+ */
+ @SafeVarargs
+ public static List concat(List list1, List extends T>... otherLists) {
+ int listsSize = list1.size() + Stream.of(otherLists).mapToInt(List::size).sum();
+ var newList = new ArrayList(listsSize);
+ newList.addAll(list1);
+ for (var list : otherLists) {
+ newList.addAll(list);
+ }
+ return newList;
+ }
+
}
diff --git a/src/main/org/firebirdsql/jaybird/util/Identifier.java b/src/main/org/firebirdsql/jaybird/util/Identifier.java
new file mode 100644
index 000000000..16b120635
--- /dev/null
+++ b/src/main/org/firebirdsql/jaybird/util/Identifier.java
@@ -0,0 +1,110 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jaybird.util;
+
+import org.firebirdsql.jdbc.QuoteStrategy;
+import org.jspecify.annotations.Nullable;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import static org.firebirdsql.jaybird.util.StringUtils.trimToNull;
+
+/**
+ * An identifier is an object reference consisting of a single name.
+ *
+ * @since 7
+ * @see ObjectReference
+ */
+public final class Identifier extends ObjectReference {
+
+ private final String name;
+
+ public Identifier(String name) {
+ name = trimToNull(name);
+ if (name == null) {
+ throw new IllegalArgumentException("name cannot be null, empty, or blank");
+ }
+ this.name = name;
+ }
+
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public int size() {
+ return 1;
+ }
+
+ @Override
+ public Identifier at(int index) {
+ if (index != 0) {
+ throw new IndexOutOfBoundsException(index);
+ }
+ return this;
+ }
+
+ @Override
+ public Identifier first() {
+ return this;
+ }
+
+ @Override
+ public Identifier last() {
+ return this;
+ }
+
+ /**
+ * The name, quoted using {@code quoteStrategy}.
+ *
+ * @param quoteStrategy
+ * quote strategy
+ * @return name, possibly quoted
+ */
+ public String toString(QuoteStrategy quoteStrategy) {
+ return quoteStrategy.quoteObjectName(name);
+ }
+
+ /**
+ * Appends name to {@code sb} using {@code quoteStrategy}.
+ *
+ * @param sb
+ * string builder to append to
+ * @param quoteStrategy
+ * quote strategy
+ * @return {@code sb} for chaining
+ */
+ public StringBuilder append(StringBuilder sb, QuoteStrategy quoteStrategy) {
+ return quoteStrategy.appendQuoted(name, sb);
+ }
+
+ @Override
+ public Stream stream() {
+ return Stream.of(this);
+ }
+
+ @Override
+ public List toList() {
+ return List.of(this);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj instanceof Identifier other) {
+ return name.equals(other.name);
+ } else if (obj instanceof ObjectReference otherRef) {
+ // We're using ObjectReference, not IdentifierChain, so it'll also work for future subclasses, if any
+ return otherRef.size() == 1 && name.equals(otherRef.at(0).name);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ // This needs to be consistent with IdentifierChain.hashCode (as if this is a chain with a single item)
+ // We're clearing the sign bit, because that is what IdentifierChain does to avoid negative values
+ return (31 + name.hashCode()) & 0x7FFF_FFFF;
+ }
+
+}
diff --git a/src/main/org/firebirdsql/jaybird/util/IdentifierChain.java b/src/main/org/firebirdsql/jaybird/util/IdentifierChain.java
new file mode 100644
index 000000000..f4f0f5bc8
--- /dev/null
+++ b/src/main/org/firebirdsql/jaybird/util/IdentifierChain.java
@@ -0,0 +1,88 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jaybird.util;
+
+import org.firebirdsql.jdbc.QuoteStrategy;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+/**
+ * An identifier chain is an object reference consisting of one or more identifiers.
+ *
+ * In practice, we'll use {@link Identifier} if there is only one identifier.
+ *
+ *
+ * The recommended way to create this object is through {@link ObjectReference#of(String...)} or
+ * {@link ObjectReference#of(List)}.
+ *
+ *
+ * @since 7
+ * @see ObjectReference
+ */
+final class IdentifierChain extends ObjectReference {
+
+ private final List identifiers;
+ // cached hashcode, -1 signals not yet cached
+ private int hashCode = -1;
+
+ IdentifierChain(List identifiers) {
+ if (identifiers.isEmpty()) {
+ throw new IllegalArgumentException("identifier chain cannot be empty");
+ }
+ this.identifiers = List.copyOf(identifiers);
+ }
+
+ @Override
+ public int size() {
+ return identifiers.size();
+ }
+
+ @Override
+ public Identifier at(int index) {
+ return identifiers.get(index);
+ }
+
+ @Override
+ public String toString(QuoteStrategy quoteStrategy) {
+ // Estimate 16 characters per element (including quotes and separator)
+ return append(new StringBuilder(size() * 16), quoteStrategy).toString();
+ }
+
+ @Override
+ public StringBuilder append(StringBuilder sb, QuoteStrategy quoteStrategy) {
+ for (Identifier identifier : identifiers) {
+ identifier.append(sb, quoteStrategy).append('.');
+ }
+ // Remove last dot separator
+ sb.setLength(sb.length() - 1);
+ return sb;
+ }
+
+ @Override
+ public Stream stream() {
+ return identifiers.stream();
+ }
+
+ @Override
+ public List toList() {
+ return identifiers;
+ }
+
+ @Override
+ public int hashCode() {
+ int hashCode = this.hashCode;
+ return hashCode != -1 ? hashCode : hashCode0();
+ }
+
+ private int hashCode0() {
+ // This needs to be consistent with Identifier.hashCode for an instance with a single Identifier
+ int hashCode = 1;
+ for (Identifier identifier : identifiers) {
+ hashCode = 31 * hashCode + identifier.name().hashCode();
+ }
+ // Clear sign bit to avoid -1 (and any other negative value)
+ return this.hashCode = hashCode & 0x7FFF_FFFF;
+ }
+
+}
diff --git a/src/main/org/firebirdsql/jaybird/util/ObjectReference.java b/src/main/org/firebirdsql/jaybird/util/ObjectReference.java
new file mode 100644
index 000000000..eb2e1d868
--- /dev/null
+++ b/src/main/org/firebirdsql/jaybird/util/ObjectReference.java
@@ -0,0 +1,202 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jaybird.util;
+
+import org.firebirdsql.gds.ng.fields.FieldDescriptor;
+import org.firebirdsql.jdbc.QuoteStrategy;
+import org.jspecify.annotations.Nullable;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+/**
+ * An object reference is a — possibly ambiguous — identification of an object, like a {@code column},
+ * {@code table}, {@code table.column}, {@code schema.table.column}, {@code alias.column}, etc.
+ *
+ * An object reference consists of one or more identifiers. If it has a single identifier, it is
+ * an {@link Identifier}, otherwise it is an identifier chain.
+ *
+ *
+ * @since 7
+ */
+public sealed abstract class ObjectReference permits Identifier, IdentifierChain {
+
+ /**
+ * Creates an object reference ({@link Identifier}) from {@code name}.
+ *
+ * @param name
+ * name (cannot be {@code null}, empty, or blank)
+ * @return identifier
+ * @throws IllegalArgumentException
+ * if {@code name} is {@code null}, empty, or blank
+ */
+ public static Identifier of(String name) {
+ return new Identifier(name);
+ }
+
+ /**
+ * Creates an object reference (a single {@link Identifier} or an identifier chain) from {@code names}.
+ *
+ * The prefix of the {@code names} may be {@code null} or empty strings, these are ignored and excluded from
+ * the final object reference, as long as there is at least one non-blank name remaining (the suffix)
+ *
+ *
+ * @param names
+ * one or more names
+ * @return an object reference
+ * @throws IllegalArgumentException
+ * if {@code names} is empty, all names are {@code null} or empty, or at least one name in the suffix is
+ * blank or null
+ * @see #of(List)
+ */
+ public static ObjectReference of(@Nullable String... names) {
+ return of(Arrays.asList(names));
+ }
+
+ /**
+ * Creates an object reference (a single {@link Identifier} or an identifier chain) from {@code names}.
+ *
+ * @param names
+ * one or more names
+ * @return an object reference
+ * @throws IllegalArgumentException
+ * if {@code names} is empty, all names are {@code null} or empty, or at least one name in the suffix is
+ * blank or null
+ */
+ public static ObjectReference of(List<@Nullable String> names) {
+ //noinspection DataFlowIssue : Identifier(String) is @NonNull, and produce an IllegalArgumentException for null
+ List nameList = names.stream().dropWhile(StringUtils::isNullOrEmpty).map(Identifier::new).toList();
+ if (nameList.size() == 1) {
+ return nameList.get(0);
+ }
+ return new IdentifierChain(nameList);
+ }
+
+ /**
+ * Creates an object reference of the original table in {@code fieldDescriptor} (from {@code originalSchema} and
+ * {@code originalTableName}).
+ *
+ * @param fieldDescriptor
+ * field descriptor
+ * @return a possibly schema-qualified name of the original table from {@code fieldDescriptor} or empty if its
+ * {@code originalTableName} is empty string or {@code null}
+ */
+ public static Optional ofTable(FieldDescriptor fieldDescriptor) {
+ String tableName = fieldDescriptor.getOriginalTableName();
+ if (StringUtils.isNullOrEmpty(tableName)) {
+ return Optional.empty();
+ }
+ // NOTE: This will produce an exception if tableName is blank and not empty, we accept that as that shouldn't
+ // happen in normal use
+ return Optional.of(
+ ObjectReference.of(fieldDescriptor.getOriginalSchema(), fieldDescriptor.getOriginalTableName()));
+ }
+
+ /**
+ * @return number of identifiers in this object reference ({@code >= 1})
+ */
+ public abstract int size();
+
+ /**
+ * Gets identifier at 0-based position {@code index}.
+ *
+ * @param index
+ * index of the identifier
+ * @return the identifier
+ * @throws IndexOutOfBoundsException
+ * if {@code index < 0 || index > size()}
+ */
+ public abstract Identifier at(int index);
+
+ public Identifier first() {
+ return at(0);
+ }
+
+ public Identifier last() {
+ return at(size() - 1);
+ }
+
+ /**
+ * The name(s), quoted using {@code quoteStrategy}.
+ *
+ * @param quoteStrategy
+ * quote strategy
+ * @return name, possibly quoted
+ */
+ public abstract String toString(QuoteStrategy quoteStrategy);
+
+ /**
+ * Quoted name(s).
+ *
+ * @return quoted name(s) equivalent of {@link #toString(QuoteStrategy)} with {@link QuoteStrategy#DIALECT_3}
+ */
+ @Override
+ public final String toString() {
+ return toString(QuoteStrategy.DIALECT_3);
+ }
+
+ /**
+ * Appends name(s) to {@code sb} using {@code quoteStrategy}.
+ *
+ * @param sb
+ * string builder to append to
+ * @param quoteStrategy
+ * quote strategy
+ * @return {@code sb} for chaining
+ */
+ public abstract StringBuilder append(StringBuilder sb, QuoteStrategy quoteStrategy);
+
+ /**
+ * @return stream of identifiers in this object reference
+ */
+ public abstract Stream stream();
+
+ /**
+ * @return list of identifiers in this object reference
+ */
+ public abstract List toList();
+
+ /**
+ * Resolves the given object reference against this object reference.
+ *
+ * For example, if this object reference is {@code schema.table}, and {@code other} is {@code column}, then the
+ * result is {@code schema.table.column}.
+ *
+ *
+ * Or in other words, this method concatenates {@code this} and {@code other} and returns it as a new object
+ * reference.
+ *
+ *
+ * @param other
+ * other object reference
+ * @return new object reference (an identifier chain)
+ */
+ public ObjectReference resolve(ObjectReference other) {
+ return new IdentifierChain(CollectionUtils.concat(toList(), other.toList()));
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Implementation note: Subclasses need to ensure consistency of equals for logically equivalent references in
+ * different types (e.g. an Identifier and an IdentifierChain with a single Identifier).
+ *
+ */
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ return obj instanceof ObjectReference other && size() == other.size() && toList().equals(other.toList());
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Implementation note: Subclasses need to ensure consistency of hashCode for logically equivalent references in
+ * different types (e.g. an Identifier and an IdentifierChain with a single Identifier).
+ *
+ */
+ @Override
+ public abstract int hashCode();
+
+}
\ No newline at end of file
diff --git a/src/main/org/firebirdsql/jaybird/util/QualifiedName.java b/src/main/org/firebirdsql/jaybird/util/QualifiedName.java
deleted file mode 100644
index cd63cd119..000000000
--- a/src/main/org/firebirdsql/jaybird/util/QualifiedName.java
+++ /dev/null
@@ -1,122 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
-// SPDX-License-Identifier: LGPL-2.1-or-later
-package org.firebirdsql.jaybird.util;
-
-import org.firebirdsql.gds.ng.fields.FieldDescriptor;
-import org.firebirdsql.jdbc.QuoteStrategy;
-import org.jspecify.annotations.Nullable;
-
-import java.util.Optional;
-
-import static java.util.Objects.requireNonNull;
-import static java.util.Objects.requireNonNullElse;
-
-/**
- * An object name qualified by a schema.
- *
- * NOTE: This class is not usable for packaged objects, as it has only one level of qualification.
- *
- *
- * @param schema
- * unquoted, case-sensitive schema name, {@code null} is returned as empty string ({@code ""})
- * @param object
- * unquoted, case-sensitive object name (e.g. a table name)
- * @since 7
- */
-public record QualifiedName(String schema, String object) {
-
- /**
- * Canonical constructor.
- *
- * @param schema
- * unquoted, case-sensitive schema name, {@code null} is returned as empty string ({@code ""})
- * @param object
- * unquoted, case-sensitive object name (e.g. a table name)
- * @throws IllegalArgumentException if {@code object} is blank
- */
- public QualifiedName(@Nullable String schema, String object) {
- // defined explicitly to annotate @Nullable on schema for constructor only
- this.schema = requireNonNullElse(schema, "");
- if (object.isBlank()) {
- throw new IllegalArgumentException("object cannot be blank");
- }
- this.object = requireNonNull(object, "object");
- }
-
- @Override
- public String schema() {
- return schema;
- }
-
- /**
- * Estimated length of the quoted identifier.
- *
- * The estimate might be of if {@link #schema()} or {@link #object()} contains double quotes, or if
- * {@link QuoteStrategy#DIALECT_1} is used.
- *
- *
- * This can be used for pre-sizing a string builder for {@link #append(StringBuilder, QuoteStrategy)}.
- *
- *
- * @return estimated length of the quoted identifier
- */
- public int estimatedLength() {
- // 2: double quotes, 1: separator
- return (schema.isEmpty() ? 0 : 2 + schema.length() + 1) + 2 + object.length();
- }
-
- /**
- * Produces the string of the identifier chain.
- *
- * @param quoteStrategy
- * quote strategy to apply on {@code schema} and {@code object}
- * @return identifier chain
- */
- public String toString(QuoteStrategy quoteStrategy) {
- if (!schema.isEmpty()) {
- var sb = new StringBuilder(estimatedLength());
- quoteStrategy.appendQuoted(schema, sb).append('.');
- quoteStrategy.appendQuoted(object, sb);
- return sb.toString();
- }
- return quoteStrategy.quoteObjectName(object);
- }
-
- /**
- * Appends the identifier chain to {@code sb}, using {@code quoteStrategy}.
- *
- * @param sb
- * StringBuilder for appending
- * @param quoteStrategy
- * quote strategy to apply on {@code schema} and {@code object}
- * @return the StringBuilder for method chaining
- * @see #estimatedLength()
- */
- public StringBuilder append(StringBuilder sb, QuoteStrategy quoteStrategy) {
- if (!schema.isEmpty()) {
- quoteStrategy.appendQuoted(schema, sb).append('.');
- }
- quoteStrategy.appendQuoted(object, sb);
- return sb;
- }
-
- /**
- * Creates a qualified name from {@code originalSchema} and {@code originalTableName} of
- * {@code fieldDescriptor}.
- *
- * @param fieldDescriptor
- * field descriptor
- * @return a schema-qualified name of the original table from {@code fieldDescriptor} or empty if its
- * {@code originalTableName} is empty string or {@code null}
- */
- public static Optional of(FieldDescriptor fieldDescriptor) {
- String tableName = fieldDescriptor.getOriginalTableName();
- if (StringUtils.isNullOrEmpty(tableName)) {
- return Optional.empty();
- }
- // NOTE: This will produce an exception if tableName is blank and not empty, we accept that as that shouldn't
- // happen in normal use
- return Optional.of(new QualifiedName(fieldDescriptor.getOriginalSchema(), tableName));
- }
-
-}
diff --git a/src/main/org/firebirdsql/jaybird/util/StringUtils.java b/src/main/org/firebirdsql/jaybird/util/StringUtils.java
index 59dd19db6..a8bc50a2f 100644
--- a/src/main/org/firebirdsql/jaybird/util/StringUtils.java
+++ b/src/main/org/firebirdsql/jaybird/util/StringUtils.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2019-2024 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2019-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jaybird.util;
@@ -40,13 +40,25 @@ private StringUtils() {
*
* @param value
* value to test
- * @return {@code true} if {@code value} is {@code null} or emoty, {@code false} for non-empty strings
+ * @return {@code true} if {@code value} is {@code null} or empty, {@code false} for non-empty strings
* @since 6
*/
public static boolean isNullOrEmpty(@Nullable String value) {
return value == null || value.isEmpty();
}
+ /**
+ * Checks if {@code value} is {@code null} or blank.
+ *
+ * @param value
+ * value to test
+ * @return {@code true} if {@code value} is {@code null} or blank, {@code false} for non-blank strings
+ * @since 6
+ */
+ public static boolean isNullOrBlank(@Nullable String value) {
+ return value == null || value.isBlank();
+ }
+
/**
* Null-safe trim.
*
diff --git a/src/main/org/firebirdsql/jdbc/FBRowUpdater.java b/src/main/org/firebirdsql/jdbc/FBRowUpdater.java
index bbe8923c4..3beae4086 100644
--- a/src/main/org/firebirdsql/jdbc/FBRowUpdater.java
+++ b/src/main/org/firebirdsql/jdbc/FBRowUpdater.java
@@ -10,8 +10,8 @@
import org.firebirdsql.gds.ng.fields.RowDescriptor;
import org.firebirdsql.gds.ng.fields.RowValue;
import org.firebirdsql.gds.ng.listeners.StatementListener;
+import org.firebirdsql.jaybird.util.ObjectReference;
import org.firebirdsql.jaybird.util.SQLExceptionChainBuilder;
-import org.firebirdsql.jaybird.util.QualifiedName;
import org.firebirdsql.jaybird.util.UncheckedSQLException;
import org.firebirdsql.jdbc.field.FBField;
import org.firebirdsql.jdbc.field.FBFlushableField;
@@ -68,7 +68,7 @@ final class FBRowUpdater implements FirebirdRowUpdater {
private static final byte[][] EMPTY_2D_BYTES = new byte[0][];
- private final QualifiedName tableName;
+ private final ObjectReference tableName;
private final FBObjectListener.ResultSetListener rsListener;
private final GDSHelper gdsHelper;
private final RowDescriptor rowDescriptor;
@@ -127,12 +127,12 @@ private FBField createFieldUnchecked(FieldDescriptor fieldDescriptor, boolean ca
* @throws SQLException
* if {@code rowDescriptor} references multiple tables or has derived columns
*/
- private static QualifiedName requireSingleTable(RowDescriptor rowDescriptor, QuoteStrategy quoteStrategy)
+ private static ObjectReference requireSingleTable(RowDescriptor rowDescriptor, QuoteStrategy quoteStrategy)
throws SQLException {
// find the tableName (there can be only one tableName per updatable result set)
- QualifiedName tableName = null;
+ ObjectReference tableName = null;
for (FieldDescriptor fieldDescriptor : rowDescriptor) {
- var currentTable = QualifiedName.of(fieldDescriptor).orElse(null);
+ var currentTable = ObjectReference.ofTable(fieldDescriptor).orElse(null);
if (currentTable == null) {
// No table => derived column => not updatable
throw new FBResultSetNotUpdatableException(
@@ -220,7 +220,7 @@ public FBField getField(int fieldPosition) {
* @throws SQLException
* for errors looking up the best row identifier
*/
- private static List deriveKeyColumns(QualifiedName table, RowDescriptor rowDescriptor,
+ private static List deriveKeyColumns(ObjectReference table, RowDescriptor rowDescriptor,
DatabaseMetaData dbmd) throws SQLException {
// first try best row identifier
List keyColumns = keyColumnsOfBestRowIdentifier(table, rowDescriptor, dbmd);
@@ -251,10 +251,22 @@ private static List deriveKeyColumns(QualifiedName table, RowDe
* @throws SQLException
* for errors looking up the best row identifier
*/
- private static List keyColumnsOfBestRowIdentifier(QualifiedName table,
+ private static List keyColumnsOfBestRowIdentifier(ObjectReference table,
RowDescriptor rowDescriptor, DatabaseMetaData dbmd) throws SQLException {
+ // Method local wrapper to extract schema and table name from an ObjectReference
+ // NOTE: We're assuming, but not verifying, a size of 1 or 2.
+ record TableRef(ObjectReference ref) {
+ String schema() {
+ return ref.size() == 1 ? "" : ref.first().name();
+ }
+
+ String tableName() {
+ return ref.size() == 1 ? ref.first().name() : ref.last().name();
+ }
+ }
+ TableRef tableRef = new TableRef(table);
try (ResultSet bestRowIdentifier = dbmd.getBestRowIdentifier(
- "", table.schema(), table.object(), DatabaseMetaData.bestRowTransaction, true)) {
+ "", tableRef.schema(), tableRef.tableName(), DatabaseMetaData.bestRowTransaction, true)) {
int bestRowIdentifierColumnCount = 0;
List keyColumns = new ArrayList<>();
while (bestRowIdentifier.next()) {
@@ -378,10 +390,11 @@ private String buildInsertStatement() {
params.append('?');
}
- // 27 = length of appended literals + 2 quote characters
- var sb = new StringBuilder(27 + tableName.estimatedLength() + columns.length() + params.length())
+ // 25 = length of appended literals, 32 = guesstimate for (schema +) table
+ var sb = new StringBuilder(25 + 32 + columns.length() + params.length())
.append("insert into ");
- tableName.append(sb,quoteStrategy).append(" (").append(columns).append(") values (").append(params).append(')');
+ tableName.append(sb, quoteStrategy)
+ .append(" (").append(columns).append(") values (").append(params).append(')');
return sb.toString();
}
@@ -405,7 +418,8 @@ private String buildSelectStatement() {
}
}
- var sb = new StringBuilder(EST_STATEMENT_SIZE + columns.length() + tableName.estimatedLength())
+ // 32 = guesstimate for (schema +) table
+ var sb = new StringBuilder(EST_STATEMENT_SIZE + columns.length() + 32)
.append("select ").append(columns).append("\nfrom ");
tableName.append(sb, quoteStrategy).append('\n');
appendWhereClause(sb);
diff --git a/src/test/org/firebirdsql/jaybird/util/CollectionUtilsTest.java b/src/test/org/firebirdsql/jaybird/util/CollectionUtilsTest.java
index c3e5a00c0..78ba8a674 100644
--- a/src/test/org/firebirdsql/jaybird/util/CollectionUtilsTest.java
+++ b/src/test/org/firebirdsql/jaybird/util/CollectionUtilsTest.java
@@ -18,6 +18,7 @@
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -90,13 +91,32 @@ void getLast_multipleItems() {
}
@Test
- void concat() {
+ void concat_oneList() {
+ var list1 = List.of("item1", "item2");
+
+ List concatList = CollectionUtils.concat(list1);
+ assertEquals(list1, concatList);
+ assertNotSame(list1, concatList, "Expected a different instance");
+ }
+
+ @Test
+ void concat_twoLists() {
var list1 = List.of("item1", "item2");
var list2 = List.of("item3", "item4");
assertEquals(List.of("item1", "item2", "item3", "item4"), CollectionUtils.concat(list1, list2));
}
+ @Test
+ void concat_threeLists() {
+ var list1 = List.of("item1", "item2");
+ var list2 = List.of("item3", "item4");
+ var list3 = List.of("item5", "item6");
+
+ assertEquals(List.of("item1", "item2", "item3", "item4", "item5", "item6"),
+ CollectionUtils.concat(list1, list2, list3));
+ }
+
static Stream listFactories() {
return Stream.of(
Arguments.of(factory(ArrayList::new)),
diff --git a/src/test/org/firebirdsql/jaybird/util/IdentifierChainTest.java b/src/test/org/firebirdsql/jaybird/util/IdentifierChainTest.java
new file mode 100644
index 000000000..b248f1e8a
--- /dev/null
+++ b/src/test/org/firebirdsql/jaybird/util/IdentifierChainTest.java
@@ -0,0 +1,169 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jaybird.util;
+
+import org.firebirdsql.jdbc.QuoteStrategy;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import static java.util.Collections.emptyList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for {@link IdentifierChain}.
+ */
+class IdentifierChainTest {
+
+ @ParameterizedTest
+ @CsvSource(useHeadersInDisplayName = true, textBlock = """
+ nameList, expectedSize, expectedDialect3
+ SIMPLE_NAME, 1, "SIMPLE_NAME"
+ SIMPLE_NAME.lower_case, 2, "SIMPLE_NAME"."lower_case"
+ ONE.TWO.THREE, 3, "ONE"."TWO"."THREE"
+ """)
+ void identifier(String nameList, int expectedSize, String expectedDialect3) {
+ List identifiers = toIdentifiers(nameList);
+ var chain = new IdentifierChain(identifiers);
+
+ assertEquals(expectedDialect3, chain.toString(), "toString()");
+ assertEquals(expectedDialect3, chain.toString(QuoteStrategy.DIALECT_3), "toString(DIALECT_3)");
+ assertEquals(nameList, chain.toString(QuoteStrategy.DIALECT_1), "toString(DIALECT_1)");
+ assertEquals(expectedDialect3, chain.append(new StringBuilder(), QuoteStrategy.DIALECT_3).toString(),
+ "append(..., DIALECT_3)");
+ assertEquals(nameList, chain.append(new StringBuilder(), QuoteStrategy.DIALECT_1).toString(),
+ "append(..., DIALECT_1)");
+ assertEquals(expectedSize, chain.size(), "size");
+ assertEquals(identifiers, chain.toList(), "toList");
+ }
+
+ @Test
+ void emptyIdentifierList_throwsIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> new IdentifierChain(emptyList()));
+ }
+
+ @SuppressWarnings({ "SimplifiableAssertion", "EqualsBetweenInconvertibleTypes" })
+ @ParameterizedTest
+ @ValueSource(strings = { "SIMPLE_NAME", "lower_case", "Example3" })
+ void equalsAndHashCode_betweenIdentifierAndIdentifierChain(String name) {
+ var identifier = new Identifier(name);
+ var chain = new IdentifierChain(List.of(identifier));
+
+ assertTrue(chain.equals(identifier), "chain.equals(identifier)");
+ assertTrue(identifier.equals(chain), "identifier.equals(chain)");
+ assertEquals(chain.hashCode(), identifier.hashCode(), "hashCode");
+ }
+
+ @SuppressWarnings("SimplifiableAssertion")
+ @Test
+ void equalsBetweenIdentifierChain() {
+ var identifiers = List.of(new Identifier("NAME1"), new Identifier("NAME2"));
+
+ assertTrue(new IdentifierChain(identifiers).equals(new IdentifierChain(identifiers)), "chain.equals(chain)");
+ }
+
+ @SuppressWarnings({ "SimplifiableAssertion", "EqualsBetweenInconvertibleTypes" })
+ @ParameterizedTest
+ @CsvSource(useHeadersInDisplayName = true, textBlock = """
+ nameList1, nameList2
+ EXAMPLE, example
+ EXAMPLE, EXAMPLE.example
+ Example, exAmple
+ example.EXAMPLE, EXAMPLE.example
+ example.EXAMPLE, EXAMPLE.example.Example
+ example.EXAMPLE, EXAMPLE
+ """)
+ void notEquals(String nameList1, String nameList2) {
+ List identifiers1 = toIdentifiers(nameList1);
+ IdentifierChain chain1 = new IdentifierChain(identifiers1);
+ List identifiers2 = toIdentifiers(nameList2);
+ IdentifierChain chain2 = new IdentifierChain(identifiers2);
+
+ assertFalse(chain1.equals(chain2), "equals");
+ if (chain2.size() == 1) {
+ assertFalse(chain1.equals(chain2.at(0)), "equals with identifier");
+ }
+ assertFalse(chain1.equals(new IdentifierChain(CollectionUtils.concat(identifiers1, identifiers2))),
+ "equals with chain with same prefix");
+ }
+
+ @Test
+ void toList() {
+ List identifiers = Stream.of("ONE", "TWO", "THREE").map(Identifier::new).toList();
+ var chain = new IdentifierChain(identifiers);
+
+ assertEquals(identifiers, chain.toList(), "toList");
+ }
+
+ @Test
+ void at() {
+ List identifiers = Stream.of("ONE", "TWO", "THREE").map(Identifier::new).toList();
+ var chain = new IdentifierChain(identifiers);
+
+ for (int i = 0; i < identifiers.size(); i++) {
+ assertEquals(identifiers.get(i), chain.at(i), "at(" + i + ")");
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = { -1, 3, 10 })
+ void at_outOfRange(int index) {
+ List identifiers = Stream.of("ONE", "TWO", "THREE").map(Identifier::new).toList();
+ var chain = new IdentifierChain(identifiers);
+
+ assertThrows(IndexOutOfBoundsException.class, () -> chain.at(index));
+ }
+
+ @Test
+ void first() {
+ List identifiers = Stream.of("ONE", "TWO", "THREE").map(Identifier::new).toList();
+ var chain = new IdentifierChain(identifiers);
+
+ assertEquals(identifiers.get(0), chain.first(), "first");
+ }
+
+ @Test
+ void last() {
+ List identifiers = Stream.of("ONE", "TWO", "THREE").map(Identifier::new).toList();
+ var chain = new IdentifierChain(identifiers);
+
+ assertEquals(identifiers.get(2), chain.last(), "last");
+ }
+
+ @Test
+ void stream() {
+ List identifiers = Stream.of("ONE", "TWO", "THREE").map(Identifier::new).toList();
+ var chain = new IdentifierChain(identifiers);
+
+ assertEquals(identifiers, chain.stream().toList(), "stream");
+ }
+
+ @Test
+ void resolve_twoChains() {
+ List allIdentifiers = Stream.of("ONE", "TWO", "THREE", "FOUR").map(Identifier::new).toList();
+ var chain1 = new IdentifierChain(allIdentifiers.subList(0, 2));
+ var chain2 = new IdentifierChain(allIdentifiers.subList(2, 4));
+
+ assertEquals(new IdentifierChain(allIdentifiers), chain1.resolve(chain2), "resolve");
+ }
+
+ @Test
+ void resolve_chainAndIdentifier() {
+ List allIdentifiers = Stream.of("ONE", "TWO", "THREE").map(Identifier::new).toList();
+ var chain = new IdentifierChain(allIdentifiers.subList(0, 2));
+ var identifier3 = allIdentifiers.get(2);
+
+ assertEquals(new IdentifierChain(allIdentifiers), chain.resolve(identifier3), "resolve");
+ }
+
+ private static List toIdentifiers(String nameList) {
+ return Stream.of(nameList.split("\\.")).map(Identifier::new).toList();
+ }
+}
\ No newline at end of file
diff --git a/src/test/org/firebirdsql/jaybird/util/IdentifierTest.java b/src/test/org/firebirdsql/jaybird/util/IdentifierTest.java
new file mode 100644
index 000000000..7e023a3f7
--- /dev/null
+++ b/src/test/org/firebirdsql/jaybird/util/IdentifierTest.java
@@ -0,0 +1,150 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jaybird.util;
+
+import org.firebirdsql.jdbc.QuoteStrategy;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests for {@link Identifier}.
+ */
+class IdentifierTest {
+
+ @SuppressWarnings("DataFlowIssue")
+ @ParameterizedTest
+ @NullAndEmptySource
+ @ValueSource(strings = { " ", " " })
+ void nameNullEmptyOrBlank_notAllowed(@Nullable String name) {
+ assertThrows(IllegalArgumentException.class, () -> new Identifier(name));
+ }
+
+ @ParameterizedTest
+ @CsvSource(useHeadersInDisplayName = true, textBlock = """
+ name, expectedDialect3
+ SIMPLE_NAME, "SIMPLE_NAME"
+ lower_case, "lower_case"
+ """)
+ void identifier(String name, String expectedDialect3) {
+ var identifier = new Identifier(name);
+
+ assertEquals(name, identifier.name(), "name()");
+ assertEquals(expectedDialect3, identifier.toString(), "toString()");
+ assertEquals(expectedDialect3, identifier.toString(QuoteStrategy.DIALECT_3), "toString(DIALECT_3)");
+ assertEquals(name, identifier.toString(QuoteStrategy.DIALECT_1), "toString(DIALECT_1)");
+ assertEquals(expectedDialect3, identifier.append(new StringBuilder(), QuoteStrategy.DIALECT_3).toString(),
+ "append(..., DIALECT_3)");
+ assertEquals(name, identifier.append(new StringBuilder(), QuoteStrategy.DIALECT_1).toString(),
+ "append(..., DIALECT_1)");
+ assertEquals(1, identifier.size(), "size");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { " SPACE_PREFIX", "SPACE_SUFFIX ", " SPACE_BOTH " })
+ void nameIsTrimmed(String name) {
+ var identifier = new Identifier(name);
+
+ assertEquals(name.trim(), identifier.name());
+ }
+
+ @SuppressWarnings({ "SimplifiableAssertion", "EqualsBetweenInconvertibleTypes" })
+ @ParameterizedTest
+ @ValueSource(strings = { "SIMPLE_NAME", "lower_case", "Example3" })
+ void equalsAndHashCode_betweenIdentifierAndIdentifierChain(String name) {
+ var identifier = new Identifier(name);
+ var chain = new IdentifierChain(List.of(identifier));
+
+ assertTrue(identifier.equals(chain), "identifier.equals(chain)");
+ assertTrue(chain.equals(identifier), "chain.equals(identifier)");
+ assertEquals(identifier.hashCode(), chain.hashCode(), "hashCode");
+ }
+
+ @SuppressWarnings({ "SimplifiableAssertion", "EqualsBetweenInconvertibleTypes" })
+ @ParameterizedTest
+ @CsvSource(useHeadersInDisplayName = true, textBlock = """
+ name1, name2
+ EXAMPLE, example
+ Example, exAmple
+ """)
+ void notEquals(String name1, String name2) {
+ var identifier1 = new Identifier(name1);
+ var identifier2 = new Identifier(name2);
+
+ assertFalse(identifier1.equals(identifier2), "equals");
+ assertFalse(identifier1.equals(new IdentifierChain(List.of(identifier2))), "equals with chain");
+ assertFalse(identifier1.equals(new IdentifierChain(List.of(identifier1, identifier2))),
+ "equals with chain with same prefix");
+ }
+
+ @Test
+ void toList() {
+ var identifier = new Identifier("EXAMPLE");
+
+ assertEquals(List.of(identifier), identifier.toList(), "toList");
+ }
+
+ @Test
+ void at() {
+ var identifier = new Identifier("EXAMPLE");
+
+ assertEquals(identifier, identifier.at(0), "at");
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = { -1, 1, 10})
+ void at_outOfRange(int index) {
+ var identifier = new Identifier("EXAMPLE");
+
+ assertThrows(IndexOutOfBoundsException.class, () -> identifier.at(index));
+ }
+
+ @Test
+ void first() {
+ var identifier = new Identifier("EXAMPLE");
+
+ assertEquals(identifier, identifier.first(), "first");
+ }
+
+ @Test
+ void last() {
+ var identifier = new Identifier("EXAMPLE");
+
+ assertEquals(identifier, identifier.last(), "last");
+ }
+
+ @Test
+ void stream() {
+ var identifier = new Identifier("EXAMPLE");
+
+ assertEquals(List.of(identifier), identifier.stream().toList(), "stream");
+ }
+
+ @Test
+ void resolve_twoIdentifiers() {
+ var identifier1 = new Identifier("EXAMPLE_1");
+ var identifier2 = new Identifier("EXAMPLE_2");
+
+ assertEquals(new IdentifierChain(List.of(identifier1, identifier2)), identifier1.resolve(identifier2),
+ "resolve");
+ }
+
+ @Test
+ void resolve_identifierAndChain() {
+ var identifier1 = new Identifier("EXAMPLE_1");
+ var identifier2 = new Identifier("EXAMPLE_2");
+ var identifier3 = new Identifier("EXAMPLE_3");
+ var chain = new IdentifierChain(List.of(identifier2, identifier3));
+
+ assertEquals(new IdentifierChain(List.of(identifier1, identifier2, identifier3)), identifier1.resolve(chain),
+ "resolve");
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/org/firebirdsql/jaybird/util/ObjectReferenceTest.java b/src/test/org/firebirdsql/jaybird/util/ObjectReferenceTest.java
new file mode 100644
index 000000000..e2b453645
--- /dev/null
+++ b/src/test/org/firebirdsql/jaybird/util/ObjectReferenceTest.java
@@ -0,0 +1,170 @@
+// SPDX-FileCopyrightText: Copyright 2025 Mark Rotteveel
+// SPDX-License-Identifier: LGPL-2.1-or-later
+package org.firebirdsql.jaybird.util;
+
+import org.firebirdsql.encodings.EncodingFactory;
+import org.firebirdsql.gds.ng.DatatypeCoder;
+import org.firebirdsql.gds.ng.DefaultDatatypeCoder;
+import org.firebirdsql.gds.ng.fields.FieldDescriptor;
+import org.firebirdsql.gds.ng.fields.RowDescriptorBuilder;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.IntStream;
+
+import static org.firebirdsql.jaybird.util.StringUtils.isNullOrEmpty;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for {@link ObjectReference}.
+ *
+ * Some parts are tested through {@link IdentifierTest} and {@link IdentifierChainTest}.
+ *
+ */
+class ObjectReferenceTest {
+
+ private static final DatatypeCoder datatypeCoder =
+ DefaultDatatypeCoder.forEncodingFactory(EncodingFactory.createInstance(StandardCharsets.UTF_8));
+
+ @Test
+ void of_String() {
+ final String name = "TestName";
+ Identifier identifier = ObjectReference.of(name);
+ assertEquals(name, identifier.name());
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @ValueSource(strings = " ")
+ void of_String_nullOrEmptyOrBlank_throwsIllegalArgumentException(String name) {
+ assertThrows(IllegalArgumentException.class, () -> ObjectReference.of(name));
+ }
+
+ @Test
+ void of_StringArray_empty_throwsIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, ObjectReference::of);
+ }
+
+ @Test
+ void of_StringArray_singleName_returnsIdentifier() {
+ String name = "TestName";
+ var objectReference = ObjectReference.of(new String[] { name });
+
+ Identifier asIdentifier = assertInstanceOf(Identifier.class, objectReference);
+ assertEquals(name, asIdentifier.name());
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = { 2, 3, 10 })
+ void of_StringArray_multipleNames_returnsIdentifierChain(int nameCount) {
+ String[] names = IntStream.rangeClosed(1, nameCount).mapToObj(i -> "name" + i).toArray(String[]::new);
+ ObjectReference objectReference = ObjectReference.of(names);
+
+ IdentifierChain asChain = assertInstanceOf(IdentifierChain.class, objectReference);
+ List namesOfChain = asChain.stream().map(Identifier::name).toList();
+ assertEquals(Arrays.asList(names), namesOfChain);
+ }
+
+ // Given of(String...) calls of(List), we perform some tests of(List) through of(String...)
+
+ @ParameterizedTest
+ @CsvSource(useHeadersInDisplayName = true, nullValues = "", textBlock = """
+ prefixCount, prefixValue, suffixList
+ 0, , NAME1.NAME2
+ 1, , NAME1
+ 2, '', NAME1.NAME2
+ 3, '', NAME1.NAME2
+ 3,