Skip to content

Commit 0505fb6

Browse files
committed
#821 Retrofit updatable emulated scrollable cursors to have same behaviour as updatable server-side scrollable cursors
publishes jdp-2024-05
1 parent 775de2c commit 0505fb6

12 files changed

+387
-75
lines changed

devdoc/jdp/jdp-2024-05-behavior-of-updatable-result-sets.adoc

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
== Status
44

5-
* Draft
6-
* Proposed for: Jaybird 6
5+
* Published: 2024-09-24
6+
* Implemented in: Jaybird 6
77

88
== Type
99

@@ -14,11 +14,11 @@
1414
In https://github.com/FirebirdSQL/jaybird/blob/master/devdoc/jdp/jdp-2021-04-real-scrollable-cursor-support.md[jdp-2021-04], real server-side scrollable cursors where defined.
1515
In that JDP, for `CONCUR_UPDATABLE`, the behaviour of inserted, deleted, and -- in some respects -- updated rows was changed compared to the original "`emulated`" scrollable cursors:
1616

17-
* new rows are inserted at the end of the cursor;
17+
* New rows are inserted at the end of the cursor;
1818
in emulated they are inserted immediately before the current row.
19-
* deleted rows have an all-``null`` marker row;
19+
* Deleted rows have an all-``null`` marker row;
2020
in emulated the row is removed from the cursor.
21-
* the result set reports `true` for `rowDeleted()`, `rowInserted()` or `rowUpdated()` for -- respectively -- deleted, inserted or updated rows;
21+
* The result set reports `true` for `rowDeleted()`, `rowInserted()` or `rowUpdated()` for -- respectively -- deleted, inserted or updated rows;
2222
in emulated these always report `false`.
2323

2424
The first two differences were necessary to keep the accounting of row positions both server-side and locally simple and correct.

src/docs/asciidoc/release_notes.adoc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,29 @@ If you relied on this implicit close for correctness of your application, you ma
960960

961961
For more information, see also https://github.com/FirebirdSQL/jaybird/blob/master/devdoc/jdp/jdp-2024-03-do-not-close-result-set-after-last-row-in-auto-commit.adoc[jdp-2024-03: Do not close result set after last row in auto-commit^].
962962

