@@ -446,6 +446,166 @@ describe('OpenAPI Rollup field (e2e)', () => {
446446 expect ( recordAfter1 . fields [ rollupFieldVo . id ] ) . toEqual ( '123, 456' ) ;
447447 } ) ;
448448
449+ describe ( 'rollup expression coverage' , ( ) => {
450+ const baseId = globalThis . testConfig . baseId ;
451+
452+ const setupRollupFixtures = async ( ) => {
453+ const foreign = await createTable ( baseId , {
454+ name : 'RollupExpr_Foreign' ,
455+ fields : [
456+ { name : 'Label' , type : FieldType . SingleLineText } as IFieldRo ,
457+ { name : 'Amount' , type : FieldType . Number } as IFieldRo ,
458+ { name : 'Flag' , type : FieldType . Checkbox } as IFieldRo ,
459+ ] ,
460+ records : [
461+ { fields : { Label : 'Alpha' , Amount : 10 , Flag : true } } ,
462+ { fields : { Label : 'Beta' , Amount : 20 , Flag : false } } ,
463+ ] ,
464+ } ) ;
465+
466+ const host = await createTable ( baseId , {
467+ name : 'RollupExpr_Host' ,
468+ fields : [ { name : 'Name' , type : FieldType . SingleLineText } as IFieldRo ] ,
469+ records : [ { fields : { Name : 'Rollup Holder' } } ] ,
470+ } ) ;
471+
472+ const linkField = await createField ( host . id , {
473+ name : 'Links' ,
474+ type : FieldType . Link ,
475+ options : {
476+ relationship : Relationship . OneMany ,
477+ foreignTableId : foreign . id ,
478+ } ,
479+ } as IFieldRo ) ;
480+
481+ const hostRecordId = host . records [ 0 ] . id ;
482+ await updateRecordField (
483+ host . id ,
484+ hostRecordId ,
485+ linkField . id ,
486+ foreign . records . map ( ( record ) => ( { id : record . id } ) )
487+ ) ;
488+
489+ const amountId = foreign . fields . find ( ( field ) => field . name === 'Amount' ) ! . id ;
490+ const labelId = foreign . fields . find ( ( field ) => field . name === 'Label' ) ! . id ;
491+ const flagId = foreign . fields . find ( ( field ) => field . name === 'Flag' ) ! . id ;
492+
493+ return { foreign, host, linkField, hostRecordId, amountId, labelId, flagId } ;
494+ } ;
495+
496+ const rollupCases : Array < {
497+ expression : string ;
498+ lookupFieldKey : 'amountId' | 'labelId' | 'flagId' ;
499+ expected : unknown ;
500+ } > = [
501+ { expression : 'countall({values})' , lookupFieldKey : 'amountId' , expected : 2 } ,
502+ { expression : 'counta({values})' , lookupFieldKey : 'labelId' , expected : 2 } ,
503+ { expression : 'count({values})' , lookupFieldKey : 'amountId' , expected : 2 } ,
504+ { expression : 'sum({values})' , lookupFieldKey : 'amountId' , expected : 30 } ,
505+ { expression : 'average({values})' , lookupFieldKey : 'amountId' , expected : 15 } ,
506+ { expression : 'max({values})' , lookupFieldKey : 'amountId' , expected : 20 } ,
507+ { expression : 'min({values})' , lookupFieldKey : 'amountId' , expected : 10 } ,
508+ { expression : 'and({values})' , lookupFieldKey : 'flagId' , expected : true } ,
509+ { expression : 'or({values})' , lookupFieldKey : 'flagId' , expected : true } ,
510+ { expression : 'xor({values})' , lookupFieldKey : 'flagId' , expected : true } ,
511+ { expression : 'array_join({values})' , lookupFieldKey : 'labelId' , expected : 'Alpha, Beta' } ,
512+ {
513+ expression : 'array_unique({values})' ,
514+ lookupFieldKey : 'labelId' ,
515+ expected : [ 'Alpha' , 'Beta' ] ,
516+ } ,
517+ {
518+ expression : 'array_compact({values})' ,
519+ lookupFieldKey : 'labelId' ,
520+ expected : [ 'Alpha' , 'Beta' ] ,
521+ } ,
522+ { expression : 'concatenate({values})' , lookupFieldKey : 'labelId' , expected : 'Alpha, Beta' } ,
523+ ] ;
524+
525+ it . each ( rollupCases ) (
526+ 'should compute rollup using %s' ,
527+ async ( { expression, lookupFieldKey, expected } ) => {
528+ let fixtures : Awaited < ReturnType < typeof setupRollupFixtures > > | undefined ;
529+ try {
530+ fixtures = await setupRollupFixtures ( ) ;
531+ const { foreign, host, linkField, hostRecordId } = fixtures ;
532+ const lookupFieldId = fixtures [ lookupFieldKey ] ;
533+
534+ const field = await createField ( host . id , {
535+ name : `rollup ${ expression } ` ,
536+ type : FieldType . Rollup ,
537+ options : { expression } ,
538+ lookupOptions : {
539+ foreignTableId : foreign . id ,
540+ linkFieldId : linkField . id ,
541+ lookupFieldId,
542+ } as ILookupOptionsRo ,
543+ } as IFieldRo ) ;
544+
545+ const record = await getRecord ( host . id , hostRecordId ) ;
546+ const value = record . fields [ field . id ] ;
547+
548+ if ( Array . isArray ( expected ) ) {
549+ expect ( Array . isArray ( value ) ) . toBe ( true ) ;
550+ const sortedExpected = [ ...expected ] . sort ( ) ;
551+ const sortedValue = [ ...( value as unknown [ ] ) ] . sort ( ) ;
552+ expect ( sortedValue ) . toEqual ( sortedExpected ) ;
553+ } else if ( typeof expected === 'string' ) {
554+ if ( expected . includes ( ', ' ) ) {
555+ expect ( ( value as string ) . split ( ', ' ) . sort ( ) ) . toEqual ( expected . split ( ', ' ) . sort ( ) ) ;
556+ } else {
557+ expect ( value ) . toEqual ( expected ) ;
558+ }
559+ } else {
560+ expect ( value ) . toEqual ( expected ) ;
561+ }
562+ } finally {
563+ if ( fixtures ?. host ) {
564+ await permanentDeleteTable ( baseId , fixtures . host . id ) ;
565+ }
566+ if ( fixtures ?. foreign ) {
567+ await permanentDeleteTable ( baseId , fixtures . foreign . id ) ;
568+ }
569+ }
570+ }
571+ ) ;
572+ } ) ;
573+
574+ it ( 'should create rollup fields with array join, unique, and compact expressions' , async ( ) => {
575+ const textField = getFieldByType ( table2 . fields , FieldType . SingleLineText ) ;
576+ const linkFieldId = getFieldByType ( table1 . fields , FieldType . Link ) . id ;
577+
578+ // Link all foreign records to a host record for evaluation
579+ await updateRecordField ( table1 . id , table1 . records [ 1 ] . id , linkFieldId , [
580+ { id : table2 . records [ 0 ] . id } ,
581+ { id : table2 . records [ 1 ] . id } ,
582+ { id : table2 . records [ 2 ] . id } ,
583+ ] ) ;
584+
585+ // Populate duplicate values to verify join & unique behaviours
586+ await updateRecordField ( table2 . id , table2 . records [ 0 ] . id , textField . id , 'Alpha' ) ;
587+ await updateRecordField ( table2 . id , table2 . records [ 1 ] . id , textField . id , 'Alpha' ) ;
588+ await updateRecordField ( table2 . id , table2 . records [ 2 ] . id , textField . id , 'Beta' ) ;
589+
590+ const arrayJoinRollup = await rollupFrom ( table1 , textField . id , 'array_join({values})' ) ;
591+ const arrayUniqueRollup = await rollupFrom ( table1 , textField . id , 'array_unique({values})' ) ;
592+
593+ let record = await getRecord ( table1 . id , table1 . records [ 1 ] . id ) ;
594+ const joinedValues = ( record . fields [ arrayJoinRollup . id ] as string ) . split ( ', ' ) . sort ( ) ;
595+ expect ( joinedValues ) . toEqual ( [ 'Alpha' , 'Alpha' , 'Beta' ] . sort ( ) ) ;
596+ const uniqueValues = [ ...( record . fields [ arrayUniqueRollup . id ] as string [ ] ) ] . sort ( ) ;
597+ expect ( uniqueValues ) . toEqual ( [ 'Alpha' , 'Beta' ] ) ;
598+
599+ // Update values to include blanks and verify compact removes empty entries
600+ await updateRecordField ( table2 . id , table2 . records [ 0 ] . id , textField . id , 'Gamma' ) ;
601+ await updateRecordField ( table2 . id , table2 . records [ 1 ] . id , textField . id , '' ) ;
602+ await updateRecordField ( table2 . id , table2 . records [ 2 ] . id , textField . id , null ) ;
603+
604+ const arrayCompactRollup = await rollupFrom ( table1 , textField . id , 'array_compact({values})' ) ;
605+ record = await getRecord ( table1 . id , table1 . records [ 1 ] . id ) ;
606+ expect ( record . fields [ arrayCompactRollup . id ] ) . toEqual ( [ 'Gamma' ] ) ;
607+ } ) ;
608+
449609 it ( 'should roll up a flat array multiple select field -> one - many rollup field' , async ( ) => {
450610 const lookedUpToField = getFieldByType ( table2 . fields , FieldType . MultipleSelect ) ;
451611 const rollupFieldVo = await rollupFrom ( table1 , lookedUpToField . id , 'countall({values})' ) ;
0 commit comments