Skip to content

Commit 63a073b

Browse files
committed
#882 Schema support for getBestRowIdentifier
1 parent 0f80c35 commit 63a073b

File tree

6 files changed

+396
-79
lines changed

6 files changed

+396
-79
lines changed

src/main/org/firebirdsql/jdbc/FBDatabaseMetaData.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,14 +1372,19 @@ public ResultSet getTablePrivileges(String catalog, String schemaPattern, String
13721372
/**
13731373
* {@inheritDoc}
13741374
* <p>
1375-
* Jaybird considers the primary key (scoped as {@code bestRowSession} as the best identifier for all scopes.
1376-
* Pseudo column {@code RDB$DB_KEY} (scoped as {@code bestRowTransaction} is considered the second-best alternative
1375+
* Jaybird considers the primary key (scoped as {@code bestRowSession}) as the best identifier for all scopes.
1376+
* Pseudo column {@code RDB$DB_KEY} (scoped as {@code bestRowTransaction}) is considered the second-best alternative
13771377
* for scopes {@code bestRowTemporary} and {@code bestRowTransaction} if {@code table} has no primary key.
13781378
* </p>
13791379
* <p>
13801380
* Jaybird currently considers {@code RDB$DB_KEY} to be {@link DatabaseMetaData#bestRowTransaction} even if the
13811381
* dbkey_scope is set to 1 (session). This may change in the future. See also {@link #getRowIdLifetime()}.
13821382
* </p>
1383+
* <p>
1384+
* On Firebird 6.0 and higher, passing {@code null} for {@code schema} will return columns of <strong>all</strong>
1385+
* tables with name {@code name}. It is recommended to <em>always</em> specify a non-{@code null} {@code schema} on
1386+
* Firebird 6.0 and higher.
1387+
* </p>
13831388
*/
13841389
@Override
13851390
public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable)

src/main/org/firebirdsql/jdbc/FBRowUpdater.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-FileCopyrightText: Copyright 2005-2011 Roman Rokytskyy
2-
// SPDX-FileCopyrightText: Copyright 2012-2024 Mark Rotteveel
2+
// SPDX-FileCopyrightText: Copyright 2012-2025 Mark Rotteveel
33
// SPDX-License-Identifier: LGPL-2.1-or-later
44
package org.firebirdsql.jdbc;
55

