Skip to content

Releases: dwoolworth/goodm

v0.8.0

Choose a tag to compare

@dwoolworth dwoolworth released this 25 Oct 19:31
35cfbca

goodm 0.8.0 adds the three features deferred from 0.7: change streams, collection validators derived from the schema, and population of refs inside subdocuments. It also fixes SaveMany ignoring UnsetFields. Four behavior changes are called out below. Full detail is in the CHANGELOG.

Tested against

MongoDB Go
6.0.28, 7.0.45, 8.0.34 as single-node replica sets 1.21, 1.22, 1.23 in CI; 1.26 locally

The full suite runs with the race detector on every combination. Each pull request went through two or three adversarial review rounds until clean, and the merged release had a final cross-cutting review before this tag.

Added

  • Change streams. Watch(ctx, &Model{}) opens a change stream on the model's collection and returns a *ChangeStream whose Next, TryNext, and Each deliver ChangeEvent values: operation type, resume token, cluster and wall time, namespace, document key, full document, pre-image, and update description. ChangeEvent.Decode unmarshals the full document into a model and records it as the merge base, so a model decoded from an event can be saved with WithRetry. WatchOptions covers the pipeline, full-document and pre-image modes (FullDocument defaults to options.UpdateLookup), ResumeAfter/StartAfter/StartAtOperationTime, batch size, max await time, collation, and expanded events. ChangeStream.Decode decodes the current event through the collection, so client codecs and BSONOptions apply as in FindOne. Middleware runs under OpWatch and may replace OpInfo.Pipeline. See docs/watch.md.
  • Collection validators from the schema. Schema.JSONSchema() derives a $jsonSchema document from the struct's Go types and goodm tags. EnforceOptions.Validator with ValidatorApply installs it as each collection's validator (creating the collection if needed, leaving a matching validator untouched) and ValidatorVerify reports a mismatch without changing anything; ValidationLevel and ValidationAction set the server's level and action. A document goodm writes always passes the validator generated from its own schema; self-marshaling types are left unconstrained. Writes the server rejects return *DocumentValidationError (matched by ErrDocumentValidation) with the server's errInfo in Details. goodm inspect --json-schema prints each model's validator; CollectionOptions.SkipValidator exempts a model whose stored form the schema cannot describe.
  • Population of refs inside subdocuments. Populate and BatchPopulate accept dotted paths ("ship.carrier", "items.product") through struct, pointer, and slice fields, including self-recursive subdocument types. A path through a slice collects every element's refs into a slice target; a numeric segment ("items.0.product") selects one element. *bson.ObjectID ref fields are supported.

Changed

  • min/max on a string field count characters, not bytes, matching the server's minLength/maxLength.
  • required treats a value whose own IsZero method reports zero as missing (a non-nil *time.Time pointing at the zero time), as the driver's omitempty does.
  • UpdateFields given nil for a field that cannot hold one (a string, number, struct, or date) now stores the zero value the model holds instead of null.
  • Populate checks its arguments before looking anything up: the model must be a non-nil pointer to a struct (a slice used to be silently ignored), every target must be a non-nil pointer, and a path that can yield several ids must have a slice target, even when the field is currently empty. BatchPopulate reports a ref field of an unsupported type instead of skipping it.

Fixed

  • SaveMany silently ignored UnsetFields and WithRetry. It now removes the unset fields from every document and zeroes them on the saved models, and returns an error for WithRetry, which it cannot honor.
  • UnsetFields could remove an immutable field; unsetting one is now rejected by Update and SaveMany.
  • goodm migrate did not list planned index rebuilds; they are shown with ~ and counted.

Known limitations

Writes with UnsetFields (in Update and SaveMany) and the WithRetry merge use the default BSON registry, so a model whose stored form depends on codecs registered on the client cannot be unset-saved or merged with them; ChangeEvent.Decode on a detached event has the same limit, while ChangeStream.Decode does not. The driver exposes no way to read a collection's registry.

Pull requests: #12, #13, #14, #15, #16.

goodm 0.7.1

Choose a tag to compare

@dwoolworth dwoolworth released this 25 Oct 00:25
bb3cdc2

