@ -3,6 +3,7 @@ package filter
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
)
@ -107,6 +108,190 @@ func TestCompileTimestampAccessorsPerDialect(t *testing.T) {
}
}
func TestCompileNowAccessorsFoldToInjectedClock ( t * testing . T ) {
t . Parallel ( )
// 2026-07-07T10:30:45Z, a Tuesday (year day 188).
engine := memoEngineAt ( t , time . Date ( 2026 , time . July , 7 , 10 , 30 , 45 , 0 , time . UTC ) . Unix ( ) )
cases := [ ] struct {
name string
filter string
args [ ] any
} {
{ "on this day" , ` created_ts.getMonth() == now.getMonth() && created_ts.getDate() == now.getDate() ` , [ ] any { int64 ( 6 ) , int64 ( 7 ) } } ,
{ "year" , ` created_ts.getFullYear() < now.getFullYear() ` , [ ] any { int64 ( 2026 ) } } ,
{ "day of month" , ` created_ts.getDayOfMonth() == now.getDayOfMonth() ` , [ ] any { int64 ( 6 ) } } ,
{ "day of week" , ` created_ts.getDayOfWeek() == now.getDayOfWeek() ` , [ ] any { int64 ( 2 ) } } ,
{ "day of year" , ` created_ts.getDayOfYear() == now.getDayOfYear() ` , [ ] any { int64 ( 187 ) } } ,
{ "clock parts" , ` created_ts.getHours() == now.getHours() || created_ts.getMinutes() == now.getMinutes() || created_ts.getSeconds() == now.getSeconds() ` , [ ] any { int64 ( 10 ) , int64 ( 30 ) , int64 ( 45 ) } } ,
}
for _ , tc := range cases {
stmt , err := engine . CompileToStatement ( context . Background ( ) , tc . filter , RenderOptions { Dialect : DialectSQLite } )
require . NoError ( t , err , tc . name )
require . Equal ( t , tc . args , stmt . Args , tc . name )
}
}
func TestCompileNowAccessorRejectsTimezoneArg ( t * testing . T ) {
t . Parallel ( )
engine , err := NewEngine ( NewSchema ( ) )
require . NoError ( t , err )
_ , err = engine . Compile ( context . Background ( ) , ` created_ts.getMonth() == now.getMonth("America/New_York") ` )
require . Error ( t , err )
}
func TestCompileOnThisDayPerDialect ( t * testing . T ) {
t . Parallel ( )
const onThisDay = ` created_ts.getMonth() == now.getMonth() && created_ts.getDate() == now.getDate() `
engine := memoEngineAt ( t , time . Date ( 2026 , time . July , 7 , 10 , 30 , 45 , 0 , time . UTC ) . Unix ( ) )
cases := [ ] struct {
dialect DialectName
fragments [ ] string
} {
{ DialectSQLite , [ ] string { "strftime('%m'" , "strftime('%d'" , "'unixepoch'" } } ,
{ DialectPostgres , [ ] string { "EXTRACT(MONTH FROM" , "EXTRACT(DAY FROM" } } ,
{ DialectMySQL , [ ] string { "MONTH(`memo`.`created_ts`)" , "DAYOFMONTH(`memo`.`created_ts`)" } } ,
}
for _ , tc := range cases {
stmt , err := engine . CompileToStatement ( context . Background ( ) , onThisDay , RenderOptions { Dialect : tc . dialect } )
require . NoError ( t , err , tc . dialect )
for _ , frag := range tc . fragments {
require . Contains ( t , stmt . SQL , frag , "dialect %s" , tc . dialect )
}
require . Equal ( t , [ ] any { int64 ( 6 ) , int64 ( 7 ) } , stmt . Args , "dialect %s" , tc . dialect )
}
}
func TestCompileNowAccessorsAtBoundaryDates ( t * testing . T ) {
t . Parallel ( )
// Every accessor folded at calendar edges: year rollover, leap day, Sunday.
allAccessors := ` created_ts.getFullYear() == now.getFullYear() ` +
` && created_ts.getMonth() == now.getMonth() ` +
` && created_ts.getDate() == now.getDate() ` +
` && created_ts.getDayOfMonth() == now.getDayOfMonth() ` +
` && created_ts.getDayOfWeek() == now.getDayOfWeek() ` +
` && created_ts.getDayOfYear() == now.getDayOfYear() ` +
` && created_ts.getHours() == now.getHours() ` +
` && created_ts.getMinutes() == now.getMinutes() ` +
` && created_ts.getSeconds() == now.getSeconds() `
cases := [ ] struct {
name string
clock time . Time
// year, month, date, dayOfMonth, dayOfWeek, dayOfYear, hours, minutes, seconds
args [ ] any
} {
{
"new year's eve" ,
time . Date ( 2026 , time . December , 31 , 23 , 59 , 59 , 0 , time . UTC ) ,
[ ] any { int64 ( 2026 ) , int64 ( 11 ) , int64 ( 31 ) , int64 ( 30 ) , int64 ( 4 ) , int64 ( 364 ) , int64 ( 23 ) , int64 ( 59 ) , int64 ( 59 ) } ,
} ,
{
"new year's day" ,
time . Date ( 2027 , time . January , 1 , 0 , 0 , 0 , 0 , time . UTC ) ,
[ ] any { int64 ( 2027 ) , int64 ( 0 ) , int64 ( 1 ) , int64 ( 0 ) , int64 ( 5 ) , int64 ( 0 ) , int64 ( 0 ) , int64 ( 0 ) , int64 ( 0 ) } ,
} ,
{
"leap day" ,
time . Date ( 2028 , time . February , 29 , 12 , 0 , 0 , 0 , time . UTC ) ,
[ ] any { int64 ( 2028 ) , int64 ( 1 ) , int64 ( 29 ) , int64 ( 28 ) , int64 ( 2 ) , int64 ( 59 ) , int64 ( 12 ) , int64 ( 0 ) , int64 ( 0 ) } ,
} ,
{
"sunday" ,
time . Date ( 2026 , time . July , 5 , 8 , 15 , 30 , 0 , time . UTC ) ,
[ ] any { int64 ( 2026 ) , int64 ( 6 ) , int64 ( 5 ) , int64 ( 4 ) , int64 ( 0 ) , int64 ( 185 ) , int64 ( 8 ) , int64 ( 15 ) , int64 ( 30 ) } ,
} ,
}
for _ , tc := range cases {
engine := memoEngineAt ( t , tc . clock . Unix ( ) )
stmt , err := engine . CompileToStatement ( context . Background ( ) , allAccessors , RenderOptions { Dialect : DialectSQLite } )
require . NoError ( t , err , tc . name )
require . Equal ( t , tc . args , stmt . Args , tc . name )
}
}
func TestCompileNowAccessorFoldsInUTC ( t * testing . T ) {
t . Parallel ( )
// A clock in UTC+8 at 05:00 on July 8 is still July 7, 21:00 in UTC;
// folding must not leak the clock's zone.
engine , err := NewEngine ( NewSchema ( ) )
require . NoError ( t , err )
engine . nowFunc = func ( ) time . Time {
return time . Date ( 2026 , time . July , 8 , 5 , 0 , 0 , 0 , time . FixedZone ( "UTC+8" , 8 * 3600 ) )
}
stmt , err := engine . CompileToStatement (
context . Background ( ) ,
` created_ts.getDate() == now.getDate() && created_ts.getHours() == now.getHours() ` ,
RenderOptions { Dialect : DialectSQLite } ,
)
require . NoError ( t , err )
require . Equal ( t , [ ] any { int64 ( 7 ) , int64 ( 21 ) } , stmt . Args )
}
func TestCompileNowAccessorOnLeftSide ( t * testing . T ) {
t . Parallel ( )
engine := memoEngineAt ( t , time . Date ( 2026 , time . July , 7 , 10 , 30 , 45 , 0 , time . UTC ) . Unix ( ) )
stmt , err := engine . CompileToStatement ( context . Background ( ) , ` now.getMonth() == created_ts.getMonth() ` , RenderOptions { Dialect : DialectSQLite } )
require . NoError ( t , err )
require . Contains ( t , stmt . SQL , "strftime('%m'" )
require . Equal ( t , [ ] any { int64 ( 6 ) } , stmt . Args )
}
func TestCompileNowAccessorMirrorsOrderingOperator ( t * testing . T ) {
t . Parallel ( )
// Swapping the literal to the right must flip < to > to keep the meaning.
engine := memoEngineAt ( t , time . Date ( 2026 , time . July , 7 , 10 , 30 , 45 , 0 , time . UTC ) . Unix ( ) )
stmt , err := engine . CompileToStatement ( context . Background ( ) , ` now.getHours() < created_ts.getHours() ` , RenderOptions { Dialect : DialectSQLite } )
require . NoError ( t , err )
require . Contains ( t , stmt . SQL , "> ?" )
require . Equal ( t , [ ] any { int64 ( 10 ) } , stmt . Args )
}
func TestCompileNowAccessorAgainstLiteralFoldsToConstant ( t * testing . T ) {
t . Parallel ( )
// Both sides fold to literals; the comparison folds to a constant condition.
engine := memoEngineAt ( t , time . Date ( 2026 , time . July , 7 , 10 , 30 , 45 , 0 , time . UTC ) . Unix ( ) )
// True → trivial filter (empty SQL, matches everything).
stmt , err := engine . CompileToStatement ( context . Background ( ) , ` now.getFullYear() >= 2026 ` , RenderOptions { Dialect : DialectSQLite } )
require . NoError ( t , err )
require . Empty ( t , stmt . SQL )
require . Empty ( t , stmt . Args )
// False → unsatisfiable filter.
stmt , err = engine . CompileToStatement ( context . Background ( ) , ` now.getFullYear() < 2026 ` , RenderOptions { Dialect : DialectSQLite } )
require . NoError ( t , err )
require . Equal ( t , "1 = 0" , stmt . SQL )
require . Empty ( t , stmt . Args )
}
func TestCompileAttachmentNowAccessors ( t * testing . T ) {
t . Parallel ( )
engine , err := NewEngine ( NewAttachmentSchema ( ) )
require . NoError ( t , err )
engine . nowFunc = fixedClock ( time . Date ( 2026 , time . July , 7 , 10 , 30 , 45 , 0 , time . UTC ) . Unix ( ) )
stmt , err := engine . CompileToStatement (
context . Background ( ) ,
` create_time.getMonth() == now.getMonth() && create_time.getDate() == now.getDate() ` ,
RenderOptions { Dialect : DialectSQLite } ,
)
require . NoError ( t , err )
require . Contains ( t , stmt . SQL , "`attachment`.`created_ts`" )
require . Equal ( t , [ ] any { int64 ( 6 ) , int64 ( 7 ) } , stmt . Args )
}
func TestCompileTimestampAccessorRejectsTimezoneArg ( t * testing . T ) {
t . Parallel ( )