@@ -123,6 +123,8 @@ async function boot(makeDriver: () => Driver, predicate: string) {
123123 due_on : { name : 'due_on' , type : 'date' } ,
124124 due_at : { name : 'due_at' , type : 'datetime' } ,
125125 start_time : { name : 'start_time' , type : 'time' } ,
126+ tags : { name : 'tags' , type : 'tags' } ,
127+ owners : { name : 'owners' , type : 'select' , multiple : true , options : [ { label : 'X' , value : 'x' } , { label : 'XY' , value : 'xy' } ] } ,
126128 } ,
127129 } ,
128130 ] ,
@@ -199,6 +201,16 @@ const CELLS: Cell[] = [
199201 // Control: a TEXT column is judged as written, whatever its value looks like.
200202 { predicate : "record.title == '2026-01-05'" , column : 'title' , value : '2026-01-05T15:00:00Z' , stored : '2026-01-05T15:00:00Z' , admitted : false } ,
201203 { predicate : "record.title > '2026-01-05'" , column : 'title' , value : '2026-01-05T15:00:00Z' , stored : '2026-01-05T15:00:00Z' , admitted : true } ,
204+ // [#21238] A declared multi-valued column: a lone scalar is stored as a one-member list.
205+ { predicate : "record.tags.contains('x')" , column : 'tags' , value : 'x' , stored : [ 'x' ] , admitted : true } ,
206+ { predicate : "record.tags.contains('x')" , column : 'tags' , value : 'xy' , stored : [ 'xy' ] , admitted : false } ,
207+ { predicate : "record.tags.contains('x')" , column : 'tags' , value : [ 'x' ] , stored : [ 'x' ] , admitted : true } ,
208+ { predicate : "!record.tags.contains('x')" , column : 'tags' , value : 'x' , stored : [ 'x' ] , admitted : false } ,
209+ { predicate : "record.owners.contains('x')" , column : 'owners' , value : 'x' , stored : [ 'x' ] , admitted : true } ,
210+ { predicate : "record.owners.contains('x')" , column : 'owners' , value : 'xy' , stored : [ 'xy' ] , admitted : false } ,
211+ // Control: a TEXT column keeps its scalar, and `contains` stays a substring test.
212+ { predicate : "record.title == 'x'" , column : 'title' , value : 'x' , stored : 'x' , admitted : true } ,
213+ { predicate : "record.title.contains('x')" , column : 'title' , value : 'xy' , stored : 'xy' , admitted : true } ,
202214] ;
203215
204216describe ( "formula's whole-day copy is out of reach in this file" , ( ) => {
@@ -235,6 +247,29 @@ for (const [driverName, makeDriver] of DRIVERS) {
235247 . toEqual ( DENIED ) ;
236248 expect ( ( await r . storedRow ( 'u' ) ) ?. due_on ) . toBe ( '2026-01-05' ) ;
237249 } ) ;
250+
251+ it ( "[#21238] a by-id update judges a lone scalar on a multi-valued column as its stored list: 'x' admitted, 'xy' 403 and unchanged" , async ( ) => {
252+ const r = await boot ( makeDriver , "record.tags.contains('x')" ) ;
253+ await r . engine . insert ( r . OBJ , { id : 'u' , tags : [ 'x' , 'z' ] } , { context : SYS_CTX } as never ) ;
254+ expect ( await outcome ( r . engine . update ( r . OBJ , { tags : 'x' } , { where : { id : 'u' } , context : r . caller } as never ) ) )
255+ . toBe ( 'admitted' ) ;
256+ expect ( ( await r . storedRow ( 'u' ) ) ?. tags ) . toEqual ( [ 'x' ] ) ;
257+ expect ( await outcome ( r . engine . update ( r . OBJ , { tags : 'xy' } , { where : { id : 'u' } , context : r . caller } as never ) ) )
258+ . toEqual ( DENIED ) ;
259+ expect ( ( await r . storedRow ( 'u' ) ) ?. tags ) . toEqual ( [ 'x' ] ) ;
260+ expect ( await r . shownTo ( 'u' ) ) . toBe ( true ) ;
261+ } ) ;
262+
263+ it ( "[#21238] a predicate update judges a lone scalar on a multi-valued column as its stored list: 'x' admitted, 'xy' 403 and unchanged" , async ( ) => {
264+ const r = await boot ( makeDriver , "record.tags.contains('x')" ) ;
265+ await r . engine . insert ( r . OBJ , { id : 'p' , title : 'batch' , tags : [ 'x' ] } , { context : SYS_CTX } as never ) ;
266+ expect ( await outcome ( r . engine . update ( r . OBJ , { tags : 'x' } , { where : { title : 'batch' } , multi : true , context : r . caller } as never ) ) )
267+ . toBe ( 'admitted' ) ;
268+ expect ( ( await r . storedRow ( 'p' ) ) ?. tags ) . toEqual ( [ 'x' ] ) ;
269+ expect ( await outcome ( r . engine . update ( r . OBJ , { tags : 'xy' } , { where : { title : 'batch' } , multi : true , context : r . caller } as never ) ) )
270+ . toEqual ( DENIED ) ;
271+ expect ( ( await r . storedRow ( 'p' ) ) ?. tags ) . toEqual ( [ 'x' ] ) ;
272+ } ) ;
238273 } ) ;
239274}
240275
0 commit comments