Skip to content

DRIVER-153: negotiate and implement SCYLLA_USE_METADATA_ID extension#770

Open
nikagra wants to merge 2 commits into
scylladb:masterfrom
nikagra:driver-153-scylla-use-metadata-id
Open

DRIVER-153: negotiate and implement SCYLLA_USE_METADATA_ID extension#770
nikagra wants to merge 2 commits into
scylladb:masterfrom
nikagra:driver-153-scylla-use-metadata-id

Conversation

@nikagra
Copy link
Copy Markdown

@nikagra nikagra commented Mar 26, 2026

Summary

Implements the SCYLLA_USE_METADATA_ID Scylla CQL protocol extension (DRIVER-153), which backports the prepared-statement metadata-ID mechanism from CQL v5 to earlier protocol versions.

When the extension is negotiated:

  • The server includes a result metadata hash in the PREPARE response
  • The driver sends that hash back with every EXECUTE request, allowing the server to skip sending full result metadata on every response (skip_meta=True)
  • If the result schema has changed, the server sets the METADATA_CHANGED flag and includes the new metadata ID + new column metadata in the response — the driver picks this up and updates its cached metadata automatically

Changes

cassandra/protocol_features.py

  • Add USE_METADATA_ID = "SCYLLA_USE_METADATA_ID" constant and use_metadata_id field to ProtocolFeatures
  • Parse the extension from the SUPPORTED frame; include it in STARTUP when present

cassandra/protocol.py

  • Bug fix: _write_query_params now actually writes _SKIP_METADATA_FLAG on the wire — it was stored on _QueryMessage but never sent (effectively dead code)
  • recv_results_prepared: read result_metadata_id for Scylla extension (pre-v5) in addition to standard CQL v5+
  • ExecuteMessage: add use_metadata_id flag (default False); send_body gates the result_metadata_id field on ProtocolVersion.uses_prepared_metadata(protocol_version) or self.use_metadata_id — both paths unified. An empty sentinel (b'') is written when the hash is None (LWT / mixed cluster) so the frame layout is always correct and no TypeError crash on v5

cassandra/cluster.py

  • _create_response_future: build ExecuteMessage with safe defaults (skip_meta=False, result_metadata_id=None, use_metadata_id=False)
  • _query: after borrowing the connection, set can_skip_meta, skip_meta, result_metadata_id, and use_metadata_id based on the actual connection's negotiated features. skip_meta is only enabled when the prepared statement has both a result_metadata_id and usable cached result_metadata — guards against LWT/NO_METADATA statements and mixed-cluster scenarios
  • _set_result: on METADATA_CHANGED update prepared_statement.result_metadata then result_metadata_id in that order (safe write ordering for concurrent readers)

docs/scylla-specific.rst

  • Document the extension, its behaviour, and the reference to the ScyllaDB CQL extensions spec

Test plan

  • 19 new unit tests across 3 files covering: feature negotiation, STARTUP options, skip_meta flag encoding, metadata_id in ExecuteMessage (v4/v5, sentinel for None), PREPARE response decoding with/without extension, METADATA_CHANGED and NO_METADATA flag handling, _set_result METADATA_CHANGED path, and per-connection feature gating in _query (5 scenarios: extension on/off, metadata id present/absent, LWT, protocol v5)
  • Full unit test suite passes (57 passed in affected files; 627+ overall)
  • Integration tests against a Scylla node with the extension: verify that schema changes after PREPARE are detected and metadata is updated without re-preparation

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements negotiation and support for Scylla’s SCYLLA_USE_METADATA_ID protocol extension to enable metadata-id based skip_meta behavior (backporting CQL v5 prepared-statement metadata-id semantics to earlier protocol versions).

Changes:

  • Adds SCYLLA_USE_METADATA_ID parsing from SUPPORTED and includes it in STARTUP when negotiated.
  • Extends protocol encode/decode to read/write result_metadata_id for PREPARE/EXECUTE on pre-v5 when the extension is used, and fixes on-wire encoding of _SKIP_METADATA_FLAG.
  • Updates execution/result handling to conditionally use skip_meta and to refresh cached prepared metadata when the server reports metadata changes.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
