Attempted during #1422 (issue #1387) and deliberately reverted. This issue records the problem, three designs that were each found broken in review, and what a correct fix has to satisfy — so the next attempt does not rediscover them.
The problem
toSdkGlobalSecondaryIndexes / toSdkReplicaGlobalSecondaryIndexes in src/provisioning/providers/dynamodb-globaltable-provider.ts forward an explicitly-supplied, already-SDK-shaped throughput block verbatim:
if (explicitProvisioned) {
sdk.ProvisionedThroughput = explicitProvisioned as unknown as ProvisionedThroughput;
} else if (billingMode === 'PROVISIONED') { /* derive */ }
Four sites: GSI ProvisionedThroughput / OnDemandThroughput, replica ProvisionedThroughputOverride / OnDemandThroughputOverride.
Three defects follow from that shape:
- No numeric coercion. This is the ONE path that skips
toFiniteNumber, so a stringly-typed CFn "5" reaches the SDK unnormalized while every derived value is a number.
- Whole-block replacement. A PARTIAL explicit block suppresses valid derived siblings — e.g. an explicit
{MaxReadRequestUnits: 41} discards a perfectly good WriteOnDemandThroughputSettings.MaxWriteRequestUnits from the same template.
- No billing-mode gate. The
if (explicit…) branch fires regardless of billingMode, so an explicit ProvisionedThroughput is emitted on a PAY_PER_REQUEST table and vice versa. AWS rejects both.
Reachability
The CFn schema forbids these members, so CDK never emits them. Reachable only via pre-#1387 cdkd state and hand-authored templates — the latter being a first-class cdkd surface (cdkd import --migrate-from-cloudformation, cdkd migrate --from-cfn-stack).
Three designs that were tried and are WRONG
Each passed the author's own review and was caught by an independent reviewer.
v1 — coerce, return a partial block, assign it whole. Broke two ways: a partial explicit block still suppressed derived siblings (defect 2 unfixed), and an all-unparseable block became {}. AWS documents an EMPTY ProvisionedThroughputOverride / OnDemandThroughputOverride as "inherit the source table's settings", so that turned a loud failure into a silent inherit. On the GSI side {} additionally suppressed the derived fallback.
v2 — return undefined when nothing coerces, fall through to derived. Fixed the empty-block inherit, but a fully-unparseable block then fell into the derived branch, which defaults to DEFAULT_CAPACITY_UNITS (5). A table the template explicitly sized was silently deployed at 5/5 — a throttling symptom days later, not an error. Partial-suppression still unfixed.
v3 — throw on a present-but-uncoercible member; merge partial blocks per member. Fixed 1 and 2, and was mutation-proofed. Still broken:
- Unfixable state. The helper is also called on
previousProperties (from cdkd state) at the billing-flip and GSI-diff sites. A garbage value already in state made EVERY subsequent update() throw — including the deploy that would have removed it. No template edit escapes it; the user would have to hand-edit S3 state. Pre-change that deploy succeeded.
- Throw after
CreateTable. The replica path runs inside the post-CreateTable wiring, so a statically-detectable garbage override created a real table and then best-effort-deleted it. Under DeletionProtectionEnabled: true that delete fails, orphaning a table with no state record.
toFiniteNumber uses bare Number(), so [] → 0, true → 1, '0x10' → 16 slip through without throwing — and [] → 0 flips the presence gate on. Only {Ref: …}-style intrinsics actually threw.
What a correct fix must satisfy
- Coerce numerics, merge per member over derived values, AND gate on billing mode. Fixing any subset leaves a live defect.
- Be asymmetric between the desired side and the previous side: strict handling may apply to the template, never to
previousProperties, or bad state becomes unfixable.
- Validate BEFORE any mutating AWS call, not inside post-
CreateTable wiring.
- Tighten the value predicate (
typeof value === 'number' | 'string') rather than relying on Number().
- Prefer warn-and-fall-back over throw unless the loudness is genuinely required; three of the defects above exist only because a throw was introduced.
Related: #1427 (a separate divergence in the same derivation — cdkd takes the literal ReadCapacityUnits where CFn may take max(literal, SeedCapacity)).
Attempted during #1422 (issue #1387) and deliberately reverted. This issue records the problem, three designs that were each found broken in review, and what a correct fix has to satisfy — so the next attempt does not rediscover them.
The problem
toSdkGlobalSecondaryIndexes/toSdkReplicaGlobalSecondaryIndexesinsrc/provisioning/providers/dynamodb-globaltable-provider.tsforward an explicitly-supplied, already-SDK-shaped throughput block verbatim:Four sites: GSI
ProvisionedThroughput/OnDemandThroughput, replicaProvisionedThroughputOverride/OnDemandThroughputOverride.Three defects follow from that shape:
toFiniteNumber, so a stringly-typed CFn"5"reaches the SDK unnormalized while every derived value is a number.{MaxReadRequestUnits: 41}discards a perfectly goodWriteOnDemandThroughputSettings.MaxWriteRequestUnitsfrom the same template.if (explicit…)branch fires regardless ofbillingMode, so an explicitProvisionedThroughputis emitted on aPAY_PER_REQUESTtable and vice versa. AWS rejects both.Reachability
The CFn schema forbids these members, so CDK never emits them. Reachable only via pre-#1387 cdkd state and hand-authored templates — the latter being a first-class cdkd surface (
cdkd import --migrate-from-cloudformation,cdkd migrate --from-cfn-stack).Three designs that were tried and are WRONG
Each passed the author's own review and was caught by an independent reviewer.
v1 — coerce, return a partial block, assign it whole. Broke two ways: a partial explicit block still suppressed derived siblings (defect 2 unfixed), and an all-unparseable block became
{}. AWS documents an EMPTYProvisionedThroughputOverride/OnDemandThroughputOverrideas "inherit the source table's settings", so that turned a loud failure into a silent inherit. On the GSI side{}additionally suppressed the derived fallback.v2 — return
undefinedwhen nothing coerces, fall through to derived. Fixed the empty-block inherit, but a fully-unparseable block then fell into the derived branch, which defaults toDEFAULT_CAPACITY_UNITS(5). A table the template explicitly sized was silently deployed at 5/5 — a throttling symptom days later, not an error. Partial-suppression still unfixed.v3 — throw on a present-but-uncoercible member; merge partial blocks per member. Fixed 1 and 2, and was mutation-proofed. Still broken:
previousProperties(from cdkd state) at the billing-flip and GSI-diff sites. A garbage value already in state made EVERY subsequentupdate()throw — including the deploy that would have removed it. No template edit escapes it; the user would have to hand-edit S3 state. Pre-change that deploy succeeded.CreateTable. The replica path runs inside the post-CreateTablewiring, so a statically-detectable garbage override created a real table and then best-effort-deleted it. UnderDeletionProtectionEnabled: truethat delete fails, orphaning a table with no state record.toFiniteNumberuses bareNumber(), so[]→ 0,true→ 1,'0x10'→ 16 slip through without throwing — and[]→ 0 flips the presence gate on. Only{Ref: …}-style intrinsics actually threw.What a correct fix must satisfy
previousProperties, or bad state becomes unfixable.CreateTablewiring.typeof value === 'number' | 'string') rather than relying onNumber().Related: #1427 (a separate divergence in the same derivation — cdkd takes the literal
ReadCapacityUnitswhere CFn may takemax(literal, SeedCapacity)).