Skip to content

perf: Optimize column_encryption_policy checks in recv_results_rows (1.7x speedup for Cython, 1.2x for Python) - #630

Open
mykaul wants to merge 5 commits into
scylladb:masterfrom
mykaul:remove_enc_cols
Open

perf: Optimize column_encryption_policy checks in recv_results_rows (1.7x speedup for Cython, 1.2x for Python)#630
mykaul wants to merge 5 commits into
scylladb:masterfrom
mykaul:remove_enc_cols

Conversation

@mykaul

@mykaul mykaul commented Dec 25, 2025

Copy link
Copy Markdown

Optimize column_encryption_policy checks in the result parsing hot path.

Previously, every decoded value checked ce_policy and ce_policy.contains_column(coldesc) even when no encryption policy was configured (the common case). This PR splits the parsing into two code paths — unpack_plain_row() and unpack_col_encrypted_row() — and branches once per result set instead of per value.

Changes:

  • protocol.py: Split recv_results_rows() pure-Python path
  • obj_parser.pyx: Split TupleRowParser.unpack_row() into unpack_plain_row() / unpack_col_encrypted_row(), with @boundscheck(False) / @wraparound(False) and try/except moved outside the per-column loop
  • ListParser / LazyParser / row_parser: Branch once on column_encryption_policy
  • numpy_parser.pyx: NumpyParser.parse_rows() raises NotImplementedError when column encryption is configured (it has no decryption support)
  • query.py: Same split-path optimization in BoundStatement.bind()

Benchmark (Cython ListParser, 10k rows, no encryption policy)

                      Master            PR            Speedup
2 cols (Int32+UTF8):  3.67 ms           2.08 ms       1.77x  (2.7M -> 4.8M rows/s)
10 cols (all Int32):  8.37 ms           5.18 ms       1.62x  (1.2M -> 1.9M rows/s)

Improvement comes from eliminating per-value overhead in the hot loop:

  • ce_policy and ce_policy.contains_column(coldesc) truthiness check
  • coldesc = desc.coldescs[i] lookup (not needed in plain path)
  • try/except per value moved outside the loop
  • @cython.boundscheck(False) / @cython.wraparound(False) decorators

Fixes: #582

Pre-review checklist

  • I have split my patch into logically separate commits.
  • All commit messages clearly explain what they change and why.
  • I added relevant tests for new features and bug fixes.
  • All commits compile, pass static checks and pass test.
  • PR description sums up the changes and reasons why they should be introduced.
  • I have provided docstrings for the public items that I want to introduce.
  • I have adjusted the documentation in ./docs/source/.
  • I added appropriate Fixes: annotations to PR description.

@mykaul

mykaul commented Dec 25, 2025

Copy link
Copy Markdown
Author

CI failure is unrelated, and I've seen it happening on too many PRs already :-/

Comment thread tests/unit/test_protocol_decode_optimization.py Outdated
Comment thread tests/unit/test_protocol_decode_optimization.py Outdated
@mykaul
mykaul force-pushed the remove_enc_cols branch 2 times, most recently from 3a9031e to ad8b6f9 Compare January 5, 2026 08:01

@dkropachev dkropachev left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

numpy_parser.pyx, parsing.pyx also needs to be fixed

Comment thread tests/unit/test_protocol_decode_optimization.py Outdated
@mykaul

mykaul commented Jan 6, 2026

Copy link
Copy Markdown
Author

numpy_parser.pyx, parsing.pyx also needs to be fixed

It wasn't clear to me what needs to fixing there:
numpy - it doesn't use/call column_encryption
parsing.pyx - there's just 'not implemented' functions there.

@dkropachev

Copy link
Copy Markdown

numpy_parser.pyx, parsing.pyx also needs to be fixed

It wasn't clear to me what needs to fixing there: numpy - it doesn't use/call column_encryption parsing.pyx - there's just 'not implemented' functions there.

Instead of changing only TupleRowParser class you need to do similar changes to ColumnParser and RowParser and all their implementations: ListParser, LazyParser and NumpyParser, you will discover that NumpyParser disregards column_encryption_policy which means that it does not work properly when when columns are encrypted, have a plug there that throwing NotImplementedError.
In total all Parser implementation classes should contain implementations for:

    cpdef parse_plain_rows(self, BytesIOReader reader, ParseDesc desc)

    cpdef parse_enc_rows(self, BytesIOReader reader, ParseDesc desc)

