@@ -11,6 +11,7 @@ import (
1111
1212 sq "github.com/Masterminds/squirrel"
1313 "github.com/fxamacker/cbor/v2"
14+ "github.com/samber/lo"
1415)
1516
1617const (
@@ -89,10 +90,6 @@ type Cursor[T any] struct {
8990func CreatePageT [T1 any , T2 any ](data []T1 , cursor Cursor [T2 ]) Page [T1 ] {
9091 var cursorString string
9192
92- // TODO: Small bug, at this point the data has already been Limited and the data will always be equal to the Limit or smaller
93- // this means that a cursor will be made if the set is exactly equal to the limit but in reality, there is no more data
94- // meaning a new cursor is sent that won't result in any data
95- // https://github.com/sensorbucket/SensorBucket/issues/82
9693 if len (data ) >= int (cursor .Limit ) {
9794 cursorString = EncodeCursor (cursor )
9895 }
@@ -137,24 +134,54 @@ func multiColumnCompare(columns []whereCol) sq.Sqlizer {
137134 if len (columns ) == 0 {
138135 return nil
139136 }
140- clause := sq.Or {}
141- for i := 0 ; i < len (columns ); i ++ {
142- and := sq.And {}
143- for j := 0 ; j <= i ; j ++ {
144- col := columns [j ]
145- if j == i {
146- if col .order == "ASC" {
147- and = append (and , sq.Gt {col .column : col .value })
148- } else {
149- and = append (and , sq.Lt {col .column : col .value })
150- }
151- continue
152- }
153- and = append (and , sq.Eq {col .column : col .value })
137+
138+ // Determine the comparison operator based on the first column's order.
139+ // This function assumes all columns in the cursor are sorted in the same direction (all ASC or all DESC).
140+ // If mixed orders are present, the simple tuple comparison `(col1, col2) OP (val1, val2)` is semantically incorrect.
141+ op := ""
142+ switch columns [0 ].order {
143+ case "ASC" :
144+ op = ">"
145+ case "DESC" :
146+ op = "<"
147+ default :
148+ // This panic indicates an internal inconsistency: `whereCol.order` should only be "ASC" or "DESC".
149+ panic (
150+ fmt .Sprintf (
151+ "multiColumnCompare: invalid order type %q encountered for column %q. Expected 'ASC' or 'DESC'." ,
152+ columns [0 ].order ,
153+ columns [0 ].column ,
154+ ),
155+ )
156+ }
157+
158+ // Validate that all columns have the same sorting order.
159+ // This is a strict requirement for the simple tuple comparison syntax used here.
160+ for i := 1 ; i < len (columns ); i ++ {
161+ if columns [i ].order != columns [0 ].order {
162+ // This panic indicates a misconfiguration in the pagination tags or an invalid cursor.
163+ // Mixed ASC/DESC orders require a more complex WHERE clause (e.g., `(A > X) OR (A = X AND B < Y)`).
164+ panic (
165+ "multiColumnCompare: mixed ASC/DESC orders detected in pagination columns. This function supports only consistent ordering for tuple comparison." ,
166+ )
154167 }
155- clause = append (clause , and )
156168 }
157- return clause
169+
170+ placeholders := make ([]string , len (columns ))
171+ for i := range columns {
172+ placeholders [i ] = "?"
173+ }
174+ columnNames := lo .Map (columns , func (col whereCol , _ int ) string { return col .column })
175+ columnValues := lo .Map (columns , func (col whereCol , _ int ) any { return col .value })
176+
177+ return sq .Expr (
178+ fmt .Sprintf (
179+ "(%s) %s (%s)" ,
180+ strings .Join (columnNames , ", " ),
181+ op ,
182+ strings .Join (placeholders , "," ),
183+ ),
184+ columnValues ... )
158185}
159186
160187func columnAlias (name string ) string {
@@ -166,19 +193,14 @@ func columnAlias(name string) string {
166193 return "paginated_" + name
167194}
168195
169- func ApplyNoLimit [T any ](q sq.SelectBuilder , c Cursor [T ]) (sq.SelectBuilder , error ) {
170- c .Limit = 0
171- return Apply (q , c )
172- }
173-
174196func Apply [T any ](q sq.SelectBuilder , c Cursor [T ]) (sq.SelectBuilder , error ) {
175197 if c .Limit > 0 {
176198 q = q .Limit (c .Limit )
177199 }
178200 rt := reflect .TypeOf (c .Columns )
179201 rv := reflect .ValueOf (c .Columns )
180202 columns := []whereCol {}
181- for ix := 0 ; ix < rt .NumField (); ix ++ {
203+ for ix := range rt .NumField () {
182204 rf := rt .Field (ix )
183205 if ! rf .IsExported () {
184206 continue
@@ -191,12 +213,20 @@ func Apply[T any](q sq.SelectBuilder, c Cursor[T]) (sq.SelectBuilder, error) {
191213
192214 tagParts := strings .Split (tag , "," )
193215 if len (tagParts ) != 2 {
194- return q , fmt .Errorf ("invalid pagination tag on struct %s, for field %s" , rt .Name (), rf .Name )
216+ return q , fmt .Errorf (
217+ "invalid pagination tag on struct %s, for field %s" ,
218+ rt .Name (),
219+ rf .Name ,
220+ )
195221 }
196222
197223 column , order := tagParts [0 ], strings .ToUpper (tagParts [1 ])
198224 if order != "ASC" && order != "DESC" {
199- return q , fmt .Errorf ("invalid order in pagination tag on struct %s, for field %s" , rt .Name (), rf .Name )
225+ return q , fmt .Errorf (
226+ "invalid order in pagination tag on struct %s, for field %s" ,
227+ rt .Name (),
228+ rf .Name ,
229+ )
200230 }
201231 q = q .OrderBy (column + " " + order ).Column (column + " AS " + columnAlias (column ))
202232
0 commit comments