@@ -516,6 +516,133 @@ describe('DynamoDBGlobalTable GSI throughput translation (issue #1387)', () => {
516516 } ) ;
517517
518518 describe ( 'update()' , ( ) => {
519+ it ( 'resets a REMOVED per-GSI on-demand limit with -1 instead of no-oping (issue #1423)' , async ( ) => {
520+ // Deleting `maxReadRequestUnits` / `maxWriteRequestUnits` from a template
521+ // used to emit nothing, so the old ceiling stayed live in AWS forever
522+ // while cdkd reported success — the absent-field-reset silent-drop class
523+ // (#1160). `-1` is the reset sentinel, LIVE-VERIFIED against real AWS:
524+ // UpdateTable accepted it on the per-GSI Update action and DescribeTable
525+ // then reported the member ABSENT.
526+ const previous = structuredClone ( ON_DEMAND_TABLE_PROPS ) as Record < string , unknown > ;
527+ const next = structuredClone ( ON_DEMAND_TABLE_PROPS ) as Record < string , unknown > ;
528+ // Drop BOTH limits: write lives on the GSI, read on the local replica.
529+ delete ( next [ 'GlobalSecondaryIndexes' ] as Record < string , unknown > [ ] ) [ 0 ] ! [
530+ 'WriteOnDemandThroughputSettings'
531+ ] ;
532+ delete (
533+ ( next [ 'Replicas' ] as Record < string , unknown > [ ] ) [ 0 ] ! [ 'GlobalSecondaryIndexes' ] as Record <
534+ string ,
535+ unknown
536+ > [ ]
537+ ) [ 0 ] ! [ 'ReadOnDemandThroughputSettings' ] ;
538+
539+ await provider . update ( 'OnDemand' , 'od-table' , RESOURCE_TYPE , next , previous ) ;
540+
541+ const gsiUpdates = mockSend . mock . calls
542+ . map ( ( c ) => c [ 0 ] )
543+ . filter (
544+ ( c ) : c is UpdateTableCommand =>
545+ c instanceof UpdateTableCommand &&
546+ ( c . input . GlobalSecondaryIndexUpdates ?? [ ] ) . some ( ( u ) => u . Update !== undefined )
547+ ) ;
548+ expect ( gsiUpdates ) . toHaveLength ( 1 ) ;
549+ expect ( gsiUpdates [ 0 ] ! . input . GlobalSecondaryIndexUpdates ) . toEqual ( [
550+ {
551+ Update : {
552+ IndexName : 'gsi2' ,
553+ OnDemandThroughput : { MaxReadRequestUnits : - 1 , MaxWriteRequestUnits : - 1 } ,
554+ } ,
555+ } ,
556+ ] ) ;
557+ } ) ;
558+
559+ it ( 'resets the DROPPED member while KEEPING the one still declared (partial removal)' , async ( ) => {
560+ // The likeliest user edit: read and write limits are two independent CDK
561+ // props (read via the local replica, write on the GSI), so removing ONE
562+ // is common. A branch that only reset when the new side had NO on-demand
563+ // block at all would silently leave the other ceiling live — the very
564+ // #1423 bug, shipped as fixed. Live-probed: the MIXED payload
565+ // {MaxReadRequestUnits: 50, MaxWriteRequestUnits: -1} is accepted and
566+ // reads back as {MaxReadRequestUnits: 50}.
567+ const previous = structuredClone ( ON_DEMAND_TABLE_PROPS ) as Record < string , unknown > ;
568+ const next = structuredClone ( ON_DEMAND_TABLE_PROPS ) as Record < string , unknown > ;
569+ // Drop ONLY the write limit; the replica-side read limit (50) stays.
570+ delete ( next [ 'GlobalSecondaryIndexes' ] as Record < string , unknown > [ ] ) [ 0 ] ! [
571+ 'WriteOnDemandThroughputSettings'
572+ ] ;
573+
574+ await provider . update ( 'OnDemand' , 'od-table' , RESOURCE_TYPE , next , previous ) ;
575+
576+ const gsiUpdates = mockSend . mock . calls
577+ . map ( ( c ) => c [ 0 ] )
578+ . filter (
579+ ( c ) : c is UpdateTableCommand =>
580+ c instanceof UpdateTableCommand &&
581+ ( c . input . GlobalSecondaryIndexUpdates ?? [ ] ) . some ( ( u ) => u . Update !== undefined )
582+ ) ;
583+ expect ( gsiUpdates ) . toHaveLength ( 1 ) ;
584+ expect (
585+ gsiUpdates [ 0 ] ! . input . GlobalSecondaryIndexUpdates ?. [ 0 ] ?. Update ?. OnDemandThroughput
586+ ) . toEqual ( { MaxReadRequestUnits : 50 , MaxWriteRequestUnits : - 1 } ) ;
587+ } ) ;
588+
589+ it ( 'resets ONLY the member that was actually set before' , async ( ) => {
590+ // A blanket {-1, -1} would clear a sibling limit the template still
591+ // declares.
592+ const previous = structuredClone ( ON_DEMAND_TABLE_PROPS ) as Record < string , unknown > ;
593+ // Previous side has WRITE only (drop the replica-side read limit).
594+ delete (
595+ ( previous [ 'Replicas' ] as Record < string , unknown > [ ] ) [ 0 ] ! [
596+ 'GlobalSecondaryIndexes'
597+ ] as Record < string , unknown > [ ]
598+ ) [ 0 ] ! [ 'ReadOnDemandThroughputSettings' ] ;
599+ const next = structuredClone ( previous ) as Record < string , unknown > ;
600+ delete ( next [ 'GlobalSecondaryIndexes' ] as Record < string , unknown > [ ] ) [ 0 ] ! [
601+ 'WriteOnDemandThroughputSettings'
602+ ] ;
603+
604+ await provider . update ( 'OnDemand' , 'od-table' , RESOURCE_TYPE , next , previous ) ;
605+
606+ const gsiUpdates = mockSend . mock . calls
607+ . map ( ( c ) => c [ 0 ] )
608+ . filter (
609+ ( c ) : c is UpdateTableCommand =>
610+ c instanceof UpdateTableCommand &&
611+ ( c . input . GlobalSecondaryIndexUpdates ?? [ ] ) . some ( ( u ) => u . Update !== undefined )
612+ ) ;
613+ expect ( gsiUpdates [ 0 ] ! . input . GlobalSecondaryIndexUpdates ?. [ 0 ] ?. Update ?. OnDemandThroughput ) . toEqual (
614+ { MaxWriteRequestUnits : - 1 }
615+ ) ;
616+ } ) ;
617+
618+ it ( 'does NOT emit a reset when the index simply had no limits before' , async ( ) => {
619+ const previous = structuredClone ( ON_DEMAND_TABLE_PROPS ) as Record < string , unknown > ;
620+ delete ( previous [ 'GlobalSecondaryIndexes' ] as Record < string , unknown > [ ] ) [ 0 ] ! [
621+ 'WriteOnDemandThroughputSettings'
622+ ] ;
623+ delete (
624+ ( previous [ 'Replicas' ] as Record < string , unknown > [ ] ) [ 0 ] ! [
625+ 'GlobalSecondaryIndexes'
626+ ] as Record < string , unknown > [ ]
627+ ) [ 0 ] ! [ 'ReadOnDemandThroughputSettings' ] ;
628+ const next = structuredClone ( previous ) as Record < string , unknown > ;
629+ // Unrelated edit so the table still goes through update().
630+ next [ 'TableClass' ] = 'STANDARD_INFREQUENT_ACCESS' ;
631+
632+ await provider . update ( 'OnDemand' , 'od-table' , RESOURCE_TYPE , next , previous ) ;
633+
634+ const resets = mockSend . mock . calls
635+ . map ( ( c ) => c [ 0 ] )
636+ . filter (
637+ ( c ) : c is UpdateTableCommand =>
638+ c instanceof UpdateTableCommand &&
639+ ( c . input . GlobalSecondaryIndexUpdates ?? [ ] ) . some (
640+ ( u ) => u . Update ?. OnDemandThroughput !== undefined
641+ )
642+ ) ;
643+ expect ( resets ) . toHaveLength ( 0 ) ;
644+ } ) ;
645+
519646 it ( 'carries ProvisionedThroughput on a GSI Create action (added index)' , async ( ) => {
520647
521648 const previous = { ...PROVISIONED_TABLE_PROPS , GlobalSecondaryIndexes : [ ] } ;
0 commit comments