963+
[#scroll-rs-update-behavior]
964+
=== Changes to behaviour of updatable scrollable result sets
965+
966+
Jaybird 5 introduced support for server-side scrollable cursors on Firebird 5.0 and higher in the pure Java protocol.
967+
This can be enabled using the connection property `scrollableCursor=SERVER`.
968+
969+
For implementation reasons, updatable server-side scrollable cursors had a different behaviour than the emulated client-side scrollable cursors.
970+
These differences are:
971+
972+
* New rows are inserted at the end of the cursor;
973+
in emulated they were inserted immediately before the current row.
974+
* Deleted rows have an all-``null`` marker row;
975+
in emulated the row was removed from the cursor.
976+
* The result set reports `true` for `rowDeleted()`, `rowInserted()` or `rowUpdated()` for -- respectively -- deleted, inserted or updated rows;
977+
in emulated these always reported `false`.
978+
979+
In Jaybird 6, this new behaviour is now also used for the updatable emulated scrollable cursors.
980+
The reason is that having two different sets of behaviours can be confusing, as it makes it impossible to switch between the two without having to account for the behavioural differences (either intentionally, or because you're connecting with the native or embedded protocol, or to an older version of Firebird).
981+
982+
We're considering to make server-side scrollable cursors the default in a future Jaybird version (Jaybird 7 or later).
983+
984+
See also https://github.com/FirebirdSQL/jaybird/blob/master/devdoc/jdp/jdp-2024-05-behavior-of-updatable-result-sets.adoc[jdp-2024-05: Behaviour of Updatable Result Sets^].
985+
963986
// TODO add major changes
964987

965988
[#other-fixes-and-changes]
@@ -1097,6 +1120,9 @@ If a type is not annotated, consider it nullable unless stated otherwise in the
10971120
+
10981121
In practice, this is an optional dependency, but Maven will pull it in by default.
10991122
If the JSpecify JAR is not included on the classpath or modulepath, Jaybird will still work.
1123+
* `DatabaseMetaData` now reports `ResultSet.TYPE_SCROLL_SENSITIVE` as not supported, as it is always downgraded to `TYPE_SCROLL_INSENSITIVE`, and thus effectively not supported.
1124+
+
1125+
This affects the return value of the methods `supportsResultSetType(int)`, `supportsResultSetConcurrency(int, int)`, `ownUpdatesAreVisible(int)`, `ownDeletesAreVisible(int)`, `ownInsertsAreVisible(int)`.
11001126
11011127
[#potentially-breaking-changes]
11021128
=== Potentially breaking changes
@@ -1278,6 +1304,12 @@ You will need to explicitly call `setReadOnly(false)`, or -- better yet -- do no
12781304

12791305
For more information, see <<no-close-after-last>>.
12801306

1307+
[#compat-scroll-rs-update-behavior]
1308+
1309+
=== Behavioural changes for updatable scrollable `ResultSet`
1310+
1311+
For more information, see <<scroll-rs-update-behavior>>.
1312+
12811313
// TODO Document compatibility issues
12821314

12831315
[#removal-of-classes-packages-and-methods-without-deprecation]

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ abstract sealed class AbstractFetcher implements FBFetcher
4343

4444
private FetchConfig fetchConfig;
4545
private FetcherListener fetcherListener;
46+
private @Nullable RowValue currentRow;
4647
private volatile boolean closed;
4748

4849
AbstractFetcher(FetchConfig fetchConfig, FetcherListener fetcherListener) {
@@ -64,9 +65,15 @@ public final void setFetcherListener(FetcherListener fetcherListener) {
6465
* for exceptions thrown by {@link FetcherListener#rowChanged(FBFetcher, RowValue)}
6566
*/
6667
protected final void notifyRowChanged(@Nullable RowValue newRow) throws SQLException {
68+
currentRow = newRow;
6769
fetcherListener.rowChanged(this, newRow);
6870
}
6971

72+
@Override
73+
public final void renotifyCurrentRow() throws SQLException {
74+
fetcherListener.rowChanged(this, currentRow);
75+
}
76+
7077
@Override
7178
public final void close() throws SQLException {
7279
close(CompletionReason.OTHER);

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

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -296,33 +296,22 @@ public boolean isAfterLast() {
296296

297297
@Override
298298
public void deleteRow() throws SQLException {
299-
if (!isAfterLast() && !isBeforeFirst()) {
300-
rows.remove(rowNum - 1);
301-
notifyRowChanged(adjustIfPositionAfterLast() ? null : rows.get(rowNum - 1));
302-
}
299+
throw calledUndecorated();
303300
}
304301

305302
@Override
306303
public void insertRow(RowValue data) throws SQLException {
307-
if (rowNum == 0) {
308-
rowNum = 1;
309-
}
310-
311-
if (rowNum > rows.size()) {
312-
rows.add(data);
313-
} else {
314-
rows.add(rowNum - 1, data);
315-
}
316-
317-
notifyRowChanged(isAfterLast() || isBeforeFirst() ? null : rows.get(rowNum - 1));
304+
throw calledUndecorated();
318305
}
319306

320307
@Override
321308
public void updateRow(RowValue data) throws SQLException {
322-
if (!isAfterLast() && !isBeforeFirst()) {
323-
rows.set(rowNum - 1, data);
324-
notifyRowChanged(data);
325-
}
309+
throw calledUndecorated();
310+
}
311+
312+
private static UnsupportedOperationException calledUndecorated() {
313+
return new UnsupportedOperationException(
314+
"Implementation error: FBServerScrollFetcher should be decorated with FBUpdatableFetcher");
326315
}
327316

328317
@Override

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

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,43 +1487,35 @@ public ResultSet getIndexInfo(String catalog, String schema, String table, boole
14871487

14881488
@Override
14891489
public boolean supportsResultSetType(int type) throws SQLException {
1490-
// TODO Return false for TYPE_SCROLL_SENSITVE as we only support it by downgrading to INSENSITIVE?
1490+
// TYPE_SCROLL_SENSITIVE is always downgraded to TYPE_SCROLL_INSENSITIVE, so we report false for it
14911491
return switch (type) {
1492-
case ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.TYPE_SCROLL_SENSITIVE ->
1493-
true;
1492+
case ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE -> true;
14941493
default -> false;
14951494
};
14961495
}
14971496

14981497
@Override
14991498
public boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException {
1500-
// TODO Return false for TYPE_SCROLL_SENSITVE as we only support it by downgrading to INSENSITIVE?
15011499
return switch (type) {
1502-
case ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.TYPE_SCROLL_SENSITIVE ->
1500+
case ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE ->
15031501
concurrency == ResultSet.CONCUR_READ_ONLY || concurrency == ResultSet.CONCUR_UPDATABLE;
15041502
default -> false;
15051503
};
15061504
}
15071505

15081506
@Override
15091507
public boolean ownUpdatesAreVisible(int type) throws SQLException {
1510-
// TODO Return false for TYPE_SCROLL_SENSITVE as we only support it by downgrading to INSENSITIVE?
1511-
return ResultSet.TYPE_SCROLL_INSENSITIVE == type ||
1512-
ResultSet.TYPE_SCROLL_SENSITIVE == type;
1508+
return ResultSet.TYPE_SCROLL_INSENSITIVE == type;
15131509
}
15141510

15151511
@Override
15161512
public boolean ownDeletesAreVisible(int type) throws SQLException {
1517-
// TODO Return false for TYPE_SCROLL_SENSITVE as we only support it by downgrading to INSENSITIVE?
1518-
return ResultSet.TYPE_SCROLL_INSENSITIVE == type ||
1519-
ResultSet.TYPE_SCROLL_SENSITIVE == type;
1513+
return ResultSet.TYPE_SCROLL_INSENSITIVE == type;
15201514
}
15211515

15221516
@Override
15231517
public boolean ownInsertsAreVisible(int type) throws SQLException {
1524-
// TODO Return false for TYPE_SCROLL_SENSITVE as we only support it by downgrading to INSENSITIVE?
1525-
return ResultSet.TYPE_SCROLL_INSENSITIVE == type ||
1526-
ResultSet.TYPE_SCROLL_SENSITIVE == type;
1518+
return ResultSet.TYPE_SCROLL_INSENSITIVE == type;
15271519
}
15281520

15291521
@Override
@@ -1543,23 +1535,20 @@ public boolean othersInsertsAreVisible(int type) throws SQLException {
15431535

15441536
@Override
15451537
public boolean updatesAreDetected(int type) throws SQLException {
1546-
// TODO Currently not correct when scrollableCursor=SERVER (and not a holdable cursor),
1547-
// change to return true when behaviour of EMULATED is the same
1548-
return false;
1538+
// Only updates through the result set
1539+
return ResultSet.TYPE_SCROLL_INSENSITIVE == type;
15491540
}
15501541

15511542
@Override
15521543
public boolean deletesAreDetected(int type) throws SQLException {
1553-
// TODO Currently not correct when scrollableCursor=SERVER (and not a holdable cursor),
1554-
// change to return true when behaviour of EMULATED is the same
1555-
return false;
1544+
// Only deletes through the result set
1545+
return ResultSet.TYPE_SCROLL_INSENSITIVE == type;
15561546
}
15571547

15581548
@Override
15591549
public boolean insertsAreDetected(int type) throws SQLException {
1560-
// TODO Currently not correct when scrollableCursor=SERVER (and not a holdable cursor),
1561-
// change to return true when behaviour of EMULATED is the same
1562-
return false;
1550+
// Only inserts through the result set
1551+
return ResultSet.TYPE_SCROLL_INSENSITIVE == type;
15631552
}
15641553

15651554
@Override

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,11 @@ default void beforeExecuteInsert() throws SQLException {
201201
*/
202202
void updateRow(RowValue data) throws SQLException;
203203

204+
/**
205+
* Notifies the fetcher listener with the row data of the current row (or {{@code null} if not currently in a row).
206+
*/
207+
void renotifyCurrentRow() throws SQLException;
208+
204209
/**
205210
* @return current fetch config of this fetcher
206211
* @since 6

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public FBResultSet(AbstractStatement statement, FBObjectListener.@Nullable Resul
140140
if (behavior.isUpdatable()) {
141141
try {
142142
rowUpdater = new FBRowUpdater(connection, rowDescriptor, cached, listener);
143-
if (fbFetcher instanceof FBServerScrollFetcher) {
143+
if (behavior.isScrollable()) {
144144
fbFetcher = new FBUpdatableFetcher(fbFetcher, this, rowDescriptor.createDeletedRowMarker());
145145
}
146146
} catch (FBResultSetNotUpdatableException ex) {
@@ -1482,6 +1482,9 @@ public void moveToInsertRow() throws SQLException {
14821482
@Override
14831483
public void moveToCurrentRow() throws SQLException {
14841484
requireRowUpdater().moveToCurrentRow();
1485+
// Make sure we have the correct data of the row
1486+
fbFetcher.renotifyCurrentRow();
1487+
notifyRowUpdater();
14851488
}
14861489

14871490
@Override

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ public boolean absolute(int row) throws SQLException {
265265
checkOpen();
266266
// Overflow beyond cursor size is handled by inWindow returning false
267267
int newLocalPosition = row >= 0 ? row : Math.max(0, requireCursorSize() + 1 + row);
268-
if (!inWindow(row)) {
268+
if (!inWindow(newLocalPosition)) {
269269
if (getMaxRows() != 0 && newLocalPosition > requireCursorSize()) {
270270
afterLast();
271271
return false;

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@
3636
/**
3737
* Decorator that handles tracking updates, deletes and inserts of an updatable result set.
3838
* <p>
39-
* This fetcher handles the updatable result set behaviour defined in jdp-2021-04 for server-side scrollable cursors
40-
* it is not yet "supported" for emulated scrollable result sets.
39+
* This fetcher handles the updatable result set behaviour defined in <a href="https://github.com/FirebirdSQL/jaybird/blob/master/devdoc/jdp/jdp-2021-04-real-scrollable-cursor-support.md">jdp-2021-04</a>
40+
* for server-side scrollable cursors and <a href="https://github.com/FirebirdSQL/jaybird/blob/master/devdoc/jdp/jdp-2024-05-behavior-of-updatable-result-sets.adoc">jdp-2024-05</a>
41+
* for emulated scrollable cursors.
4142
* </p>
4243
* <p>
4344
* This behaviour can be summarized as: updates are visible, deletes are visible (with a deletion marker row),
@@ -299,6 +300,17 @@ public void updateRow(RowValue data) throws SQLException {
299300
fetcherListener.rowChanged(this, data);
300301
}
301302

303+
@Override
304+
public void renotifyCurrentRow() throws SQLException {
305+
int position = this.position;
306+
// We can reuse the lastReceivedRow of the fetcher listener if the current row is not stored in this fetcher
307+
if (position <= fetcherSize()) {
308+
notifyFetcherRow(position);
309+
} else {
310+
notifyInsertedRow(position);
311+
}
312+
}
313+
302314
@Override
303315
public int getFetchSize() throws SQLException {
304316
return fetcher.getFetchSize();

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,11 @@ public void updateRow(RowValue data) throws SQLException {
154154
fetcher.updateRow(data);
155155
}
156156

157+
@Override
158+
public void renotifyCurrentRow() throws SQLException {
159+
fetcher.renotifyCurrentRow();
160+
}
161+
157162
@Override
158163
public int getFetchSize() throws SQLException {
159164
return fetcher.getFetchSize();

0 commit comments

Comments
 (0)