While Abstract classes:

cdef class ColumnParser:
    cpdef parse_rows(self, BytesIOReader reader, ParseDesc desc):
        if desc.column_encryption_policy:
            return self.unpack_enc_row(reader, desc)
        return self.unpack_plain_row(reader, desc)

    cpdef parse_plain_rows(self, BytesIOReader reader, ParseDesc desc)

    cpdef parse_enc_rows(self, BytesIOReader reader, ParseDesc desc)

@mykaul

mykaul commented Jan 6, 2026

Copy link
Copy Markdown
Author

numpy_parser.pyx, parsing.pyx also needs to be fixed

It wasn't clear to me what needs to fixing there: numpy - it doesn't use/call column_encryption parsing.pyx - there's just 'not implemented' functions there.

Instead of changing only TupleRowParser class you need to do similar changes to ColumnParser and RowParser and all their implementations: ListParser, LazyParser and NumpyParser, you will discover that NumpyParser disregards column_encryption_policy which means that it does not work properly when when columns are encrypted, have a plug there that throwing NotImplementedError. In total all Parser implementation classes should contain implementations for:

Let me see if I understand this:

  • TupleRowParser - Implemented unpack_ce_row() and unpack_row()
  • ColumnParser - its parse_rows() creates a TupleRowParser object - and then I call its unpack_ce_row() / unpack_row()
  • RowParser- its unpack_row() is raising NotImplementedError - I'm not sure I should change this in this PR?
  • ListParser - its parse_rows() creates a TupleRowParser object - and then I call its unpack_ce_row() / unpack_row()
  • LazyParser - its parse_rows() calls parse_rows_lazy() - which creates a TupleRowParser object - and then I call its unpack_ce_row() / unpack_row()
  • NumpyParser its parse_rows() calls its _parse_rows() which completely ignores encryption and calls unpack_row() - but I have no idea where it's implemented. BUT - I'm not introducing a regression here, no?
  • BoundStatement.bind() - added in the last commit in the series.

Is that an accurate representation?

    cpdef parse_plain_rows(self, BytesIOReader reader, ParseDesc desc)

    cpdef parse_enc_rows(self, BytesIOReader reader, ParseDesc desc)

I'm not sure I understand the interaction between Python and Cython - I was not going to change the interfaces whatsoever.

While Abstract classes:

cdef class ColumnParser:
    cpdef parse_rows(self, BytesIOReader reader, ParseDesc desc):
        if desc.column_encryption_policy:
            return self.unpack_enc_row(reader, desc)
        return self.unpack_plain_row(reader, desc)

    cpdef parse_plain_rows(self, BytesIOReader reader, ParseDesc desc)

    cpdef parse_enc_rows(self, BytesIOReader reader, ParseDesc desc)

You raise a good point about the names of the functions - that can clearly be more clear with 'plain' and 'encrypted' in them! I'll fix that.

@mykaul

mykaul commented Jan 27, 2026

Copy link
Copy Markdown
Author

You raise a good point about the names of the functions - that can clearly be more clear with 'plain' and 'encrypted' in them! I'll fix that.

Done in latest commit. I think it's ready for re-review.

@mykaul

mykaul commented Jan 27, 2026

Copy link
Copy Markdown
Author

CI failure seems like known issue #446

@mykaul mykaul added the enhancement New feature or request label Feb 8, 2026
@mykaul

mykaul commented Mar 2, 2026

Copy link
Copy Markdown
Author

Can this be reviewed? In my (limited) testing, it did show some nice improvement. It's just a branch or so, but apparently, was impacting to some degree the performance.

mykaul added a commit to mykaul/python-driver that referenced this pull request Mar 14, 2026
…nt.bind()

When Cython serializers (from cassandra.serializers) are available and no
column encryption policy is active, BoundStatement.bind() now uses
pre-built Serializer objects cached on the PreparedStatement instead of
calling cqltype classmethods. This avoids per-value Python method dispatch
overhead and enables the ~30x vector serialization speedup from the Cython
serializers module.

