Files
cursor/query_test.go
T

315 lines
7.9 KiB
Go

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) >")
}