Skip to content

DatasetReader::Scan honours options protowire-go's Scan drops (skip_validate, skip_post_decode, discard_unknown, on_secret_field, validator) #166

Closed

Description

@bfrancojr

DatasetReader::Scan binds each row with the reader's whole UnmarshalOptions. protowire-go's Scan builds fresh options that carry only the limits and the TypeResolver. So a cpp caller who sets any of five options gets a different bind than a Go caller with the same options. Found while fixing #164. That PR matched Go only on max_message_size, which the issue named.

What each side does (from the code, not measured)

Go, encoding/pxf/dataset_bind.go at protowire-go 903b4a7 (v1.16.1):

o := UnmarshalOptions{
	MaxNestingDepth:         tr.lim.maxDepth,
	MaxNumericLiteralDigits: tr.lim.maxDigits,
	MaxBytesLiteralLength:   tr.lim.maxBytesLiteral,
	MaxRepeatedCount:        tr.lim.maxRepeated,
	MaxBigFloatExponent:     tr.lim.maxBigFloatExp,
	TypeResolver:            tr.resolver,
}

cpp, src/pxf/dataset_reader.cc Scan: UnmarshalOptions opts = opts_;, then skip_validate = true (after its own once-per-type check) and max_message_size = 0.

Option Go Scan cpp Scan
skip_validate ignored: the schema is always checked (memoized) honoured, and documented in dataset_reader.h ("opts.skip_validate turns off Scan's schema check")
skip_post_decode ignored: a (pxf.required) empty cell is refused, (pxf.default) applied honoured, so the post-decode pass would be skipped
discard_unknown ignored honoured, e.g. in a block cell { unknown = 1 }
on_secret_field ignored: a pxf.Secret cell goes into the message honoured: the hook gets it
validator / report ignored honoured

These rows come from reading the code. The table has not been checked by running both ports on the same input. That should be the first step of the fix (a probe like tools/dataset_stream_sweep).

Options

  1. Match Go (the default: every port behaves as protowire-go does): Scan builds its options as Go does. cpp loses documented behaviour, at least the skip_validate line in dataset_reader.h. Reversible by a revert. It changes what callers are promised.
  2. Keep cpp's behaviour and raise it for protowire-go: if passing the options through is the better contract, Go changes first and cpp stays.
  3. Split: match Go where cpp's behaviour cannot have been relied on, and decide the rest one option at a time.

Decision needed

This is the owner's call. Option 1 changes a documented contract, and option 2 is a change to Go. No recommendation yet: the table above has to be measured first, and whether on_secret_field should reach Scan is a design question in itself.

Done when

  • The table above is measured against protowire-go for each option.
  • Scan behaves as decided, with a test per option.
  • dataset_reader.h says which options Scan uses.

Activity

  1. bfrancojr commented on Nov 7, 2026

    @bfrancojr
    CollaboratorAuthor

    Measured: the table holds on every row

    The same 12 cases ran through each port's Scan, each option off and on. Go was protowire-go 903b4a7 (v1.16.1). cpp was PR #167's head 50953c5. Row type dataset.v1.Event comes from testdata/dataset/paths.proto. The skip_validate cases use a probe type with a field named null, which the schema check refuses.

    Case Go Scan cpp Scan
    required req.n missing 2:1: required field "req.n" is absent same
    … with skip_post_decode same error OK
    default on req.d OK, req.d="dflt" same
    … with skip_post_decode req.d="dflt" req.d=""
    unknown field in a block cell 2:1: column desc: unknown field "nope" in dataset.v1.Desc same
    … with discard_unknown same error OK
    pxf.Secret cell OK, pw.value="s3cret" same
    … with on_secret_field hook not called, pw.value="s3cret" hook gets pw=s3cret, pw.value=""
    validator that always reports a violation OK 2:1: check: id: denied (probe.deny)
    type with a field named null PXF schema violations: … uses PXF-reserved name "null" same
    … with skip_validate same error OK

    So Go's Scan ignores all five options, and cpp's honours all five.

    Go is not consistent with itself

    Go's options-taking BindRow, the materializing path, does honour them: func (o UnmarshalOptions) BindRow(...) { return o.bindRow(msg, columns, row, false) }. So in Go today, the same row with the same options binds differently depending on whether it came from Parse + BindRow or from DatasetReader.Scan.

    The history reads as incidental. In d1d30ac (protowire-go#152, value cells), the reader started keeping only lim limits instead of the options. Scan rebuilt its options from those limits. Fields came back one at a time when a gap was found: TypeResolver in f1d98b8 (#173), MaxBigFloatExponent in 56afac9 (#185). I looked for anything that would say the dropping is intended and found nothing. No Go test runs Scan with any of the five options, and the spec says nothing about which options apply to streamed rows.

    Recommendation: option 2. Go changes, and cpp keeps its behaviour

    Go's reader would keep the caller's options, and Scan would bind with them except MaxMessageSize, as it does for the limits today. Then Scan and BindRow agree in Go, and cpp already matches. cpp then only needs the tests and the header line in this issue's done-when.

    • Classification: reversible by a revert. It changes a public behaviour of protowire-go, with five options that Go callers can pass today and that have no effect. Measured, not inferred. Live: any Go caller passing them to NewDatasetReader gets them silently ignored.
    • What would flip it: if Go's Scan dropping these is intended, option 1 is right instead, and Go's BindRow would be the one to change. A protowire-go maintainer's statement or a design note saying so would settle that. I found none.
    • Edge to decide with it: whether on_secret_field should reach streamed rows. cpp says yes. Under option 2, Go would say yes too.

    Waiting on your decision. I've changed no code for this issue.

  2. bfrancojr commented on Nov 7, 2026

    @bfrancojr
    CollaboratorAuthor

    Decided (owner, 2026-11-07)

    • Option 2. protowire-go changes: its reader keeps the caller's options, and Scan binds with them, except MaxMessageSize. cpp keeps its behaviour and gets a test per option, plus a header line saying which options Scan uses.
    • on_secret_field fires for streamed rows, as it does for Parse + BindRow.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions