683b0ddbf4
Prepare the extracted library for external consumption with grouped godoc, extension point docs, and setup requirements. Wire ErrBadCursorString into decode paths and drop redundant Paginate*Conds aliases. Co-authored-by: Cursor <cursoragent@cursor.com>
179 lines
5.1 KiB
Go
179 lines
5.1 KiB
Go
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...)
|
|
}
|