The bind loop is split into three paths:
1. Column encryption policy path (unchanged behavior)
2. Cython serializers path (new fast path)
3. Plain Python path (no CE, no Cython -- removes per-value ColDesc/CE check)

Depends on PR scylladb#748 (Cython serializers module) and PR scylladb#630 (CE-policy
bind split).
@mykaul mykaul self-assigned this Mar 16, 2026
mykaul added a commit to mykaul/python-driver that referenced this pull request Mar 16, 2026
…nt.bind()

When Cython serializers (from cassandra.serializers) are available and no
column encryption policy is active, BoundStatement.bind() now uses
pre-built Serializer objects cached on the PreparedStatement instead of
calling cqltype classmethods. This avoids per-value Python method dispatch
overhead and enables the ~30x vector serialization speedup from the Cython
serializers module.

The bind loop is split into three paths:
1. Column encryption policy path (unchanged behavior)
2. Cython serializers path (new fast path)
3. Plain Python path (no CE, no Cython -- removes per-value ColDesc/CE check)

Depends on PR scylladb#748 (Cython serializers module) and PR scylladb#630 (CE-policy
bind split).
mykaul added a commit to mykaul/python-driver that referenced this pull request Mar 20, 2026
…nt.bind()

When Cython serializers (from cassandra.serializers) are available and no
column encryption policy is active, BoundStatement.bind() now uses
pre-built Serializer objects cached on the PreparedStatement instead of
calling cqltype classmethods. This avoids per-value Python method dispatch
overhead and enables the ~30x vector serialization speedup from the Cython
serializers module.

The bind loop is split into three paths:
1. Column encryption policy path (unchanged behavior)
2. Cython serializers path (new fast path)
3. Plain Python path (no CE, no Cython -- removes per-value ColDesc/CE check)

Depends on PR scylladb#748 (Cython serializers module) and PR scylladb#630 (CE-policy
bind split).
mykaul added a commit to mykaul/python-driver that referenced this pull request Mar 25, 2026
Split recv_results_rows into fast path (no column encryption) and slow
path (column encryption enabled):

Fast path (common case):
  - Reads raw column bytes and decodes types in a single pass per row
    via _decode_row_inline(), eliminating the intermediate list-of-lists
  - Skips ColDesc namedtuple creation entirely (only needed for CE)
  - No closure allocation per call

Slow path (column encryption):
  - Preserves full CE logic with ColDesc creation
  - Moves decode_val/decode_row closures to module-level functions
    (_decode_val_ce, _decode_row_ce) to avoid per-call closure overhead

Note: This PR modifies the same method as PR scylladb#630 (which also splits
recv_results_rows into CE/non-CE branches). There will be a merge
conflict that needs manual resolution if both PRs are accepted.
Copilot AI review requested due to automatic review settings July 29, 2026 20:24
@mykaul
mykaul force-pushed the remove_enc_cols branch from 3ec0c94 to 465fd9e Compare July 29, 2026 20:24
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

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: a7d3a305-7784-4d49-b12c-eda6be82fc1c

📥 Commits

Reviewing files that changed from the base of the PR and between 189b887 and 69f46f4.

📒 Files selected for processing (8)
  • cassandra/numpy_parser.pyx
  • cassandra/obj_parser.pyx
  • cassandra/parsing.pxd
  • cassandra/parsing.pyx
  • cassandra/protocol.py
  • cassandra/query.py
  • cassandra/row_parser.pyx
  • tests/unit/test_protocol.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • cassandra/parsing.pxd
  • cassandra/numpy_parser.pyx
  • cassandra/row_parser.pyx
  • cassandra/query.py
  • cassandra/obj_parser.pyx
  • tests/unit/test_protocol.py

📝 Walkthrough

Walkthrough

The change separates encrypted and plain value serialization and result decoding. Cython row parsers now expose dedicated encrypted and plain unpacking methods. NumPy parsing rejects encrypted columns. Row descriptor construction and exception handling select the appropriate path. Tests cover policy-aware decoding, Cython parsing, call-count optimization, descriptor validation, and NumPy behavior.

Sequence Diagram(s)