goodm 0.7.1 is a patch release. It fixes a lost-update bug in Update's WithRetry merge that dates from 0.5.0, the items the adversarial reviews of 0.7.0 flagged as pre-existing, and the housekeeping left after 0.7.0. One behavior change is called out below. Full detail is in the CHANGELOG.

Tested against

MongoDB Go
6.0.28, 7.0.45, 8.0.34 as single-node replica sets 1.21, 1.22, 1.23 in CI; 1.26 locally

The full suite runs with the race detector on every combination. The change went through seven adversarial review rounds plus a confirmation pass; the last full round also ran a 160-case WithRetry matrix and a six-writer stress run on all three servers with no lost updates.

Fixed

  • Update with WithRetry lost the caller's changes. The 3-way merge base was the model at the moment Update was called, which already held the caller's edits, so the diff found nothing to apply: on a version conflict the retry wrote the database copy back and returned nil, and a same-field conflict never produced a MergeConflictError. goodm.Model now remembers the document as last loaded or saved through goodm (FindOne, FindByID, Find, FindOneAndUpdate, FindOneAndReplace, Create, Update, UpdateFields, SaveMany) and the merge diffs against that. The base is normalized through the model type, advances after each merge round, is used only when it describes the same document at the model's current version, and is not recorded for writes inside a transaction. The merged document is applied without resurrecting map keys or omitempty values another writer removed, while bson:"-" and unexported fields keep their values. UnsetFields counts as the caller's removals. A model without a base (built by hand, loaded through FindCursor, or without an embedded goodm.Model) gets ErrVersionConflict instead of a merge.
  • enum on a slice or array field rejected every non-empty value. Each element is now checked and reported as field[i], including UpdateFields element paths; nil pointer elements are skipped.
  • UpdatedAt not restored on failed Update and SaveMany items.
  • goodm discover typed date fields as interface{}; they are time.Time.
  • GenerateModel with EmbedModel: false omitted the goodm import its init() needs.
  • Several UpdateOptions values passed to one call are combined, so UnsetFields(...) and WithRetry(n) work together. After an Unset save the fields are zeroed on the model.

Changed

  • ErrVersionConflict no longer refreshes the model's version. The refresh added in 0.5.0 let the next plain Update match the version filter and overwrite the other writer's changes. A conflicting model now keeps its stale version, in Update and SaveMany, until it is reloaded or saved with WithRetry. Models loaded or saved inside a transaction that later aborted must be reloaded before they are saved again.
  • The 0.7.0 changelog entry names the tested servers and Go versions.

Docs

Snapshot sessions in WithTransaction; enum semantics on slices; the WithRetry base and conflict rules; models after an aborted transaction.

Pull request: #11.

goodm 0.7.0

Choose a tag to compare

@dwoolworth dwoolworth released this 24 Oct 23:12
00ec44c

goodm 0.7.0 closes the MongoDB feature gaps found in a review of 0.6.1 and fixes the correctness bugs that review and the follow-up adversarial reviews surfaced. It contains breaking changes; see the end of these notes. Full detail is in the CHANGELOG.

Tested against

MongoDB Go
6.0.28, 7.0.45, 8.0.34 as single-node replica sets 1.21, 1.22, 1.23 in CI; 1.26 locally