cassandra/protocol_features.py Adds the SCYLLA_USE_METADATA_ID feature flag and includes it in negotiated STARTUP options.
cassandra/protocol.py Writes _SKIP_METADATA_FLAG in query params; adds pre-v5 extension handling for result_metadata_id in PREPARE/EXECUTE.
cassandra/cluster.py Adjusts when skip_meta is enabled and updates cached prepared metadata/id on METADATA_CHANGED responses.
tests/unit/test_protocol_features.py Adds unit tests for feature parsing and STARTUP option inclusion.
tests/unit/test_protocol.py Adds unit tests for skip-meta flag encoding and metadata-id handling in pre-v5 PREPARE/EXECUTE paths.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cassandra/cluster.py Outdated
Comment thread cassandra/protocol.py Outdated
Comment on lines +648 to +649
elif self.result_metadata_id is not None:
write_string(f, self.result_metadata_id)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

ExecuteMessage.send_body() now writes result_metadata_id for protocol versions that don’t use standard prepared-metadata (pre-v5) whenever self.result_metadata_id is non-None. For pre-v5 this extra field is only valid when SCYLLA_USE_METADATA_ID was negotiated on that connection; otherwise it changes the wire layout (the server will interpret the metadata id bytes as the query parameters) and the request will fail.

To make this safe, ensure the decision to include result_metadata_id is gated by the negotiated feature (e.g., add an explicit use_metadata_id/send_result_metadata_id flag on the message that the caller sets based on connection.features.use_metadata_id, or guarantee centrally that result_metadata_id is cleared unless the extension is active for that connection).

Suggested change
elif self.result_metadata_id is not None:
write_string(f, self.result_metadata_id)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed. The elif self.result_metadata_id is not None path in send_body is now only reached when the caller explicitly set the field — which only happens in _query() after confirming connection.features.use_metadata_id (or CQL v5). For any connection that didn't negotiate the extension, result_metadata_id remains None and the branch is never taken, so the wire layout is unaffected.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

There is still a problem here. If the result id feature is negotiated on the connection, then you need to ALWAYS send some result metadata id in EXECUTE. Skipping the write_string will result in a protocol error.
Your use_metadata_id may be False even if extension was negotiated, if the server decided to skip the metadata in PREPARED response. In such case, you'll skip writing the id here, and encounter protocol error.

Even if you fix this specific case, there is still possibility of mixed cluster, with some nodes supporting the extension. In that case result_metadata_id will be None, and if you send to a node that has the extension negotiated, you'll again not send the id and encounter protocol error.

To sum up: this serialization here should check if feature is negotiated, and base sending this field only on that.

Copy link
Copy Markdown
Author

@nikagra nikagra May 14, 2026

Choose a reason for hiding this comment

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

👨‍💻: Addressed your concerns

🤖:

Automated response

Fixed. Added a `use_metadata_id` boolean flag to `ExecuteMessage` (default `False`). `send_body` now gates the `result_metadata_id` field on `ProtocolVersion.uses_prepared_metadata(protocol_version) or self.use_metadata_id` — both branches unified — writing an empty sentinel (`b''`) when the hash is `None` rather than omitting the field or crashing.

In _query(), message.use_metadata_id = connection.features.use_metadata_id is set unconditionally after borrowing the connection, so the decision is always based on what was negotiated on that specific connection.

This also fixes a second issue: the old separate v5 branch would crash with TypeError when result_metadata_id is None (LWT statement on v5, or statement prepared before the extension was active in a mixed cluster). The unified sentinel handles that case correctly on both code paths.

Two new tests cover this: test_execute_message_scylla_metadata_id_none_writes_sentinel (extension active, None hash → sentinel on v4) and test_execute_message_v5_metadata_id_none_writes_sentinel (v5 native path, None hash → sentinel instead of crash).

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cassandra/cluster.py Outdated
@nikagra nikagra force-pushed the driver-153-scylla-use-metadata-id branch from ade35d8 to f42e225 Compare March 27, 2026 12:32
@mykaul
Copy link
Copy Markdown

mykaul commented Mar 29, 2026

I'm not sure where, but we should document this - with reference mainly to the scylladb docs about this feature.

@nikagra
Copy link
Copy Markdown
Author

nikagra commented Mar 30, 2026

@mykaul Documentation I'm aware of is MetadataId extension in CQLv4 Requirement Document

