Releases: apstndb/spanvalue
Release list
v0.8.3
Patch release since v0.8.2. Additive only.
Wire value extraction (#264)
WireValue exposes a GenericColumnValue's protobuf wire value for low-level ARRAY or STRUCT assembly, materializing an absent Value as an explicit protobuf NULL. Existing non-nil values are borrowed without cloning and must be treated as read-only.
WireValues applies the same contract to a GCV slice and returns a fresh result slice. The helpers replace duplicated wire extraction and NULL materialization in downstream memebridge integration code.
The WireValue / WireValues names replace the provisional ToProtoValue / ArrayWireValues names from issue #262: they avoid confusion with gcvctor.ProtoValue and apply equally to ARRAY and STRUCT assembly.
Full changelog
v0.8.2
Patch release since v0.8.1. Additive only.
gcvctor: WithType helpers (#259)
WithType, WithEquivalentType, and WithExactType retype an existing GenericColumnValue using spantype equivalence checks.
Dependencies
Full changelog
v0.8.1
Patch release since v0.8.0. Additive only.
gcvctor: PGNumericValueExact (#249 / #257)
PGNumericValueExact renders a *big.Rat as PG_NUMERIC at the exact decimal (smallest sufficient scale) instead of PGNumericValue's GoogleSQL-scale NumericString (9 fractional digits, silently rounding) — the PostgreSQL-dialect numeric value space is far wider, so exactness is refusable-loss territory: rationals without a finite decimal expansion return the new ErrInexactNumeric; nil returns ErrNilNumeric. MustPGNumericValueExact joins the fixture family. Feedback from apstndb/spanpg's dialect-adapter work, where the rounding had to be documented as a hazard.
Full changelog
v0.8.0
Breaking release. FormatConfig collapses to two fields — NullString and the ordered FormatComplexPlugins chain — completing the #250→#253 redesign: every formatting concern is now a plugin, assembled by hand or through the validating NewFormatConfig builder. Preset outputs are byte-identical to v0.7.6 (pinned by the golden batteries). Closes #252, #253, #217, #185, #205, #219, #221, #250. Minimum Go remains 1.24.
Removed → replacement
| Removed (deprecated in v0.7.6) | Replacement |
|---|---|
FormatConfig.FormatNullable |
append PluginFromNullable(f) last in the chain / NewFormatConfig(WithScalarFormatter(f), …) |
FormatConfig.FormatArray |
PluginForArray / WithArrayFormat |
FormatConfig.FormatStruct (+ the FormatStruct type, TypedStructFormat()) |
PluginForStruct / WithStructFormat; FormatTypedStruct is the exported paren function |
FormatConfig.Literal |
literal quote options are constructor-captured plugin state (LiteralFormatConfigWithOptions / WithLiteralQuote signatures unchanged) |
FormatLiteralValue (plugin value) |
LiteralValuePlugin(opts LiteralFormatOptions) constructor |
FormatConfigWithoutScalarPlugins |
prepend a total PluginFromNullable(f) via WithComplexPlugin, or build with NewFormatConfig |
ErrFormatNullableRequired, ErrNilFormatArray, ErrNilFormatStructField, ErrNilFormatStructParen, nil-callback panics, built-in ErrUnknownType for unknown codes |
one sentinel ErrUnhandledValue wrapping the type (coverage is a runtime property; Validate checks NullString + a non-empty, nil-free chain) |
writer DelimitedWriter.Header / .UnnamedFieldNamer, JSONLWriter.UnnamedFieldNamer fields |
constructor-only WithHeader / WithUnnamedFieldNamer (#221) |
Signature changes: FormatStructFieldFunc now takes Formatter instead of *FormatConfig; FormatComplexFunc and FormatNullableFunc are defined types instead of aliases (plain functions remain assignable). Formatter.GetNullString keeps its name (rename considered, declined — plugin-facing churn without payoff).
Internals deleted (#217 closed)
The function-pointer identity machinery (scalarFastPathActive, nullableFuncsEqual, the slow-path literal-quote interception) is gone: preset scalar plugins are unconditional chain members, and custom behavior enters the chain at an explicit position instead of replacing a field the framework then has to sniff.
JSON preset contract (#205 closed)
FormatJSONSimpleValue now gates on the supported scalar set, validates wire payloads, and falls through for unknown codes; stripped/escape-hatch configs error (ErrUnhandledValue) instead of silently emitting invalid JSON. JSON wire-as-is is documented as the contract and pinned with a denormalized-wire test; NULL renders via NullString: "null" as before.
New since v0.7.6
PluginForNullable[T]— the pre-composedPluginFromNullable(NullableFormatterFor(f))for the dominant single-scalar-type override; annotation-aware via the Decode dispatch (PGNumericmatches only PG_NUMERIC).ExampleNewFormatConfigand rewritten customization docs (doc.go).
Downstream impact (verified against all four consumers via local replaces before release)
- spanner-mycli, execspansql: no changes required; builds and test suites pass unmodified.
- spannersh: one line — the tuple-STRUCT recipe becomes
SpannerCLICompatibleFormatConfig().WithComplexPlugin(PluginForStruct(FormatSimpleStructField, FormatTupleStruct)). - spanpg:
PostgreSQLLiteralFormatConfigmoves toNewFormatConfig(WithPlugin×2 preserving plugin order,WithArrayFormat,WithStructFormatwith the field callback retyped toFormatter,WithScalarFormatter); its nested integration module needs no changes. - No consumer used the other removed identifiers; the alias→defined-type change was transparent everywhere.
Full changelog
v0.7.6
Final v0.7.x release. Additive plugin combinators and a validating config builder land the migration surface for the next breaking release (v0.8.0, #252/#253), with the affected FormatConfig fields now formally deprecated. Plus writer.RunRowSeqDeferredMetadata. No runtime behavior changes to existing call paths. Minimum Go remains 1.24.
Highlights
Per-type plugin combinators (#250 / #251, #254)
- Guards:
PluginForType(predicate, annotation/FQN-aware),PluginForTypeCode,PluginSkippingNull— lift the TypeCode/NULL boilerplate everyFormatComplexFuncauthor hand-rolled. NULL stays deliberately un-pre-filtered in the chain (plugins may own type-specific NULL renderings); the combinator is the per-plugin opt-out. Dogfooded in-repo: the protofmt PROTO/ENUM plugins now use them (plugin-level note: typed NULL falls through instead of returning the null string from inside the plugin; chain-level output is unchanged on every preset). - Lifts:
PluginFromNullable(the FormatNullable slow path as a chain plugin — same Decode dispatch incl. PG-annotated wrappers),NullableFormatterFor(single-wrapper-type formatter),PluginForArray,PluginForStruct(share the built-in branches' extraction and error classes; the struct field callback takesFormatter— the signature v0.8 alignsFormatStructFieldFuncto). Per-scalar-type overrides now compose with the chain as the base:SimpleFormatConfig().WithComplexPlugin(PluginFromNullable(NullableFormatterFor(...))). - New capability: plugins see NULL ARRAY/STRUCT values, so typed NULL renderings (
CAST(NULL AS bigint[])) are now expressible — the retired callbacks structurally could not (#253).
NewFormatConfig builder (#253 / #254)
NewFormatConfigwithWithNullString/WithPlugin(most-recent-first) /WithArrayFormat/WithStructFormat/WithScalarFormatterassembles a chain-only config (the v0.8 shape) in canonical order with build-time validation (ErrScalarFormatterRequiredetc.). All four presets rebuilt through the builder are byte-identical across the full type battery (pinned by tests).Validate()relaxation: nilFormatArray/FormatStruct/FormatNullableare accepted whenFormatComplexPluginsis non-empty (plugins may cover those shapes).
Deprecations (removed in v0.8.0)
| Deprecated | Replacement |
|---|---|
FormatConfig.FormatNullable |
append PluginFromNullable(f) last in the chain, or NewFormatConfig(WithScalarFormatter(f), …) (#252) |
FormatConfig.FormatArray |
PluginForArray / WithArrayFormat |
FormatConfig.FormatStruct |
PluginForStruct / WithStructFormat |
FormatConfig.Literal |
literal preset constructor options (#185) |
Lint will not warn you: staticcheck SA1019 does not flag deprecated struct fields used as composite-literal keys (
FormatConfig{FormatArray: …}), which is how hand-built configs typically use them. Audit by inventory (grep the field names) rather than relying on lint; the v0.8.0 removal will otherwise surface as a compile error.
writer: RunRowSeqDeferredMetadata (#248)
RunRowSeqvariant for row sources whose row type is known only after the first row.
Downstream verification
spanner-mycli, spannersh, execspansql, and spanpg (root + nested integration module) were built, tested, and linted against this release candidate via local replaces: all green, zero behavior diffs. spanpg is the only consumer using the deprecated fields directly (3 sites, migration inventoried).
Upgrading to v0.7.6
Compile-time: additive only. Runtime: no changes to existing call paths. Plan the v0.8.0 migration with the deprecations table above.
Full changelog
v0.7.5
Patch release since v0.7.4. Correctness fixes (GoogleSQL identifier quoting, writer error latching, error classification), additive gcvctor / protofmt APIs, dbsqlrows semantics hardening, docs, and CI. Minimum Go remains 1.24.
Highlights
dialect: GoogleSQL identifier escaping fix (#200 / #239)
QuoteIdentifiernow uses GoogleSQL string-literal escapes (\→\\,`→\`) instead of MySQL-style backtick doubling. A trailing backslash could previously escape the closing backtick (injection-shaped failure for SQL INSERT export with hostile identifiers). PostgreSQL quoting is unchanged.
writer: sticky write errors and whole-statement SQL emission (#204 / #246)
- All three writers latch the first output write failure; subsequent
Write*/Flushcalls return it and write nothing — the documented discard-after-write-error contract is now enforced instead of risking silent output corruption. - SQL INSERT statements are built in memory and emitted with a single
Write, so a mid-tuple I/O failure can no longer leave dangling output that a later nil-returningFlushterminates into invalid SQL. Batched rows are emitted at the batch boundary orFlushrather than streamed per row. - New
ErrInvalidSQLInsertKind: out-of-rangeWithSQLInsertKindvalues are rejected at construction instead of silently emitting plainINSERT. DelimitedWriter.Flushafter a lateHeader = trueno longer strands buffered rows behindErrHeaderAfterData.
spanvalue: ErrMalformedWire sentinel (#216 / #245)
- Malformed wire payloads (wrong
structpbkind, unexpected NULL in the wire validator, nilTypewith a value) now wrap the newErrMalformedWireinstead ofErrUnknownType, which is reserved for genuinely unknown type codes. Migrated sites include the PROTO/ENUM CAST wire-kind rejections (v0.7.1 #228) andprotofmt's non-string wire rejection.errors.Is(err, ErrUnknownType)consumers: reclassified cases no longer match; matchErrMalformedWirefor data problems.
gcvctor: remaining constructor gaps (#207 / #243)
PGOIDValue(PG-dialect PG.OID, decimal-string wire).- Validated string inputs:
UUIDStringValue(canonicalizes case/braced/URN forms) andJSONStringValue(validates, stores wire as-is; newErrInvalidJSON). Must*completions:MustArrayValue,MustNumericValueChecked,MustPGNumericValueChecked,MustUUIDStringValue,MustJSONStringValue.
protofmt: OnUnresolved handler (#180 / #244)
- Opt-in
OnUnresolved func(typeFQN string, code sppb.TypeCode) erroron both option types: invoked when a non-nil resolver fails to resolve a non-NULL PROTO/ENUM value. Return an error for strict mode, nil to keep the lenient fallthrough; nil handler = unchanged behavior. Runnable example included.
dbsqlrows (experimental): semantics hardening (#209, #210 / #242)
RowsReadstays zero whenWriteDataRowis nil, matchingwriter.RowIteratorResult.RowsRead.- New
ErrMissingStatsResultSetreplaces silent nilStatswhenReadResultSetStatsis requested without driver-side stats; scanned stats are preserved when the trailingNextResultSetadvance fails.
Documentation and CI
- dbsqlrows godoc renders correctly on pkg.go.dev; REPL guidance moved to
dbsqlrows/README.md(#211 / #241). - govulncheck is PR-blocking with a clean symbol-level baseline;
make vulncheckfor local parity (#177 / #240, toolchain-resolution fix in #247).
Upgrading to v0.7.5
Compile-time: additive APIs only; no removals or signature changes.
Runtime behavior changes (review if you use these patterns):
| Area | v0.7.4 | v0.7.5 |
|---|---|---|
QuoteIdentifier (GoogleSQL) with backtick/backslash in the name |
MySQL-style doubling; trailing \ broke quoting |
GoogleSQL string-literal escapes (#239) |
| Writer call after an output write error | Could keep writing / Flush could return nil on corrupt output |
Sticky error returned by every later call (#246) |
| Batched SQL INSERT emission | Streamed per row | Whole statement per Write at batch boundary / Flush (#246) |
WithSQLInsertKind(SQLInsertKind(99)) |
Plain INSERT silently |
ErrInvalidSQLInsertKind at construction (#246) |
| Malformed wire payloads | errors.Is(err, ErrUnknownType) |
errors.Is(err, ErrMalformedWire) (#245) |
dbsqlrows RowsRead with nil WriteDataRow |
Counted drained rows | Stays zero (#242) |
| dbsqlrows stats requested but driver stats disabled | Silent Stats == nil |
ErrMissingStatsResultSet (#242) |
Full changelog
v0.7.4
Patch release since v0.7.3. Additive writer APIs for rows that do not come from a *spanner.RowIterator, plus documentation. No compile-time API removals, signature changes, or runtime behavior changes to existing APIs. Minimum Go remains 1.24.
Highlights
writer: RunRowSeq / WriteRowSeq for rows without a RowIterator (#238)
RunRowSeqandWriteRowSeqdriveRowIteratorHooks/RowIteratorWritersinks from client-side (virtual) result sets: explicit*sppb.ResultSetMetadataplus a fallible row sequence (iter.Seq2[*spanner.Row, error]).- The hook contract is identical to
RunRowIteratorby construction (shared internal loop):PrepareMetadataruns once before the first data row including for an empty sequence (header-only delimited output), a yielded error aborts withoutFinish,RowsReadcounts successful writes,Statsstays zero. RowSeqadapts pre-built rows to the sequence shape. NewErrNilRowSeq; a(nil, nil)pair yielded by a sequence is rejected at the boundary withErrNilRow.- Pairs with lazy per-row encoders that can fail — designed against spanenc
RowEncoder.Rows(apstndb/spanenc#2), surveyed from spanner-mycli SHOW/HELP virtual result sets (apstndb/spanner-mycli#657).
Documentation
ExampleWriteRowSeq(runnable, pkg.go.dev), writer godoc# RowIteratorsection, andwriter/README.mdgoal-table row for in-memory / virtual rows.
Upgrading to v0.7.4
From v0.7.3
Compile-time: additive APIs only (RunRowSeq, WriteRowSeq, RowSeq, ErrNilRowSeq).
Runtime: no behavior changes to existing APIs.
Full changelog
v0.7.3
Patch release since v0.7.2. Fixes wire output from JSONFromNullable and PGJSONBFromNullable when Value is a Go string, aligning with the official Spanner client's encodeValue. No compile-time API removals or signature changes. Minimum Go remains 1.24.
Highlights
gcvctor JSON nullable wire fix (#237, #236)
JSONFromNullableandPGJSONBFromNullableno longer pass a GostringValuethrough as raw wire JSON. Both helpers now marshal likeJSONValue/PGJSONBValue: a string becomes a quoted JSON string on the wire, matching the official client.- To store pre-encoded wire JSON as-is (validated and compacted), pass it as
encoding/json.RawMessage— the same convention the client follows. - Package docs and
ExampleJSONFromNullabledocument the string-vs-RawMessage semantics.
Input Value |
v0.7.2 wire | v0.7.3 wire |
|---|---|---|
"x" (string) |
x (invalid JSON) |
"x" |
json.RawMessage("{\"a\":1}") |
{"a":1} |
{"a":1} |
Upgrading to v0.7.3
From v0.7.2
Compile-time: no API removals or signature changes.
Runtime: if you relied on JSONFromNullable / PGJSONBFromNullable treating a Go string Value as pre-encoded wire JSON, switch that input to json.RawMessage. This matches the official client's encoding semantics and fixes invalid wire output for string column round-trips (e.g. spanenc adoption of #232).
Full changelog
v0.7.2
Patch release since v0.7.1. Additive gcvctor constructors for string-based and nullable inputs, documentation for gcvctor traps and the spanenc adoption boundary, deprecation of JSONObjectStructFormat, and internal dead-code cleanup. No compile-time API removals or signature changes. Minimum Go remains 1.24.
Highlights
gcvctor string-based and nullable constructors (#232)
StringBasedValueOfandStringBasedValueFromCodeconstruct GCVs from wire strings with caller-supplied types (no validation beyond type assignment).- Nullable input helpers for spanenc-style construction: pointer-based (
BoolFromPtr,Int64FromPtr,Float64FromPtr,Float32FromPtr,StringFromPtr,BytesFromSlice,DateFromPtr,TimestampFromPtr,UUIDFromPtr,IntervalFromPtr) andspanner.Null*wrappers (BoolFromNullable,Int64FromNullable,Float64FromNullable,Float32FromNullable,StringFromNullable,DateFromNullable,TimestampFromNullable,UUIDFromNullable,IntervalFromNullable,NumericFromNullable,JSONFromNullable,PGNumericFromNullable,PGJSONBFromNullable).
Documentation (#222, #233)
gcvctorpackage docs: wire-string traps,StringBasedValueOfsemantics, and when to use explicit constructors vs spanenc.- Root package and
AGENTS.md: spanenc adoption boundary (reflection / client-tag encoding stays in spanenc;gcvctorremains explicit and strict). - Godoc gaps filled for dialect helpers, literal quoting, and JSON row formatting.
JSONObjectStructFormat deprecated (#215)
JSONObjectStructFormatis deprecated in favor ofNewJSONObjectStructFormatter. Behavior is unchanged; migrate call sites at your convenience.
Internal cleanup (#214)
- Removed unused literal helper functions from
internal/(no public API impact).
Upgrading to v0.7.2
From v0.7.1
Compile-time: additive APIs only. No signature removals or renames.
Runtime: no intentional behavior changes to formatting presets or export paths. Existing JSONObjectStructFormat call sites continue to work; prefer NewJSONObjectStructFormatter for new code.
Full changelog
v0.7.1
Patch release since v0.7.0. Correctness fixes for gcvctor wire strings, SQL INSERT zero-column writes, Spanner CLI / Literal float formatting, and ENUM CAST validation. Documentation-only updates for godoc accuracy and Gemini review styleguide. No compile-time API removals or signature changes. Minimum Go remains 1.24.
Highlights
gcvctor wire string correctness (#225)
TimestampValuenormalizes to UTC Zulu wire strings (matches Spanner client andTimestampStringValue).JSONValueandPGJSONBValuemarshal without HTML character escaping, matching Spanner-emitted JSON wire for fixtures.dbsqlrows/gospannerrequire bumped tospanvalue v0.7.0(nested module metadata only).
SQL INSERT zero-column guard (#226)
SQLInsertWriterWriteValues/WriteRownow returnErrMissingColumnNamesfor a registered empty row type, matchingWriteGCVsand preventing invalidINSERT INTO t () VALUES ();output.
Float and ENUM literal formatting (#228 / #206)
- Literal preset: finite integral FLOAT64 values append
.0when the shorteststrconvform would lex as INT64 (e.g.1.0not1). - Spanner CLI preset: integral finite floats omit the fractional part (
1not1.000000); non-integral values keep six decimal places (matches spanner-cli). FormatEnumAsCastvalidates ENUM wire payloads withParseIntbefore emittingCAST(... AS ...)SQL.
Documentation (#227, #229)
- Godoc accuracy sweep for
writer,gcvctor, anddbsqlrows(no behavior change). - Gemini styleguide: clarify
[Type.Method]vs[*Type.Method]doc links.
Upgrading to v0.7.1
From v0.7.0
Compile-time: no signature removals or renames. Additive test coverage only.
Runtime behavior changes (review if you use these patterns or assert golden strings):
| Area | v0.7.0 | v0.7.1 |
|---|---|---|
gcvctor.TimestampValue with non-UTC time.Time |
Local offset in wire string | UTC Zulu wire (#225) |
gcvctor.JSONValue / PGJSONBValue with <, >, & in JSON |
HTML-escaped wire (\u003c etc.) |
Unescaped wire matching Spanner (#225) |
SQLInsertWriter write after zero-column PrepareRowType |
Could emit invalid INSERT ... () VALUES () |
ErrMissingColumnNames (#226) |
Literal preset integral FLOAT64 (e.g. 1.0) |
Shortest form without .0 |
1.0 when needed to avoid INT64 lex (#228) |
| Spanner CLI preset integral floats | 1.000000 |
1 (#228) |
ENUM CAST with non-integer wire string |
Could emit invalid SQL | Parse error (#228) |
Preset-backed Simple export and typical CSV/JSONL paths are unaffected. Re-golden Literal, Spanner CLI, or hand-built gcvctor fixture tests if you assert the rows above.