package cursor import ( "strings" "testing" "time" "github.com/go-jet/jet/v2/mysql" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestTimestampValue(t *testing.T) { assert := assert.New(t) RegisterColumn(Meeting.StartTime) when := time.Date(2026, 3, 15, 14, 30, 0, 0, time.UTC) v := NewTimestampValue(when, Meeting.StartTime) assert.Equal(when, v.Val) assert.Equal(Meeting.StartTime, v.Col()) assert.False(v.IsEmpty()) assert.True(NewTimestampValue(time.Time{}, Meeting.StartTime).IsEmpty()) } func TestMeetingCompositeCursor(t *testing.T) { assert := assert.New(t) registerMeetingCursorColumns(t) when := time.Date(2026, 6, 1, 9, 0, 0, 0, time.UTC) base := NewCursor( NewStringValue("", Meeting.ID), Meeting.StartTime, OrderDescending, ) cursor := base.CopyWithVals( NewStringValue("meeting-uuid-1", Meeting.ID), NewTimestampValue(when, Meeting.StartTime), ) assert.Equal(NewColumnKey(Meeting.ID), cursor.Index.ColumnKey()) assert.Equal(NewColumnKey(Meeting.StartTime), cursor.OrderColumnKey) assert.True(cursor.IsComposite()) assert.False(cursor.IsEmpty()) encoded, err := cursor.Encode() require.NoError(t, err) assert.Contains(encoded, `"order_val"`) assert.Contains(encoded, `"start_time"`) decoded := NewCursor(NewStringValue("", Meeting.ID), Meeting.StartTime, OrderDescending) require.NoError(t, decoded.Decode(encoded)) assert.True(decoded.IsComposite()) assert.Equal("meeting-uuid-1", decoded.Index.(*StringValue).Val) assert.Equal(when, decoded.GenericOrderValue().(*TimestampValue).Val) } func TestMeetingCursorWithoutOrderValueIsNotComposite(t *testing.T) { assert := assert.New(t) registerMeetingCursorColumns(t) cursor := NewCursor( NewStringValue("", Meeting.ID), Meeting.StartTime, OrderDescending, ) assert.False(cursor.IsComposite()) } func TestMeetingCursorUsesTupleOrdering(t *testing.T) { assert := assert.New(t) registerMeetingCursorColumns(t) cursor := NewCursor( NewStringValue("", Meeting.ID), Meeting.StartTime, OrderDescending, ) assert.True(cursor.UsesTupleOrdering()) assert.False(cursor.IsComposite()) } func TestPaginateCompositeDESC(t *testing.T) { assert := assert.New(t) registerMeetingCursorColumns(t) when := time.Date(2026, 6, 1, 9, 0, 0, 0, time.UTC) cursor := NewCursor( NewStringValue("", Meeting.ID), Meeting.StartTime, OrderDescending, ).CopyWithVals( NewStringValue("meeting-uuid-1", Meeting.ID), NewTimestampValue(when, Meeting.StartTime), ) stmt := mysqlSELECTWhere(PaginateConds(cursor)) sql := stmt.DebugSql() assert.Contains(sql, "(meeting.start_time, meeting.id) < ('") } func TestPaginateCompositeASC(t *testing.T) { assert := assert.New(t) registerMeetingCursorColumns(t) when := time.Date(2026, 6, 1, 9, 0, 0, 0, time.UTC) cursor := NewCursor( NewStringValue("", Meeting.ID), Meeting.StartTime, OrderAscending, ).CopyWithVals( NewStringValue("meeting-uuid-1", Meeting.ID), NewTimestampValue(when, Meeting.StartTime), ) stmt := mysqlSELECTWhere(PaginateConds(cursor)) sql := stmt.DebugSql() assert.Contains(sql, "(meeting.start_time, meeting.id) > ('") } func TestOrderByClausesComposite(t *testing.T) { assert := assert.New(t) registerMeetingCursorColumns(t) when := time.Date(2026, 6, 1, 9, 0, 0, 0, time.UTC) cursor := NewCursor( NewStringValue("", Meeting.ID), Meeting.StartTime, OrderDescending, ).CopyWithVals( NewStringValue("meeting-uuid-1", Meeting.ID), NewTimestampValue(when, Meeting.StartTime), ) clauses := OrderByClauses(cursor) require.Len(t, clauses, 2) stmt := Meeting.SELECT(Meeting.AllColumns).ORDER_BY(clauses...) sql := stmt.DebugSql() assert.True( strings.Contains(sql, "start_time DESC") && strings.Contains(sql, "id DESC"), "expected composite order by, got: %s", sql, ) } func TestOrderByClausesTupleOrderingDefaultCursor(t *testing.T) { assert := assert.New(t) registerMeetingCursorColumns(t) cursor := NewCursor( NewStringValue("", Meeting.ID), Meeting.StartTime, OrderDescending, ) assert.True(cursor.UsesTupleOrdering()) assert.False(cursor.IsComposite()) clauses := OrderByClauses(cursor) require.Len(t, clauses, 2) stmt := Meeting.SELECT(Meeting.AllColumns).ORDER_BY(clauses...) sql := stmt.DebugSql() assert.True( strings.Contains(sql, "start_time DESC") && strings.Contains(sql, "id DESC"), "expected tuple order by on default cursor, got: %s", sql, ) } func TestOrderByClausesNil(t *testing.T) { assert.Nil(t, OrderByClauses(nil)) } func TestOrderByClausesSingleColumnASC(t *testing.T) { RegisterColumn(User.ID) cursor := NewCursor(NewInt64Value(1, User.ID), User.ID, OrderAscending) clauses := OrderByClauses(cursor) require.Len(t, clauses, 1) stmt := User.SELECT(User.AllColumns).ORDER_BY(clauses...) sql := stmt.DebugSql() assert.Contains(t, sql, "id ASC") } func TestPaginateValueTypes(t *testing.T) { RegisterColumn(User.ID, Meeting.StartTime) tests := []struct { name string cond mysql.BoolExpression wantSubstr string }{ { name: "int64 desc", cond: PaginateConds(NewCursor( NewInt64Value(10, User.ID), User.ID, OrderDescending, )), wantSubstr: "user.id < 10", }, { name: "int64 asc", cond: PaginateConds(NewCursor( NewInt64Value(10, User.ID), User.ID, OrderAscending, )), wantSubstr: "user.id > 10", }, { name: "uint64 desc", cond: PaginateConds(NewCursor( NewUint64Value(10, User.ID), User.ID, OrderDescending, )), wantSubstr: "user.id < 10", }, { name: "timestamp desc", cond: PaginateConds(NewCursor( NewTimestampValue(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), Meeting.StartTime), Meeting.StartTime, OrderDescending, )), wantSubstr: "meeting.start_time <", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { sql := User.SELECT(User.AllColumns).WHERE(tc.cond).DebugSql() assert.Contains(t, sql, tc.wantSubstr) }) } } func TestQueryCountCompositeSQL(t *testing.T) { assert := assert.New(t) registerMeetingCursorColumns(t) when := time.Date(2026, 6, 1, 9, 0, 0, 0, time.UTC) start := NewCursor( NewStringValue("", Meeting.ID), Meeting.StartTime, OrderDescending, ).CopyWithVals( NewStringValue("meeting-uuid-1", Meeting.ID), NewTimestampValue(when, Meeting.StartTime), ) end := start.CopyWithVals( NewStringValue("meeting-uuid-2", Meeting.ID), NewTimestampValue(when, Meeting.StartTime), ) stmt := Meeting.SELECT( mysql.COUNT(countBoundExpr(start, ">")).AS("before"), mysql.COUNT(countBoundExpr(end, "<")).AS("after"), ).WHERE(mysql.Bool(true)) sql := stmt.DebugSql() assert.Contains(sql, "IF((meeting.start_time, meeting.id) > ('") assert.Contains(sql, "IF((meeting.start_time, meeting.id) < ('") } func TestCountBoundExprSimple(t *testing.T) { RegisterColumn(MeetingRoom.ID) cursor := NewCursor( NewStringValue("room-1", MeetingRoom.ID), MeetingRoom.ID, OrderDescending, ) stmt := MeetingRoom.SELECT( mysql.COUNT(countBoundExpr(cursor, "<")).AS("after"), ).WHERE(mysql.Bool(true)) sql := stmt.DebugSql() assert.Contains(t, sql, "IF(meeting_room.id < 'room-1', 1, NULL)") } func TestBuildQueryCountFn(t *testing.T) { registerMeetingCursorColumns(t) fn := BuildQueryCountFn(Meeting.ID, Meeting, mysql.Bool(true)) require.NotNil(t, fn) when := time.Date(2026, 6, 1, 9, 0, 0, 0, time.UTC) start := NewCursor( NewStringValue("", Meeting.ID), Meeting.StartTime, OrderDescending, ).CopyWithVals( NewStringValue("meeting-uuid-1", Meeting.ID), NewTimestampValue(when, Meeting.StartTime), ) // Verify the returned fn builds the same SQL shape as QueryCount would. stmt := Meeting.SELECT( mysql.COUNT(Meeting.ID).AS("QueryCountResult.Total"), mysql.COUNT(countBoundExpr(start, ">")).AS("QueryCountResult.Before"), mysql.COUNT(countBoundExpr(start, "<")).AS("QueryCountResult.After"), ).WHERE(mysql.Bool(true)) sql := stmt.DebugSql() assert.Contains(t, sql, "COUNT(meeting.id)") assert.Contains(t, sql, "IF((meeting.start_time, meeting.id) >") }