@nikagra nikagra requested a review from sylwiaszunejko April 9, 2026 11:12
@nikagra nikagra marked this pull request as ready for review April 9, 2026 21:30
Copy link
Copy Markdown
Collaborator

@dkropachev dkropachev left a comment

Choose a reason for hiding this comment

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

One blocking correctness issue below: skip_meta is being enabled for prepared statements that can still have empty/absent cached result metadata.

Comment thread cassandra/cluster.py Outdated
@nikagra nikagra force-pushed the driver-153-scylla-use-metadata-id branch from 6eea397 to a86fd53 Compare April 15, 2026 09:09
@nikagra nikagra requested a review from dkropachev April 15, 2026 09:12
@nikagra nikagra force-pushed the driver-153-scylla-use-metadata-id branch from 7ba5835 to a86fd53 Compare April 15, 2026 11:12
@nikagra nikagra force-pushed the driver-153-scylla-use-metadata-id branch from a86fd53 to 8880f03 Compare April 22, 2026 12:34
@nikagra nikagra force-pushed the driver-153-scylla-use-metadata-id branch from 170fd31 to 5fe1902 Compare May 14, 2026 14:45
@nikagra nikagra requested a review from Lorak-mmk May 14, 2026 14:45
@nikagra nikagra force-pushed the driver-153-scylla-use-metadata-id branch from fcd3eba to 5fe1902 Compare May 15, 2026 09:22
@nikagra nikagra force-pushed the driver-153-scylla-use-metadata-id branch from 5fe1902 to 251b1a8 Compare May 28, 2026 20:32
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0118783e-7c2e-4842-b6eb-ececaf6869f2

📥 Commits

Reviewing files that changed from the base of the PR and between de8d3fc and bfc9760.

📒 Files selected for processing (3)
  • tests/unit/test_protocol.py
  • tests/unit/test_protocol_features.py
  • tests/unit/test_response_future.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/unit/test_protocol_features.py
  • tests/unit/test_response_future.py
  • tests/unit/test_protocol.py

📝 Walkthrough

Walkthrough

This PR adds Scylla's SCYLLA_USE_METADATA_ID support: negotiates a new protocol feature, encodes/decodes result_metadata_id and skip_meta/use_metadata_id in QUERY/EXECUTE/PREPARE/RESULT messages, defers attaching metadata-id info until a connection is borrowed, and updates prepared-statement cached metadata when servers send new result_metadata_id. It includes docs and unit tests covering v4/v5 and the Scylla extension paths.

Sequence Diagram

sequenceDiagram
  participant Client
  participant Connection
  participant Server
  Client->>Connection: execute(prepared_statement)
  activate Connection
  Connection->>Connection: ResponseFuture._query(): check cached metadata<br/>& connection features
  alt has cached metadata & supports metadata-id
    Connection->>Server: ExecuteMessage(skip_meta=True, result_metadata_id=cached_id)
  else no cached metadata or unsupported
    Connection->>Server: ExecuteMessage(skip_meta=False, result_metadata_id=None)
  end
  activate Server
  alt metadata unchanged
    Server->>Connection: ResultMessage(result_metadata_id=cached_id, no column_metadata)
  else metadata changed
    Server->>Connection: ResultMessage(result_metadata_id=new_id, column_metadata=[...])
  end
  deactivate Server
  Connection->>Connection: ResponseFuture._set_result(): if result_metadata_id present,<br/>update prepared_statement metadata cache
  deactivate Connection
  Connection->>Client: return rows with cached metadata
Loading

</details>

<!-- walkthrough_end -->
<!-- pre_merge_checks_walkthrough_start -->

<details>
<summary>🚥 Pre-merge checks | ✅ 4 | ❌ 1</summary>

### ❌ Failed checks (1 warning)

|     Check name     | Status     | Explanation                                                                           | Resolution                                                                         |
| :----------------: | :--------- | :------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------- |
| Docstring Coverage | ⚠️ Warning | Docstring coverage is 65.96% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |

<details>
<summary>✅ Passed checks (4 passed)</summary>

|         Check name         | Status   | Explanation                                                                                                                                                                     |
| :------------------------: | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|         Title check        | ✅ Passed | The title clearly and specifically summarizes the main change: implementing the SCYLLA_USE_METADATA_ID extension with the DRIVER ticket reference.                              |
|      Description check     | ✅ Passed | The description covers all required template sections with detailed implementation details, test coverage, and documentation updates, meeting or exceeding all checklist items. |
|     Linked Issues check    | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                                                                        |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                                                                        |

