Skip to content

Commit 02a7242

Browse files
committed
#882 Added setters to FirebirdConnection to set search path
1 parent 38daa2d commit 02a7242

File tree

6 files changed

+209
-10
lines changed

6 files changed

+209
-10
lines changed

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,15 @@ The following changes are made to Jaybird to support schemas when connecting to
7979
+
8080
On Firebird 5.0 and older, this will be silently ignored.
8181
* 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.
82-
* `Connection.getSchema()` will return the result of `select CURRENT_SCHEMA from SYSTEM.RDB$DATABASE`;
82+
* `Connection`
83+
** `getSchema()` will return the result of `select CURRENT_SCHEMA from SYSTEM.RDB$DATABASE`;
8384
the connection will not store this value
84-
* `Connection.setSchema(String)` will query the current search path, and if not previously called, it will prepend the schema name to the search path, otherwise it will _replace_ the previously prepended schema name.
85+
** `setSchema(String)` will query the current search path, and if not previously called, it will prepend the schema name to the search path, otherwise it will _replace_ the previously prepended schema name.
8586
The schema name is stored _only_ for this replacement operation (i.e. it will not be returned by `getSchema`!)
86-
** The name must match exactly as is stored in the metadata (it is always case-sensitive!)
87-
** Jaybird will take care of quoting, and will always quote on dialect 3
88-
** 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
89-
** JDBC specifies that "`__Calling ``setSchema`` has no effect on previously created or prepared Statement objects.__`";
87+
*** The name must match exactly as is stored in the metadata (it is always case-sensitive!)
88+
*** Jaybird will take care of quoting, and will always quote on dialect 3
89+
*** 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
90+
*** JDBC specifies that "`__Calling ``setSchema`` has no effect on previously created or prepared Statement objects.__`";
9091
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, though we do try to identify the procedure when the callable statement is created and use that to fully-qualify the procedure.
9192
* 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)`)
9293
** 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)
@@ -102,6 +103,7 @@ We considered adding a column that lists the schema(s) that contain the package
102103
* `FirebirdConnection`
103104
** 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
104105
** Added method `List<String> getSearchPathList()` to obtain the search path as a list of unquoted object names, or empty list if schemas are not supported
106+
** Added methods `setSearchPath(String)` and `setSearchPathList` with overloads `(String...)` and `(List<String>)` to set the search path.
105107
* `FBCallableStatement`
106108
** On creating the instance, the stored procedure is parsed and identified in the database metadata, including selectability, unless `ignoreProcedureType` is `true`
107109
*** Parsing of callable statements is changed to be able to identify schema, package and procedure name, including scope specifiers

src/docs/asciidoc/release_notes.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,8 @@ we recommend to always use `null` for `catalog`
560560
* `FirebirdConnection`/`FBConnection`
561561
** 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
562562
** Added method `List<String> getSearchPatList()` to obtain the search path as a list of unquoted object names, or empty list if schemas are not supported
563+
** Added methods `setSearchPath(String)` and `setSearchPathList(String...)/(List<String>)` where added to set the search path;
564+
these methods throw `SQLFeatureNotSupportedException` if schemas are not supported.
563565
* `StatisticsManager`
564566
** `getTableStatistics`
565567
*** `getTableStatistics(String[] tableNames)` was changed to accept varargs (`getTableStatistics(String... tableNames)`)

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.firebirdsql.jaybird.props.DatabaseConnectionProperties;
2424
import org.firebirdsql.jaybird.props.PropertyConstants;
2525
import org.firebirdsql.jaybird.util.SQLExceptionChainBuilder;
26+
import org.firebirdsql.jaybird.util.SearchPathHelper;
2627
import org.firebirdsql.jaybird.xca.FBLocalTransaction;
2728
import org.firebirdsql.jaybird.xca.FBManagedConnection;
2829
import org.firebirdsql.jdbc.InternalTransactionCoordinator.MetaDataTransactionCoordinator;
@@ -1118,11 +1119,21 @@ public final String getSearchPath() throws SQLException {
11181119
return getSchemaInfo().searchPath();
11191120
}
11201121

1122+
@Override
1123+
public final void setSearchPath(String searchPath) throws SQLException {
1124+
getSchemaChanger().setSearchPath(searchPath);
1125+
}
1126+
11211127
@Override
11221128
public final List<String> getSearchPathList() throws SQLException {
11231129
return getSchemaInfo().toSearchPathList();
11241130
}
11251131

1132+
@Override
1133+
public void setSearchPathList(List<String> schemas) throws SQLException {
1134+
getSchemaChanger().setSearchPath(SearchPathHelper.toSearchPath(schemas, getQuoteStrategy()));
1135+
}
1136+
11261137
private SchemaChanger.SchemaInfo getSchemaInfo() throws SQLException {
11271138
try (var ignored = withLock()) {
11281139
return getSchemaChanger().getCurrentSchemaInfo();

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.sql.Blob;
1212
import java.sql.Connection;
1313
import java.sql.SQLException;
14+
import java.util.Arrays;
1415
import java.util.List;
1516

1617
/**
@@ -128,24 +129,79 @@ public interface FirebirdConnection extends Connection {
128129
*/
129130
void resetKnownClientInfoProperties();
130131

132+
/**
133+
* Sets the search path as if executing {@code SET SEARCH_PATH TO ...}.
134+
*
135+
* @param searchPath
136+
* comma-separated search path (names must be correctly quoted &mdash; if needed)
137+
* @throws java.sql.SQLFeatureNotSupportedException
138+
* if the server does not support schemas (Firebird 5.0 or older)
139+
* @throws SQLException
140+
* if {@code schemas} is null or blank, or for database access errors
141+
* @see #getSearchPath()
142+
* @see #setSearchPathList(List)
143+
* @see #setSchema(String)
144+
* @since 7
145+
*/
146+
void setSearchPath(String searchPath) throws SQLException;
147+
131148
/**
132149
* Returns the schema search path.
133150
*
134151
* @return comma-separated list of quoted schema names of the search path, or {@code null} if schemas are not
135152
* supported
136153
* @throws SQLException
137154
* if the connections is closed, or for database access errors
155+
* @see #setSearchPath(String)
138156
* @see #getSearchPathList()
139157
* @since 7
140158
*/
141159
String getSearchPath() throws SQLException;
142160

161+
/**
162+
* Sets the search path as if executing {@code SET SEARCH_PATH TO ...}.
163+
*
164+
* @param schemas
165+
* schemas to set as search path (names must be unquoted)
166+
* @throws java.sql.SQLFeatureNotSupportedException
167+
* if the server does not support schemas (Firebird 5.0 or older)
168+
* @throws SQLException
169+
* if {@code schemas} is empty, or for database access errors
170+
* @see #setSearchPathList(List)
171+
* @see #getSearchPathList()
172+
* @see #setSearchPath(String)
173+
* @see #setSchema(String)
174+
* @since 7
175+
*/
176+
default void setSearchPathList(String... schemas) throws SQLException {
177+
setSearchPathList(Arrays.asList(schemas));
178+
}
179+
180+
/**
181+
* Sets the search path as if executing {@code SET SEARCH_PATH TO ...}.
182+
*
183+
* @param schemas
184+
* schemas to set as search path (names must be unquoted)
185+
* @throws java.sql.SQLFeatureNotSupportedException
186+
* if the server does not support schemas (Firebird 5.0 or older)
187+
* @throws SQLException
188+
* if {@code schemas} is empty, or for database access errors
189+
* @see #setSearchPathList(List)
190+
* @see #getSearchPathList()
191+
* @see #setSearchPath(String)
192+
* @see #setSchema(String)
193+
* @since 7
194+
*/
195+
void setSearchPathList(List<String> schemas) throws SQLException;
196+
143197
/**
144198
* Returns the schema search path as a list of unquoted schema names.
145199
*
146200
* @return list of unquoted schema names, or an empty list if schemas are not supported
147201
* @throws SQLException
148202
* if the connection is closed, or for database access errors
203+
* @see #setSearchPathList(String...)
204+
* @see #setSearchPathList(List)
149205
* @see #getSearchPath()
150206
* @since 7
151207
*/

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

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.List;
1414
import java.util.Objects;
1515

16+
import static org.firebirdsql.jaybird.util.StringUtils.isNullOrBlank;
1617
import static org.firebirdsql.util.FirebirdSupportInfo.supportInfoFor;
1718

1819
/**
@@ -41,9 +42,20 @@ sealed abstract class SchemaChanger {
4142
* @throws SQLException
4243
* for database access errors, or if {@code schema} is {@code null} or blank <em>if</em> schemas are
4344
* supported
45+
* @see #setSearchPath(String)
4446
*/
4547
abstract void setSchema(String schema) throws SQLException;
4648

49+
/**
50+
* Sets the search path, overriding any previously set current schema or search path.
51+
*
52+
* @param searchPath new search path to set (non-{@code null} and not blank, comma-separate, and quoted if needed)
53+
* @throws java.sql.SQLFeatureNotSupportedException if schemas are not supported (Firebird 5.0 and older)
54+
* @throws SQLException for database access errors, or if {@code searchPath} is {@code null} or blank
55+
* @see #setSchema(String)
56+
*/
57+
abstract void setSearchPath(String searchPath) throws SQLException;
58+
4759
/**
4860
* Current schema and search path.
4961
* <p>
@@ -134,7 +146,7 @@ SchemaInfo getCurrentSchemaInfo() throws SQLException {
134146

135147
@Override
136148
void setSchema(String schema) throws SQLException {
137-
if (schema == null || schema.isBlank()) {
149+
if (isNullOrBlank(schema)) {
138150
// TODO externalize?
139151
throw new SQLDataException("schema must be non-null and not blank",
140152
SQLStateConstants.SQL_STATE_INVALID_USE_NULL);
@@ -166,14 +178,30 @@ void setSchema(String schema) throws SQLException {
166178
newSearchPath.addAll(originalSearchPath);
167179
}
168180

169-
//noinspection SqlSourceToSinkFlow
170-
getStatement().execute("set search_path to "
171-
+ SearchPathHelper.toSearchPath(newSearchPath, connection.getQuoteStrategy()));
181+
setSearchPath0(SearchPathHelper.toSearchPath(newSearchPath, connection.getQuoteStrategy()));
172182
schemaInfoAfterLastChange = getCurrentSchemaInfo();
173183
lastSearchPath = List.copyOf(newSearchPath);
174184
lastSchemaChange = schema;
175185
}
176186
}
187+
188+
@Override
189+
void setSearchPath(String searchPath) throws SQLException {
190+
if (isNullOrBlank(searchPath)) {
191+
// TODO externalize?
192+
throw new SQLDataException("search path must have at least one schema",
193+
SQLStateConstants.SQL_STATE_INVALID_USE_NULL);
194+
}
195+
setSearchPath0(searchPath);
196+
schemaInfoAfterLastChange = getCurrentSchemaInfo();
197+
lastSearchPath = schemaInfoAfterLastChange.toSearchPathList();
198+
// given this change was no explicit call to setSchema, clear it
199+
lastSchemaChange = null;
200+
}
201+
202+
private void setSearchPath0(String searchPath) throws SQLException {
203+
getStatement().execute("set search_path to " + searchPath);
204+
}
177205
}
178206

179207
/**
@@ -188,6 +216,11 @@ void setSchema(String schema) {
188216
// do nothing (not even validate the name)
189217
}
190218

219+
@Override
220+
void setSearchPath(String searchPath) throws SQLException {
221+
throw new FBDriverNotCapableException("Schema support required for setSearchPath");
222+
}
223+
191224
@Override
192225
SchemaInfo getCurrentSchemaInfo() {
193226
return SchemaInfo.NULL_INSTANCE;

src/test/org/firebirdsql/jdbc/FBConnectionSchemaTest.java

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import org.firebirdsql.common.extension.UsesDatabaseExtension;
66
import org.firebirdsql.common.extension.UsesDatabaseExtension.UsesDatabaseForAll;
7+
import org.firebirdsql.common.matchers.SQLExceptionMatchers;
78
import org.firebirdsql.jaybird.props.PropertyNames;
89
import org.firebirdsql.jaybird.util.SearchPathHelper;
910
import org.junit.jupiter.api.Test;
@@ -17,15 +18,18 @@
1718
import java.sql.ResultSetMetaData;
1819
import java.sql.SQLDataException;
1920
import java.sql.SQLException;
21+
import java.sql.SQLFeatureNotSupportedException;
2022
import java.util.List;
2123

2224
import static org.firebirdsql.common.FBTestProperties.getConnectionViaDriverManager;
2325
import static org.firebirdsql.common.FBTestProperties.getDefaultSupportInfo;
2426
import static org.firebirdsql.common.FbAssumptions.assumeNoSchemaSupport;
2527
import static org.firebirdsql.common.FbAssumptions.assumeSchemaSupport;
28+
import static org.firebirdsql.common.matchers.SQLExceptionMatchers.message;
2629
import static org.hamcrest.MatcherAssert.assertThat;
2730
import static org.hamcrest.Matchers.empty;
2831
import static org.hamcrest.Matchers.is;
32+
import static org.hamcrest.Matchers.startsWith;
2933
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
3034
import static org.junit.jupiter.api.Assertions.assertEquals;
3135
import static org.junit.jupiter.api.Assertions.assertNull;
@@ -203,6 +207,97 @@ void connectionSearchPath(String searchPath, String expectedSchema, String expec
203207
}
204208
}
205209

210+
@Test
211+
void setSearchPath_noSchemaSupport_throwsFBDriverNotCapable() throws Exception {
212+
assumeNoSchemaSupport();
213+
try (var connection = getConnectionViaDriverManager()) {
214+
assertThrows(FBDriverNotCapableException.class, () -> connection.setSearchPath("SYSTEM"));
215+
}
216+
}
217+
218+
@ParameterizedTest
219+
@NullAndEmptySource
220+
@ValueSource(strings = { " ", " " })
221+
void setSearchPath_schemaSupport_nullOrBlank_notAccepted(String searchPath) throws Exception {
222+
assumeSchemaSupport();
223+
try (var connection = getConnectionViaDriverManager()) {
224+
var exception = assertThrows(SQLDataException.class, () -> connection.setSearchPath(searchPath));
225+
assertThat(exception, message(startsWith("search path must have at least one schema")));
226+
}
227+
}
228+
229+
@ParameterizedTest
230+
@CsvSource(useHeadersInDisplayName = true, textBlock = """
231+
searchPath, expectedSearchPath
232+
PUBLIC, '"PUBLIC", "SYSTEM"'
233+
'PUBLIC, SYSTEM', '"PUBLIC", "SYSTEM"'
234+
public, '"PUBLIC", "SYSTEM"'
235+
"public", '"public", "SYSTEM"'
236+
SCHEMA_1, '"SCHEMA_1", "SYSTEM"'
237+
"case_sensitive", '"case_sensitive", "SYSTEM"'
238+
# NOTE Unquoted!
239+
case_sensitive, '"CASE_SENSITIVE", "SYSTEM"'
240+
'SCHEMA_1, "case_sensitive", SYSTEM, PUBLIC', '"SCHEMA_1", "case_sensitive", "SYSTEM", "PUBLIC"'
241+
""")
242+
void setSearchPath_schemaSupport(String searchPath, String expectedSearchPath) throws Exception {
243+
assumeSchemaSupport();
244+
try (var connection = getConnectionViaDriverManager()) {
245+
connection.setSearchPath(searchPath);
246+
247+
assertEquals(expectedSearchPath, connection.getSearchPath(), "searchPath");
248+
}
249+
}
250+
251+
@Test
252+
void setSearchPathList_stringArr_noSchemaSupport_throwsFBDriverNotCapable() throws Exception {
253+
assumeNoSchemaSupport();
254+
try (var connection = getConnectionViaDriverManager()) {
255+
assertThrows(SQLFeatureNotSupportedException.class, () -> connection.setSearchPathList("SYSTEM"));
256+
}
257+
}
258+
259+
// As setSearchPathList(String...) goes through setSearchPathList(List<String>), we only tests through the latter
260+
261+
@Test
262+
void setSearchPathList_stringList_noSchemaSupport_throwsFBDriverNotCapable() throws Exception {
263+
assumeNoSchemaSupport();
264+
try (var connection = getConnectionViaDriverManager()) {
265+
assertThrows(SQLFeatureNotSupportedException.class, () -> connection.setSearchPathList(List.of("SYSTEM")));
266+
}
267+
}
268+
269+
@Test
270+
void setSearchPathList_stringList_schemaSupport_empty_notAccepted() throws Exception {
271+
assumeSchemaSupport();
272+
try (var connection = getConnectionViaDriverManager()) {
273+
var exception = assertThrows(SQLDataException.class, () -> connection.setSearchPathList(List.of()));
274+
assertThat(exception, message(startsWith("search path must have at least one schema")));
275+
}
276+
}
277+
278+
@ParameterizedTest
279+
@CsvSource(useHeadersInDisplayName = true, textBlock = """
280+
searchPath, expectedSearchPath
281+
PUBLIC, '"PUBLIC", "SYSTEM"'
282+
'PUBLIC, SYSTEM', '"PUBLIC", "SYSTEM"'
283+
public, '"PUBLIC", "SYSTEM"'
284+
"public", '"public", "SYSTEM"'
285+
SCHEMA_1, '"SCHEMA_1", "SYSTEM"'
286+
"case_sensitive", '"case_sensitive", "SYSTEM"'
287+
# NOTE Unquoted!
288+
case_sensitive, '"CASE_SENSITIVE", "SYSTEM"'
289+
'SCHEMA_1, "case_sensitive", SYSTEM, PUBLIC', '"SCHEMA_1", "case_sensitive", "SYSTEM", "PUBLIC"'
290+
""")
291+
void setSearchPathList_stringList_schemaSupport(String searchPath, String expectedSearchPath) throws Exception {
292+
assumeSchemaSupport();
293+
try (var connection = getConnectionViaDriverManager()) {
294+
List<String> searchPathList = SearchPathHelper.parseSearchPath(searchPath);
295+
connection.setSearchPathList(searchPathList);
296+
297+
assertEquals(expectedSearchPath, connection.getSearchPath(), "searchPath");
298+
}
299+
}
300+
206301
private static void checkSchemaResolution(Connection connection, String expectedSchema) throws SQLException {
207302
try (var pstmt = connection.prepareStatement("select * from TABLE_ONE")) {
208303
ResultSetMetaData rsmd = pstmt.getMetaData();

0 commit comments

Comments
 (0)