sequenceDiagram
  participant BoundStatement
  participant EncryptionPolicy
  participant ResultMessage
  participant CythonParser
  BoundStatement->>EncryptionPolicy: determine encrypted columns
  BoundStatement->>EncryptionPolicy: serialize and encrypt selected values
  ResultMessage->>EncryptionPolicy: precompute column encryption decisions
  ResultMessage->>EncryptionPolicy: decrypt encrypted values
  ResultMessage->>CythonParser: select encrypted or plain row parser
  CythonParser-->>ResultMessage: decoded rows
Loading

Suggested labels: area/Driver_-_python-driver

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The query.py binding optimization is outside issue #582, which targets policy checks during result decoding. Move the query.py binding optimization to a separate issue or explain its required scope in the linked issue.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary optimization in result-row parsing and states the reported performance improvement.
Description check ✅ Passed The description explains the changes, rationale, benchmarks, linked issue, and tests; only documentation and public-item docstrings remain unchecked.
Linked Issues check ✅ Passed The changes satisfy issue #582 by selecting plain or encrypted decoding once per result set instead of checking the policy for every value.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Comment @coderabbitai help to get the list of available commands.

@mykaul

mykaul commented Jul 29, 2026

Copy link
Copy Markdown
Author

Rebased onto current origin/master (branch had drifted ~64 commits, including the SCYLLA_USE_METADATA_ID extension and ProtocolFeatures plumbing work around protocol.py). One real conflict, in tests/unit/test_protocol.py: master had independently added ProtocolFeaturesPlumbingTest / FrameByteIdentityTest plus new imports in the same region as this PR's ResultTest / CythonParserTest additions. Resolved by keeping both sets of tests and de-duplicating imports (dropped a couple that turned out to be unused leftovers from the pre-rebase base: _PAGING_OPTIONS_FLAG/uint32_unpack/ContinuousPagingOptions).

No other conflicts — protocol.py's recv_results_rows, query.py's bind(), and the Cython parser files applied cleanly since master hasn't touched that code independently. Verified against current master that the underlying premise still holds: recv_results_rows, BoundStatement.bind(), and NumpyParser.parse_rows() are unchanged there, so this is still a real, non-redundant optimization (#582 is still open).

Ran tests/unit/column_encryption/, tests/unit/test_protocol.py, and the full tests/unit/ suite (778 passed, 38 pre-existing skips, no failures) against a fresh in-place Cython build, including the Cython-path tests (CythonParserTest, test_numpy_parser_rejects_encryption).

One note for awareness: #765 (draft, also mine) independently optimizes the pure-Python recv_results_rows path with a more aggressive single-pass inline decode. It doesn't overlap with the Cython/bind() work here, but both touch the same function in protocol.py — worth reconciling the two before either merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Optimizes result parsing and prepared-statement binding by selecting encrypted or plain paths once per operation.

Changes:

  • Splits Python and Cython row decoding into encrypted/plain paths.
  • Adds NumPy encryption rejection and parser tests.
  • Optimizes BoundStatement.bind() policy checks.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/unit/test_protocol.py Adds decoding and parser tests.
cassandra/row_parser.pyx Selects the row parser path once.
cassandra/query.py Splits encrypted/plain binding paths.
cassandra/protocol.py Splits pure-Python result decoding.
cassandra/parsing.pyx Defines separate row parser methods.
cassandra/parsing.pxd Updates Cython parser declarations.
cassandra/obj_parser.pyx Implements optimized tuple decoding paths.
cassandra/numpy_parser.pyx Rejects column encryption configurations.

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

Comment thread tests/unit/test_protocol.py
Comment thread cassandra/numpy_parser.pyx
Comment thread tests/unit/test_protocol.py
Copilot AI review requested due to automatic review settings July 30, 2026 06:32
@mykaul
mykaul force-pushed the remove_enc_cols branch from 465fd9e to e90898b Compare July 30, 2026 06:32

Copilot AI left a comment

Copy link
Copy Markdown

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 8 out of 8 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

cassandra/numpy_parser.pyx:86

  • This exception is swallowed in production by make_recv_results_rows, whose broad except Exception fallback rewinds the buffer and successfully parses encrypted rows with TupleRowParser instead. Consequently NumpyProtocolHandler returns with parsed_rows still unset rather than rejecting the unsupported configuration; the test only calls NumpyParser directly and misses the wrapper behavior.
        if desc.column_encryption_policy:
            raise NotImplementedError(
                "NumpyParser does not support column encryption")

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cassandra/row_parser.pyx (1)

40-53: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Re-raise NotImplementedError before the fallback decode

colparser.parse_rows(reader, desc) is wrapped by a blanket except Exception, so NumpyParser.parse_rows()’s NotImplementedError for encrypted results gets caught here. The TupleRowParser fallback then decodes the rows successfully but never assigns self.parsed_rows, so NumpyProtocolHandler can return with missing results instead of failing on unsupported column encryption.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cassandra/row_parser.pyx` around lines 40 - 53, Update the exception handling
around colparser.parse_rows in the row parsing flow to immediately re-raise
NotImplementedError before entering the TupleRowParser fallback. Preserve the
existing fallback for other decoding exceptions, ensuring unsupported encrypted
results fail explicitly instead of continuing without assigning
self.parsed_rows.
🧹 Nitpick comments (2)
tests/unit/test_protocol.py (1)

799-814: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test bypasses the real NumpyProtocolHandler codepath.

This test calls NumpyParser().parse_rows(reader, desc) directly. In production, NumpyProtocolHandler invokes this through make_recv_results_rows's recv_results_rows wrapper (cassandra/row_parser.pyx), whose broad except Exception currently swallows this exact NotImplementedError and silently falls back to TupleRowParser without setting self.parsed_rows (see comment on cassandra/row_parser.pyx). A test that exercises recv_results_rows end-to-end (e.g., via make_recv_results_rows(NumpyParser())) would have caught this.

Want me to draft this additional test case?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_protocol.py` around lines 799 - 814, The test should exercise
the production NumpyProtocolHandler path rather than calling
NumpyParser.parse_rows directly. Update test_numpy_parser_rejects_encryption to
invoke make_recv_results_rows(NumpyParser()) and its recv_results_rows wrapper
with the same reader and parse description, then assert NotImplementedError
propagates instead of being swallowed or falling back to TupleRowParser.
cassandra/query.py (1)

687-731: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated boilerplate from splitting encrypted/plain paths. Both sites split a single per-value loop into two policy-specific loops (as intended by this PR), but in doing so duplicated the surrounding value-guard/error-handling boilerplate verbatim, rather than only branching on the actual serialize/deserialize step.

  • cassandra/query.py#L687-L731: extract the None/UNSET_VALUE handling and the except (TypeError, struct.error) message construction into a single loop, branching only on how col_bytes is computed (a small _serialize(value, col_spec) closure per branch), instead of duplicating the whole loop body.
  • cassandra/obj_parser.pyx#L69-L128: factor the identical except Exception as e: raise DriverException(...) block in unpack_col_encrypted_row/unpack_plain_row into a shared helper (e.g. a cdef function taking desc, i, e), so future changes to the error message only need to be made once.
♻️ Example for query.py
-        if ce_policy:
-            for value, col_spec in zip(values, col_meta):
-                if value is None:
-                    self.values.append(None)
-                elif value is UNSET_VALUE:
-                    ...
-                else:
-                    try:
-                        col_desc = ColDesc(col_spec.keyspace_name, col_spec.table_name, col_spec.name)
-                        uses_ce = ce_policy.contains_column(col_desc)
-                        if uses_ce:
-                            col_type = ce_policy.column_type(col_desc)
-                            col_bytes = col_type.serialize(value, proto_version)
-                            col_bytes = ce_policy.encrypt(col_desc, col_bytes)
-                        else:
-                            col_type = col_spec.type
-                            col_bytes = col_type.serialize(value, proto_version)
-                        self.values.append(col_bytes)
-                    except (TypeError, struct.error) as exc:
-                        ...
-        else:
-            for value, col_spec in zip(values, col_meta):
-                if value is None:
-                    self.values.append(None)
-                elif value is UNSET_VALUE:
-                    ...
-                else:
-                    try:
-                        col_type = col_spec.type
-                        col_bytes = col_type.serialize(value, proto_version)
-                        self.values.append(col_bytes)
-                    except (TypeError, struct.error) as exc:
-                        ...
+        if ce_policy:
+            def _serialize(value, col_spec):
+                col_desc = ColDesc(col_spec.keyspace_name, col_spec.table_name, col_spec.name)
+                if ce_policy.contains_column(col_desc):
+                    col_bytes = ce_policy.column_type(col_desc).serialize(value, proto_version)
+                    return ce_policy.encrypt(col_desc, col_bytes)
+                return col_spec.type.serialize(value, proto_version)
+        else:
+            def _serialize(value, col_spec):
+                return col_spec.type.serialize(value, proto_version)
+
+        for value, col_spec in zip(values, col_meta):
+            if value is None:
+                self.values.append(None)
+            elif value is UNSET_VALUE:
+                ...
+            else:
+                try:
+                    self.values.append(_serialize(value, col_spec))
+                except (TypeError, struct.error) as exc:
+                    ...
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cassandra/query.py` around lines 687 - 731, In cassandra/query.py lines
687-731, consolidate the encrypted and plain serialization paths into one value
loop, reusing shared None/UNSET_VALUE guards and TypeError/struct.error handling
while branching only through a small per-branch serializer for col_bytes. In
cassandra/obj_parser.pyx lines 69-128, extract the identical
exception-to-DriverException logic from unpack_col_encrypted_row and
unpack_plain_row into a shared cdef helper accepting desc, i, and the exception,
and use it from both functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@cassandra/row_parser.pyx`:
- Around line 40-53: Update the exception handling around colparser.parse_rows
in the row parsing flow to immediately re-raise NotImplementedError before
entering the TupleRowParser fallback. Preserve the existing fallback for other
decoding exceptions, ensuring unsupported encrypted results fail explicitly
instead of continuing without assigning self.parsed_rows.

---

Nitpick comments:
In `@cassandra/query.py`:
- Around line 687-731: In cassandra/query.py lines 687-731, consolidate the
encrypted and plain serialization paths into one value loop, reusing shared
None/UNSET_VALUE guards and TypeError/struct.error handling while branching only
through a small per-branch serializer for col_bytes. In cassandra/obj_parser.pyx
lines 69-128, extract the identical exception-to-DriverException logic from
unpack_col_encrypted_row and unpack_plain_row into a shared cdef helper
accepting desc, i, and the exception, and use it from both functions.

In `@tests/unit/test_protocol.py`:
- Around line 799-814: The test should exercise the production
NumpyProtocolHandler path rather than calling NumpyParser.parse_rows directly.
Update test_numpy_parser_rejects_encryption to invoke
make_recv_results_rows(NumpyParser()) and its recv_results_rows wrapper with the
same reader and parse description, then assert NotImplementedError propagates
instead of being swallowed or falling back to TupleRowParser.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b8064b11-ebec-435f-928c-f0425296ed7e

📥 Commits

Reviewing files that changed from the base of the PR and between 9b5b037 and e90898b.

📒 Files selected for processing (8)
  • cassandra/numpy_parser.pyx
  • cassandra/obj_parser.pyx
  • cassandra/parsing.pxd
  • cassandra/parsing.pyx
  • cassandra/protocol.py
  • cassandra/query.py
  • cassandra/row_parser.pyx
  • tests/unit/test_protocol.py

mykaul added a commit to mykaul/python-driver that referenced this pull request Jul 30, 2026
…nt.bind()

When Cython serializers (from cassandra.serializers) are available and no
column encryption policy is active, BoundStatement.bind() now uses
pre-built Serializer objects cached on the PreparedStatement instead of
calling cqltype classmethods. This avoids per-value Python method dispatch
overhead and enables the ~30x vector serialization speedup from the Cython
serializers module.

The bind loop is split into three paths:
1. Column encryption policy path (unchanged behavior)
2. Cython serializers path (new fast path)
3. Plain Python path (no CE, no Cython -- removes per-value ColDesc/CE check)