</details>

<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>

</details>

<!-- pre_merge_checks_walkthrough_end -->
<!-- tips_start -->

---




<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>

<!-- tips_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

Actionable comments posted: 0

Backport the prepared-statement metadata-ID mechanism from CQL v5 to
earlier protocol versions via the SCYLLA_USE_METADATA_ID Scylla extension.

protocol_features.py:
- Add USE_METADATA_ID constant and use_metadata_id field to ProtocolFeatures
- parse_from_supported: detect SCYLLA_USE_METADATA_ID in SUPPORTED options
- add_startup_options: echo the extension back in STARTUP when negotiated

protocol.py:
- _write_query_params: fix _SKIP_METADATA_FLAG — it was stored but never
  written on the wire (dead code); now correctly sets the flag in the frame
- recv_results_prepared: read result_metadata_id when the Scylla extension
  is active (pre-v5), in addition to the existing CQL v5 path
- ExecuteMessage: add use_metadata_id flag (default False); send_body gates
  the result_metadata_id field on uses_prepared_metadata(protocol_version)
  OR use_metadata_id, writing an empty sentinel (b'') when the hash is
  unavailable (LWT / mixed cluster) instead of omitting the field entirely
  or crashing with TypeError

cluster.py:
- _create_response_future: build ExecuteMessage with defaults (skip_meta=False,
  result_metadata_id=None); set these in _query() once the connection is known
- _query: after borrowing the connection set can_skip_meta, skip_meta,
  result_metadata_id, and use_metadata_id on the ExecuteMessage based on the
  actual connection's negotiated features; skip_meta is only enabled when the
  prepared statement has both a result_metadata_id AND cached result_metadata
  (guards against LWT/NO_METADATA statements and mixed-cluster re-prepare)
- _set_result: on METADATA_CHANGED (new result_metadata_id in EXECUTE response)
  update prepared_statement.result_metadata then result_metadata_id in that
  order — safe write ordering so a concurrent reader using the old id gets
  full metadata from the server while a reader seeing the new id has the
  correct cached metadata immediately

docs/scylla-specific.rst: document the extension and its behaviour
@nikagra nikagra force-pushed the driver-153-scylla-use-metadata-id branch from 251b1a8 to de8d3fc Compare June 1, 2026 15:57
test_protocol_features.py:
- test_use_metadata_id_parsing: SCYLLA_USE_METADATA_ID parsed from SUPPORTED
- test_use_metadata_id_missing: use_metadata_id is False when key is absent
- test_use_metadata_id_startup_options: key present in STARTUP when negotiated
- test_use_metadata_id_not_in_startup_when_not_negotiated: absent otherwise

test_protocol.py -- ExecuteMessage wire encoding:
- test_execute_message_skip_meta_flag: _SKIP_METADATA_FLAG (0x02) written on v4
- test_execute_message_v5_skip_meta_sets_flag: _SKIP_METADATA_FLAG written in the
  4-byte v5 flags word; confirms the flag was dead code in upstream before this PR
- test_execute_message_scylla_metadata_id_v4: result_metadata_id written on v4
  when use_metadata_id=True (Scylla extension)
- test_execute_message_scylla_metadata_id_none_writes_sentinel: extension active
  but result_metadata_id=None writes empty sentinel b'' (LWT / mixed cluster)
- test_execute_message_v5_metadata_id_none_writes_sentinel: v5 with
  result_metadata_id=None writes empty sentinel instead of TypeError crash

test_protocol.py -- ResultMessage decoding:
- test_recv_results_prepared_scylla_extension_reads_metadata_id: result_metadata_id
  read from PREPARE response on v4 when extension is active
- test_recv_results_prepared_no_extension_skips_metadata_id: result_metadata_id
  not read on v4 without extension
- test_recv_results_prepared_v5_reads_metadata_id: result_metadata_id read on v5
  via the native uses_prepared_metadata() path (use_metadata_id=False)
- test_recv_results_metadata_changed_flag: _METADATA_ID_FLAG in ROWS response
  causes result_metadata_id to be read and stored
- test_recv_results_metadata_no_metadata_flag_skips_metadata_id: NO_METADATA early
  return leaves result_metadata_id unset (asserts not hasattr, not merely None)

