Skip to content

Commit 431ed1d

Browse files
committed
#882 Schema support for FBTableStatisticsManager
1 parent eebc4e9 commit 431ed1d

5 files changed

Lines changed: 182 additions & 49 deletions

File tree

devdoc/jdp/jdp-2025-06-schema-support.adoc

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,16 @@ this fulfills the JDBC requirements that a `CallableStatement` is not sensitive
110110
** 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.
111111
** 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
112112
** Support for packages was missing in the handling of callable statements, and is added, also for older versions
113-
* TODO: Define effects for management API
113+
* Effects for management API
114114
** `StatisticsManager`
115115
*** `getTableStatistics` received an overload to also accept a list of schemas (`sts_schema`)
116116
** `FBTableStatisticsManager`/`TableStatistics`
117-
*** TODO: API and internals need to be redesigned to account for schemas
117+
*** Internally `ObjectReference` is used for the table instead of a String
118+
*** The key of the map returned by `getTableStatistics()` is a qualified table reference (i.e. `{<table-name> | <quoted-schema>.<quoted-table-name>}`.
119+
For schemaless tables, the unquoted table name is used as the key for backwards compatibility when used against Firebird 5.0 and older.
120+
*** `TableStatistics` received two extra accessors:
121+
**** `schema()` with the schema, or empty string if schemaless (or not found)
122+
**** `tableReference()` with the qualified table reference (i.e. `[<quoted-schema>.]<quoted-table-name>` (contrary to the key of getTableStatistics, it's always quoted!))
118123
* TODO: Add information to Jaybird manual
119124

120125
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

src/docs/asciidoc/release_notes.adoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,14 @@ we recommend to always use `null` for `catalog`
565565
*** `getTableStatistics(String[] tableNames)` was changed to accept varargs (`getTableStatistics(String... tableNames)`)
566566
*** Added overload `getTableStatistics(List<String> tableNames)` with same behaviour as `getTableStatistics(String... tableNames)`
567567
*** 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
568+
* `FBTableStatisticsManager` (experimental feature)
569+
** 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>`).
570+
For schemaless tables (Firebird 5.0 and older, or tables that were not found), the key is still the unquoted `<table-name>`.
571+
** 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()`.
572+
The `schema` can be `null` or empty string for schemaless tables (i.e. Firebird 5.0 or older)
573+
** The `TableStatistics` object received additional accessors:
574+
*** `schema()` with the schema, or empty string for schemaless (Firebird 5.0 or older) or if the table was not found
575+
*** `tableReference()` with the fully qualified and quoted table reference (i.e. `[<quoted-schema>.]<quoted-table-name>`)
568576
569577
// TODO add major changes
570578

src/main/org/firebirdsql/management/FBTableStatisticsManager.java

Lines changed: 84 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
1+
// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
22
// SPDX-License-Identifier: LGPL-2.1-or-later
33
package org.firebirdsql.management;
44

@@ -7,8 +7,11 @@
77
import org.firebirdsql.gds.ng.FbExceptionBuilder;
88
import org.firebirdsql.gds.ng.InfoProcessor;
99
import org.firebirdsql.gds.ng.InfoTruncatedException;
10+
import org.firebirdsql.jaybird.util.ObjectReference;
1011
import org.firebirdsql.jdbc.FirebirdConnection;
1112
import org.firebirdsql.util.Volatile;
13+
import org.jspecify.annotations.NullMarked;
14+
import org.jspecify.annotations.Nullable;
1215

1316
import java.sql.Connection;
1417
import java.sql.DatabaseMetaData;
@@ -17,9 +20,11 @@
1720
import java.sql.SQLNonTransientException;
1821
import java.util.HashMap;
1922
import java.util.Map;
20-
import java.util.stream.Collectors;
23+
import java.util.function.Function;
2124

25+
import static java.util.stream.Collectors.toMap;
2226
import static org.firebirdsql.gds.ISCConstants.*;
27+
import static org.firebirdsql.jaybird.util.StringUtils.isNullOrEmpty;
2328

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

4450
private static final int MAX_RETRIES = 3;
4551

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

125+
/**
126+
* Produces a key to the map returned by {@link #getTableStatistics()}.
127+
*
128+
* @param schema
129+
* schema, or {@code null} or empty string for schemaless
130+
* @param tableName
131+
* table name
132+
* @return key: {@code <table-name>} for schemaless, or {@code <quoted-schema>.<quoted-table-name>} for schema-bound
133+
* @since 7
134+
*/
135+
public static String toKey(@Nullable String schema, String tableName) {
136+
return isNullOrEmpty(schema) ? tableName : toKey(ObjectReference.of(schema, tableName));
137+
}
138+
139+
/**
140+
* Produces a key to the map returned by {@link #getTableStatistics()}.
141+
*
142+
* @param tableStatistics
143+
* table statistics object
144+
* @return key
145+
* @see #toKey(String, String)
146+
* @since 7
147+
*/
148+
public static String toKey(TableStatistics tableStatistics) {
149+
return toKey(tableStatistics.table());
150+
}
151+
152+
/**
153+
* Produces a key to the map returned by {@link #getTableStatistics()}.
154+
* <p>
155+
* The behaviour is undefined when called with an {@link ObjectReference} of more than two identifiers.
156+
* </p>
157+
*
158+
* @param objectReference
159+
* table object reference
160+
* @return key
161+
* @see #toKey(String, String)
162+
* @since 7
163+
*/
164+
static String toKey(ObjectReference objectReference) {
165+
if (objectReference.size() == 1) {
166+
return objectReference.first().name();
167+
}
168+
// We assume object reference is size 2, but this will 'work' even if that assumption is wrong
169+
return objectReference.toString();
170+
}
171+
112172
/**
113173
* @return the actual table count (so excluding {@link #tableSlack}).
114174
*/
@@ -132,8 +192,7 @@ private int getTableCount() throws SQLException {
132192
@Override
133193
public void close() {
134194
connection = null;
135-
tableMapping.clear();
136-
tableMapping = null;
195+
tableMapping = Map.of();
137196
}
138197

139198
private void checkClosed() throws SQLException {
@@ -146,11 +205,12 @@ private void checkClosed() throws SQLException {
146205
}
147206

148207
private void updateTableMapping() throws SQLException {
149-
DatabaseMetaData md = connection.getMetaData();
208+
@SuppressWarnings("DataFlowIssue") DatabaseMetaData md = connection.getMetaData();
150209
try (ResultSet rs = md.getTables(
151210
null, null, "%", new String[] { "SYSTEM TABLE", "TABLE", "GLOBAL TEMPORARY" })) {
152211
while (rs.next()) {
153-
tableMapping.put(rs.getInt("JB_RELATION_ID"), rs.getString("TABLE_NAME"));
212+
tableMapping.put(rs.getInt("JB_RELATION_ID"),
213+
ObjectReference.of(rs.getString("TABLE_SCHEM"), rs.getString("TABLE_NAME")));
154214
}
155215
}
156216
}
@@ -186,17 +246,17 @@ private static byte[] getInfoItems() {
186246
* {@link #updateTableMapping()} from this processor.
187247
* </p>
188248
*/
189-
private final class TableStatisticsProcessor implements InfoProcessor<Map<String, TableStatistics>> {
249+
private final class TableStatisticsProcessor implements InfoProcessor<Map<ObjectReference, TableStatistics>> {
190250

191-
private final Map<String, TableStatistics.TableStatisticsBuilder> statisticsBuilders = new HashMap<>();
251+
private final Map<ObjectReference, TableStatistics.TableStatisticsBuilder> statisticsBuilders = new HashMap<>();
192252
private boolean allowTableMappingUpdate = true;
193253

194254
@Override
195-
public Map<String, TableStatistics> process(byte[] infoResponse) throws SQLException {
255+
public Map<ObjectReference, TableStatistics> process(byte[] infoResponse) throws SQLException {
196256
try {
197257
decodeResponse(infoResponse);
198258
return statisticsBuilders.entrySet().stream()
199-
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toTableStatistics()));
259+
.collect(toMap(Map.Entry::getKey, e -> e.getValue().toTableStatistics()));
200260
} finally {
201261
statisticsBuilders.clear();
202262
}
@@ -245,28 +305,27 @@ void processStatistics(int statistic, byte[] buffer, int start, int end) throws
245305
}
246306
}
247307

248-
private String getTableName(Integer tableId) throws SQLException {
249-
String tableName = tableMapping.get(tableId);
250-
if (tableName == null) {
308+
private ObjectReference getTable(Integer tableId) throws SQLException {
309+
ObjectReference table = tableMapping.get(tableId);
310+
if (table == null) {
251311
// mapping empty or out of date (e.g. new table created since the last update)
252312
if (allowTableMappingUpdate) {
253313
updateTableMapping();
254314
// Ensure that if we have multiple tables missing, we don't repeatedly update the table mapping, as
255315
// that wouldn't result in new information.
256316
allowTableMappingUpdate = false;
257-
tableName = tableMapping.get(tableId);
317+
table = tableMapping.get(tableId);
258318
}
259-
if (tableName == null) {
319+
if (table == null) {
260320
// fallback
261-
tableName = "UNKNOWN_TABLE_ID_" + tableId;
321+
table = ObjectReference.of("UNKNOWN_TABLE_ID_" + tableId);
262322
}
263323
}
264-
return tableName;
324+
return table;
265325
}
266326

267327
private TableStatistics.TableStatisticsBuilder getBuilder(int tableId) throws SQLException {
268-
String tableName = getTableName(tableId);
269-
return statisticsBuilders.computeIfAbsent(tableName, TableStatistics::builder);
328+
return statisticsBuilders.computeIfAbsent(getTable(tableId), TableStatistics::builder);
270329
}
271330
}
272331
}

src/main/org/firebirdsql/management/TableStatistics.java

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
// SPDX-FileCopyrightText: Copyright 2022-2023 Mark Rotteveel
1+
// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
22
// SPDX-License-Identifier: LGPL-2.1-or-later
33
package org.firebirdsql.management;
44

5+
import org.firebirdsql.jaybird.util.ObjectReference;
56
import org.firebirdsql.util.Volatile;
7+
import org.jspecify.annotations.NullMarked;
68

7-
import static java.util.Objects.requireNonNull;
89
import static org.firebirdsql.gds.ISCConstants.isc_info_backout_count;
910
import static org.firebirdsql.gds.ISCConstants.isc_info_delete_count;
1011
import static org.firebirdsql.gds.ISCConstants.isc_info_expunge_count;
@@ -22,10 +23,11 @@
2223
* @since 5
2324
*/
2425
@SuppressWarnings("unused")
26+
@NullMarked
2527
@Volatile(reason = "Experimental")
2628
public final class TableStatistics {
2729

28-
private final String tableName;
30+
private final ObjectReference table;
2931
private final long readSeqCount;
3032
private final long readIdxCount;
3133
private final long insertCount;
@@ -35,9 +37,9 @@ public final class TableStatistics {
3537
private final long purgeCount;
3638
private final long expungeCount;
3739

38-
private TableStatistics(String tableName, long readSeqCount, long readIdxCount, long insertCount, long updateCount,
39-
long deleteCount, long backoutCount, long purgeCount, long expungeCount) {
40-
this.tableName = requireNonNull(tableName, "tableName");
40+
private TableStatistics(ObjectReference table, long readSeqCount, long readIdxCount, long insertCount,
41+
long updateCount, long deleteCount, long backoutCount, long purgeCount, long expungeCount) {
42+
this.table = table;
4143
this.readSeqCount = readSeqCount;
4244
this.readIdxCount = readIdxCount;
4345
this.insertCount = insertCount;
@@ -49,10 +51,46 @@ private TableStatistics(String tableName, long readSeqCount, long readIdxCount,
4951
}
5052

5153
/**
52-
* @return table name
54+
* @return table name (or {@code UNKNOWN_TABLE_ID_<table id>} if the table was not found)
55+
* @see #tableReference()
5356
*/
5457
public String tableName() {
55-
return tableName;
58+
return table.last().name();
59+
}
60+
61+
/**
62+
* @return schema of table (or empty string if schemaless or the table was not found)
63+
* @see #tableReference()
64+
* @since 7
65+
*/
66+
public String schema() {
67+
return table.size() == 2 ? table.first().name() : "";
68+
}
69+
70+
/**
71+
* The table reference.
72+
* <p>
73+
* Contrary to the key used for {@link FBTableStatisticsManager#getTableStatistics()}, the table name is always
74+
* quoted, even for schemaless tables. If you want to derive a key for the table from an instance of this class, use
75+
* {@link FBTableStatisticsManager#toKey(TableStatistics)}.
76+
* </p>
77+
*
78+
* @return fully qualified and quoted table reference ({@code [<quoted-schema>.]<quoted-table-name>})
79+
* @see #tableName()
80+
* @see #schema()
81+
* @see FBTableStatisticsManager#toKey(TableStatistics)
82+
* @since 7
83+
*/
84+
public String tableReference() {
85+
return table.toString();
86+
}
87+
88+
/**
89+
* @return object reference of the table
90+
* @since 7
91+
*/
92+
ObjectReference table() {
93+
return table;
5694
}
5795

5896
/**
@@ -115,7 +153,7 @@ public long expungeCount() {
115153
@Override
116154
public String toString() {
117155
return "TableStatistics{" +
118-
"tableName='" + tableName + '\'' +
156+
"table='" + table + '\'' +
119157
", readSeqCount=" + readSeqCount +
120158
", readIdxCount=" + readIdxCount +
121159
", insertCount=" + insertCount +
@@ -127,13 +165,13 @@ public String toString() {
127165
'}';
128166
}
129167

130-
static TableStatisticsBuilder builder(String tableName) {
131-
return new TableStatisticsBuilder(tableName);
168+
static TableStatisticsBuilder builder(ObjectReference table) {
169+
return new TableStatisticsBuilder(table);
132170
}
133171

134172
static final class TableStatisticsBuilder {
135173

136-
private final String tableName;
174+
private final ObjectReference table;
137175
private long readSeqCount;
138176
private long readIdxCount;
139177
private long insertCount;
@@ -143,8 +181,9 @@ static final class TableStatisticsBuilder {
143181
private long purgeCount;
144182
private long expungeCount;
145183

146-
private TableStatisticsBuilder(String tableName) {
147-
this.tableName = tableName;
184+
private TableStatisticsBuilder(ObjectReference table) {
185+
assert table.size() <= 2 : "table should be an object reference of at most two identifiers";
186+
this.table = table;
148187
}
149188

150189
void addStatistic(int statistic, long value) {
@@ -164,7 +203,7 @@ void addStatistic(int statistic, long value) {
164203
}
165204

166205
TableStatistics toTableStatistics() {
167-
return new TableStatistics(tableName, readSeqCount, readIdxCount, insertCount, updateCount, deleteCount,
206+
return new TableStatistics(table, readSeqCount, readIdxCount, insertCount, updateCount, deleteCount,
168207
backoutCount, purgeCount, expungeCount);
169208
}
170209

0 commit comments

Comments
 (0)