@@ -250,8 +250,9 @@ private static List<FieldDescriptor> deriveKeyColumns(String tableName, RowDescr
250250
*/
251251
private static List<FieldDescriptor> keyColumnsOfBestRowIdentifier(String tableName, RowDescriptor rowDescriptor,
252252
DatabaseMetaData dbmd) throws SQLException {
253+
// TODO Add schema support
253254
try (ResultSet bestRowIdentifier = dbmd
254-
.getBestRowIdentifier("", "", tableName, DatabaseMetaData.bestRowTransaction, true)) {
255+
.getBestRowIdentifier("", null, tableName, DatabaseMetaData.bestRowTransaction, true)) {
255256
int bestRowIdentifierColumnCount = 0;
256257
List<FieldDescriptor> keyColumns = new ArrayList<>();
257258
while (bestRowIdentifier.next()) {

src/main/org/firebirdsql/jdbc/metadata/GetBestRowIdentifier.java

Lines changed: 129 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
// SPDX-FileCopyrightText: Copyright 2001-2024 Firebird development team and individual contributors
2-
// SPDX-FileCopyrightText: Copyright 2022-2024 Mark Rotteveel
1+
// SPDX-FileCopyrightText: Copyright 2001-2025 Firebird development team and individual contributors
2+
// SPDX-FileCopyrightText: Copyright 2022-2025 Mark Rotteveel
33
// SPDX-License-Identifier: LGPL-2.1-or-later
44
package org.firebirdsql.jdbc.metadata;
55

@@ -41,7 +41,7 @@
4141
* @author Mark Rotteveel
4242
* @since 5
4343
*/
44-
public final class GetBestRowIdentifier extends AbstractMetadataMethod {
44+
public abstract class GetBestRowIdentifier extends AbstractMetadataMethod {
4545

4646
private static final String ROWIDENTIFIER = "ROWIDENTIFIER";
4747

@@ -56,31 +56,6 @@ public final class GetBestRowIdentifier extends AbstractMetadataMethod {
5656
.at(7).simple(SQL_SHORT, 0, "PSEUDO_COLUMN", ROWIDENTIFIER).addField()
5757
.toRowDescriptor();
5858

59-
//@formatter:off
60-
private static final String GET_BEST_ROW_IDENT_START =
61-
"select\n"
62-
+ " RF.RDB$FIELD_NAME as COLUMN_NAME,\n"
63-
+ " F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n"
64-
+ " F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n"
65-
+ " F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n"
66-
+ " F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n"
67-
+ " F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n"
68-
+ " F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n"
69-
+ " F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + "\n"
70-
+ "from RDB$RELATION_CONSTRAINTS RC\n"
71-
+ "inner join RDB$INDEX_SEGMENTS IDX\n"
72-
+ " on IDX.RDB$INDEX_NAME = RC.RDB$INDEX_NAME\n"
73-
+ "inner join RDB$RELATION_FIELDS RF\n"
74-
+ " on RF.RDB$FIELD_NAME = IDX.RDB$FIELD_NAME and RF.RDB$RELATION_NAME = RC.RDB$RELATION_NAME\n"
75-
+ "inner join RDB$FIELDS F\n"
76-
+ " on F.RDB$FIELD_NAME = RF.RDB$FIELD_SOURCE\n"
77-
+ "where RC.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY'\n"
78-
+ "and ";
79-
80-
private static final String GET_BEST_ROW_IDENT_END =
81-
"\norder by IDX.RDB$FIELD_POSITION";
82-
//@formatter:on
83-
8459
private GetBestRowIdentifier(DbMetadataMediator mediator) {
8560
super(ROW_DESCRIPTOR, mediator);
8661
}
@@ -93,7 +68,7 @@ public ResultSet getBestRowIdentifier(String catalog, String schema, String tabl
9368
}
9469

9570
RowValueBuilder valueBuilder = new RowValueBuilder(ROW_DESCRIPTOR);
96-
List<RowValue> rows = getPrimaryKeyIdentifier(table, valueBuilder);
71+
List<RowValue> rows = getPrimaryKeyIdentifier(schema, table, valueBuilder);
9772

9873
// if no primary key exists, add RDB$DB_KEY as pseudo-column
9974
if (rows.isEmpty()) {
@@ -105,8 +80,8 @@ public ResultSet getBestRowIdentifier(String catalog, String schema, String tabl
10580
return createEmpty();
10681
}
10782

108-
try (ResultSet pseudoColumns =
109-
dbmd.getPseudoColumns(catalog, schema, escapeWildcards(table), "RDB$DB\\_KEY")) {
83+
try (ResultSet pseudoColumns = dbmd.getPseudoColumns(
84+
catalog, escapeWildcards(schema), escapeWildcards(table), "RDB$DB\\_KEY")) {
11085
if (!pseudoColumns.next()) {
11186
return createEmpty();
11287
}
@@ -118,7 +93,7 @@ public ResultSet getBestRowIdentifier(String catalog, String schema, String tabl
11893
.at(1).setString("RDB$DB_KEY")
11994
.at(2).setInt(Types.ROWID)
12095
.at(3).setString(getDataTypeName(char_type, 0, CS_BINARY))
121-
.at(4).setInt(pseudoColumns.getInt(8))
96+
.at(4).setInt(pseudoColumns.getInt(6))
12297
.at(5).set(null)
12398
.at(6).set(null)
12499
.at(7).setShort(DatabaseMetaData.bestRowPseudo)
@@ -132,22 +107,20 @@ public ResultSet getBestRowIdentifier(String catalog, String schema, String tabl
132107
/**
133108
* Get primary key of the table as best row identifier.
134109
*
110+
* @param schema
111+
* name of the schema
135112
* @param table
136-
* name of the table.
113+
* name of the table
137114
* @param valueBuilder
138115
* builder for row values
139116
* @return list of result set values, when empty, no primary key has been defined for a table or the table does not
140117
* exist. The returned list can be modified by caller if needed.
141118
* @throws SQLException
142119
* if something went wrong.
143120
*/
144-
private List<RowValue> getPrimaryKeyIdentifier(String table, RowValueBuilder valueBuilder) throws SQLException {
145-
Clause tableClause = Clause.equalsClause("RC.RDB$RELATION_NAME", table);
146-
String sql = GET_BEST_ROW_IDENT_START
147-
+ tableClause.getCondition(false)
148-
+ GET_BEST_ROW_IDENT_END;
149-
150-
MetadataQuery metadataQuery = new MetadataQuery(sql, Clause.parameters(tableClause));
121+
private List<RowValue> getPrimaryKeyIdentifier(String schema, String table, RowValueBuilder valueBuilder)
122+
throws SQLException {
123+
MetadataQuery metadataQuery = createGetPrimaryKeyIdentifierQuery(schema, table);
151124
try (ResultSet rs = mediator.performMetaDataQuery(metadataQuery)) {
152125
List<RowValue> rows = new ArrayList<>();
153126
while (rs.next()) {
@@ -157,6 +130,8 @@ private List<RowValue> getPrimaryKeyIdentifier(String table, RowValueBuilder val
157130
}
158131
}
159132

133+
abstract MetadataQuery createGetPrimaryKeyIdentifierQuery(String schema, String table);
134+
160135
@Override
161136
RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQLException {
162137
TypeMetadata typeMetadata = TypeMetadata.builder(mediator.getFirebirdSupportInfo())
@@ -176,6 +151,119 @@ RowValue createMetadataRow(ResultSet rs, RowValueBuilder valueBuilder) throws SQ
176151
}
177152

178153
public static GetBestRowIdentifier create(DbMetadataMediator mediator) {
179-
return new GetBestRowIdentifier(mediator);
154+
// NOTE: Indirection through static method prevents unnecessary classloading
155+
if (mediator.getFirebirdSupportInfo().isVersionEqualOrAbove(6)) {
156+
return FB6.createInstance(mediator);
157+
} else {
158+
return FB5.createInstance(mediator);
159+
}
180160
}
161+
162+
/**
163+
* Implementation for Firebird 5.0 and older.
164+
*/
165+
private static final class FB5 extends GetBestRowIdentifier {
166+
167+
//@formatter:off
168+
private static final String GET_BEST_ROW_IDENT_START = """
169+
select
170+
RF.RDB$FIELD_NAME as COLUMN_NAME,
171+
""" +
172+
" F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n" +
173+
" F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n" +
174+
" F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n" +
175+
" F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n" +
176+
" F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n" +
177+
" F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n" +
178+
" F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + "\n" + """
179+
from RDB$RELATION_CONSTRAINTS RC
180+
inner join RDB$INDEX_SEGMENTS IDX
181+
on IDX.RDB$INDEX_NAME = RC.RDB$INDEX_NAME
182+
inner join RDB$RELATION_FIELDS RF
183+
on RF.RDB$FIELD_NAME = IDX.RDB$FIELD_NAME and RF.RDB$RELATION_NAME = RC.RDB$RELATION_NAME
184+
inner join RDB$FIELDS F
185+
on F.RDB$FIELD_NAME = RF.RDB$FIELD_SOURCE
186+
where RC.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY'
187+
and\s""";
188+
//@formatter:on
189+
190+
private static final String GET_BEST_ROW_IDENT_END = "\norder by IDX.RDB$FIELD_POSITION";
191+
192+
193+
private FB5(DbMetadataMediator mediator) {
194+
super(mediator);
195+
}
196+
197+
private static GetBestRowIdentifier createInstance(DbMetadataMediator mediator) {
198+
return new FB5(mediator);
199+
}
200+
201+
@Override
202+
MetadataQuery createGetPrimaryKeyIdentifierQuery(String schema, String table) {
203+
Clause tableClause = Clause.equalsClause("RC.RDB$RELATION_NAME", table);
204+
String sql = GET_BEST_ROW_IDENT_START
205+
+ tableClause.getCondition(false)
206+
+ GET_BEST_ROW_IDENT_END;
207+
208+
return new MetadataQuery(sql, Clause.parameters(tableClause));
209+
}
210+
}
211+
212+
/**
213+
* Implementation for Firebird 6.0 and higher.
214+
*/
215+
private static final class FB6 extends GetBestRowIdentifier {
216+
217+
//@formatter:off
218+
private static final String GET_BEST_ROW_IDENT_START_6 = """
219+
select
220+
RF.RDB$FIELD_NAME as COLUMN_NAME,
221+
""" +
222+
" F.RDB$FIELD_TYPE as " + FIELD_TYPE + ",\n" +
223+
" F.RDB$FIELD_SUB_TYPE as " + FIELD_SUB_TYPE + ",\n" +
224+
" F.RDB$FIELD_PRECISION as " + FIELD_PRECISION + ",\n" +
225+
" F.RDB$FIELD_SCALE as " + FIELD_SCALE + ",\n" +
226+
" F.RDB$FIELD_LENGTH as " + FIELD_LENGTH + ",\n" +
227+
" F.RDB$CHARACTER_LENGTH as " + CHAR_LEN + ",\n" +
228+
" F.RDB$CHARACTER_SET_ID as " + CHARSET_ID + "\n" + """
229+
from SYSTEM.RDB$RELATION_CONSTRAINTS RC
230+
inner join SYSTEM.RDB$INDEX_SEGMENTS IDX
231+
on IDX.RDB$SCHEMA_NAME = RC.RDB$SCHEMA_NAME and IDX.RDB$INDEX_NAME = RC.RDB$INDEX_NAME
232+
inner join SYSTEM.RDB$RELATION_FIELDS RF
233+
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
234+
inner join SYSTEM.RDB$FIELDS F
235+
on F.RDB$SCHEMA_NAME = RF.RDB$FIELD_SOURCE_SCHEMA_NAME and F.RDB$FIELD_NAME = RF.RDB$FIELD_SOURCE
236+
where RC.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY'
237+
and\s""";
238+
//@formatter:on
239+
240+
// The order by schema name is to ensure a consistent order when this is called with schema = null, as that will
241+
// not narrow the search by schema, so can return columns of multiple same named tables in different schemas.
242+
private static final String GET_BEST_ROW_IDENT_END_6 = "\norder by RC.RDB$SCHEMA_NAME, IDX.RDB$FIELD_POSITION";
243+
244+
private FB6(DbMetadataMediator mediator) {
245+
super(mediator);
246+
}
247+
248+
private static GetBestRowIdentifier createInstance(DbMetadataMediator mediator) {
249+
return new FB6(mediator);
250+
}
251+
252+
@Override
253+
MetadataQuery createGetPrimaryKeyIdentifierQuery(String schema, String table) {
254+
List<Clause> clauses = new ArrayList<>(2);
255+
if (schema != null) {
256+
// NOTE: empty string will return no rows as required ("" retrieves those without a schema)
257+
clauses.add(Clause.equalsClause("RC.RDB$SCHEMA_NAME", schema));
258+
}
259+
clauses.add(Clause.equalsClause("RC.RDB$RELATION_NAME", table));
260+
String sql = GET_BEST_ROW_IDENT_START_6
261+
+ Clause.conjunction(clauses)
262+
+ GET_BEST_ROW_IDENT_END_6;
263+
264+
return new MetadataQuery(sql, Clause.parameters(clauses));
265+
}
266+
267+
}
268+
181269
}

src/test/org/firebirdsql/common/FBTestProperties.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,19 @@ public static void defaultDatabaseTearDown(FBManager fbManager) throws Exception
366366
}
367367
}
368368

369+
/**
370+
* If schema support is available, returns {@code forSchema}, otherwise returns {@code withoutSchema}.
371+
*
372+
* @param forSchema
373+
* value to return when schema support is available
374+
* @param withoutSchema
375+
* value to return when schema support is not available
376+
* @return {@code forSchema} if schema support is available, otherwise {@code withoutSchema}
377+
*/
378+
public static <T> T ifSchemaElse(T forSchema, T withoutSchema) {
379+
return getDefaultSupportInfo().supportsSchemas() ? forSchema : withoutSchema;
380+
}
381+
369382
private FBTestProperties() {
370383
// No instantiation
371384
}

0 commit comments

Comments
 (0)