test_response_future.py -- _set_result METADATA_CHANGED update path:
- test_set_result_updates_metadata_when_metadata_changed: both result_metadata and
  result_metadata_id updated when server sends a new metadata id
- test_set_result_does_not_update_metadata_when_metadata_id_absent: cached metadata
  untouched when response carries no new metadata id (normal skip-meta path)
- test_set_result_warns_when_metadata_id_but_no_column_metadata: warning emitted
  and result_metadata_id updated when new id arrives with empty column list
- test_set_result_warns_when_metadata_id_but_column_metadata_is_none: same as
  above but with column_metadata=None (absent) rather than []

test_response_future.py -- _execute_after_prepare reprepare path:
- test_repeat_orig_query_after_succesful_reprepare: fixed result_metadata_id
  type str->bytes; added assertion that value is stored on prepared_statement
- test_execute_after_prepare_updates_result_metadata_id: both result_metadata and
  result_metadata_id refreshed from reprepare response when extension is active
- test_execute_after_prepare_no_metadata_id_in_response: result_metadata_id left
  unchanged when reprepare response carries no metadata id

test_response_future.py -- _query per-connection feature gating (6 scenarios):
- test_query_sets_skip_meta_with_scylla_extension: skip_meta=True when connection
  has negotiated SCYLLA_USE_METADATA_ID and statement has cached metadata
- test_query_no_skip_meta_without_extension: skip_meta=False when use_metadata_id
  is False; fixture uses truthy result_metadata to isolate this guard correctly
- test_query_no_skip_meta_when_prepared_statement_has_no_metadata_id: skip_meta
  False when result_metadata_id=None (prepared before extension was active)
- test_query_sets_skip_meta_for_protocol_v5: skip_meta=True on v5 via the native
  uses_prepared_metadata() path (use_metadata_id=False)
- test_query_no_skip_meta_when_result_metadata_is_none: LWT guard -- skip_meta
  False when result_metadata=None despite extension being active
@nikagra nikagra force-pushed the driver-153-scylla-use-metadata-id branch from de8d3fc to bfc9760 Compare June 2, 2026 20:42
@nikagra
Copy link
Copy Markdown
Author

nikagra commented Jun 2, 2026

🤖: All review issues (1–9) have been addressed and the branch has been squashed to the required 2-commit shape. Requesting re-review from @Lorak-mmk and @dkropachev.

What changed since last review:

Production commit (8accdb5a) — no functional changes, two cleanups squashed in:

  • Issue 8 (docs scope claim): corrected docs/scylla-specific.rst to say the extension applies to EXECUTE requests, not PREPARE
  • Issue 9 (GIL comment): added a note in _set_result explaining why the metadata update write ordering is safe under the GIL

Test commit (bfc97602) — all 6 fix/addition commits squashed in, commit message fully replaced to enumerate every test:

  • Issue 1: test_query_no_skip_meta_without_extension fixture corrected (result_metadata=[] was falsy, defeating the assertion)
  • Issue 2: test_execute_after_prepare_updates_result_metadata_id and test_execute_after_prepare_no_metadata_id_in_response added to cover the _execute_after_prepare reprepare path
  • Issue 3: test_recv_results_metadata_no_metadata_flag_skips_metadata_id tightened — now asserts not hasattr(result, 'result_metadata_id') rather than is None, and checks column_metadata not result_metadata
  • Issue 4: test_recv_results_prepared_v5_reads_metadata_id added — covers the v5 native uses_prepared_metadata() decode path
  • Issue 5: test_execute_message_v5_skip_meta_sets_flag added — confirms _SKIP_METADATA_FLAG is correctly written into the 4-byte v5 flags word (this flag was dead code in upstream before this PR)
  • Issue 6: test_repeat_orig_query_after_succesful_reprepare fixed — result_metadata_id value changed from str to bytes; assertion that value is stored on prepared_statement added
  • Issue 7: test_set_result_warns_when_metadata_id_but_column_metadata_is_none added — covers the column_metadata=None (absent) variant of the METADATA_CHANGED warning path

CI on the 8-commit pre-squash branch: 18/19 passed; test libev (3.14t) was cancelled after a 6-hour runner stall (infrastructure timeout — test asyncio (3.14t) on the same commit passed cleanly).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants