Skip to content

feat(python): value rules for every producible type - #1511

Draft
yujiyokoo wants to merge 4 commits into
feat/python-sdk-queryfrom
feat/python-sdk-values
Draft

yujiyokoo wants to merge 4 commits into
feat/python-sdk-queryfrom
feat/python-sdk-values

Conversation

@yujiyokoo

Copy link
Copy Markdown
Contributor

Summary

The Python SDK's value rules are now tested for every EQL type it can produce, through the public API. EQL (Encrypt Query Language) is CipherStash's set of PostgreSQL types for encrypted values. A value of the wrong type, out of range, or naive is refused before any request, from encrypt and from query alike. The conversion is also checked against vitaminc's shared value corpus, and fuzzed.

Changes

  • Value corpus: crates/core/testdata/value-conformance.json is a copy of vitaminc's shared corpus, at the commit the Go module uses. The published vitaminc crates do not include it. A README.md beside it records the source and how to update it.
  • Corpus tests: crates/core/tests/value_conformance.rs checks three things:
    • Each vector of a kind the SDK writes gives the exact bytes and reads back as its value.
    • Every malformed vector is refused.
    • Every normalized vector reads as the canonical value the corpus states.
  • Value tests: tests/test_values.py runs 18 refused values and 19 accepted edge values through encrypt, and through query where the marker has an eq index. Every InvalidValueError code is covered with no request.
  • Floats: NaN, infinity, and -0.0 are accepted on Real and Double fields. The engine and PostgreSQL both keep them, and a live test round-trips them.
  • Fuzz target: crates/core/fuzz is a detached cargo-fuzz crate with one target, value_frame.
    • An accepted value must frame, read back as itself, and match the bytes that ValueFrame::of gives the plain Rust type.
    • Arbitrary bytes read as a passthrough must never panic.
    • The root task fuzz:python-values runs it, and fuzz.yml replays its seeds on a pull request.
  • Fuzz path filter: fuzz.yml now also runs for stack-profile, stack-encrypt-derive, eql-bindings, and the Go guest's targets.rs. A fuzz target compiles all four. The new scripts/__tests__/fuzz-workflow-paths.test.mjs derives the filter from each fuzz crate's Cargo.lock.
  • Live round trip: one model with a field for each of the 49 producible types goes through PostgreSQL and back. Each field is compared exactly, so a Decimal's scale and a float's sign count.
  • Shared code: values::Typed and Scalar::value() replace five copies of a frame writer and two of a value conversion.

Verification

  • pytest on Python 3.14 and 3.9: 353 passed, 25 skipped. The skips are the 23 live tests and 2 gate-probe tests.
  • mise run test:cargo: all core, corpus, and extension tests pass.
  • clippy and cargo fmt --check: no findings, on the workspace and on the fuzz crate.
  • mypy: no findings. pyright: 0 errors and the 3 earlier warnings.
  • pnpm run test:scripts: 84 of 84 files pass.
  • An offline check declares the 49-field model and runs all 49 values through the value checks. The call stops only at the missing client key.
  • The fuzz target has not run locally. The local nightly toolchain, rustc 1.88.0-nightly, is too old for the build. The fuzz-regression job on this PR is its first run.
  • The live tests have not run locally. This machine has no CipherStash credentials. The live job on this PR is their first run.

Related

Closes #1392. Refs #1341.

Review notes

  • Out of scope: JsonSearch. The catalog marks it producible, but the json kind has no Python type yet, and the decorator refuses it. Supporting it needs its own design: the Python types a JSON field takes, the rules for the values inside, and JSON queries.
  • The corpus copy can miss rows that vitaminc adds later. vitaminc never changes or removes a row, so an older copy is still correct.
  • fuzz:python-values is in the root mise.toml, not in languages/python/mise.toml. A script test requires every task there to run from tests-python.yml, and the fuzz replay runs from fuzz.yml.
  • The order query for NaN is not checked here. That belongs to the later order query ticket.

🤖 Generated with Claude Code

yujiyokoo and others added 4 commits November 3, 2026 00:41
Run vitaminc's shared value corpus against the SDK's value conversion.
crates/core/testdata/value-conformance.json is a copy at vitaminc
06568ba93, the commit Go uses, because the published crates do not
include it. Each vector of a kind the SDK writes must give its exact
bytes and read back as its value. Every malformed vector is refused.
A new vector of an unknown kind fails the test until it is handled.

Test every InvalidValueError code through encrypt and through query,
with no request. A float NaN, infinity or -0.0 is accepted: the engine
and PostgreSQL both keep them, and a live test round-trips them.

Add a cargo-fuzz crate, crates/core/fuzz, over the conversion both
ways. An accepted value must frame, read back as itself, and match the
bytes ValueFrame::of gives the plain Rust type. Arbitrary bytes read
as a passthrough must never panic. The root task fuzz:python-values
runs it, and fuzz.yml replays its seeds on a pull request.

A live test round-trips one field of each of the 49 producible types
through PostgreSQL. JsonSearch is out of scope: the json kind has no
Python type yet, and the decorator refuses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run fuzz:python-values and the campaign's cmin step from the root,
naming the fuzz crate with --fuzz-dir. From a folder under
languages/python, mise also loads that folder's mise.toml, and
installs its Python and uv, which the fuzz jobs do not cache.

Compare each field of the 49-type live round trip exactly: a Decimal
by as_tuple(), so its scale counts, and a float by its bytes, so the
sign of zero counts. Python's == treats 1.50 as 1.5 and -0.0 as 0.0.

Check the normalized vectors against the corpus's reencode_value, not
against a second read of the same bytes. The canonical value must
write back as the reencode bytes, and a vector whose two byte strings
are equal fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fuzz.yml's pull_request filter missed four paths that a fuzz target
compiles: stack-profile, stack-encrypt-derive, eql-bindings, and the
Go guest's targets.rs, which the Python core crate compiles by #[path].
A change to any of them could break a harness with no job to notice.
The stack-* fuzz crates already compiled the first two.

Add the four paths. The new fuzz-workflow-paths.test.mjs reads each
fuzz crate's Cargo.lock and fails when the filter misses a crate a
target compiles, or a file one compiles by #[path].

Move the frame writer into values::Typed, and the scalar-to-value
conversion into Scalar::value(). They replace three Written copies,
Carried in decrypt.rs, Typed in query.rs, and two value_of copies.

Correct the fuzz comment: values::read range-checks a timestamp but
not a date or a decimal, so those are the cases read accepts and
write refuses. The testdata README now names the two counts the
corpus test checks, which a corpus update may change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lk (#1392)

fuzz-workflow-paths.test.mjs scanned #[path] with its own regex over
raw source. It read only each crate's src/, and counted a #[path]
quoted in a comment. It now uses compiledCrateFiles from
lib/rust-compiled-files.mjs, which strips comments and follows mod,

The lockfile walk moves to lib/cargo-lock.mjs, and python-ci.test.mjs
uses it too.

The corpus test now reads a typed null's reencode bytes, which must
be a plain null. The fuzz.yml header no longer counts six targets,
and docs/fuzzing.md lists mise.test.toml in the path filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Nov 2, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 10bdd53

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Mutation testing (cargo-mutants, --in-diff, stack-auth + stack-encrypt)

No mutants were generated for the changed lines.

@blacksmith-sh

blacksmith-sh Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Found 2 test failures on Blacksmith runners:

Failures

Test View Logs
test_encrypt_live/test_a_sqlalchemy_class_is_built_without_its_ignored_relationship View Logs
test_encrypt_live/
test_rows_read_back_with_dict_row_decrypt_into_the_models[SqlAlchemyUser]
View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

This branch has not been deployed

No deployments
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.

1 participant