package cursor import ( "context" "database/sql" "testing" "github.com/DATA-DOG/go-sqlmock" "github.com/go-jet/jet/v2/mysql" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestValueMethods(t *testing.T) { RegisterColumn(Scratch.ID) intVal, err := NewInt64Value(7, Scratch.ID).Value() require.NoError(t, err) assert.Equal(t, int64(7), intVal) uintVal, err := NewUint64Value(9, Spare.ID).Value() require.NoError(t, err) assert.Equal(t, uint64(9), uintVal) } func TestCursorString(t *testing.T) { RegisterColumn(User.ID) c := NewCursor(NewInt64Value(3, User.ID), User.ID, OrderAscending) assert.JSONEq(t, `{"index":{"key":{"table":"user","column":"id"},"val":3},"order_col":{"table":"user","column":"id"},"order_dir":"ASC"}`, c.String()) } func TestUsesTupleOrderingEdgeCases(t *testing.T) { assert.False(t, (*Cursor[mysql.IntegerExpression, mysql.ColumnInteger])(nil).UsesTupleOrdering()) RegisterColumn(User.ID) sameCol := NewCursor(NewInt64Value(0, User.ID), User.ID, OrderDescending) assert.False(t, sameCol.UsesTupleOrdering()) } func TestPaginateCondsValueTypes(t *testing.T) { RegisterColumn(User.ID, Meeting.StartTime) intCursor := NewCursor(NewInt64Value(1, User.ID), User.ID, OrderDescending) sql := User.SELECT(User.AllColumns).WHERE(PaginateConds(intCursor)).DebugSql() assert.Contains(t, sql, "user.id < 1") strCursor := NewCursor(NewStringValue("x", MeetingRoom.ID), MeetingRoom.ID, OrderDescending) RegisterColumn(MeetingRoom.ID) sql = MeetingRoom.SELECT(MeetingRoom.AllColumns).WHERE(PaginateConds(strCursor)).DebugSql() assert.Contains(t, sql, "meeting_room.id < 'x'") tsCursor := NewCursor( NewTimestampValue(mustTime("2026-01-01T00:00:00Z"), Meeting.StartTime), Meeting.StartTime, OrderDescending, ) sql = Meeting.SELECT(Meeting.AllColumns).WHERE(PaginateConds(tsCursor)).DebugSql() assert.Contains(t, sql, "meeting.start_time <") } func TestNewCursorFromJSONError(t *testing.T) { RegisterColumn(User.ID) _, err := NewCursorFromJSON(NewInt64Value(0, User.ID), []byte(`{invalid`)) require.Error(t, err) require.ErrorIs(t, err, ErrBadCursorString) } func TestColWrongTypePanics(t *testing.T) { RegisterColumn(User.ID) // Key points at user.id (integer) but we construct StringValue for it. sv := &StringValue{Key: NewColumnKey(User.ID), Val: "x"} assert.Panics(t, func() { sv.Col() }) } func TestQueryCount(t *testing.T) { registerMeetingCursorColumns(t) db, mock, err := sqlmock.New() require.NoError(t, err) t.Cleanup(func() { db.Close() }) rows := sqlmock.NewRows([]string{ "QueryCountResult.Total", "QueryCountResult.Before", "QueryCountResult.After", }).AddRow(10, 3, 5) mock.ExpectQuery("SELECT").WillReturnRows(rows) when := mustTime("2026-06-01T09:00:00Z") 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), ) sqlo := &queryableDB{db: db} res, err := QueryCount(sqlo, Meeting.ID, Meeting, mysql.Bool(true), start, end) require.NoError(t, err) assert.Equal(t, QueryCountResult{Total: 10, Before: 3, After: 5}, res) require.NoError(t, mock.ExpectationsWereMet()) } func TestQueryCountError(t *testing.T) { registerMeetingCursorColumns(t) db, mock, err := sqlmock.New() require.NoError(t, err) t.Cleanup(func() { db.Close() }) mock.ExpectQuery("SELECT").WillReturnError(assert.AnError) when := mustTime("2026-06-01T09:00:00Z") cursor := NewCursor( NewStringValue("", Meeting.ID), Meeting.StartTime, OrderDescending, ).CopyWithVals( NewStringValue("meeting-uuid-1", Meeting.ID), NewTimestampValue(when, Meeting.StartTime), ) sqlo := &queryableDB{db: db} _, err = QueryCount(sqlo, Meeting.ID, Meeting, mysql.Bool(true), cursor, cursor) require.Error(t, err) assert.Contains(t, err.Error(), "failed to query count") } func TestBuildQueryCountFnInvoke(t *testing.T) { registerMeetingCursorColumns(t) db, mock, err := sqlmock.New() require.NoError(t, err) t.Cleanup(func() { db.Close() }) rows := sqlmock.NewRows([]string{ "QueryCountResult.Total", "QueryCountResult.Before", "QueryCountResult.After", }).AddRow(1, 0, 0) mock.ExpectQuery("SELECT").WillReturnRows(rows) when := mustTime("2026-06-01T09:00:00Z") cursor := NewCursor( NewStringValue("", Meeting.ID), Meeting.StartTime, OrderDescending, ).CopyWithVals( NewStringValue("meeting-uuid-1", Meeting.ID), NewTimestampValue(when, Meeting.StartTime), ) fn := BuildQueryCountFn(Meeting.ID, Meeting, mysql.Bool(true)) res, err := fn(&queryableDB{db: db}, cursor, cursor) require.NoError(t, err) assert.Equal(t, 1, res.Total) } // queryableDB implements dbx.Queryable for tests. type queryableDB struct { db *sql.DB } func (q *queryableDB) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { return q.db.QueryContext(ctx, query, args...) } func (q *queryableDB) Query(query string, args ...any) (*sql.Rows, error) { return q.db.Query(query, args...) }