Depends on PR scylladb#748 (Cython serializers module) and PR scylladb#630 (CE-policy
bind split).
Copilot AI review requested due to automatic review settings July 30, 2026 13:43
@mykaul
mykaul force-pushed the remove_enc_cols branch from e90898b to 189b887 Compare July 30, 2026 13:43
@coderabbitai coderabbitai Bot added the P2 label Jul 30, 2026
@mykaul

mykaul commented Jul 30, 2026

Copy link
Copy Markdown
Author

Addressing CodeRabbit's review comments (posted as PR comments rather than inline review threads, so replying here):

1. Critical: row_parser.pyx swallowing NotImplementedError (cassandra/row_parser.pyx#L40-53) - Fixed. Added except NotImplementedError: raise before the broad except Exception as e: around colparser.parse_rows(reader, desc), so the NumPy+column-encryption rejection propagates instead of silently falling back to TupleRowParser without setting self.parsed_rows. Same root cause Copilot flagged on numpy_parser.pyx line 86; resolved there too.

2. Nitpick: test bypasses the real NumpyProtocolHandler codepath (tests/unit/test_protocol.py#L799-814) - Done as suggested. test_numpy_parser_rejects_encryption now builds a ResultMessage subclass with recv_results_rows = make_recv_results_rows(NumpyParser()) (mirroring cython_protocol_handler) and asserts NotImplementedError propagates through recv_results_rows itself. Confirmed this fails against the pre-fix wrapper and passes with the fix.

3. Nitpick: duplicated boilerplate (query.py#L687-731, obj_parser.pyx#L69-128) -

  • cassandra/query.py: done, using essentially the suggested _serialize(value, col_spec) closure approach - the policy-truthiness check happens once (outside the loop) to pick the closure, then a single loop handles None/UNSET_VALUE/error-message construction for both branches.
  • cassandra/obj_parser.pyx: skipped intentionally. unpack_col_encrypted_row/unpack_plain_row are on the hot per-row/per-column decode path this whole PR is optimizing. Factoring the duplicated exception handling via a Python-level closure would reintroduce per-column call overhead in the exact loop this PR removes it from; doing it via a C-level function pointer/fused-type dispatch to avoid that overhead is a meaningfully larger, riskier change for a cosmetic cleanup, and isn't something we could fully validate without a proper benchmark run. Happy to revisit as a follow-up if desired.

All three items verified with the Cython/NumPy extensions built locally (python setup.py build_ext --inplace) and the full tests/unit/ suite green (778 passed, 38 pre-existing skips, 0 failures).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cassandra/obj_parser.pyx`:
- Around line 69-103: Add a one-time length validation in ParseDesc.__init__
ensuring deserializers has the same number of entries as colnames before
unpack_col_encrypted_row can run. Reject mismatches with a clear assertion or
established validation error, preserving valid ParseDesc construction and
preventing the boundscheck-disabled loop from indexing past deserializers.

In `@tests/unit/test_protocol.py`:
- Line 658: Replace the non-ASCII multiplication symbol in the comments near the
call-count assertions with ASCII wording or an ASCII operator, including both
affected comments. Preserve the comments’ meaning that the call occurs four
times across two rows and two columns.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 01123c65-85cd-4b34-a673-8c1516710444

📥 Commits

Reviewing files that changed from the base of the PR and between e90898b and 189b887.

📒 Files selected for processing (7)
  • cassandra/numpy_parser.pyx
  • cassandra/obj_parser.pyx
  • cassandra/parsing.pxd
  • cassandra/parsing.pyx
  • cassandra/query.py
  • cassandra/row_parser.pyx
  • tests/unit/test_protocol.py

Comment thread cassandra/obj_parser.pyx
Comment thread tests/unit/test_protocol.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

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 8 out of 8 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

tests/unit/test_protocol.py:804

  • This Cython-path test still does not verify the optimization it is intended to protect: the pre-change ce_policy and ce_policy.contains_column(...) implementation also calls contains_column twice, so this test would pass before and after the split. Reuse _BoolCountingPolicy here and assert its truthiness is evaluated once; that will catch a regression back to the per-column check in TupleRowParser.
        mock_policy = Mock()
        mock_policy.contains_column = Mock(return_value=False)

        desc = self._make_parse_desc(column_encryption_policy=mock_policy)

cassandra/query.py:695

  • The refactored column-encryption binding branch has no runnable automated coverage. The unit binding tests never configure column_encryption_policy, while the existing column-encryption integration class is unconditionally skipped, so regressions in contains_column/column_type/encrypt behavior or the one-time truthiness branch will not be detected. Add a focused BoundStatement.bind() unit test covering both encrypted and unencrypted columns under a policy.
        if ce_policy:
            def _serialize(value, col_spec):
                col_desc = ColDesc(col_spec.keyspace_name, col_spec.table_name, col_spec.name)
                if ce_policy.contains_column(col_desc):
                    col_type = ce_policy.column_type(col_desc)

mykaul added 5 commits July 31, 2026 18:56
Split recv_results_rows() into two code paths: one for when
column_encryption_policy is set and one for the common case without it.
This avoids the per-value 'ce_policy and ce_policy.contains_column(...)'
check, which is redundant for the vast majority of queries.
Split TupleRowParser.unpack_row() into unpack_plain_row() and
unpack_col_encrypted_row(), branching once in the callers (ListParser,
LazyParser, row_parser error path) rather than per value.

- Add boundscheck/wraparound Cython optimizations on both methods
- Extract try/except outside the per-column loop
- NumpyParser.parse_rows() raises NotImplementedError when column
  encryption is configured, since it has no decryption support
- Update parsing.pxd declarations to match
Apply the same split-path optimization to the bind() method, checking
for column_encryption_policy once rather than per parameter.
Cover both the pure-Python path (ResultMessage.recv_results_rows) and
the Cython fast path (ListParser, NumpyParser) with and without
column encryption policy.
…licy

When column_encryption_policy is None (the common case), the ColDesc
namedtuple list is never accessed by unpack_plain_row(). Skip building
it entirely, passing None to ParseDesc instead.

This avoids N namedtuple allocations per query (where N = number of
columns) on the Cython fast path for non-encrypted workloads.
@mykaul
mykaul force-pushed the remove_enc_cols branch from 189b887 to 69f46f4 Compare July 31, 2026 15:58
Copilot AI review requested due to automatic review settings July 31, 2026 15:58
@coderabbitai coderabbitai Bot removed the P2 label Jul 31, 2026

Copilot AI left a comment

Copy link
Copy Markdown

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 8 out of 8 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

cassandra/numpy_parser.pyx:86

  • This rejects every NumPy result whenever the cluster has a policy, including queries whose returned columns are all unencrypted. Column-encryption policies are selective (contains_column determines membership), so enabling encryption for one table would now break NumpyProtocolHandler queries against unrelated columns that previously decoded correctly. Reject only result sets containing an encrypted column.
        if desc.column_encryption_policy:
            raise NotImplementedError(
                "NumpyParser does not support column encryption")

cassandra/query.py:695

  • The new encryption-policy serialization branch has no active test coverage. tests/unit/test_parameter_binding.py extensively covers BoundStatement.bind() only without a policy, while the column-encryption integration suite is class-level skipped. Add unit cases proving an encrypted column uses column_type/encrypt, an unencrypted column keeps its schema serializer, and the no-policy path remains unchanged.
        if ce_policy:
            def _serialize(value, col_spec):
                col_desc = ColDesc(col_spec.keyspace_name, col_spec.table_name, col_spec.name)
                if ce_policy.contains_column(col_desc):
                    col_type = ce_policy.column_type(col_desc)

Comment thread cassandra/parsing.pyx
Comment on lines +23 to +34
if len(deserializers) != len(colnames):
# The row parsers (obj_parser.pyx TupleRowParser.unpack_plain_row /
# unpack_col_encrypted_row) index into `deserializers` with
# @cython.boundscheck(False), bounded by rowsize == len(colnames).
# A length mismatch here would turn into an out-of-bounds memory
# read at parse time instead of a clean, immediate error, so this
# invariant is validated once at construction time. Use a real
# exception (not `assert`) so the guard cannot be stripped by
# running Python with optimizations enabled (-O).
raise ValueError(
"deserializers must have the same length as colnames "
"(got %d deserializers for %d columns)" % (len(deserializers), len(colnames)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

column_encryption_policy is checked in reading every single value in decode_val()

3 participants