Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/adapter/src/frontend_peek.rs
Original file line number Diff line number Diff line change
Expand Up @@ -732,10 +732,16 @@ impl PeekClient {
)
.await?;

// If this is the first (non-AS OF) query in a multi-statement transaction, store
// the read holds in the coordinator, so subsequent queries can validate against
// them.
if in_immediate_multi_stmt_txn {
// If this query pins the timestamp of a multi-statement transaction, store
// the read holds in the coordinator, so subsequent queries can validate
// against them. The stored holds define the transaction's timedomain, so
// only the statement that determines the transaction timestamp may establish
// them, over the same id bundle. A timestamp-less determination (e.g. a
// constant query) doesn't pin the transaction timestamp, so its holds must
// not be stored.
if in_immediate_multi_stmt_txn

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you remind me why it is that a timestampless query even acquires read holds and we tried and stored them before? What are the read holds for there?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the constant query referenced a constant view, so the timedomain we determined (for the multi-statement transaction) included the whole schema of the view.

Claude, in more detail:

Read holds are acquired before the timestamp determination is computed (that's the general determine_timestamp contract, we only know afterwards that it came out as NoTimestamp), and they got stored simply because the frontend port dropped the old path's "only store when there is a timestamp" filter. They weren't for anything in this case, the old path explicitly dropped them ("We don't need the read holds and shouldn't add them to the txn").

The reason they were non-empty at all: the constant query referenced a constant view, so the timedomain we determined (for the multi-statement transaction) included the whole schema of the view. This wouldn't have happened for a query not selecting from any object, like SELECT 1 (though the old code would still have stored an empty hold set, which is what causes the spurious RelationOutsideTimeDomain also fixed by this PR).

&& determination.timestamp_context.contains_timestamp()
{
self.call_coordinator(|tx| Command::StoreTransactionReadHolds {
conn_id: session.conn_id().clone(),
read_holds: read_holds.clone(),
Expand Down
60 changes: 60 additions & 0 deletions test/sqllogictest/timedomain.slt
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,63 @@ EOF

statement ok
COMMIT

# Regression test for SQL-520: a timestamp-independent query must not extend
# the transaction's timedomain. The frontend peek sequencing used to store the
# read holds it acquired for a timestamp-independent query as the
# transaction's read holds. A later statement then established the
# transaction's timestamp determination over a smaller id bundle, but the
# timedomain validation of subsequent statements ran against the stored
# (larger) read hold set. Such statements could thus read collections whose
# since was not covered by the transaction timestamp, leading to
# SinceViolation panics.

statement ok
CREATE SCHEMA other

statement ok
CREATE VIEW other.const_view AS SELECT 1 AS x

statement ok
CREATE TABLE other.t2 (i INT)

simple
BEGIN;
SELECT * FROM other.const_view;
SELECT * FROM t;
----
COMPLETE 0
1
COMPLETE 1
COMPLETE 0

# `other.t2` is outside the timedomain established by `SELECT * FROM t`. The
# constant view query must not have widened the timedomain to the `other`
# schema.
query error Transactions can only reference objects in the same timedomain.
SELECT * FROM other.t2

statement ok
ROLLBACK

# The same bug also affected statements sequenced by the old peek path
# (EXECUTE always is): the stored read holds of the timestamp-independent
# query made the old path validate the timedomain against them and spuriously
# fail.
simple
BEGIN;
SELECT * FROM other.const_view;
PREPARE p AS SELECT * FROM t;
EXECUTE p;
----
COMPLETE 0
1
COMPLETE 1
COMPLETE 0
COMPLETE 0

statement ok
ROLLBACK

statement ok
DROP SCHEMA other CASCADE
Loading