You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(dynamodb): correct the #1440 guard - explicit block precedence + accurate warning
Review of PR #1443 probed the tip and disproved this PR own claim that the
guard keeps an unresolvable value on a path that reaches AWS. The coercion
happens upstream, so the value is already gone before the reset loop and
nothing is sent for that member either way. The guard removes the
DESTRUCTIVE half; what remains is a no-op - and the only output was the
immutable-field warning below, which tells the user to RECREATE THE INDEX.
The suppression now warns accurately instead, naming the member.
Review also caught a source divergence that reintroduced the bug the guard
closes: an already-SDK-shaped OnDemandThroughput WINS over the derived
members in the translation, so reading the derived spellings reported a
member DECLARED that the translation never sends. An explicit read-only
block beside a leftover WriteOnDemandThroughputSettings suppressed the write
reset and left the old ceiling live. The collector now derives both flags
from the explicit block alone when present.
Also moves the new declarations above toSdkReplicaGlobalSecondaryIndexes,
whose JSDoc they had been inserted underneath, and corrects an inverted
rationale comment (a missing entry lets the reset FIRE - the destructive
direction, safe only because the translation refuses malformed shapes
loudly first).
Refs #1440
Copy file name to clipboardExpand all lines: docs/changelog-cdkd.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -17,7 +17,7 @@ The CLAUDE.md `## Known Limitations` section retains the load-bearing summary
17
17
---
18
18
19
19
**Recently Implemented** (2026-08-10):
20
-
- ✅ **`AWS::DynamoDB::GlobalTable`: the per-GSI on-demand reset stops firing on a present-but-unresolvable value (issue [#1440](https://github.com/go-to-k/cdkd/issues/1440))** — `src/provisioning/providers/dynamodb-globaltable-provider.ts` + `tests/unit/provisioning/dynamodb-globaltable-provider-gsi-throughput.test.ts`. **The class:** the #1423 per-GSI reset decided "the template DROPPED this member" from the TRANSLATED side, and `toSdkGlobalSecondaryIndexes` runs every value through `toFiniteNumber` — which returns `undefined` for a present-but-unparseable value (an unresolved `{Ref: …}`, `''`, an object). Such a GSI was therefore indistinguishable from a removal and was answered with the `-1` sentinel, **silently CLEARING the ceiling the template was trying to SET**. That is the same silent-wrong-action class the reset exists to remove, and the identical hazard was caught in self-review on the table-level path during #1434's PR (the gate there tests RAW key presence); this closes the per-GSI half, which #1434 deliberately left alone because it needed more than a one-line gate change. **The fix** adds `collectRawOnDemandDeclarations(properties, region)` beside `toSdkGlobalSecondaryIndexes` — a per-`IndexName` map of RAW CFn key presence walking the SAME two sources the translation does (top-level GSI for the write half; the LOCAL replica's index entry for the read half, with the GSI-level spelling as the documented hand-authored fallback). Keeping the two functions adjacent is deliberate: they must agree on where each member lives, and a divergence between them is exactly what would re-open the bug. Malformed shapes report "not declared" rather than throwing — `toSdkGlobalSecondaryIndexes` already refuses a non-array `GlobalSecondaryIndexes` loudly, and this helper is consulted only to SUPPRESS a reset, so the conservative answer leaves the live value alone. **Tests:** 4 units (unresolved intrinsic on the write half; on the read half via the replica entry; the GSI-level read fallback spelling counted as a declaration; and a fence that a GENUINE removal still resets, so the guard narrows the reset rather than disabling it). Revert-proofed — neutering the guard fails exactly the first three while the #1423 fence stays green.
20
+
- ✅ **`AWS::DynamoDB::GlobalTable`: the per-GSI on-demand reset stops firing on a present-but-unresolvable value (issue [#1440](https://github.com/go-to-k/cdkd/issues/1440))** — `src/provisioning/providers/dynamodb-globaltable-provider.ts` + `tests/unit/provisioning/dynamodb-globaltable-provider-gsi-throughput.test.ts`. **The class:** the #1423 per-GSI reset decided "the template DROPPED this member" from the TRANSLATED side, and `toSdkGlobalSecondaryIndexes` runs every value through `toFiniteNumber` — which returns `undefined` for a present-but-unparseable value (an unresolved `{Ref: …}`, `''`, an object). Such a GSI was therefore indistinguishable from a removal and was answered with the `-1` sentinel, **silently CLEARING the ceiling the template was trying to SET**. That is the same silent-wrong-action class the reset exists to remove, and the identical hazard was caught in self-review on the table-level path during #1434's PR (the gate there tests RAW key presence); this closes the per-GSI half, which #1434 deliberately left alone because it needed more than a one-line gate change. **The fix** adds `collectRawOnDemandDeclarations(properties, region)` beside `toSdkGlobalSecondaryIndexes` — a per-`IndexName` map of RAW CFn key presence walking the SAME two sources the translation does (top-level GSI for the write half; the LOCAL replica's index entry for the read half, with the GSI-level spelling as the documented hand-authored fallback). Keeping the two functions adjacent is deliberate: they must agree on where each member lives, and a divergence between them is exactly what would re-open the bug. Malformed shapes report "not declared" rather than throwing — `toSdkGlobalSecondaryIndexes` already refuses a non-array `GlobalSecondaryIndexes` loudly, and this helper is consulted only to SUPPRESS a reset, so the conservative answer leaves the live value alone. **Be precise about what the guard buys** — review probed the PR tip and disproved the obvious claim: it does NOT put the value back on a path that reaches AWS. The coercion happens upstream in `toSdkGlobalSecondaryIndexes`, so the unparseable value is already gone before the reset loop and NOTHING is sent for that member either way. What the guard removes is the DESTRUCTIVE half; what remains is a no-op — and a silent no-op is its own trap, since the only output was the immutable-field warning further down, which tells the user to RECREATE THE INDEX (useless and alarming advice for an unresolved intrinsic). The suppression therefore now WARNS, naming the member and saying the limit was left unchanged. Review also caught a **source divergence that reintroduced the very bug**: an already-SDK-shaped `gsi['OnDemandThroughput']` WINS over the derived members in the translation, so reading the derived spellings reported a member DECLARED that the translation never sends — an explicit `{MaxReadRequestUnits: 50}` beside a leftover `WriteOnDemandThroughputSettings` suppressed the write reset and left the old ceiling live, the #1160 silent drop re-created by the guard itself. The collector now derives both flags from the explicit block alone when present. **Tests:** 6 units (unresolved intrinsic on the write half; on the read half via the replica entry; the GSI-level read fallback spelling counted as a declaration; the accurate warning; the explicit-block precedence; and a fence that a GENUINE removal still resets, so the guard narrows the reset rather than disabling it). Revert-proofed — neutering the raw-presence guard fails three, and neutering the explicit-block precedence fails its own test, while the #1423 fence stays green throughout.
21
21
- ✅ **`AWS::DynamoDB::GlobalTable`: the TABLE-level on-demand write ceiling resets when the template drops it; the two REPLICA overrides are proven un-resettable and deliberately left alone (issue [#1434](https://github.com/go-to-k/cdkd/issues/1434))** — `src/provisioning/providers/dynamodb-globaltable-provider.ts` + `tests/unit/provisioning/dynamodb-globaltable-provider-gsi-throughput.test.ts`. **The class:** removing `WriteOnDemandThroughputSettings` from a template never set `flatChanged`, so no `UpdateTable` went out and the old ceiling stayed live in AWS while cdkd reported success — the absent-field-reset silent drop (#1160), the table-level sibling of the per-GSI case #1423 closed. **The fix** merges per member rather than branching on "the whole block disappeared" (the shape the #1433 review caught: a block can survive with its member dropped, which is the likelier user edit) and is SUPPRESSED while the billing mode is flipping, since dropping the on-demand block on the way to PROVISIONED is the natural template edit rather than a clear request, and step 4's flip owns that call. The `oldBilling` / `newBilling` pair is now resolved ONCE above step 3 and reused by step 4, so the two sites cannot disagree about what "flipping" means. **The issue asked for three fields; only ONE turned out to be implementable, and the live probes are what settled it** (us-east-1 source + us-west-2 replica, one global table per probe, both torn down — transcripts on the issue). Table-level `-1` behaves exactly as the issue assumed: `{MaxWriteRequestUnits: -1}` was accepted and `DescribeTable` then returned `{MaxReadRequestUnits: 100}`, i.e. the dropped member cleared and the untouched sibling preserved, with the reset reading back as ABSENCE never as -1 — verified independently rather than inherited from #1423, since reset semantics are field-specific. The two REPLICA overrides have NO reset mechanism at all: `OnDemandThroughputOverride: {MaxReadRequestUnits: -1}` is accepted but **stored literally as -1** on readback (writing it would be strictly worse than the current no-op — a nonsense value instead of a stale-but-valid one); the documented "empty override means inherit the source table" form `{}` is accepted and then **hangs the table in `UPDATING` for over an hour** with the override unchanged and every later call returning `ResourceInUseException` (it left a table AWS refused to delete for having "acted as a source region for new replica(s) ... in the last 24 hours" — treat `{}` as hazardous in any future probe); an entry carrying only `IndexName` is rejected outright (`ValidationException: There are no actions specified in the Replica Update Action`). `ProvisionedThroughputOverride` is the same story by construction — the registry schema declares `ReadCapacityUnits` / `MaxReadRequestUnits` `"minimum": 1`, and live `-1` / `0` both fail validation while `{}` returns `InternalServerError`. So no in-band sentinel exists, and shipping any of them would trade a silent no-op for a live defect. **Tests:** 6 units covering the full removal, the PARTIAL removal, value-changed (never the sentinel), stringly-typed coercion, billing-flip suppression, and the no-change redeploy; revert-proofed — neutering the reset fails exactly the first two while the other four stay green, since they pin behavior the change does not alter. **Related finding filed, not fixed here:** the probes surfaced that the table-level on-demand READ ceiling (`Replicas[local].ReadOnDemandThroughputSettings`, i.e. the canonical `Billing.onDemand({maxReadRequestUnits})`) is never wired AT ALL — dropped on the way in rather than merely un-reset, and the repo's own verbatim-`cdk synth` fixture already carried the value with no assertion over it (issue [#1436](https://github.com/go-to-k/cdkd/issues/1436)).
0 commit comments