Skip to content

Commit f6033b9

Browse files
committed
#882 schema support for generated keys
1 parent fa51218 commit f6033b9

12 files changed

+779
-219
lines changed

src/main/org/firebirdsql/jaybird/parser/StatementDetector.java

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

@@ -14,8 +14,8 @@
1414
* Detects the type of statement, and - optionally - whether a DML statement has a {@code RETURNING} clause.
1515
* <p>
1616
* If the detected statement type is {@code UPDATE}, {@code DELETE}, {@code INSERT}, {@code UPDATE OR INSERT} and
17-
* {@code MERGE}, it identifies the affected table and - optionally - whether or not a {@code RETURNING} clause is
18-
* present (delegated to a {@link ReturningClauseDetector}).
17+
* {@code MERGE}, it identifies the affected table and - optionally - if a {@code RETURNING} clause is present
18+
* (delegated to a {@link ReturningClauseDetector}).
1919
* </p>
2020
* <p>
2121
* The types of statements detected are informed by the needs of Jaybird, and may change between point releases.
@@ -27,8 +27,6 @@
2727
@InternalApi
2828
public final class StatementDetector implements TokenVisitor {
2929

30-
// TODO Add schema support: identify (optional) schema
31-
3230
private static final StateAfterStart INITIAL_OTHER =
3331
new StateAfterStart(ParserState.OTHER, LocalStatementType.OTHER);
3432
private static final Map<CharSequence, StateAfterStart> NEXT_AFTER_START;
@@ -55,6 +53,7 @@ public final class StatementDetector implements TokenVisitor {
5553
private final boolean detectReturning;
5654
private LocalStatementType statementType = LocalStatementType.UNKNOWN;
5755
private ParserState parserState = ParserState.START;
56+
private Token schemaToken;
5857
private Token tableNameToken;
5958
private ReturningClauseDetector returningClauseDetector;
6059

@@ -105,10 +104,13 @@ public void visitToken(Token token, VisitorRegistrar visitorRegistrar) {
105104
if (parserState.isFinalState()) {
106105
// We're not interested anymore
107106
visitorRegistrar.removeVisitor(this);
108-
} else if (parserState == ParserState.FIND_RETURNING) {
109-
// We're not interested anymore
110-
visitorRegistrar.removeVisitor(this);
111-
if (detectReturning) {
107+
} else if (parserState == ParserState.FIND_RETURNING
108+
|| parserState == ParserState.FIND_SCHEMA_SEPARATOR_OR_RETURNING) {
109+
if (parserState == ParserState.FIND_RETURNING) {
110+
// We're not interested anymore
111+
visitorRegistrar.removeVisitor(this);
112+
}
113+
if (detectReturning && returningClauseDetector == null) {
112114
// Use ReturningClauseDetector to handle detection
113115
returningClauseDetector = new ReturningClauseDetector();
114116
visitorRegistrar.addVisitor(returningClauseDetector);
@@ -124,8 +126,7 @@ public void complete(VisitorRegistrar visitorRegistrar) {
124126
}
125127

126128
public StatementIdentification toStatementIdentification() {
127-
return new StatementIdentification(statementType, tableNameToken != null ? tableNameToken.text() : null,
128-
returningClauseDetected());
129+
return new StatementIdentification(statementType, schemaToken, tableNameToken, returningClauseDetected());
129130
}
130131

131132
boolean returningClauseDetected() {
@@ -139,6 +140,10 @@ public LocalStatementType getStatementType() {
139140
return statementType;
140141
}
141142

143+
Token getSchemaToken() {
144+
return schemaToken;
145+
}
146+
142147
Token getTableNameToken() {
143148
return tableNameToken;
144149
}
@@ -151,6 +156,10 @@ private void updateStatementType(LocalStatementType statementType) {
151156
}
152157
}
153158

159+
private void setSchemaToken(Token schemaToken) {
160+
this.schemaToken = schemaToken;
161+
}
162+
154163
private void setTableNameToken(Token tableNameToken) {
155164
this.tableNameToken = tableNameToken;
156165
}
@@ -213,12 +222,46 @@ ParserState next(Token token, StatementDetector detector) {
213222
},
214223
// Shared by UPDATE, DELETE and MERGE
215224
DML_TARGET {
225+
@Override
226+
ParserState next(Token token, StatementDetector detector) {
227+
if (token.isValidIdentifier()) {
228+
detector.setTableNameToken(token);
229+
return DML_SCHEMA_SEPARATOR_OR_POSSIBLE_ALIAS;
230+
}
231+
return forceOther(detector);
232+
}
233+
},
234+
// Shared by UPDATE, DELETE and MERGE
235+
DML_SCHEMA_SEPARATOR_OR_POSSIBLE_ALIAS {
236+
@Override
237+
ParserState next(Token token, StatementDetector detector) {
238+
if (token instanceof PeriodToken) {
239+
// What was detected as table, is actually the schema
240+
detector.setSchemaToken(detector.getTableNameToken());
241+
detector.setTableNameToken(null);
242+
return DML_SCHEMA_QUALIFIED_TABLE_NAME;
243+
} else if (token.isValidIdentifier()) {
244+
// either alias or possibly returning clause
245+
return FIND_RETURNING;
246+
} else if (token instanceof ReservedToken) {
247+
if (token.equalsIgnoreCase("AS")) {
248+
return DML_ALIAS;
249+
}
250+
return FIND_RETURNING;
251+
}
252+
// Unexpected or invalid token at this point
253+
return forceOther(detector);
254+
}
255+
},
256+
// Shared by UPDATE, DELETE and MERGE
257+
DML_SCHEMA_QUALIFIED_TABLE_NAME {
216258
@Override
217259
ParserState next(Token token, StatementDetector detector) {
218260
if (token.isValidIdentifier()) {
219261
detector.setTableNameToken(token);
220262
return DML_POSSIBLE_ALIAS;
221263
}
264+
// Unexpected or invalid token at this point
222265
return forceOther(detector);
223266
}
224267
},
@@ -263,7 +306,7 @@ ParserState next(Token token, StatementDetector detector) {
263306
ParserState next(Token token, StatementDetector detector) {
264307
if (token.isValidIdentifier()) {
265308
detector.setTableNameToken(token);
266-
return FIND_RETURNING;
309+
return FIND_SCHEMA_SEPARATOR_OR_RETURNING;
267310
}
268311
// Syntax error
269312
return forceOther(detector);
@@ -279,6 +322,29 @@ ParserState next(Token token, StatementDetector detector) {
279322
return forceOther(detector);
280323
}
281324
},
325+
FIND_SCHEMA_SEPARATOR_OR_RETURNING {
326+
@Override
327+
ParserState next(Token token, StatementDetector detector) {
328+
if (token instanceof PeriodToken) {
329+
detector.setSchemaToken(detector.getTableNameToken());
330+
detector.setTableNameToken(null);
331+
return FIND_SCHEMA_QUALIFIED_TABLE_OR_RETURNING;
332+
} else {
333+
return FIND_RETURNING;
334+
}
335+
}
336+
},
337+
FIND_SCHEMA_QUALIFIED_TABLE_OR_RETURNING {
338+
@Override
339+
ParserState next(Token token, StatementDetector detector) {
340+
if (token.isValidIdentifier()) {
341+
detector.setTableNameToken(token);
342+
return FIND_RETURNING;
343+
}
344+
// Syntax error
345+
return forceOther(detector);
346+
}
347+
},
282348
// finding itself is offloaded to ReturningClauseDetector
283349
FIND_RETURNING,
284350
COMMIT_ROLLBACK {
Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
// SPDX-FileCopyrightText: Copyright 2021-2022 Mark Rotteveel
1+
// SPDX-FileCopyrightText: Copyright 2021-2025 Mark Rotteveel
22
// SPDX-License-Identifier: LGPL-2.1-or-later
33
package org.firebirdsql.jaybird.parser;
44

55
import org.firebirdsql.util.InternalApi;
6+
import org.jspecify.annotations.NullMarked;
7+
import org.jspecify.annotations.Nullable;
8+
9+
import java.util.Locale;
610

711
import static java.util.Objects.requireNonNull;
812

@@ -13,32 +17,74 @@
1317
* @since 5
1418
*/
1519
@InternalApi
20+
@NullMarked
1621
public final class StatementIdentification {
1722

1823
private final LocalStatementType statementType;
19-
private final String tableName;
24+
private final @Nullable String schema;
25+
private final @Nullable String tableName;
2026
private final boolean returningClauseDetected;
2127

22-
StatementIdentification(LocalStatementType statementType, String tableName, boolean returningClauseDetected) {
28+
StatementIdentification(LocalStatementType statementType, @Nullable Token schema, @Nullable Token tableName,
29+
boolean returningClauseDetected) {
2330
this.statementType = requireNonNull(statementType, "statementType");
24-
this.tableName = tableName;
31+
this.schema = normalizeObjectName(schema);
32+
this.tableName = normalizeObjectName(tableName);
2533
this.returningClauseDetected = returningClauseDetected;
2634
}
2735

2836
public LocalStatementType getStatementType() {
2937
return statementType;
3038
}
3139

40+
/**
41+
* Schema, if this is a DML statement (other than {@code SELECT}), and if the table is qualified.
42+
* <p>
43+
* It reports the name normalized to its metadata storage representation.
44+
* </p>
45+
*
46+
* @return Schema, {@code null} if the table was not qualified, or for {@code SELECT} and other non-DML statements
47+
*/
48+
public @Nullable String getSchema() {
49+
return schema;
50+
}
51+
3252
/**
3353
* Table name, if this is a DML statement (other than {@code SELECT}).
54+
* <p>
55+
* It reports the name normalized to its metadata storage representation.
56+
* </p>
3457
*
3558
* @return Table name, {@code null} for {@code SELECT} and other non-DML statements
3659
*/
37-
public String getTableName() {
60+
public @Nullable String getTableName() {
3861
return tableName;
3962
}
4063

4164
public boolean returningClauseDetected() {
4265
return returningClauseDetected;
4366
}
67+
68+
/**
69+
* Normalizes an object name from the parser to its storage representation.
70+
* <p>
71+
* Unquoted identifiers are uppercased, and quoted identifiers are returned with the quotes stripped and doubled
72+
* double quotes replaced by a single double quote.
73+
* </p>
74+
*
75+
* @param objectToken
76+
* token with the object name (can be {@code null})
77+
* @return normalized object name, or {@code null} if {@code objectToken} was {@code null}
78+
*/
79+
private static @Nullable String normalizeObjectName(@Nullable Token objectToken) {
80+
if (objectToken == null) return null;
81+
String objectName = objectToken.text().trim();
82+
if (objectName.length() > 2
83+
&& objectName.charAt(0) == '"'
84+
&& objectName.charAt(objectName.length() - 1) == '"') {
85+
return objectName.substring(1, objectName.length() - 1).replace("\"\"", "\"");
86+
}
87+
return objectName.toUpperCase(Locale.ROOT);
88+
}
89+
4490
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,6 +1258,29 @@ private GetTables createGetTablesInstance() {
12581258
return GetTables.create(getDbMetadataMediator());
12591259
}
12601260

1261+
@Override
1262+
public Optional<String> findTableSchema(String tableName) throws SQLException {
1263+
if (!supportsSchemasInDataManipulation()) return Optional.of("");
1264+
final String findSchema = """
1265+
with SEARCH_PATH as (
1266+
select row_number() over() as PRIO, NAME as SCHEMA_NAME
1267+
from SYSTEM.RDB$SQL.PARSE_UNQUALIFIED_NAMES(rdb$get_context('SYSTEM', 'SEARCH_PATH'))
1268+
)
1269+
select r.RDB$SCHEMA_NAME
1270+
from RDB$RELATIONS as r
1271+
inner join SEARCH_PATH s on r.RDB$SCHEMA_NAME = s.SCHEMA_NAME and r.RDB$RELATION_NAME = ?
1272+
order by s.PRIO
1273+
fetch first row only""";
1274+
1275+
var metadataQuery = new DbMetadataMediator.MetadataQuery(findSchema, List.of(tableName));
1276+
try (ResultSet rs = getDbMetadataMediator().performMetaDataQuery(metadataQuery)) {
1277+
if (rs.next()) {
1278+
return Optional.of(rs.getString(1));
1279+
}
1280+
return Optional.empty();
1281+
}
1282+
}
1283+
12611284
@Override
12621285
public ResultSet getSchemas() throws SQLException {
12631286
return getSchemas(null, null);

0 commit comments

Comments
 (0)