Skip to content

fix(dfns): multiple fixes/improvements - #349

Merged
wpbonelli merged 11 commits into
MODFLOW-ORG:developfrom
wpbonelli:migration-fixes
Aug 20, 2026
Merged

fix(dfns): multiple fixes/improvements#349
wpbonelli merged 11 commits into
MODFLOW-ORG:developfrom
wpbonelli:migration-fixes

Conversation

@wpbonelli

@wpbonelli wpbonelli commented Aug 20, 2026

Copy link
Copy Markdown
Member

Migration fixes, schema improvements, and cleanup motivated by migrating the flopy4 prototype to the latest schema

  • try_parse_bool considered no value to be false instead of true
  • split overloaded pk/fk semantics into three distinct concerns
    • index (needs MF6's 1-based↔0-based conversion)
    • node (grid-cell reference, resolved from the model's DIS/DISV/DISU at runtime)
    • pk/fk (pure relational identity)
  • remove unused, never-wired-up Array.repeat attribute
  • rename _can_share_model -> _receives_from

wpbonelli and others added 11 commits August 20, 2026 07:42
v1 DFN syntax allows a boolean attribute line with no value at all (e.g.
a bare `optional` line, as opposed to `optional true`/`optional false`),
meaning the attribute is set. `load_dfn` parses such a line to an empty
string (as opposed to `None` for a wholly absent attribute), but
`try_parse_bool` treated "" as `False` unconditionally, silently
inverting the flag regardless of the caller's default.

Fixed at the root in `try_parse_bool` rather than patching call sites
individually, since the bug affects every boolean DFN attribute that
uses the bare-line convention, not just `optional`. Real-corpus impact
turned out wider than the printrecord family originally scoped: chf-dfw,
chf-disv1d, gwf-npf, olf-dfw, olf-disv1d, swf-dfw, swf-disv1d, and
sim-nam also had bare-`optional` fields now migrated correctly.

Updates one existing test that had the old buggy rendering of gwf-oc's
columns/width/digits baked in as required; they render bracketed as
optional now, matching flopy4's independently-confirmed real MF6
behavior for the same fields.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Array.repeat was added in the v2 schema rewrite (MODFLOW-ORG#311) as a
`str | None` field-name reference, replacing v1's plain `repeating: bool`,
but the migration was never wired up to populate it, and the spec's
promised elaboration ("See `repeat` section below") was never written --
no such section exists.

Checked what the 3 real corpus fields with v1 `repeating: true`
(utl-tas.tas_array, prt-oc.times, prt-prp.times) would even reference:
all 3 also have v1 `shape (any1d)`, which the mapper already collapses
to `shape=[]` (self-sizing), matching dfnspec.md's existing self-sizing
semantics exactly. None has a sibling field to point to as a count
source, so the reference design never matched real data. Removing
rather than backfilling a migration for an attribute nothing consumes
and no data supports.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…fixup

_mark_lonely_pk matched a block's list field by the *block's own* name
(gated to a "packagedata"/"perioddata" allowlist), but MF6's convention
names the block "period" and its list field "perioddata" -- the two are
never the same string, so the "perioddata" entry in the allowlist could
never match any component. Fixed by matching on the list field's own
name instead (_LONELY_PK_FIELDS). Confirmed via corpus scan: this was
dead for every package, not just the gwf-mvr case the old docstring
called out; utl-spc.bndno is the first real catch.

This also required moving _mark_lonely_pk's call to after
_fix_lak_relations/_fix_mvr_relations: LAK's period `number` field (the
deliberately-ambiguous lake-or-outlet field split by _fix_lak_relations)
structurally looks exactly like a lonely pk before that split runs, and
_fix_lak_relations builds its lakeno/outletno fk copies via
number_field.model_copy(...), which would carry a stale pk=True into
fields that are actually foreign keys. Verified no other pass between
the old and new call sites reads pk/fk state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…te plan

Adds todo.md (working log of the v1->v2 migration gaps found during the
flopy4 dev3 migration, including the fixes in this branch and a
corpus-scan breakdown of the remaining numeric_index -> pk/fk gap into
five structurally distinct sub-issues) and
index-node-attributes-plan.md (implementation plan for splitting the
pk/fk vocabulary's conflated "needs 1-based/0-based conversion" and
"is a relational key" concerns into dedicated `index` and `node`
attributes, with a phased sequence and the flopy4-side coordination
this requires).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Splits the numeric_index -> pk/fk gap's serialization concern out of the
relational pk/fk vocabulary, per index-node-attributes-plan.md Phase 1.
Adds Integer.index and Array.index (dtype="integer" only, enforced by a
validator; never valid on String) to schema.py, documents both in
dfnspec.md, regenerates schema.json (also picking up pre-existing
unrelated drift: removed/deprecated/layered were missing from Array's
schema), and migrates index directly from v1 numeric_index in
migrate_to_v2_0_0_dev2.py for every Integer/Array(dtype="integer") field.

Confirmed this closes the gwf-lak.iconn regression tracked in todo.md
(MF6 rejected the file without the 1-based/0-based conversion index=True
provides) and coexists correctly with existing pk/fk (e.g. chf-cxs.idcxs
keeps pk=true and gains index=true). Full autotest/dfns/ snapshot suite
regenerated; diff is additive-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTmmUhFmfsjihggQUcZ3Ja
Splits the grid-cell-reference concern out of pk/fk entirely, per
index-node-attributes-plan.md Phase 2. Adds Integer.node to schema.py
(documentation-level placement convention, same as pk/fk — no schema
validator, mirroring how pk/fk placement is already unenforced);
removes _validate_fk_fields's fk == "node" exemption so it's a single
hierarchical-path-vs-fk_ref invariant check — an unqualified fk="node"
is no longer special-cased and must resolve to a real list block like
any other bare fk value, which structurally deprecates it without a
hard reject. Updates dfnspec.md (node documented under Integer,
"Primary and foreign keys" grid-cell-sentinel form replaced with a
pointer to node) and regenerates schema.json.

Backfills the 6 known corpus candidates (5 exchange components'
exchangedata.cellidm1/cellidm2, gwf-gnc's gncdata.cellidm/cellidn) via
a new _mark_node_refs pass in migrate_to_v2_0_0_dev2.py, keyed on an
explicit _NODE_REF_FIELDS allowlist — audited per field rather than
derived, since v1 has no attribute this maps from mechanically.

test_schema_relations.py's node-sentinel-specific rejection test
replaced with one confirming the new (unremarkable) behavior. Full
autotest/dfns/ suite green; snapshots regenerated, additive-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTmmUhFmfsjihggQUcZ3Ja
Completes the pk/fk backfill items from index-node-attributes-plan.md
Phase 3 (todo.md gap #2 sub-issues (b)/(d)/(f)), now that pk/fk no
longer double as the serialization signal (index/node cover that).

- _LONELY_PK_FIELDS widened to vertices/cell2d/cell1d: DISU/DISV/
  DISV1D/DISV2D's leading iv/icell2d/icell1d columns are genuinely the
  same lonely-pk shape _mark_lonely_pk already detects (12 components).
- New _apply_fk_backfill pass (mirrors Phase 2's _mark_node_refs) sets
  fk on three real single-column relations the general passes can't
  infer: SFR diversions.iconr -> packagedata.ifno (a real reference
  into reach numbers, confirmed from its description -- not a
  compound-scoped id like its sibling idv, which correctly keeps
  index only), UZF packagedata.ivertcon -> packagedata.ifno (a
  self-referential relation: a UZF cell may point to another UZF cell
  in the same list), and idcxs on chf-cdb/chf-zdg/olf-zdg/swf-zdg ->
  the matching *-cxs component's packagedata.idcxs (cross-component,
  which _resolve_relations is structurally single-component-scoped
  and can't reach).
- Cross-component fk backfill surfaced a latent validator bug:
  _validate_fk_fields's fk.split(".")[0] only ever read the first
  dot-segment as the block name, so the documented 3-segment
  "component.block.field" form would have been misparsed and
  rejected -- nobody had hit it before because no fk had ever
  actually used that form. Fixed to resolve the target component via
  spec.components when 3 segments are present.

Investigated and deliberately left alone (documented in todo.md/
index-node-attributes-plan.md rather than silently dropped): MAW's
connectiondata.icon (same compound-scoped, no-real-target shape as
gwf-lak.iconn -- index only is correct) and chf/olf/swf-dfw's idcxs
(an Array, shape=["nodes"], which can't carry fk under the current
schema at all).

All five decomposed sub-issues from the original numeric_index ->
pk/fk gap scan (a)-(g) are now resolved. Full autotest/dfns/ suite
green; snapshots regenerated, additive-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTmmUhFmfsjihggQUcZ3Ja
Follow-up to the Phase 3 pk/fk backfill commit, which left two cases
deliberately alone: MAW's connectiondata.icon (no real target -- same
shape as gwf-lak.iconn, index alone is correct and complete, reconfirmed
here rather than changed) and chf/olf/swf-dfw's idcxs (a per-cell grid
Array referencing the matching *-cxs component's packagedata.idcxs,
which couldn't be expressed because Array had no fk attribute at all).

The idcxs case turned out to be a real, closable gap rather than a
permanent schema limitation: adds Array.fk (dtype="integer" only, via
the same validator index already uses) with no pk/fk_ref counterpart
(an array has no rows of its own to key, and no sibling record for a
runtime component selector). _validate_fk_fields already validates it
for free -- its field walk reads fk/fk_ref via getattr regardless of
type, so no validator change was needed beyond the attribute existing.

Backfills fk = "<component>-cxs.packagedata.idcxs" on all 3 *-dfw
components via a new _apply_array_fk_backfill pass. Updates dfnspec.md
(Array's fk documented; "Primary and foreign keys" scope note widened)
and regenerates schema.json. Full autotest/dfns/ suite green; snapshots
regenerated (18 files, additive-only).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTmmUhFmfsjihggQUcZ3Ja
…control

Working notes for the dfns index/node/pk/fk migration plan, now that all
phases described in them are complete. Kept locally, untracked, rather
than deleted outright.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTmmUhFmfsjihggQUcZ3Ja
…schema.py comments

try_parse_bool is only used by the v2 dfn migration code, so move it out of
the generic misc module and into migrate_to_v2_0_0_dev2 (dev3 already
imports from that module). Also drop docstrings/comments on private schema.py
helpers that mostly restated the code, and rename _can_share_model to
_receives_from with requester/provider-named params to make the direction of
the model-scope dim-visibility check explicit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCFraGPS9DuhabpEnreYUy
@wpbonelli wpbonelli added this to the 1.10.0 milestone Aug 20, 2026
@wpbonelli wpbonelli added the bug Something isn't working label Aug 20, 2026
@wpbonelli
wpbonelli marked this pull request as ready for review August 20, 2026 22:44
@wpbonelli
wpbonelli merged commit 41dca93 into MODFLOW-ORG:develop Aug 20, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant