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..9c42211d5
--- /dev/null
+++ b/devdoc/jdp/jdp-2025-06-schema-support.adoc
@@ -0,0 +1,120 @@
+= 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.
+* If `SYSTEM` is not on the search path, it is automatically searched last
+* The "`current`" schema cannot be set separately;
+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.
+* 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 related to schemas.
+
+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 the 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 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`!)
++
+** 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 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 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
+
+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
+
+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
diff --git a/src/docs/asciidoc/release_notes.adoc b/src/docs/asciidoc/release_notes.adoc
index 14633f919..32d54392c 100644
--- a/src/docs/asciidoc/release_notes.adoc
+++ b/src/docs/asciidoc/release_notes.adoc
@@ -508,6 +508,43 @@ 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:
+
+* 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
+** `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. 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;
+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 -- 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
[#other-fixes-and-changes]
@@ -648,13 +685,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/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/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..b06d66952 100644
--- a/src/main/org/firebirdsql/gds/ng/StatementInfoProcessor.java
+++ b/src/main/org/firebirdsql/gds/ng/StatementInfoProcessor.java
@@ -1,12 +1,14 @@
-// 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;
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
@@ -166,6 +168,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;
@@ -208,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/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/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/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/parser/StatementDetector.java b/src/main/org/firebirdsql/jaybird/parser/StatementDetector.java
index 8bf545596..df9f3cba8 100644
--- a/src/main/org/firebirdsql/jaybird/parser/StatementDetector.java
+++ b/src/main/org/firebirdsql/jaybird/parser/StatementDetector.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;
@@ -14,8 +14,8 @@
* Detects the type of statement, and - optionally - whether a DML statement has a {@code RETURNING} clause.
*
* If the detected statement type is {@code UPDATE}, {@code DELETE}, {@code INSERT}, {@code UPDATE OR INSERT} and
- * {@code MERGE}, it identifies the affected table and - optionally - whether or not a {@code RETURNING} clause is
- * present (delegated to a {@link ReturningClauseDetector}).
+ * {@code MERGE}, it identifies the affected table and - optionally - if a {@code RETURNING} clause is present
+ * (delegated to a {@link ReturningClauseDetector}).
*
*
* The types of statements detected are informed by the needs of Jaybird, and may change between point releases.
@@ -53,6 +53,7 @@ public final class StatementDetector implements TokenVisitor {
private final boolean detectReturning;
private LocalStatementType statementType = LocalStatementType.UNKNOWN;
private ParserState parserState = ParserState.START;
+ private Token schemaToken;
private Token tableNameToken;
private ReturningClauseDetector returningClauseDetector;
@@ -103,10 +104,13 @@ public void visitToken(Token token, VisitorRegistrar visitorRegistrar) {
if (parserState.isFinalState()) {
// We're not interested anymore
visitorRegistrar.removeVisitor(this);
- } else if (parserState == ParserState.FIND_RETURNING) {
- // We're not interested anymore
- visitorRegistrar.removeVisitor(this);
- if (detectReturning) {
+ } else if (parserState == ParserState.FIND_RETURNING
+ || parserState == ParserState.FIND_SCHEMA_SEPARATOR_OR_RETURNING) {
+ if (parserState == ParserState.FIND_RETURNING) {
+ // We're not interested anymore
+ visitorRegistrar.removeVisitor(this);
+ }
+ if (detectReturning && returningClauseDetector == null) {
// Use ReturningClauseDetector to handle detection
returningClauseDetector = new ReturningClauseDetector();
visitorRegistrar.addVisitor(returningClauseDetector);
@@ -122,8 +126,7 @@ public void complete(VisitorRegistrar visitorRegistrar) {
}
public StatementIdentification toStatementIdentification() {
- return new StatementIdentification(statementType, tableNameToken != null ? tableNameToken.text() : null,
- returningClauseDetected());
+ return new StatementIdentification(statementType, schemaToken, tableNameToken, returningClauseDetected());
}
boolean returningClauseDetected() {
@@ -137,6 +140,10 @@ public LocalStatementType getStatementType() {
return statementType;
}
+ Token getSchemaToken() {
+ return schemaToken;
+ }
+
Token getTableNameToken() {
return tableNameToken;
}
@@ -149,6 +156,10 @@ private void updateStatementType(LocalStatementType statementType) {
}
}
+ private void setSchemaToken(Token schemaToken) {
+ this.schemaToken = schemaToken;
+ }
+
private void setTableNameToken(Token tableNameToken) {
this.tableNameToken = tableNameToken;
}
@@ -211,12 +222,46 @@ ParserState next(Token token, StatementDetector detector) {
},
// Shared by UPDATE, DELETE and MERGE
DML_TARGET {
+ @Override
+ ParserState next(Token token, StatementDetector detector) {
+ if (token.isValidIdentifier()) {
+ detector.setTableNameToken(token);
+ return DML_SCHEMA_SEPARATOR_OR_POSSIBLE_ALIAS;
+ }
+ return forceOther(detector);
+ }
+ },
+ // Shared by UPDATE, DELETE and MERGE
+ DML_SCHEMA_SEPARATOR_OR_POSSIBLE_ALIAS {
+ @Override
+ ParserState next(Token token, StatementDetector detector) {
+ if (token instanceof PeriodToken) {
+ // What was detected as table, is actually the schema
+ detector.setSchemaToken(detector.getTableNameToken());
+ detector.setTableNameToken(null);
+ return DML_SCHEMA_QUALIFIED_TABLE_NAME;
+ } else if (token.isValidIdentifier()) {
+ // either alias or possibly returning clause
+ return FIND_RETURNING;
+ } else if (token instanceof ReservedToken) {
+ if (token.equalsIgnoreCase("AS")) {
+ return DML_ALIAS;
+ }
+ return FIND_RETURNING;
+ }
+ // Unexpected or invalid token at this point
+ return forceOther(detector);
+ }
+ },
+ // Shared by UPDATE, DELETE and MERGE
+ DML_SCHEMA_QUALIFIED_TABLE_NAME {
@Override
ParserState next(Token token, StatementDetector detector) {
if (token.isValidIdentifier()) {
detector.setTableNameToken(token);
return DML_POSSIBLE_ALIAS;
}
+ // Unexpected or invalid token at this point
return forceOther(detector);
}
},
@@ -261,7 +306,7 @@ ParserState next(Token token, StatementDetector detector) {
ParserState next(Token token, StatementDetector detector) {
if (token.isValidIdentifier()) {
detector.setTableNameToken(token);
- return FIND_RETURNING;
+ return FIND_SCHEMA_SEPARATOR_OR_RETURNING;
}
// Syntax error
return forceOther(detector);
@@ -277,6 +322,29 @@ ParserState next(Token token, StatementDetector detector) {
return forceOther(detector);
}
},
+ FIND_SCHEMA_SEPARATOR_OR_RETURNING {
+ @Override
+ ParserState next(Token token, StatementDetector detector) {
+ if (token instanceof PeriodToken) {
+ detector.setSchemaToken(detector.getTableNameToken());
+ detector.setTableNameToken(null);
+ return FIND_SCHEMA_QUALIFIED_TABLE_OR_RETURNING;
+ } else {
+ return FIND_RETURNING;
+ }
+ }
+ },
+ FIND_SCHEMA_QUALIFIED_TABLE_OR_RETURNING {
+ @Override
+ ParserState next(Token token, StatementDetector detector) {
+ if (token.isValidIdentifier()) {
+ detector.setTableNameToken(token);
+ return FIND_RETURNING;
+ }
+ // Syntax error
+ return forceOther(detector);
+ }
+ },
// finding itself is offloaded to ReturningClauseDetector
FIND_RETURNING,
COMMIT_ROLLBACK {
diff --git a/src/main/org/firebirdsql/jaybird/parser/StatementIdentification.java b/src/main/org/firebirdsql/jaybird/parser/StatementIdentification.java
index e4d7cbca3..4c5d3527d 100644
--- a/src/main/org/firebirdsql/jaybird/parser/StatementIdentification.java
+++ b/src/main/org/firebirdsql/jaybird/parser/StatementIdentification.java
@@ -1,8 +1,12 @@
-// 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;
import org.firebirdsql.util.InternalApi;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+import java.util.Locale;
import static java.util.Objects.requireNonNull;
@@ -13,15 +17,19 @@
* @since 5
*/
@InternalApi
+@NullMarked
public final class StatementIdentification {
private final LocalStatementType statementType;
- private final String tableName;
+ private final @Nullable String schema;
+ private final @Nullable String tableName;
private final boolean returningClauseDetected;
- StatementIdentification(LocalStatementType statementType, String tableName, boolean returningClauseDetected) {
+ StatementIdentification(LocalStatementType statementType, @Nullable Token schema, @Nullable Token tableName,
+ boolean returningClauseDetected) {
this.statementType = requireNonNull(statementType, "statementType");
- this.tableName = tableName;
+ this.schema = normalizeObjectName(schema);
+ this.tableName = normalizeObjectName(tableName);
this.returningClauseDetected = returningClauseDetected;
}
@@ -29,16 +37,54 @@ public LocalStatementType getStatementType() {
return statementType;
}
+ /**
+ * Schema, if this is a DML statement (other than {@code SELECT}), and if the table is qualified.
+ *
+ * It reports the name normalized to its metadata storage representation.
+ *
+ *
+ * @return Schema, {@code null} if the table was not qualified, or for {@code SELECT} and other non-DML statements
+ */
+ public @Nullable String getSchema() {
+ return schema;
+ }
+
/**
* Table name, if this is a DML statement (other than {@code SELECT}).
+ *
+ * It reports the name normalized to its metadata storage representation.
+ *
*
* @return Table name, {@code null} for {@code SELECT} and other non-DML statements
*/
- public String getTableName() {
+ public @Nullable String getTableName() {
return tableName;
}
public boolean returningClauseDetected() {
return returningClauseDetected;
}
+
+ /**
+ * Normalizes an object name from the parser to its storage representation.
+ *
+ * Unquoted identifiers are uppercased, and quoted identifiers are returned with the quotes stripped and doubled
+ * double quotes replaced by a single double quote.
+ *
+ *
+ * @param objectToken
+ * token with the object name (can be {@code null})
+ * @return normalized object name, or {@code null} if {@code objectToken} was {@code null}
+ */
+ private static @Nullable String normalizeObjectName(@Nullable Token objectToken) {
+ if (objectToken == null) return null;
+ String objectName = objectToken.text().trim();
+ if (objectName.length() > 2
+ && objectName.charAt(0) == '"'
+ && objectName.charAt(objectName.length() - 1) == '"') {
+ return objectName.substring(1, objectName.length() - 1).replace("\"\"", "\"");
+ }
+ return objectName.toUpperCase(Locale.ROOT);
+ }
+
}
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/main/org/firebirdsql/jaybird/util/CollectionUtils.java b/src/main/org/firebirdsql/jaybird/util/CollectionUtils.java
index 115a9f760..e45a1ef48 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;
@@ -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
@@ -69,4 +70,48 @@ 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;
+ }
+
+ /**
+ * 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/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/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/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/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/AbstractFieldMetaData.java b/src/main/org/firebirdsql/jdbc/AbstractFieldMetaData.java
index 1aab3d20f..14661dd26 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,33 @@ 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 ""} 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 relationName, String fieldName) {
+ protected record FieldKey(String schema, String relationName, String fieldName) {
+
+ protected FieldKey {
+ if (schema == null) {
+ schema = "";
+ }
+ }
+
public FieldKey(FieldDescriptor fieldDescriptor) {
- this(fieldDescriptor.getOriginalTableName(), fieldDescriptor.getOriginalName());
+ this(fieldDescriptor.getOriginalSchema(), fieldDescriptor.getOriginalTableName(),
+ fieldDescriptor.getOriginalName());
}
}
}
diff --git a/src/main/org/firebirdsql/jdbc/ClientInfoProvider.java b/src/main/org/firebirdsql/jdbc/ClientInfoProvider.java
index f5f2ad649..7b3ae5df9 100644
--- a/src/main/org/firebirdsql/jdbc/ClientInfoProvider.java
+++ b/src/main/org/firebirdsql/jdbc/ClientInfoProvider.java
@@ -1,13 +1,11 @@
-// 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;
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();
}
/**
@@ -179,7 +170,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 +186,10 @@ public String getClientInfo(String name) throws SQLException {
}
}
+ private boolean hasSystemSchema() {
+ return supportInfoFor(connection).supportsSchemas();
+ }
+
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)) {
diff --git a/src/main/org/firebirdsql/jdbc/FBConnection.java b/src/main/org/firebirdsql/jdbc/FBConnection.java
index e5c91d4e5..efdfad17c 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) {
@@ -1373,7 +1418,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/FBDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java
index 40d560f8a..4f1c8ca48 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;
@@ -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;
}
/**
@@ -1204,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);
}
/**
@@ -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";
@@ -1251,23 +1251,52 @@ 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() {
return GetTables.create(getDbMetadataMediator());
}
+ @Override
+ public Optional findTableSchema(String tableName) throws SQLException {
+ if (!supportsSchemasInDataManipulation()) return Optional.of("");
+ final String findSchema = """
+ with SEARCH_PATH as (
+ select row_number() over() as PRIO, NAME as SCHEMA_NAME
+ from SYSTEM.RDB$SQL.PARSE_UNQUALIFIED_NAMES(rdb$get_context('SYSTEM', 'SEARCH_PATH'))
+ )
+ select r.RDB$SCHEMA_NAME
+ from RDB$RELATIONS as r
+ inner join SEARCH_PATH s on r.RDB$SCHEMA_NAME = s.SCHEMA_NAME and r.RDB$RELATION_NAME = ?
+ order by s.PRIO
+ fetch first row only""";
+
+ var metadataQuery = new DbMetadataMediator.MetadataQuery(findSchema, List.of(tableName));
+ try (ResultSet rs = getDbMetadataMediator().performMetaDataQuery(metadataQuery)) {
+ if (rs.next()) {
+ return Optional.of(rs.getString(1));
+ }
+ return Optional.empty();
+ }
+ }
+
@Override
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}
*
* 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
@@ -1314,17 +1343,20 @@ 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);
}
/**
* {@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!).
*
*
*
@@ -1332,24 +1364,32 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa
*
*
* 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);
}
/**
* {@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!).
*
*
*
@@ -1361,20 +1401,25 @@ 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);
}
/**
* {@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)
@@ -1411,7 +1456,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);
}
/**
@@ -1426,7 +1471,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);
}
/**
@@ -1441,7 +1486,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);
}
/**
@@ -1456,9 +1501,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
@@ -1479,7 +1525,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
@@ -1724,7 +1770,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);
}
/**
@@ -1762,12 +1808,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);
- }
-
- @Override
- public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException {
- return GetSchemas.create(getDbMetadataMediator()).getSchemas();
+ return GetFunctions.create(getDbMetadataMediator()).getFunctions(catalog, schemaPattern, functionNamePattern);
}
@Override
@@ -1804,7 +1845,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
@@ -1815,42 +1857,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;
}
- return sResult;
+ 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);
+ }
+
+ 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/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/main/org/firebirdsql/jdbc/FBRowUpdater.java b/src/main/org/firebirdsql/jdbc/FBRowUpdater.java
index 37060e979..3beae4086 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;
@@ -10,6 +10,7 @@
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.UncheckedSQLException;
import org.firebirdsql.jdbc.field.FBField;
@@ -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 ObjectReference 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 ObjectReference requireSingleTable(RowDescriptor rowDescriptor, QuoteStrategy quoteStrategy)
+ throws SQLException {
+ // find the tableName (there can be only one tableName per updatable result set)
+ ObjectReference tableName = null;
for (FieldDescriptor fieldDescriptor : rowDescriptor) {
- // TODO This will not detect derived columns in the prefix of the select list
+ var currentTable = ObjectReference.ofTable(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(ObjectReference 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,10 +251,22 @@ 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 {
- try (ResultSet bestRowIdentifier = dbmd
- .getBestRowIdentifier("", "", tableName, DatabaseMetaData.bestRowTransaction, true)) {
+ 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(
+ "", tableRef.schema(), tableRef.tableName(), DatabaseMetaData.bestRowTransaction, true)) {
int bestRowIdentifierColumnCount = 0;
List keyColumns = new ArrayList<>();
while (bestRowIdentifier.next()) {
@@ -325,7 +340,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) {
@@ -348,7 +363,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();
@@ -375,9 +390,10 @@ private String buildInsertStatement() {
params.append('?');
}
- // 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)
+ // 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(')');
return sb.toString();
@@ -402,9 +418,10 @@ private String buildSelectStatement() {
}
}
- var sb = new StringBuilder(EST_STATEMENT_SIZE + columns.length())
+ // 32 = guesstimate for (schema +) table
+ var sb = new StringBuilder(EST_STATEMENT_SIZE + columns.length() + 32)
.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/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/FirebirdDatabaseMetaData.java b/src/main/org/firebirdsql/jdbc/FirebirdDatabaseMetaData.java
index fb0c07381..a231a640a 100644
--- a/src/main/org/firebirdsql/jdbc/FirebirdDatabaseMetaData.java
+++ b/src/main/org/firebirdsql/jdbc/FirebirdDatabaseMetaData.java
@@ -1,68 +1,126 @@
// SPDX-FileCopyrightText: Copyright 2005 Michael Romankiewicz
// SPDX-FileCopyrightText: Copyright 2005 Roman Rokytskyy
// SPDX-FileCopyrightText: Copyright 2007 Gabriel Reid
-// SPDX-FileCopyrightText: Copyright 2012-2023 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2012-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later OR BSD-3-Clause
package org.firebirdsql.jdbc;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
+import java.util.Optional;
/**
* Extension of {@link DatabaseMetaData} interface providing access to Firebird
* specific features.
- *
+ *
* @author Michael Romankiewicz
*/
@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.
- *
+ *
* @return The major version number of the database itself
- * @exception SQLException if a database access error occurs
+ * @throws SQLException
+ * if a database access error occurs
*/
int getOdsMajorVersion() throws SQLException;
-
+
/**
* Get the minor version of the ODS (On-Disk Structure) of the database.
- *
+ *
* @return The minor version number of the database itself
- * @exception SQLException if a database access error occurs
+ * @throws SQLException
+ * if a database access error occurs
*/
int getOdsMinorVersion() throws SQLException;
@@ -70,7 +128,8 @@ public interface FirebirdDatabaseMetaData extends DatabaseMetaData {
* Get the dialect of the database.
*
* @return The dialect of the database
- * @throws SQLException if a database access error occurs
+ * @throws SQLException
+ * if a database access error occurs
* @see #getConnectionDialect()
*/
int getDatabaseDialect() throws SQLException;
@@ -82,7 +141,8 @@ public interface FirebirdDatabaseMetaData extends DatabaseMetaData {
*
*
* @return The dialect of the connection
- * @throws SQLException if a database access error occurs
+ * @throws SQLException
+ * if a database access error occurs
* @see #getDatabaseDialect()
*/
int getConnectionDialect() throws SQLException;
@@ -102,7 +162,7 @@ public interface FirebirdDatabaseMetaData extends DatabaseMetaData {
* @throws SQLException
* For problems determining supported table types
* @see #getTableTypes()
- * @since 4.0
+ * @since 4
*/
String[] getTableTypeNames() throws SQLException;
@@ -117,4 +177,29 @@ public interface FirebirdDatabaseMetaData extends DatabaseMetaData {
*/
int getMaxObjectNameLength() throws SQLException;
+ /**
+ * Attempts to find the schema of {@code tableName} on the current search path.
+ *
+ * On Firebird versions that support schemas, this will return either a non-empty optional with the first schema
+ * containing {@code tableName}, or an empty optional if {@code tableName} was not found in the schemas on
+ * the search path.
+ *
+ *
+ * On Firebird versions that do not support schemas, this will always return a non-empty optional
+ * with an empty string ({@code ""}), meaning "table has no schema". This is an analogue to
+ * the meaning of empty string for {@code schema} or {@code schemaPattern} in other {@link DatabaseMetaData}
+ * methods. It will not query the server to check for existence of the table.
+ *
+ *
+ * @param tableName
+ * table name, matching exactly as stored in the metadata (not a like-pattern)
+ * @return the first schema name of the search path containing {@code tableName}, or empty string ({@code ""}) if
+ * schemas are not supported; returns an empty optional if schemas are supported, but {@code tableName} was not
+ * found on the search path
+ * @throws SQLException
+ * for database access errors
+ * @since 7
+ */
+ Optional findTableSchema(String tableName) throws SQLException;
+
}
diff --git a/src/main/org/firebirdsql/jdbc/GeneratedKeysQueryBuilder.java b/src/main/org/firebirdsql/jdbc/GeneratedKeysQueryBuilder.java
index 370a643d0..02fca5fd0 100644
--- a/src/main/org/firebirdsql/jdbc/GeneratedKeysQueryBuilder.java
+++ b/src/main/org/firebirdsql/jdbc/GeneratedKeysQueryBuilder.java
@@ -1,25 +1,28 @@
-// 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.jdbc;
import org.firebirdsql.gds.JaybirdErrorCodes;
import org.firebirdsql.gds.ng.FbExceptionBuilder;
-import org.firebirdsql.jdbc.metadata.MetadataPattern;
import org.firebirdsql.jaybird.parser.StatementDetector;
import org.firebirdsql.jaybird.parser.LocalStatementType;
import org.firebirdsql.jaybird.parser.FirebirdReservedWords;
import org.firebirdsql.jaybird.parser.SqlParser;
import org.firebirdsql.jaybird.parser.StatementIdentification;
+import org.firebirdsql.jaybird.util.ObjectReference;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
+import static java.util.Objects.requireNonNull;
+import static org.firebirdsql.jdbc.metadata.MetadataPattern.escapeWildcards;
+
/**
* Builds (updates) queries to add generated keys support.
*
* @author Mark Rotteveel
- * @since 4.0
+ * @since 4
*/
final class GeneratedKeysQueryBuilder {
@@ -124,10 +127,7 @@ boolean isSupportedType() {
* value {@code true} if the original statement already had a {@code RETURNING} clause.
*/
GeneratedKeysSupport.Query forNoGeneratedKeysOption() {
- if (hasReturning()) {
- return new GeneratedKeysSupport.Query(true, originalSql);
- }
- return new GeneratedKeysSupport.Query(false, originalSql);
+ return new GeneratedKeysSupport.Query(hasReturning(), originalSql);
}
/**
@@ -144,9 +144,8 @@ GeneratedKeysSupport.Query forReturnGeneratedKeysOption(FirebirdDatabaseMetaData
if (hasReturning()) {
// See also comment on forNoGeneratedKeysOption
return new GeneratedKeysSupport.Query(true, originalSql);
- }
- if (isSupportedType()) {
- // TODO Use an strategy when creating this builder or even push this up to the GeneratedKeysSupportFactory?
+ } else if (isSupportedType()) {
+ // TODO Use a strategy when creating this builder or even push this up to the GeneratedKeysSupportFactory?
if (supportsReturningAll(databaseMetaData)) {
return useReturningAll();
}
@@ -159,7 +158,7 @@ GeneratedKeysSupport.Query forReturnGeneratedKeysOption(FirebirdDatabaseMetaData
* Determines support for {@code RETURNING *}.
*
* @param databaseMetaData
- * Database meta data
+ * database metadata
* @return {@code true} if this version of Firebird supports {@code RETURNING *}.
* @throws SQLException
* for database access errors
@@ -182,7 +181,7 @@ private GeneratedKeysSupport.Query useReturningAll() {
*/
private GeneratedKeysSupport.Query useReturningAllColumnsByName(FirebirdDatabaseMetaData databaseMetaData)
throws SQLException {
- List columnNames = getAllColumnNames(statementIdentification.getTableName(), databaseMetaData);
+ List columnNames = getAllColumnNames(databaseMetaData);
QuoteStrategy quoteStrategy = QuoteStrategy.forDialect(databaseMetaData.getConnectionDialect());
return addColumnsByNameImpl(columnNames, quoteStrategy);
}
@@ -212,8 +211,8 @@ GeneratedKeysSupport.Query forColumnsByIndex(int[] columnIndexes, FirebirdDataba
.messageParameter("columnIndexes")
.toSQLException();
} else if (isSupportedType()) {
- List columnNames = getColumnNames(statementIdentification.getTableName(), columnIndexes, databaseMetaData);
- QuoteStrategy quoteStrategy = QuoteStrategy.forDialect(databaseMetaData.getConnectionDialect());
+ List columnNames = getColumnNames(columnIndexes, databaseMetaData);
+ var quoteStrategy = QuoteStrategy.forDialect(databaseMetaData.getConnectionDialect());
return addColumnsByNameImpl(columnNames, quoteStrategy);
} else {
// Unsupported type, ignore column indexes
@@ -263,9 +262,7 @@ private GeneratedKeysSupport.Query addColumnsByNameImpl(List columnNames
break;
}
}
- returningQuery
- .append('\n')
- .append("RETURNING ");
+ returningQuery.append("\nRETURNING ");
for (String columnName : columnNames) {
quoteStrategy
.appendQuoted(columnName, returningQuery)
@@ -276,9 +273,10 @@ private GeneratedKeysSupport.Query addColumnsByNameImpl(List columnNames
return new GeneratedKeysSupport.Query(true, returningQuery.toString());
}
- private List getAllColumnNames(String tableName, FirebirdDatabaseMetaData databaseMetaData)
- throws SQLException {
- try (ResultSet rs = databaseMetaData.getColumns(null, null, normalizeObjectName(tableName), null)) {
+ private List getAllColumnNames(FirebirdDatabaseMetaData databaseMetaData) throws SQLException {
+ // We're not using schema, as this is only called for Firebird 3.0 and older (no RETURNING * support)
+ String tableName = statementIdentification.getTableName();
+ try (ResultSet rs = databaseMetaData.getColumns(null, null, escapeWildcards(tableName), null)) {
if (rs.next()) {
List columns = new ArrayList<>();
do {
@@ -292,17 +290,26 @@ private List getAllColumnNames(String tableName, FirebirdDatabaseMetaDat
}
}
- private List getColumnNames(String tableName, int[] columnIndexes,
- FirebirdDatabaseMetaData databaseMetaData) throws SQLException {
- Map columnByIndex = mapColumnNamesByIndex(tableName, columnIndexes, databaseMetaData);
+ private List getColumnNames(int[] columnIndexes, FirebirdDatabaseMetaData databaseMetaData)
+ throws SQLException {
+ String tableName = requireNonNull(statementIdentification.getTableName());
+ String schema = statementIdentification.getSchema();
+ if (schema == null) {
+ schema = databaseMetaData.findTableSchema(tableName)
+ .orElseThrow(() -> FbExceptionBuilder
+ .forNonTransientException(JaybirdErrorCodes.jb_generatedKeysNoColumnsFound)
+ .messageParameter(ObjectReference.of(tableName), "schemaless table not on the search path")
+ .toSQLException());
+ }
+ Map columnByIndex = mapColumnNamesByIndex(schema, tableName, columnIndexes, databaseMetaData);
List columns = new ArrayList<>(columnIndexes.length);
for (int indexToAdd : columnIndexes) {
String columnName = columnByIndex.get(indexToAdd);
if (columnName == null) {
throw FbExceptionBuilder
.forNonTransientException(JaybirdErrorCodes.jb_generatedKeysInvalidColumnPosition)
- .messageParameter(indexToAdd, tableName)
+ .messageParameter(indexToAdd, ObjectReference.of(schema, tableName))
.toSQLException();
}
columns.add(columnName);
@@ -310,12 +317,13 @@ private List getColumnNames(String tableName, int[] columnIndexes,
return columns;
}
- private Map mapColumnNamesByIndex(String tableName, int[] columnIndexes,
+ private Map mapColumnNamesByIndex(String schema, String tableName, int[] columnIndexes,
FirebirdDatabaseMetaData databaseMetaData) throws SQLException {
- try (ResultSet rs = databaseMetaData.getColumns(null, null, normalizeObjectName(tableName), null)) {
+ try (ResultSet rs = databaseMetaData.getColumns(
+ null, escapeWildcards(schema), escapeWildcards(tableName), null)) {
if (!rs.next()) {
throw FbExceptionBuilder.forNonTransientException(JaybirdErrorCodes.jb_generatedKeysNoColumnsFound)
- .messageParameter(tableName)
+ .messageParameter(ObjectReference.of(schema, tableName))
.toSQLException();
}
@@ -333,27 +341,4 @@ private Map mapColumnNamesByIndex(String tableName, int[] colum
}
}
- /**
- * Normalizes an object name from the parser.
- *
- * Like-wildcard characters are escaped, and unquoted identifiers are uppercased, and quoted identifiers are
- * returned with the quotes stripped and double double quotes replaced by a single double quote.
- *
- *
- * @param objectName
- * Object name
- * @return Normalized object name
- */
- private String normalizeObjectName(String objectName) {
- if (objectName == null) return null;
- objectName = objectName.trim();
- objectName = MetadataPattern.escapeWildcards(objectName);
- if (objectName.length() > 2
- && objectName.charAt(0) == '"'
- && objectName.charAt(objectName.length() - 1) == '"') {
- return objectName.substring(1, objectName.length() - 1).replace("\"\"", "\"");
- }
- return objectName.toUpperCase(Locale.ROOT);
- }
-
}
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/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 633ef17f8..eab78c4c8 100644
--- a/src/main/org/firebirdsql/jdbc/StoredProcedureMetaDataFactory.java
+++ b/src/main/org/firebirdsql/jdbc/StoredProcedureMetaDataFactory.java
@@ -1,22 +1,23 @@
// 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;
-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() {
@@ -44,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());
@@ -59,20 +60,27 @@ 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
+ // 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)
+ 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 = "SELECT RDB$PROCEDURE_NAME FROM RDB$PROCEDURES WHERE RDB$PROCEDURE_TYPE = 1";
- try (ResultSet resultSet = stmt.executeQuery(sql)) {
+ 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 (var resultSet = stmt.executeQuery(sql)) {
while (resultSet.next()) {
selectableProcedureNames.add(resultSet.getString(1).trim().toUpperCase(Locale.ROOT));
}
@@ -80,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));
}
@@ -92,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);
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/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));
}
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java b/src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java
index d631431df..a2cf72228 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;
@@ -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;
@@ -41,7 +42,7 @@
* @author Mark Rotteveel
* @since 5
*/
-public final class GetBestRowIdentifier extends AbstractMetadataMethod {
+public abstract sealed class GetBestRowIdentifier extends AbstractMetadataMethod {
private static final String ROWIDENTIFIER = "ROWIDENTIFIER";
@@ -56,44 +57,19 @@ 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);
}
@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 (table == null || table.isEmpty()) {
+ if (isNullOrEmpty(table)) {
return createEmpty();
}
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 +81,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 +94,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 +108,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 +119,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 +131,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 +152,118 @@ 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
+ 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" +
+ " 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) {
+ 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_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/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);
}
+
}
+
}
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java b/src/main/org/firebirdsql/jdbc/metadata/GetColumnPrivileges.java
index f57bac6fd..01858fc43 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,10 +30,10 @@
* @author Mark Rotteveel
* @since 5
*/
-public final 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(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()
@@ -40,35 +43,9 @@ public final 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();
- //@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 +53,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 final 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"))
@@ -103,10 +75,123 @@ 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);
}
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,
+ 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
+ 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,
+ 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
+ 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_SCHEMA_NAME nulls first, 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/GetColumns.java b/src/main/org/firebirdsql/jdbc/metadata/GetColumns.java
index 39ee3d221..6afb758cc 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;
@@ -35,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";
@@ -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);
@@ -151,31 +156,31 @@ 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 class FB2_5 extends GetColumns {
+
+ 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 class FB3 extends GetColumns {
+
+ 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 final 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/main/org/firebirdsql/jdbc/metadata/GetCrossReference.java b/src/main/org/firebirdsql/jdbc/metadata/GetCrossReference.java
index 7c2a8dd4f..36f9f0500 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 sealed 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..2459ea103 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 sealed 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/GetFunctionColumns.java b/src/main/org/firebirdsql/jdbc/metadata/GetFunctionColumns.java
index 1b4860dc5..88ec1425e 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;
@@ -40,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";
@@ -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/main/org/firebirdsql/jdbc/metadata/GetFunctions.java b/src/main/org/firebirdsql/jdbc/metadata/GetFunctions.java
index ea9243cf3..0d14ef598 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;
@@ -27,11 +28,13 @@
* @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";
+ 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/main/org/firebirdsql/jdbc/metadata/GetImportedKeys.java b/src/main/org/firebirdsql/jdbc/metadata/GetImportedKeys.java
index fd3343a3e..ad0fc6bad 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 sealed 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/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/main/org/firebirdsql/jdbc/metadata/GetPrimaryKeys.java b/src/main/org/firebirdsql/jdbc/metadata/GetPrimaryKeys.java
index 777638300..9ef6a16da 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 sealed 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/main/org/firebirdsql/jdbc/metadata/GetProcedureColumns.java b/src/main/org/firebirdsql/jdbc/metadata/GetProcedureColumns.java
index 83fca3254..3567fbf4e 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;
@@ -39,9 +39,11 @@
* @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";
+ 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);
}
@@ -144,30 +153,33 @@ 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 =
- "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 {
+ private static final 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 final 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 22d0f7b5e..65a089d1a 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;
@@ -29,14 +29,16 @@
* @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";
+ 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);
}
@@ -97,7 +107,8 @@ private static final class FB2_5 extends GetProcedures {
private static final String GET_PROCEDURES_FRAGMENT_2_5 = """
select
- null as PROCEDURE_CAT,
+ 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
@@ -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,87 @@ 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));
+ String sql = GET_PROCEDURES_FRAGMENT_6_W_PKG
+ + (Clause.anyCondition(clauses) ? "\nwhere " + Clause.conjunction(clauses) : "")
+ + GET_PROCEDURES_ORDER_BY_6_W_PKG;
+ return new MetadataQuery(sql, Clause.parameters(clauses));
+ }
+ }
}
diff --git a/src/main/org/firebirdsql/jdbc/metadata/GetPseudoColumns.java b/src/main/org/firebirdsql/jdbc/metadata/GetPseudoColumns.java
index 0e1812c22..af25c3620 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;
@@ -29,9 +29,10 @@
* @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";
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/main/org/firebirdsql/jdbc/metadata/GetSchemas.java b/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java
index a4206c29e..fd9d152a3 100644
--- a/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java
+++ b/src/main/org/firebirdsql/jdbc/metadata/GetSchemas.java
@@ -1,17 +1,18 @@
-// 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;
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 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.jaybird.util.StringUtils.isNullOrEmpty;
import static org.firebirdsql.jdbc.metadata.FbMetadataConstants.OBJECT_NAME_LENGTH;
/**
@@ -20,23 +21,99 @@
* @author Mark Rotteveel
* @since 5
*/
-public final class GetSchemas {
+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()
.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 (!isNullOrEmpty(catalog) || "".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();
+ // 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 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/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/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/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";
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/main/org/firebirdsql/util/FirebirdSupportInfo.java b/src/main/org/firebirdsql/util/FirebirdSupportInfo.java
index c93a6da1e..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) {
@@ -517,8 +519,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,11 +786,33 @@ 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);
+ }
+
+ /**
+ * 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
*/
public boolean isSupportedVersion() {
- return isVersionEqualOrAbove(3);
+ return isVersionEqualOrAbove(SUPPORTED_MIN_VERSION);
}
/**
@@ -823,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 {
@@ -846,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);
+ }
+ }
+
}
diff --git a/src/test/org/firebirdsql/common/FBTestProperties.java b/src/test/org/firebirdsql/common/FBTestProperties.java
index f6d8077b3..68a396c47 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;
}
@@ -366,6 +368,36 @@ 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}
+ * @see FirebirdSupportInfo#ifSchemaElse(Object, Object)
+ */
+ public static T ifSchemaElse(T forSchema, T withoutSchema) {
+ return getDefaultSupportInfo().ifSchemaElse(forSchema, withoutSchema);
+ }
+
+ /**
+ * Helper method that replaces {@code "PUBLIC"} or {@code "SYSTEM"} with {@code ""} if schemas are not supported.
+ *
+ * @param schemaName
+ * schema name
+ * @return {@code schemaName}, or — if {@code schemaName} is {@code "PUBLIC"} or {@code "SYSTEM"} and schemas
+ * are not supported — {@code ""}
+ */
+ public static String resolveSchema(String schemaName) {
+ if (!getDefaultSupportInfo().supportsSchemas()
+ && ("PUBLIC".equals(schemaName) || "SYSTEM".equals(schemaName))) {
+ return "";
+ }
+ return schemaName;
+ }
+
private FBTestProperties() {
// No instantiation
}
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/gds/ng/AbstractStatementTest.java b/src/test/org/firebirdsql/gds/ng/AbstractStatementTest.java
index eb738ac96..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");
}
@@ -385,7 +377,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 +424,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
@@ -742,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/jaybird/parser/GrammarTest.java b/src/test/org/firebirdsql/jaybird/parser/GrammarTest.java
index a6fd4f3d0..919e67908 100644
--- a/src/test/org/firebirdsql/jaybird/parser/GrammarTest.java
+++ b/src/test/org/firebirdsql/jaybird/parser/GrammarTest.java
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2021-2023 Mark Rotteveel
+// SPDX-FileCopyrightText: Copyright 2021-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.jaybird.parser;
@@ -38,7 +38,8 @@ void insert_values() {
"insert into someTable(a, \"\u0442\u0435\"\"\u0441\u0442\", aaa) values('a', -1.23, a(a,aa))");
assertEquals(INSERT, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName(), "Unexpected table name");
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName(), "Unexpected table name");
assertFalse(statementModel.returningClauseDetected(), "Statement should have no returning");
}
@@ -48,7 +49,8 @@ void insert_values_quotedTable() {
"insert into \"someTable\"(a, b, c) values('a', -1.23, a(a,aa))");
assertEquals(INSERT, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("\"someTable\"", statementModel.getTableName(), "Unexpected table name");
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("someTable", statementModel.getTableName(), "Unexpected table name");
assertFalse(statementModel.returningClauseDetected(), "Statement should have no returning");
}
@@ -58,7 +60,8 @@ void insert_values_withReturning() {
"insert into someTable(a, b, c) values('a', -1.23, a(a,aa)) returning id");
assertEquals(INSERT, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName(), "Unexpected table name");
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName(), "Unexpected table name");
assertTrue(statementModel.returningClauseDetected(), "Statement should have returning");
}
@@ -68,7 +71,8 @@ void insert_values_withReturning_aliases() {
"insert into someTable(a, b, c) values('a', -1.23, a(a,aa)) returning id as \"ID\", b,c no_as");
assertEquals(INSERT, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName(), "Unexpected table name");
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName(), "Unexpected table name");
assertTrue(statementModel.returningClauseDetected(), "Statement should have returning");
}
@@ -78,7 +82,8 @@ void insert_values_commentedOutReturning_lineComment() {
"insert into someTable(a, b, c) values('a', -1.23, a(a,aa)) -- returning id");
assertEquals(INSERT, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName(), "Unexpected table name");
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName(), "Unexpected table name");
assertFalse(statementModel.returningClauseDetected(), "Statement should not have returning");
}
@@ -88,7 +93,8 @@ void insert_values_commentedOutReturning_blockComment() {
"insert into someTable(a, b, c) values('a', -1.23, a(a,aa)) /* returning id */");
assertEquals(INSERT, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName(), "Unexpected table name");
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName(), "Unexpected table name");
assertFalse(statementModel.returningClauseDetected(), "Statement should not have returning");
}
@@ -97,7 +103,8 @@ void insertIntoSelect() {
StatementIdentification statementModel = parseStatement("Insert Into someTable Select * From anotherTable");
assertEquals(INSERT, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName());
assertFalse(statementModel.returningClauseDetected(), "Statement should have no returning");
}
@@ -107,7 +114,8 @@ void insertIntoSelect_withReturning() {
parseStatement("Insert Into someTable Select * From anotherTable returning id");
assertEquals(INSERT, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName());
assertTrue(statementModel.returningClauseDetected(), "Statement should have returning");
}
@@ -117,7 +125,8 @@ void insertWithCase() {
"Insert Into someTable ( col1, col2) values((case when a = 1 Then 2 else 3 end), 2)");
assertEquals(INSERT, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName());
assertFalse(statementModel.returningClauseDetected(), "Statement should have no returning");
}
@@ -127,7 +136,8 @@ void insertReturningWithCase() {
"Insert Into someTable ( col1, col2) values((case when a = 1 Then 2 else 3 end), 2) returning id");
assertEquals(INSERT, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName());
assertTrue(statementModel.returningClauseDetected(), "Statement should have returning");
}
@@ -136,7 +146,8 @@ void insertDefaultValues() {
StatementIdentification statementModel = parseStatement("INSERT INTO someTable DEFAULT VALUES");
assertEquals(INSERT, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName());
assertFalse(statementModel.returningClauseDetected(), "Statement should have no returning");
}
@@ -146,7 +157,8 @@ void insertDefaultValues_withReturning() {
parseStatement("INSERT INTO someTable DEFAULT VALUES RETURNING \"ID\"");
assertEquals(INSERT, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName());
assertTrue(statementModel.returningClauseDetected(), "Statement should have returning");
}
@@ -156,7 +168,8 @@ void update() {
"Update someTable Set col1 = 25, col2 = 'abc' Where 1=0");
assertEquals(UPDATE, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName());
assertFalse(statementModel.returningClauseDetected(), "Statement should not have returning");
}
@@ -166,7 +179,8 @@ void update_quotedTableName() {
"Update \"someTable\" Set col1 = 25, col2 = 'abc' Where 1=0");
assertEquals(UPDATE, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("\"someTable\"", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("someTable", statementModel.getTableName());
assertFalse(statementModel.returningClauseDetected(), "Statement should not have returning");
}
@@ -176,7 +190,8 @@ void update_quotedTableNameWithSpace() {
"Update \"some Table\" Set col1 = 25, col2 = 'abc' Where 1=0");
assertEquals(UPDATE, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("\"some Table\"", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("some Table", statementModel.getTableName());
assertFalse(statementModel.returningClauseDetected(), "Statement should not have returning");
}
@@ -186,7 +201,8 @@ void update_withReturning() {
"Update someTable Set col1 = 25, col2 = 'abc' Where 1=0 Returning col3");
assertEquals(UPDATE, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName());
assertTrue(statementModel.returningClauseDetected(), "Statement should have returning");
}
@@ -196,7 +212,8 @@ void delete() {
"DELETE FROM someTable Where 1=0");
assertEquals(DELETE, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName());
assertFalse(statementModel.returningClauseDetected(), "Statement should not have returning");
}
@@ -205,7 +222,8 @@ void delete_quotedTableName() {
StatementIdentification statementModel = parseStatement("delete from \"someTable\"");
assertEquals(DELETE, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("\"someTable\"", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("someTable", statementModel.getTableName());
assertFalse(statementModel.returningClauseDetected(), "Statement should not have returning");
}
@@ -214,7 +232,8 @@ void delete_withReturning() {
StatementIdentification statementModel = parseStatement("Delete From someTable Returning col3");
assertEquals(DELETE, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName());
assertTrue(statementModel.returningClauseDetected(), "Statement should have returning");
}
@@ -223,7 +242,8 @@ void delete_withWhere_withReturning() {
StatementIdentification statementModel = parseStatement("Delete From someTable where 1 = 1 Returning col3");
assertEquals(DELETE, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName());
assertTrue(statementModel.returningClauseDetected(), "Statement should have returning");
}
@@ -232,6 +252,7 @@ void select() {
StatementIdentification statementModel = parseStatement("select * from RDB$DATABASE");
assertEquals(SELECT, statementModel.getStatementType(), "Expected SELECT statement type");
+ assertNull(statementModel.getSchema(), "Unexpected schema");
assertNull(statementModel.getTableName(), "Expected no table name");
assertFalse(statementModel.returningClauseDetected(), "Statement should not have returning");
}
@@ -241,7 +262,8 @@ void insertWithQString() {
StatementIdentification statementModel = parseStatement("insert into someTable values (Q'[a'bc]')");
assertEquals(INSERT, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName());
assertFalse(statementModel.returningClauseDetected(), "Statement should not have returning");
}
@@ -251,7 +273,8 @@ void insertWithQStringWithReturning() {
parseStatement("insert into someTable values (Q'[a'bc]') returning id, \"ABC\"");
assertEquals(INSERT, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("someTable", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("SOMETABLE", statementModel.getTableName());
assertTrue(statementModel.returningClauseDetected(), "Statement should not have returning");
}
@@ -270,6 +293,7 @@ void testQLiterals() {
checkQLiteralsStartEnd('>', '>');
}
+ @SuppressWarnings("resource")
private void checkQLiteralsStartEnd(char start, char end) {
final String input = "q'" + start + "a'bc" + end + "'";
Token token = SqlTokenizer.withReservedWords(FirebirdReservedWords.latest())
@@ -291,7 +315,8 @@ void merge() {
INSERT (title, desc, bought) values (p.title, p.desc, p.bought)""");
assertEquals(MERGE, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("books", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("BOOKS", statementModel.getTableName());
assertFalse(statementModel.returningClauseDetected(), "Statement should not have returning");
}
@@ -307,7 +332,8 @@ void merge_quotedTableName() {
INSERT (title, desc, bought) values (p.title, p.desc, p.bought)""");
assertEquals(MERGE, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("\"books\"", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("books", statementModel.getTableName());
assertFalse(statementModel.returningClauseDetected(), "Statement should not have returning");
}
@@ -323,7 +349,8 @@ void merge_quotedTableNameWithSpace() {
INSERT (title, desc, bought) values (p.title, p.desc, p.bought)""");
assertEquals(MERGE, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("\"more books\"", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("more books", statementModel.getTableName());
assertFalse(statementModel.returningClauseDetected(), "Statement should not have returning");
}
@@ -341,49 +368,52 @@ void merge_withReturning() {
""");
assertEquals(MERGE, statementModel.getStatementType(), "Unexpected statement type");
- assertEquals("books", statementModel.getTableName());
+ assertNull(statementModel.getSchema(), "Unexpected schema");
+ assertEquals("BOOKS", statementModel.getTableName());
assertTrue(statementModel.returningClauseDetected(), "Statement should have returning");
}
@ParameterizedTest
@MethodSource("testData")
- void testParser(boolean expectedReturning, LocalStatementType expectedStatementType, String expectedTableName,
- String statementText) {
+ void testParser(boolean expectedReturning, LocalStatementType expectedStatementType, String expectedSchema,
+ String expectedTableName, String statementText) {
StatementIdentification statementIdentification = parseStatement(statementText);
assertEquals(expectedReturning, statementIdentification.returningClauseDetected(),
"returningClauseDetected for: " + statementText);
assertEquals(expectedStatementType, statementIdentification.getStatementType(),
"statementType for: " + statementText);
+ assertEquals(expectedSchema, statementIdentification.getSchema(), "schema for: " + statementText);
assertEquals(expectedTableName, statementIdentification.getTableName(), "tableName for: " + statementText);
}
static Stream testData() {
return Stream.of(
// @formatter:off
- /* 0*/ testCase(false, SELECT, null, "select * from rdb$database"),
- /* 1*/ testCase(false, INSERT, "\"TABLE\"", "insert into \"TABLE\" (x, y, z) values ('ab', ?, Q'[xyz]')"),
- /* 2*/ testCase(true, INSERT, "\"TABLE\"",
+ /* 0*/ testCase(false, SELECT, null, null, "select * from rdb$database"),
+ /* 1*/ testCase(false, INSERT, null, "TABLE", "insert into \"TABLE\" (x, y, z) values ('ab', ?, Q'[xyz]')"),
+ /* 2*/ testCase(true, INSERT, null, "TABLE",
"insert into \"TABLE\" (x, y, z) values ('ab', ?, Q'[xyz]') returning id"),
- /* 3*/ testCase(false, UPDATE, "sometable", "update sometable set x = ?, y = Q'[xy'z]' where a and b > 1"),
- /* 4*/ testCase(true, UPDATE, "SOMETABLE",
+ /* 3*/ testCase(false, UPDATE, null, "SOMETABLE",
+ "update sometable set x = ?, y = Q'[xy'z]' where a and b > 1"),
+ /* 4*/ testCase(true, UPDATE, null, "SOMETABLE",
"update SOMETABLE set x = ?, y = Q'[xy'z]' where a and b > 1 returning \"A\" as a"),
- /* 5*/ testCase(false, DELETE, "sometable", "DELETE FROM sometable where x"),
- /* 6*/ testCase(true, DELETE, "sometable", """
+ /* 5*/ testCase(false, DELETE, null, "SOMETABLE", "DELETE FROM sometable where x"),
+ /* 6*/ testCase(true, DELETE, null, "SOMETABLE", """
DELETE FROM sometable
where x = (select y from "TABLE"
where startdate = {d'2018-05-1'})
returning x, a, b "A\""""),
- /* 7*/ testCase(false, UPDATE_OR_INSERT, "Cows", """
+ /* 7*/ testCase(false, UPDATE_OR_INSERT, null, "COWS", """
UPDATE OR INSERT INTO Cows (Name, Number, Location)
VALUES ('Suzy Creamcheese', 3278823, 'Green Pastures')
MATCHING (Number);"""),
- /* 8*/ testCase(true, UPDATE_OR_INSERT, "Cows", """
+ /* 8*/ testCase(true, UPDATE_OR_INSERT, null, "COWS", """
UPDATE OR INSERT INTO Cows (Name, Number, Location)
VALUES ('Suzy Creamcheese', 3278823, 'Green Pastures')
MATCHING (Number)
RETURNING rec_id;"""),
- /* 9*/ testCase(false, MERGE, "customers", """
+ /* 9*/ testCase(false, MERGE, null, "CUSTOMERS", """
MERGE INTO customers c
USING
(SELECT * FROM customers_delta WHERE id > 10) cd
@@ -393,7 +423,7 @@ UPDATE OR INSERT INTO Cows (Name, Number, Location)
WHEN NOT MATCHED THEN
INSERT (id, name)
VALUES (cd.id, cd.name)"""),
- /*10*/ testCase(true, MERGE, "customers", """
+ /*10*/ testCase(true, MERGE, null, "CUSTOMERS", """
MERGE INTO customers c
USING
(SELECT * FROM customers_delta WHERE id > 10) cd
@@ -404,7 +434,7 @@ UPDATE OR INSERT INTO Cows (Name, Number, Location)
INSERT (id, name)
VALUES (cd.id, cd.name)
RETURNING id"""),
- /*11*/ testCase(false, MERGE, "customers", """
+ /*11*/ testCase(false, MERGE, null, "CUSTOMERS", """
MERGE INTO customers c
USING
(SELECT * FROM customers_delta WHERE id > 10) cd
@@ -415,22 +445,22 @@ UPDATE OR INSERT INTO Cows (Name, Number, Location)
INSERT (id, name)
VALUES (cd.id, cd.name)
-- RETURNING id"""),
- /*12*/ testCase(true, INSERT, "sometable", "insert into sometable default values returning *"),
- /*13*/ testCase(true, INSERT, "sometable", "insert into sometable default values returning sometable.*"),
- /*14*/ testCase(true, INSERT, "sometable", "insert into sometable(x, y, z) values(default, 1, 2) returning *"),
- /*15*/ testCase(true, INSERT, "sometable",
+ /*12*/ testCase(true, INSERT, null, "SOMETABLE", "insert into sometable default values returning *"),
+ /*13*/ testCase(true, INSERT, null, "SOMETABLE", "insert into sometable default values returning sometable.*"),
+ /*14*/ testCase(true, INSERT, null, "SOMETABLE", "insert into sometable(x, y, z) values(default, 1, 2) returning *"),
+ /*15*/ testCase(true, INSERT, null, "SOMETABLE",
"insert into sometable(x, y, z) values(default, 1, 2) returning sometable.*"),
- /*16*/ testCase(true, UPDATE, "sometable", "update sometable set x = ? returning *"),
- /*17*/ testCase(true, UPDATE, "sometable", "update sometable set x = ? returning sometable.*"),
- /*18*/ testCase(true, UPDATE, "sometable", "update sometable a set x = ? returning a.*"),
- /*19*/ testCase(true, UPDATE, "sometable", "update sometable as a set a.x = ? returning *"),
- /*20*/ testCase(true, UPDATE, "sometable", "update sometable as a set a.x = ? returning a.id"),
- /*21*/ testCase(true, DELETE, "sometable", "delete from sometable where x = ? returning *"),
- /*22*/ testCase(true, DELETE, "sometable", "delete from sometable a where a.x = ? returning a.*"),
- /*23*/ testCase(true, DELETE, "sometable", "delete from sometable a where a.x = ? returning *"),
- /*24*/ testCase(true, DELETE, "sometable", "delete from sometable as a where a.x = ? returning a.id"),
- /*25*/ testCase(true, DELETE, "sometable", "delete from sometable as a where a.x = ? returning *"),
- /*26*/ testCase(true, MERGE, "customers", """
+ /*16*/ testCase(true, UPDATE, null, "SOMETABLE", "update sometable set x = ? returning *"),
+ /*17*/ testCase(true, UPDATE, null, "SOMETABLE", "update sometable set x = ? returning sometable.*"),
+ /*18*/ testCase(true, UPDATE, null, "SOMETABLE", "update sometable a set x = ? returning a.*"),
+ /*19*/ testCase(true, UPDATE, null, "SOMETABLE", "update sometable as a set a.x = ? returning *"),
+ /*20*/ testCase(true, UPDATE, null, "SOMETABLE", "update sometable as a set a.x = ? returning a.id"),
+ /*21*/ testCase(true, DELETE, null, "SOMETABLE", "delete from sometable where x = ? returning *"),
+ /*22*/ testCase(true, DELETE, null, "SOMETABLE", "delete from sometable a where a.x = ? returning a.*"),
+ /*23*/ testCase(true, DELETE, null, "SOMETABLE", "delete from sometable a where a.x = ? returning *"),
+ /*24*/ testCase(true, DELETE, null, "SOMETABLE", "delete from sometable as a where a.x = ? returning a.id"),
+ /*25*/ testCase(true, DELETE, null, "SOMETABLE", "delete from sometable as a where a.x = ? returning *"),
+ /*26*/ testCase(true, MERGE, null, "CUSTOMERS", """
MERGE INTO customers
USING
(SELECT * FROM customers_delta WHERE id > 10) cd
@@ -441,7 +471,7 @@ UPDATE OR INSERT INTO Cows (Name, Number, Location)
INSERT (id, name)
VALUES (cd.id, cd.name)
RETURNING *"""),
- /*27*/ testCase(true, MERGE, "customers", """
+ /*27*/ testCase(true, MERGE, null, "CUSTOMERS", """
MERGE INTO customers as c
USING
(SELECT * FROM customers_delta WHERE id > 10) cd
@@ -452,7 +482,7 @@ UPDATE OR INSERT INTO Cows (Name, Number, Location)
INSERT (id, name)
VALUES (cd.id, cd.name)
RETURNING *"""),
- /*28*/ testCase(true, MERGE, "customers", """
+ /*28*/ testCase(true, MERGE, null, "CUSTOMERS", """
MERGE INTO customers c
USING
(SELECT * FROM customers_delta WHERE id > 10) cd
@@ -463,7 +493,7 @@ UPDATE OR INSERT INTO Cows (Name, Number, Location)
INSERT (id, name)
VALUES (cd.id, cd.name)
RETURNING c.*"""),
- /*29*/ testCase(true, MERGE, "customers", """
+ /*29*/ testCase(true, MERGE, null, "CUSTOMERS", """
MERGE INTO customers c
USING
(SELECT * FROM customers_delta WHERE id > 10) cd
@@ -473,14 +503,54 @@ UPDATE OR INSERT INTO Cows (Name, Number, Location)
WHEN NOT MATCHED THEN
INSERT (id, name)
VALUES (cd.id, cd.name)
- RETURNING c.id""")
+ RETURNING c.id"""),
+ // cases with schema
+ /*30*/ testCase(false, INSERT, "other", "table",
+ "insert into \"other\".\"table\" (x, y, z) values ('ab', ?, Q'[xyz]')"),
+ /*31*/ testCase(false, UPDATE, "PUBLIC", "SOMETABLE",
+ "update public.sometable set x = ?, y = Q'[xy'z]' where a and b > 1"),
+ /*32*/ testCase(false, DELETE, "OTHER", "SOMETABLE", "DELETE FROM other.sometable where x"),
+ /*33*/ testCase(false, UPDATE_OR_INSERT, "PUBLIC", "COWS", """
+ UPDATE OR INSERT INTO public.Cows as x (Name, Number, Location)
+ VALUES ('Suzy Creamcheese', 3278823, 'Green Pastures')
+ MATCHING (x.Number);"""),
+ /*34*/ testCase(false, MERGE, "OTHER", "CUSTOMERS", """
+ MERGE INTO "OTHER".customers c
+ USING
+ (SELECT * FROM customers_delta WHERE id > 10) cd
+ ON (c.id = cd.id)
+ WHEN MATCHED THEN
+ UPDATE SET name = cd.name
+ WHEN NOT MATCHED THEN
+ INSERT (id, name)
+ VALUES (cd.id, cd.name)"""),
+ /*35*/ testCase(true, INSERT, "PUBLIC", "SOMETABLE",
+ "insert into public.sometable default values returning id, val"),
+ /*36*/ testCase(true, UPDATE, "OTHER", "SOMETABLE",
+ "update OTHER.SOMETABLE set x = ?, y = Q'[xy'z]' where a and b > 1 returning \"A\" as a"),
+ /*37*/ testCase(true, DELETE, "PUBLIC", "SOMETABLE", """
+ DELETE FROM PUBLIC.sometable
+ where x = (select y from "TABLE"
+ where startdate = {d'2018-05-1'})
+ returning x, a, b "A\""""),
+ /*38*/ testCase(true, MERGE, "with\"quote", "CUSTOMERS", """
+ MERGE INTO "with""quote".customers
+ USING
+ (SELECT * FROM customers_delta WHERE id > 10) cd
+ ON (c.id = cd.id)
+ WHEN MATCHED THEN
+ UPDATE SET name = cd.name
+ WHEN NOT MATCHED THEN
+ INSERT (id, name)
+ VALUES (cd.id, cd.name)
+ RETURNING *""")
// @formatter:on
);
}
private static Arguments testCase(boolean expectedReturning, LocalStatementType expectedStatementType,
- String expectedTableName, String statementText) {
- return Arguments.of(expectedReturning, expectedStatementType, expectedTableName, statementText);
+ String expectedSchema, String expectedTableName, String statementText) {
+ return Arguments.of(expectedReturning, expectedStatementType, expectedSchema, expectedTableName, statementText);
}
}
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/parser/StatementDetectorTest.java b/src/test/org/firebirdsql/jaybird/parser/StatementDetectorTest.java
index 06a31e5e4..5eeb0646f 100644
--- a/src/test/org/firebirdsql/jaybird/parser/StatementDetectorTest.java
+++ b/src/test/org/firebirdsql/jaybird/parser/StatementDetectorTest.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;
@@ -28,13 +28,15 @@ void initialStatementType_typeUNKNOWN() {
@ParameterizedTest
@MethodSource("detectionCases")
void testDetection(boolean detectReturning, String statement, LocalStatementType expectedType,
- Token expectedTableNameToken, boolean expectedReturningDetected, boolean expectedParserCompleted) {
+ Token expectedSchemaToken, Token expectedTableNameToken, boolean expectedReturningDetected,
+ boolean expectedParserCompleted) {
detector = new StatementDetector(detectReturning);
SqlParser parser = parserFor(statement);
parser.parse();
assertThat(detector.getStatementType()).describedAs("statementType").isEqualTo(expectedType);
+ assertThat(detector.getSchemaToken()).describedAs("schemaToken").isEqualTo(expectedSchemaToken);
assertThat(detector.getTableNameToken()).describedAs("tableNameToken").isEqualTo(expectedTableNameToken);
assertThat(detector.returningClauseDetected())
.describedAs("returningClauseDetected").isEqualTo(expectedReturningDetected);
@@ -80,6 +82,15 @@ LocalStatementType.INSERT, new GenericToken(12, "sometable"), false, true),
LocalStatementType.INSERT, new GenericToken(12, "sometable"), true, true),
detectReturning("INSERT INTO TABLE_WITH_TRIGGER(TEXT) VALUES ('Some text to insert') RETURNING *",
LocalStatementType.INSERT, new GenericToken(12, "TABLE_WITH_TRIGGER"), true, true),
+ detectReturning("insert into other_schema.sometable (id, column1, column2) values (?, ?, ?)",
+ LocalStatementType.INSERT, new GenericToken(12, "other_schema"),
+ new GenericToken(25, "sometable"), false, true),
+ detectReturning("insert into other_schema.\"sometable\" values (1, 2) returning id1, id2",
+ LocalStatementType.INSERT, new GenericToken(12, "other_schema"),
+ new QuotedIdentifierToken(25, "\"sometable\""), true, true),
+ noDetect("insert into other_schema.\"sometable\" values (1, 2) returning id1, id2",
+ LocalStatementType.INSERT, new GenericToken(12, "other_schema"),
+ new QuotedIdentifierToken(25, "\"sometable\""), false),
// delete
detectReturning("delete from sometable",
@@ -91,6 +102,15 @@ LocalStatementType.DELETE, new GenericToken(12, "sometable"), true, true),
LocalStatementType.DELETE, new GenericToken(12, "sometable"), false),
detectReturning("delete from sometable as somealias where somealias.foo = 'bar'",
LocalStatementType.DELETE, new GenericToken(12, "sometable"), false, true),
+ detectReturning("delete from \"OTHER_SCHEMA\".\"sometable\"",
+ LocalStatementType.DELETE, new QuotedIdentifierToken(12, "\"OTHER_SCHEMA\""),
+ new QuotedIdentifierToken(27, "\"sometable\""), false, true),
+ detectReturning("delete from \"OTHER_SCHEMA\".\"sometable\" returning column1",
+ LocalStatementType.DELETE, new QuotedIdentifierToken(12, "\"OTHER_SCHEMA\""),
+ new QuotedIdentifierToken(27, "\"sometable\""), true, true),
+ detectReturning("delete from \"OTHER_SCHEMA\".\"sometable\" as \"x\" returning column1",
+ LocalStatementType.DELETE, new QuotedIdentifierToken(12, "\"OTHER_SCHEMA\""),
+ new QuotedIdentifierToken(27, "\"sometable\""), true, true),
// update
detectReturning("update \"sometable\" set column1 = 1, column2 = column2 + 1 where x = y",
@@ -105,6 +125,9 @@ LocalStatementType.UPDATE, new GenericToken(7, "sometable"), false),
LocalStatementType.UPDATE, new GenericToken(7, "sometable"), true, true),
detectReturning("update sometable \"withalias\" set column1 = 1 returning (id + 1) as foo",
LocalStatementType.UPDATE, new GenericToken(7, "sometable"), true, true),
+ detectReturning("update PUBLIC.sometable set column1 = 2 returning calculated_column",
+ LocalStatementType.UPDATE, new GenericToken(7, "PUBLIC"), new GenericToken(14, "sometable"),
+ true, true),
// update or insert
detectReturning("update or insert into sometable (id, column1, column2) values (?, ?, (? * 2)) matching (id)",
@@ -209,26 +232,45 @@ LocalStatementType.UPDATE, new GenericToken(7, "returning"), true, true),
);
}
- @SuppressWarnings("SameParameterValue")
+ private static Arguments detectReturning(String statement, LocalStatementType expectedType,
+ boolean expectedParserCompleted) {
+ return detectReturning(statement, expectedType, null, false, expectedParserCompleted);
+ }
+
private static Arguments detectReturning(String statement, LocalStatementType expectedType,
Token expectedTableNameToken, boolean expectedReturningDetected, boolean expectedParserCompleted) {
- return arguments(true, statement, expectedType, expectedTableNameToken, expectedReturningDetected,
+ return detectReturning(statement, expectedType, null, expectedTableNameToken, expectedReturningDetected,
expectedParserCompleted);
}
private static Arguments detectReturning(String statement, LocalStatementType expectedType,
+ Token expectedSchemaToken, Token expectedTableNameToken, boolean expectedReturningDetected,
+ boolean expectedParserCompleted) {
+ return testCase(true, statement, expectedType, expectedSchemaToken, expectedTableNameToken,
+ expectedReturningDetected, expectedParserCompleted);
+ }
+
+ private static Arguments noDetect(String statement, LocalStatementType expectedType,
boolean expectedParserCompleted) {
- return arguments(true, statement, expectedType, null, false, expectedParserCompleted);
+ return noDetect(statement, expectedType, null, null, expectedParserCompleted);
}
private static Arguments noDetect(String statement, LocalStatementType expectedType, Token expectedTableNameToken,
boolean expectedParserCompleted) {
- return arguments(false, statement, expectedType, expectedTableNameToken, false, expectedParserCompleted);
+ return noDetect(statement, expectedType, null, expectedTableNameToken, expectedParserCompleted);
}
- private static Arguments noDetect(String statement, LocalStatementType expectedType,
+ private static Arguments noDetect(String statement, LocalStatementType expectedType, Token expectedSchemaToken,
+ Token expectedTableNameToken, boolean expectedParserCompleted) {
+ return testCase(false, statement, expectedType, expectedSchemaToken, expectedTableNameToken, false,
+ expectedParserCompleted);
+ }
+
+ private static Arguments testCase(boolean detectReturning, String statement, LocalStatementType expectedType,
+ Token expectedSchemaToken, Token expectedTableNameToken, boolean expectedReturningDetected,
boolean expectedParserCompleted) {
- return arguments(false, statement, expectedType, null, false, expectedParserCompleted);
+ return arguments(detectReturning, statement, expectedType, expectedSchemaToken, expectedTableNameToken,
+ expectedReturningDetected, expectedParserCompleted);
}
private SqlParser parserFor(String statementText) {
diff --git a/src/test/org/firebirdsql/jaybird/util/CollectionUtilsTest.java b/src/test/org/firebirdsql/jaybird/util/CollectionUtilsTest.java
index 28ff16654..78ba8a674 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;
@@ -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;
@@ -89,6 +90,33 @@ void getLast_multipleItems() {
assertEquals(item2, CollectionUtils.getLast(List.of(item1, item2)));
}
+ @Test
+ 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, , NAME1.NAME2.NAME3
+ """)
+ void of_StringList_prefixMayBeNullOrEmpty(int prefixCount, String prefixValue, String suffixList) {
+ assertTrue(isNullOrEmpty(prefixValue), "prefixValue must be null or empty");
+ List suffixNames = toNames(suffixList);
+ var names = new ArrayList(prefixCount + suffixNames.size());
+ for (int i = 0; i < prefixCount; i++) {
+ names.add(prefixValue);
+ }
+ names.addAll(suffixNames);
+
+ assertEquals(ObjectReference.of(suffixNames), ObjectReference.of(names));
+ }
+
+ @Test
+ void of_StringArray_prefixMayNotBeBlank() {
+ assertThrows(IllegalArgumentException.class, () -> ObjectReference.of(" ", "NAME"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void of_StringList_allNullOrEmpty_throwsIllegalArgumentException(boolean useNull) {
+ List names = useNull ? Arrays.asList(null, null, null) : Arrays.asList("", "", "");
+
+ assertThrows(IllegalArgumentException.class, () -> ObjectReference.of(names));
+ }
+
+ @Test
+ void of_StringArr_suffixSingleName_createsIdentifier() {
+ var objectReference = ObjectReference.of("", null, "", "NAME");
+
+ Identifier asIdentifier = assertInstanceOf(Identifier.class, objectReference);
+ assertEquals("NAME", asIdentifier.name());
+ }
+
+ @Test
+ void of_StringArr_suffixMultipleNames_createsIdentifierChain() {
+ var objectReference = ObjectReference.of("", null, "", "NAME1", "NAME2");
+
+ IdentifierChain asIdentifierChain = assertInstanceOf(IdentifierChain.class, objectReference);
+ assertEquals(ObjectReference.of("NAME1", "NAME2"), asIdentifierChain);
+ }
+
+ @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 testOfTable(@Nullable String schema, String object, boolean expectedEmpty) {
+ FieldDescriptor fieldDescriptor = new RowDescriptorBuilder(1, datatypeCoder)
+ .setOriginalSchema(schema)
+ .setOriginalTableName(object)
+ .toFieldDescriptor();
+ Optional optName = ObjectReference.ofTable(fieldDescriptor);
+ assertEquals(expectedEmpty, optName.isEmpty(), "empty");
+ if (!expectedEmpty) {
+ ObjectReference name = optName.get();
+ int tableNameIndex;
+ if (isNullOrEmpty(schema)) {
+ assertEquals(1, name.size(), "size");
+ tableNameIndex = 0;
+ } else {
+ assertEquals(2, name.size(), "size");
+ assertEquals(schema, name.at(0).name(), "schema");
+ tableNameIndex = 1;
+ }
+ assertEquals(object, name.at(tableNameIndex).name(), "object");
+ }
+ }
+
+ private static List toNames(String nameList) {
+ return List.of(nameList.split("\\."));
+ }
+
+}
\ No newline at end of file
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/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
diff --git a/src/test/org/firebirdsql/jaybird/util/StringUtilsTest.java b/src/test/org/firebirdsql/jaybird/util/StringUtilsTest.java
index 5f1ba96c2..4bff4296c 100644
--- a/src/test/org/firebirdsql/jaybird/util/StringUtilsTest.java
+++ b/src/test/org/firebirdsql/jaybird/util/StringUtilsTest.java
@@ -1,11 +1,13 @@
-// 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;
+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.NullSource;
import org.junit.jupiter.params.provider.ValueSource;
@@ -54,16 +56,29 @@ void trimToNull_endsWithSpace_yields_valueWithoutSpace() {
@ParameterizedTest
@NullSource
@EmptySource
- void testIsNullOrEmpty_nullOrEmptyYieldsTrue(String value) {
+ void testIsNullOrEmpty_nullOrEmptyYieldsTrue(@Nullable String value) {
assertTrue(StringUtils.isNullOrEmpty(value));
}
@ParameterizedTest
@ValueSource(strings = { " ", "a", "\0", "abc" })
- void testIsNullOrEmpty_nonEmptyYieldsFalse(String value) {
+ void testIsNullOrEmpty_nonEmptyYieldsFalse(@Nullable String value) {
assertFalse(StringUtils.isNullOrEmpty(value));
}
+ @ParameterizedTest
+ @NullAndEmptySource
+ @ValueSource(strings = " ")
+ void testIsNullOrBlank_nullOrBlankYieldsTrue(@Nullable String value) {
+ assertTrue(StringUtils.isNullOrBlank(value));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "a", "\0", "abc" })
+ void testIsNullOrBlank_nonBlankYieldsFalse(@Nullable String value) {
+ assertFalse(StringUtils.isNullOrBlank(value));
+ }
+
@ParameterizedTest
@CsvSource(useHeadersInDisplayName = true, textBlock = """
input, expectedOutput
@@ -74,7 +89,7 @@ void testIsNullOrEmpty_nonEmptyYieldsFalse(String value) {
' a', a
' a ', a
""")
- void testTrim(String input, String expectedOutput) {
+ void testTrim(@Nullable String input, @Nullable String expectedOutput) {
assertEquals(expectedOutput, StringUtils.trim(input));
}
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());
}
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");
+ }
+}
diff --git a/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataAbstractKeysTest.java b/src/test/org/firebirdsql/jdbc/FBDatabaseMetaDataAbstractKeysTest.java
index a1714d1df..c265878d2 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;
@@ -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,8 +20,11 @@
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;
+import static org.firebirdsql.jaybird.util.StringUtils.trimToNull;
/**
* Base test class for subclasses of {@code org.firebirdsql.jdbc.metadata.AbstractKeysMethod}.
@@ -33,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);
@@ -93,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();
@@ -161,13 +185,26 @@ protected static List