The full suite runs with the race detector on every combination. CI now starts each server as a replica set, so transactions and write concern behavior are exercised on every run (#10).

Indexes

  • Field tags index=-1, index=hashed, index=text, index=2dsphere, sparse, and ttl=<duration|seconds>.
  • CompoundIndex gains Sparse, Name, ExpireAfterSeconds (with TTL(time.Duration)), PartialFilter, and Collation; Fields entries accept -field and field:text/hashed/2dsphere markers.
  • Tags inside subdocuments create dotted-path indexes.
  • One resolved IndexDefinition list per schema drives Enforce, Migrate, and goodm inspect; Enforce detects mismatches in keys, uniqueness, sparseness, TTL, partial filter, and collation, and Migrate plans rebuilds instead of parsing index names. Partial filters are normalized and compared canonically.

Query layer

  • FindByID, Count, Exists, Distinct, FindOneAndUpdate, FindOneAndReplace, FindOneAndDelete.
  • Projection, hint, collation, and batch size on finds; upsert (Upsert()), array filters, hint, and collation on UpdateOne/UpdateMany; hint and collation on deletes.
  • Aggregation options (AllowDiskUse, collation, hint, batch size) and new stages; Pipeline now runs middleware.

Bulk

  • NewBulk: a builder over BulkWrite with per-operation failure reporting.
  • SaveMany: the Update lifecycle for a slice of models, with per-document version filters and exact per-item outcomes (conflict, not found, duplicate key, write concern, transport), concurrent outside transactions and transaction-aware inside them.
  • CreateMany gains Unordered; *BulkError reports failed items with errors.Is/errors.As support and a WriteConcernError.

Errors, middleware, registry

  • ErrDuplicateKey / *DuplicateKeyError with collection, index, and field names.
  • Middleware can rewrite OpInfo.Filter and OpInfo.Pipeline; Update, UpdateFields, Delete, and SaveMany AND the rewritten filter with their own.
  • The registry is keyed by type (SchemaOf, Unregister); custom _id types are supported; Register rejects models the driver would store differently (no _id tag, goodm.Model embedded without inline, duplicate bson names, invalid inline tags).
  • UpdateFields validates its values, converts them to the field's Go type with an explicit whitelist of safe conversions, rejects immutable fields and unreachable paths, accepts dotted paths, and leaves the caller's map alone.

Fixed

  • min/max skipped zero values and truncated floats.
  • Middleware could not change the filter.
  • Schemas keyed by bare struct name collided across packages.
  • Populate ignored the referenced model's collection options.
  • Nested WithTransaction started a second session; a session without a transaction now gets one.
  • Compound text indexes always mismatched; discover lost index kinds, options, custom names, and second indexes on a field.

Breaking

  • GetAll() returns a sorted []*Schema; PlanMigration takes []*Schema.
  • Get(name) returns false for an ambiguous name; use SchemaOf.
  • FieldSchema.Min/Max are *float64.
  • min/max apply to zero values and to slice and map lengths.
  • unique/index/sparse/ttl tags inside subdocuments now create real indexes.
  • Only exported fields are schema fields; inline flattening follows the driver's rules; managed fields are found by bson name.
  • CreateMany returns *BulkError; unique violations everywhere are *DuplicateKeyError; UpdateFields returns ValidationErrors for bad values and drops undeclared subdocument keys.
  • Pipeline.Execute/Cursor run middleware.
  • Minimum Go version is 1.21.

Pull requests: #7, #8, #9, #10.

v0.6.1

Choose a tag to compare

@dwoolworth dwoolworth released this 25 Sep 15:16
v0.6.1
37da65c

Fixed

  • IndexMismatchRebuild no longer drops a non-unique index before recreating it as unique. On MongoDB 6.0+ (binary and featureCompatibilityVersion) the index is converted in place with collMod (prepareUnique: true, then unique: true), so there is no window where the collection is unindexed and a concurrent writer can insert a duplicate that would break the recreate and every subsequent Enforce. The read-only duplicate pre-check still runs first, so known duplicates never trigger a collMod. If a duplicate slips in between the pre-check and the conversion, prepareUnique is reverted and the existing index is left untouched. On any other failure of the final step the index is left in the prepared state (present, new duplicates rejected) and the next Enforce completes the conversion. Drop + recreate remains the fallback for servers that don't support prepareUnique and for mismatches collMod cannot fix (keys, sparse, partial, unique -> non-unique) (MSA-78).

Added

  • IndexSpec.PrepareUnique reports an index left in the collMod prepareUnique state; Enforce mismatch errors name that state explicitly, and IndexMismatchRebuild clears a stale one on a non-unique field in place.
  • IndexSpec.HasCollation, IndexSpec.HasTTL, IndexSpec.Hidden. Indexes with these options are never converted in place, so a rebuild still normalizes them to a plain unique index.
  • The duplicate pre-check now mirrors unique-index semantics for array (multikey) fields and no longer conflates dotted keys such as a.b with a_b.

goodm v0.6.0

Choose a tag to compare

@dwoolworth dwoolworth released this 25 Sep 02:39
19a756f

Fixed

  • Enforce now verifies the options of existing indexes instead of accepting any index by name alone. A goodm:"unique" field whose existing index is non-unique, sparse, or partial (and the reverse: a plain index field with a unique index) is reported as an EnforcementError by default. The same check applies to compound indexes. Previously such mismatches were silently skipped, so unique tags guaranteed nothing once any index of that name existed (MSA-76 / BUG-3136).

Added

  • EnforceOptions.IndexMismatchPolicy: IndexMismatchError (default, fail loudly), IndexMismatchRebuild (drop and recreate mismatched indexes; refuses to drop when rebuilding a unique index over duplicate data), and IndexMismatchIgnore (legacy name-only behavior).
  • IndexSpec type describing an existing index (keys, unique, sparse, partial).

Changed

  • Breaking: ListExistingIndexes now returns map[string]IndexSpec instead of map[string]bool.

Upgrade note

The default policy is now IndexMismatchError, so services whose databases already have drifted indexes will fail at boot with a descriptive EnforcementError until the index is rebuilt (or IndexMismatchRebuild/IndexMismatchIgnore is set). This loud failure is the point of the fix — the previous behavior silently left unique constraints unenforced.

goodm v0.3.0

Choose a tag to compare

@dwoolworth dwoolworth released this 17 Feb 02:15

What's New

Subdocument Support

goodm now recursively parses goodm tags on nested struct fields. Validation, defaults, and schema introspection work at any nesting depth — no separate registration needed for subdocument types.

type Address struct {
    Street string `bson:"street" goodm:"required"`
    City   string `bson:"city"   goodm:"required"`
    Zip    string `bson:"zip"    goodm:"default=00000"`
}

type Order struct {
    goodm.Model `bson:",inline"`
    Name        string      `bson:"name"    goodm:"required"`
    Address     Address     `bson:"address" goodm:"required"`
    Items       []OrderItem `bson:"items"`
}

New Features

  • Recursive validation — all goodm tags (required, enum, min, max) are enforced on nested struct fields and slice elements
  • Dotted error paths — validation errors use MongoDB-style paths: "address.street", "items[0].name", "shipping.address.city"
  • Subdocument defaults — default=X tags inside nested structs and slice elements are applied during Create and CreateMany
  • Schema introspection — FieldSchema.SubFields contains parsed inner fields; FieldSchema.IsSlice is true for []struct fields

Per-Schema Read/Write Concern

  • Configurable interface — implement CollectionOptions() on your model to set read preference, read concern, and write concern per collection
  • All CRUD, bulk, and pipeline operations automatically use the configured options

Schema Defaults & Optimistic Concurrency

  • default=X tag — zero-valued fields are set to their schema default during Create and CreateMany
  • __v version key — documents get a version field that increments on each Update, enabling optimistic concurrency control with ErrVersionConflict

Design Details

  • Leaf types (time.Time, bson.ObjectID, bson.Decimal128) are never recursed into
  • Circular references handled via seen-type tracking during parsing
  • Nil pointer subdocuments are skipped (no auto-materialization)
  • Marking a subdocument field as immutable makes the entire subdocument immutable

goodm v0.2.0

Choose a tag to compare

@dwoolworth dwoolworth released this 13 Feb 02:37
4217a50

What's New

New Features

  • BatchPopulate() -- resolve ref fields across a slice of models in a single $in query, eliminating N+1 overhead
  • String length validation -- min/max tags now enforce string length in addition to numeric bounds
  • Configurable drift sample size -- EnforceOptions.DriftSampleSize and DetectDrift() accept a sample size parameter (default 100)
  • Makefile -- make test (with -race), make build (with ldflags version injection), make lint

Improvements

  • Update optimization -- Update() skips the extra fetch when the schema has no immutable fields (1 query instead of 2)
  • Duplicate registration guard -- Register() returns an error if a model is already registered
  • Version injection -- CLI version is now set at build time via -ldflags, defaults to dev
  • Generated code -- init() uses log.Fatal instead of panic

Quality

  • golangci-lint clean -- fixed all errcheck and staticcheck issues across the codebase
  • Race condition tests -- concurrent registry, middleware, and validation tests pass with -race
  • Validation test suite -- comprehensive unit tests for string length, int bounds, enum, and required fields