Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
9591d7d
#882 jdp-2025-06: Schema Support
mrotteveel Jun 18, 2025
9865b6b
#882 Fix tests failing due to Firebird 6 schemas
mrotteveel Jun 18, 2025
a3c5979
#882 Implement to DatabaseMetaData.getSchemas return schemas
mrotteveel Jun 19, 2025
7deb6ed
#882 Implement DatabaseMetaData informational methods to report schem…
mrotteveel Jun 19, 2025
740ce4d
Use isNullOrEmpty
mrotteveel Jun 19, 2025
286eea0
#882 Schema support for DatabaseMetaData.getProcedures
mrotteveel Jun 19, 2025
44cccc2
#882 Use SYSTEM unquoted due to dialect 1 support
mrotteveel Jun 19, 2025
d34af61
#882 Schema support for getBestRowIdentifier
mrotteveel Jun 21, 2025
05edd2d
#882 Schema support for getColumnPrivileges
mrotteveel Jun 21, 2025
7699234
#882 Schema support for getColumns
mrotteveel Jun 21, 2025
1d30a6d
#882 Schema support for getImportedKeys/getExportedKeys/getCrossRefer…
mrotteveel Jun 21, 2025
69effeb
#882 Schema support for getFunctionColumns
mrotteveel Jun 22, 2025
a5d8434
#882 Schema support for getFunctions
mrotteveel Jun 22, 2025
c08df8b
#882 Schema support for getIndexInfo
mrotteveel Jun 22, 2025
a1655eb
#882 Schema support for getPrimaryKeys
mrotteveel Jun 22, 2025
6b55727
#882 Schema support for getProcedureColumns
mrotteveel Jun 22, 2025
584e2cc
#882 Schema support for getPseudoColumns
mrotteveel Jun 22, 2025
6bdb4bc
#882 Schema support for getTablePrivileges
mrotteveel Jun 23, 2025
73ebef5
#882 Schema support for getTables
mrotteveel Jun 23, 2025
90ce855
#882 Schema support for getCatalogs
mrotteveel Jun 23, 2025
11349a7
Mark metadata classes sealed or final
mrotteveel Jun 23, 2025
ea0a273
#882 Schema support for getXXXSourceCode
mrotteveel Jun 23, 2025
b60c7d6
Add ifSchemaElse also to FirebirdSupportInfo
mrotteveel Jun 23, 2025
a154551
#882 Client props, xid detection
mrotteveel Jun 24, 2025
18a5228
#882 Retrieve schema name in column information
mrotteveel Jun 24, 2025
bed0828
#882 Incomplete change to selectable procedure detection
mrotteveel Jun 24, 2025
ce3abcc
#882 Misc wording of jdp-2025-06
mrotteveel Jun 24, 2025
d365d92
#882 Define connection property searchPath
mrotteveel Jun 24, 2025
276d8bc
Add supportInfoFor(FirebirdConnection) to avoid wrapper check
mrotteveel Jun 25, 2025
501eb78
Tighten up StoredProcedureMetaData, use support info
mrotteveel Jun 25, 2025
ca102d2
#882 Implement Connection.get/setSchema
mrotteveel Jul 9, 2025
6a7b308
#882 Additional schema tests for FBResultSetMetaData
mrotteveel Jul 12, 2025
aa3d585
#882 Modify FBRowUpdater to search for schema
mrotteveel Jul 13, 2025
1f839f0
Deduplicate strings in StatementInfoProcessor
mrotteveel Jul 13, 2025
fbff0a6
Add some todo for later
mrotteveel Jul 14, 2025
10ed654
#882 Improve schema test coverage
mrotteveel Jul 17, 2025
3662432
#882 Improve schema test coverage (getFunctions)
mrotteveel Jul 17, 2025
b83f275
Replace QualifiedName with ObjectReference
mrotteveel Jul 18, 2025
2cb017e
Add schema support assumption to test
mrotteveel Jul 18, 2025
b8769ad
#882 Improve schema test coverage (getProcedures/getProcedureColumns)
mrotteveel Jul 22, 2025
099a4b2
#882 Improve schema test coverage (getIndexInfo/getPrimaryKeys)
mrotteveel Jul 22, 2025
6a9febc
#882 Improve schema test coverage (getPseudoColumns/getTables)
mrotteveel Jul 22, 2025
48f4b7c
#882 Improve schema test coverage (getTablePrivileges)
mrotteveel Jul 22, 2025
24478cf
#882 schema support for generated keys
mrotteveel Jul 29, 2025
d251d84
#882 Add schema support for FBCallableStatement
mrotteveel Oct 9, 2025
82aa541
Fix test failure on Firebird 5 and older
mrotteveel Oct 9, 2025
a7542c0
#882 Document decision not to backport schema support to Jaybird 6
mrotteveel Oct 11, 2025
04523d7
Misc. copy editing and fixing typos
mrotteveel Oct 11, 2025
381b792
#882 Add JB_PROCEDURE_SOURCE column to getProcedures
mrotteveel Oct 13, 2025
102a856
#882 Reject idea to add column with schemas to getCatalogs
mrotteveel Oct 13, 2025
567cdea
Remove unnecessary caching from BasicVersion
mrotteveel Oct 17, 2025
e2e4e87
#882 Schema support for StatisticsManager.getTableStatistics
mrotteveel Oct 17, 2025
2353b24
#882 Schema support for FBTableStatisticsManager
mrotteveel Oct 18, 2025
c2320ed
Fixed wrong name
mrotteveel Oct 19, 2025
180999c
#882 Added setters to FirebirdConnection to set search path
mrotteveel Oct 19, 2025
d934271
Document compatibility issue with schema(Pattern) in dbmd
mrotteveel Oct 25, 2025
a35a3a0
Correct documentation of getXXXSourceCode methods
mrotteveel Oct 30, 2025
48514d5
Misc review changes
mrotteveel Oct 30, 2025
13e38c3
Use Collection instead of List, use common code path
mrotteveel Nov 3, 2025
4a83dec
Simplify version string creation in GDSServerVersion
mrotteveel Nov 3, 2025
ec1056e
SPECIFIC_NAME should not include the schema
mrotteveel Nov 3, 2025
0c9efec
Update error codes and messages from 6.0.0.1338
mrotteveel Nov 4, 2025
e37afa1
Ignore drop schema failure, refactor to single code path
mrotteveel Nov 4, 2025
588a017
Finalize jdp-2025-06
mrotteveel Nov 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
#882 Schema support for FBTableStatisticsManager
  • Loading branch information
mrotteveel committed Oct 25, 2025
commit 2353b24dfa9f54512192e6e4a6f86a9fcfaf9c8e
9 changes: 7 additions & 2 deletions devdoc/jdp/jdp-2025-06-schema-support.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,16 @@ this fulfills the JDBC requirements that a `CallableStatement` is not sensitive
** The API of `StoredProcedureMetaData` (internal API) is changed to not report selectability, but to update the `FBProcedureCall` instance with selectability and other information, like identified schema and/or package.
** For qualified *and* unambiguous procedure reference, the selectability is cached *per connection*, for unqualified or ambiguous procedure reference, the lookup is performed on each `Connection.prepareCall`, to account for search path changes
** Support for packages was missing in the handling of callable statements, and is added, also for older versions
* TODO: Define effects for management API
* Effects for management API
** `StatisticsManager`
*** `getTableStatistics` received an overload to also accept a list of schemas (`sts_schema`)
** `FBTableStatisticsManager`/`TableStatistics`
*** TODO: API and internals need to be redesigned to account for schemas
*** Internally `ObjectReference` is used for the table instead of a String
*** The key of the map returned by `getTableStatistics()` is a qualified table reference (i.e. `{<table-name> | <quoted-schema>.<quoted-table-name>}`.
For schemaless tables, the unquoted table name is used as the key for backwards compatibility when used against Firebird 5.0 and older.
*** `TableStatistics` received two extra accessors:
**** `schema()` with the schema, or empty string if schemaless (or not found)
**** `tableReference()` with the qualified table reference (i.e. `[<quoted-schema>.]<quoted-table-name>` (contrary to the key of getTableStatistics, it's always quoted!))
* 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
Expand Down
8 changes: 8 additions & 0 deletions src/docs/asciidoc/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,14 @@ we recommend to always use `null` for `catalog`
*** `getTableStatistics(String[] tableNames)` was changed to accept varargs (`getTableStatistics(String... tableNames)`)
*** Added overload `getTableStatistics(List<String> tableNames)` with same behaviour as `getTableStatistics(String... tableNames)`
*** Added overload `getTableStatistics(List<String> schemas, List<String> tableNames)` -- if `schemas` is non-empty, on Firebird 6.0 and higher, it will restrict the search for tables to the specified schemas
* `FBTableStatisticsManager` (experimental feature)
** For schema-bound tables, the key of the map returned by `getTableStatistics()` is a fully qualified and quoted table reference (i.e. `<quoted-schema>.<quoted-table-name>`).
For schemaless tables (Firebird 5.0 and older, or tables that were not found), the key is still the unquoted `<table-name>`.
** The static method `toTableReference(String schema, String tableName)` can be used to create a table reference in the same format as the key of the map returned by `getTableStatistics()`.
The `schema` can be `null` or empty string for schemaless tables (i.e. Firebird 5.0 or older)
** The `TableStatistics` object received additional accessors:
*** `schema()` with the schema, or empty string for schemaless (Firebird 5.0 or older) or if the table was not found
*** `tableReference()` with the fully qualified and quoted table reference (i.e. `[<quoted-schema>.]<quoted-table-name>`)

// TODO add major changes

Expand Down
109 changes: 84 additions & 25 deletions src/main/org/firebirdsql/management/FBTableStatisticsManager.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.management;

Expand All @@ -7,8 +7,11 @@
import org.firebirdsql.gds.ng.FbExceptionBuilder;
import org.firebirdsql.gds.ng.InfoProcessor;
import org.firebirdsql.gds.ng.InfoTruncatedException;
import org.firebirdsql.jaybird.util.ObjectReference;
import org.firebirdsql.jdbc.FirebirdConnection;
import org.firebirdsql.util.Volatile;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
Expand All @@ -17,9 +20,11 @@
import java.sql.SQLNonTransientException;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.function.Function;

import static java.util.stream.Collectors.toMap;
import static org.firebirdsql.gds.ISCConstants.*;
import static org.firebirdsql.jaybird.util.StringUtils.isNullOrEmpty;

/**
* Provides access to the table statistics of a {@link java.sql.Connection}.
Expand All @@ -39,12 +44,13 @@
* @since 5
*/
@Volatile(reason = "Experimental")
@NullMarked
public final class FBTableStatisticsManager implements AutoCloseable {

private static final int MAX_RETRIES = 3;

private Map<Integer, String> tableMapping = new HashMap<>();
private FirebirdConnection connection;
private Map<Integer, ObjectReference> tableMapping = new HashMap<>();
private @Nullable FirebirdConnection connection;
/**
* Table slack is a number which is used to pad the table count used for calculating the buffer size in an attempt
* to prevent or fix truncation of the info request. It is incremented when a truncation is handled.
Expand Down Expand Up @@ -77,24 +83,31 @@ public static FBTableStatisticsManager of(Connection connection) throws SQLExcep
* <p>
* A table is only present in the map if this connection touched it in a way which generated a statistic.
* </p>
* <p>
* The method {@link #toKey(String, String)} can be used to produce a key as used for entries in the map.
* </p>
*
* @return map from table name to table statistics
* @return map from table reference ({@code <table-name>} for schemaless, or
* {@code <quoted-schema>.<quoted-table-name>} for schema-bound) to table statistics
* @throws InfoTruncatedException
* if a truncated response is received, after retrying 3 times (total: 4 attempts) while increasing
* the buffer size; it is possible that subsequent calls to this method may recover (as that will increase
* the buffer size even more)
* @throws SQLException
* if the connection is closed, or if obtaining the statistics failed due to a database access error
* @see #toKey(String, String)
*/
public Map<String, TableStatistics> getTableStatistics() throws SQLException {
checkClosed();
FbDatabase db = connection.getFbDatabase();
@SuppressWarnings("DataFlowIssue") FbDatabase db = connection.getFbDatabase();
InfoTruncatedException lastTruncation;
TableStatisticsProcessor tableStatisticsProcessor = new TableStatisticsProcessor();
var tableStatisticsProcessor = new TableStatisticsProcessor();
int attempt = 0;
do {
try {
return db.getDatabaseInfo(getInfoItems(), bufferSize(getTableCount()), tableStatisticsProcessor);
return db.getDatabaseInfo(getInfoItems(), bufferSize(getTableCount()), tableStatisticsProcessor)
.values().stream()
.collect(toMap(FBTableStatisticsManager::toKey, Function.identity()));
} catch (InfoTruncatedException e) {
/* Occurrence of truncation should be rare. It could occur if all tables have all statistics items, and
new tables are added after the last updateMapping() call or statistics were previously requested by
Expand All @@ -109,6 +122,53 @@ Here, tableSlack is incremented to account for tables removed, while updateTable
throw lastTruncation;
}

/**
* Produces a key to the map returned by {@link #getTableStatistics()}.
*
* @param schema
* schema, or {@code null} or empty string for schemaless
* @param tableName
* table name
* @return key: {@code <table-name>} for schemaless, or {@code <quoted-schema>.<quoted-table-name>} for schema-bound
* @since 7
*/
public static String toKey(@Nullable String schema, String tableName) {
return isNullOrEmpty(schema) ? tableName : toKey(ObjectReference.of(schema, tableName));
}

/**
* Produces a key to the map returned by {@link #getTableStatistics()}.
*
* @param tableStatistics
* table statistics object
* @return key
* @see #toKey(String, String)
* @since 7
*/
public static String toKey(TableStatistics tableStatistics) {
return toKey(tableStatistics.table());
}

/**
* Produces a key to the map returned by {@link #getTableStatistics()}.
* <p>
* The behaviour is undefined when called with an {@link ObjectReference} of more than two identifiers.
* </p>
*
* @param objectReference
* table object reference
* @return key
* @see #toKey(String, String)
* @since 7
*/
static String toKey(ObjectReference objectReference) {
if (objectReference.size() == 1) {
return objectReference.first().name();
}
// We assume object reference is size 2, but this will 'work' even if that assumption is wrong
return objectReference.toString();
}

/**
* @return the actual table count (so excluding {@link #tableSlack}).
*/
Expand All @@ -132,8 +192,7 @@ private int getTableCount() throws SQLException {
@Override
public void close() {
connection = null;
tableMapping.clear();
tableMapping = null;
tableMapping = Map.of();
}

private void checkClosed() throws SQLException {
Expand All @@ -146,11 +205,12 @@ private void checkClosed() throws SQLException {
}

private void updateTableMapping() throws SQLException {
DatabaseMetaData md = connection.getMetaData();
@SuppressWarnings("DataFlowIssue") DatabaseMetaData md = connection.getMetaData();
try (ResultSet rs = md.getTables(
null, null, "%", new String[] { "SYSTEM TABLE", "TABLE", "GLOBAL TEMPORARY" })) {
while (rs.next()) {
tableMapping.put(rs.getInt("JB_RELATION_ID"), rs.getString("TABLE_NAME"));
tableMapping.put(rs.getInt("JB_RELATION_ID"),
ObjectReference.of(rs.getString("TABLE_SCHEM"), rs.getString("TABLE_NAME")));
}
}
}
Expand Down Expand Up @@ -186,17 +246,17 @@ private static byte[] getInfoItems() {
* {@link #updateTableMapping()} from this processor.
* </p>
*/
private final class TableStatisticsProcessor implements InfoProcessor<Map<String, TableStatistics>> {
private final class TableStatisticsProcessor implements InfoProcessor<Map<ObjectReference, TableStatistics>> {

private final Map<String, TableStatistics.TableStatisticsBuilder> statisticsBuilders = new HashMap<>();
private final Map<ObjectReference, TableStatistics.TableStatisticsBuilder> statisticsBuilders = new HashMap<>();
private boolean allowTableMappingUpdate = true;

@Override
public Map<String, TableStatistics> process(byte[] infoResponse) throws SQLException {
public Map<ObjectReference, TableStatistics> process(byte[] infoResponse) throws SQLException {
try {
decodeResponse(infoResponse);
return statisticsBuilders.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toTableStatistics()));
.collect(toMap(Map.Entry::getKey, e -> e.getValue().toTableStatistics()));
} finally {
statisticsBuilders.clear();
}
Expand Down Expand Up @@ -245,28 +305,27 @@ void processStatistics(int statistic, byte[] buffer, int start, int end) throws
}
}

private String getTableName(Integer tableId) throws SQLException {
String tableName = tableMapping.get(tableId);
if (tableName == null) {
private ObjectReference getTable(Integer tableId) throws SQLException {
ObjectReference table = tableMapping.get(tableId);
if (table == null) {
// mapping empty or out of date (e.g. new table created since the last update)
if (allowTableMappingUpdate) {
updateTableMapping();
// Ensure that if we have multiple tables missing, we don't repeatedly update the table mapping, as
// that wouldn't result in new information.
allowTableMappingUpdate = false;
tableName = tableMapping.get(tableId);
table = tableMapping.get(tableId);
}
if (tableName == null) {
if (table == null) {
// fallback
tableName = "UNKNOWN_TABLE_ID_" + tableId;
table = ObjectReference.of("UNKNOWN_TABLE_ID_" + tableId);
}
}
return tableName;
return table;
}

private TableStatistics.TableStatisticsBuilder getBuilder(int tableId) throws SQLException {
String tableName = getTableName(tableId);
return statisticsBuilders.computeIfAbsent(tableName, TableStatistics::builder);
return statisticsBuilders.computeIfAbsent(getTable(tableId), TableStatistics::builder);
}
}
}
69 changes: 54 additions & 15 deletions src/main/org/firebirdsql/management/TableStatistics.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2022-2023 Mark Rotteveel
// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
// SPDX-License-Identifier: LGPL-2.1-or-later
package org.firebirdsql.management;

import org.firebirdsql.jaybird.util.ObjectReference;
import org.firebirdsql.util.Volatile;
import org.jspecify.annotations.NullMarked;

import static java.util.Objects.requireNonNull;
import static org.firebirdsql.gds.ISCConstants.isc_info_backout_count;
import static org.firebirdsql.gds.ISCConstants.isc_info_delete_count;
import static org.firebirdsql.gds.ISCConstants.isc_info_expunge_count;
Expand All @@ -22,10 +23,11 @@
* @since 5
*/
@SuppressWarnings("unused")
@NullMarked
@Volatile(reason = "Experimental")
public final class TableStatistics {

private final String tableName;
private final ObjectReference table;
private final long readSeqCount;
private final long readIdxCount;
private final long insertCount;
Expand All @@ -35,9 +37,9 @@ public final class TableStatistics {
private final long purgeCount;
private final long expungeCount;

private TableStatistics(String tableName, long readSeqCount, long readIdxCount, long insertCount, long updateCount,
long deleteCount, long backoutCount, long purgeCount, long expungeCount) {
this.tableName = requireNonNull(tableName, "tableName");
private TableStatistics(ObjectReference table, long readSeqCount, long readIdxCount, long insertCount,
long updateCount, long deleteCount, long backoutCount, long purgeCount, long expungeCount) {
this.table = table;
this.readSeqCount = readSeqCount;
this.readIdxCount = readIdxCount;
this.insertCount = insertCount;
Expand All @@ -49,10 +51,46 @@ private TableStatistics(String tableName, long readSeqCount, long readIdxCount,
}

/**
* @return table name
* @return table name (or {@code UNKNOWN_TABLE_ID_<table id>} if the table was not found)
* @see #tableReference()
*/
public String tableName() {
return tableName;
return table.last().name();
}

/**
* @return schema of table (or empty string if schemaless or the table was not found)
* @see #tableReference()
* @since 7
*/
public String schema() {
return table.size() == 2 ? table.first().name() : "";
}

/**
* The table reference.
* <p>
* Contrary to the key used for {@link FBTableStatisticsManager#getTableStatistics()}, the table name is always
* quoted, even for schemaless tables. If you want to derive a key for the table from an instance of this class, use
* {@link FBTableStatisticsManager#toKey(TableStatistics)}.
* </p>
*
* @return fully qualified and quoted table reference ({@code [<quoted-schema>.]<quoted-table-name>})
* @see #tableName()
* @see #schema()
* @see FBTableStatisticsManager#toKey(TableStatistics)
* @since 7
*/
public String tableReference() {
return table.toString();
}

/**
* @return object reference of the table
* @since 7
*/
ObjectReference table() {
return table;
}

/**
Expand Down Expand Up @@ -115,7 +153,7 @@ public long expungeCount() {
@Override
public String toString() {
return "TableStatistics{" +
"tableName='" + tableName + '\'' +
"table='" + table + '\'' +
", readSeqCount=" + readSeqCount +
", readIdxCount=" + readIdxCount +
", insertCount=" + insertCount +
Expand All @@ -127,13 +165,13 @@ public String toString() {
'}';
}

static TableStatisticsBuilder builder(String tableName) {
return new TableStatisticsBuilder(tableName);
static TableStatisticsBuilder builder(ObjectReference table) {
return new TableStatisticsBuilder(table);
}

static final class TableStatisticsBuilder {

private final String tableName;
private final ObjectReference table;
private long readSeqCount;
private long readIdxCount;
private long insertCount;
Expand All @@ -143,8 +181,9 @@ static final class TableStatisticsBuilder {
private long purgeCount;
private long expungeCount;

private TableStatisticsBuilder(String tableName) {
this.tableName = tableName;
private TableStatisticsBuilder(ObjectReference table) {
assert table.size() <= 2 : "table should be an object reference of at most two identifiers";
this.table = table;
}

void addStatistic(int statistic, long value) {
Expand All @@ -164,7 +203,7 @@ void addStatistic(int statistic, long value) {
}

TableStatistics toTableStatistics() {
return new TableStatistics(tableName, readSeqCount, readIdxCount, insertCount, updateCount, deleteCount,
return new TableStatistics(table, readSeqCount, readIdxCount, insertCount, updateCount, deleteCount,
backoutCount, purgeCount, expungeCount);
}

Expand Down
Loading