1
1
package plugin
2
2
3
3
import (
4
+ "encoding/json"
4
5
"errors"
6
+ rand "math/rand/v2"
7
+ "strings"
8
+ "time"
5
9
6
10
"github.com/gammazero/deque"
7
11
"github.com/mattn/go-sqlite3"
@@ -43,6 +47,13 @@ type SQLiteCursor struct {
43
47
noMoreRows bool
44
48
rows * deque.Deque [[]interface {}] // A ring buffer to store the rows before sending them to SQLite
45
49
nextCursor * int
50
+ constraints QueryConstraint
51
+ }
52
+
53
+ type constraintsFromQuery struct {
54
+ columns []int
55
+ op []Operator
56
+ value []string
46
57
}
47
58
48
59
// EponymousOnlyModule is a method that is used to mark the table as eponymous-only
@@ -93,9 +104,45 @@ func (m *SQLiteModule) Connect(c *sqlite3.SQLiteConn, args []string) (sqlite3.VT
93
104
// However, we don't use it that way but only to serialize the constraints
94
105
// for the Filter method
95
106
func (t * SQLiteTable ) BestIndex (cst []sqlite3.InfoConstraint , ob []sqlite3.InfoOrderBy ) (* sqlite3.IndexResult , error ) {
96
- // The first task of BestIndex is to check if the constraints are valid
107
+ // The first task of BestIndex is to check if the required parameters are present
97
108
// If not, we return sqlite3.ErrConstraint
98
- return nil , nil
109
+ present := make ([]bool , len (t .schema .Columns ))
110
+ for _ , c := range cst {
111
+ if c .Usable && c .Op == sqlite3 .OpEQ {
112
+ present [c .Column ] = true
113
+ }
114
+ }
115
+ for i , col := range t .schema .Columns {
116
+ if col .IsParameter && ! present [i ] {
117
+ return nil , sqlite3 .ErrConstraint
118
+ }
119
+ }
120
+
121
+ // We serialize the constraints so that we can pass them to the Filter method
122
+ // The only way to communicate them to the Filter method is through the IdxStr field
123
+ // Therefore, we must serialize them as JSON and unmarshal them in the Filter method
124
+ constraints := QueryConstraint {
125
+ Limit : - 1 ,
126
+ Offset : - 1 ,
127
+ }
128
+
129
+ // Used is a boolean array that tells SQLite which constraints are used
130
+ // and that must be passed to the Filter method in the vals field
131
+ used := make ([]bool , len (cst ))
132
+ parseConstraintsFromSQLite (cst , constraints , used , t .schema )
133
+
134
+ // We store the constraints as JSON to be passed with IdxStr in IndexResult
135
+ marshal , err := json .Marshal (constraints )
136
+ if err != nil {
137
+ return nil , errors .Join (errors .New ("could not marshal the constraints" ), err )
138
+ }
139
+
140
+ return & sqlite3.IndexResult {
141
+ IdxNum : 0 ,
142
+ IdxStr : string (marshal ),
143
+ Used : used ,
144
+ }, nil
145
+
99
146
}
100
147
101
148
// Open is called when a new cursor is opened
@@ -110,6 +157,7 @@ func (t *SQLiteTable) Open() (sqlite3.VTabCursor, error) {
110
157
false ,
111
158
deque .New [[]interface {}](preAllocatedCapacity , minimumCapacityRingBuffer ),
112
159
& t .nextCursor ,
160
+ QueryConstraint {},
113
161
}
114
162
// We increment the cursor id for the next cursor by 1
115
163
// so that the next cursor will have a different id
@@ -122,28 +170,61 @@ func (t *SQLiteTable) Open() (sqlite3.VTabCursor, error) {
122
170
func (c * SQLiteCursor ) Close () error { return nil }
123
171
124
172
// These methods are not used in this plugin
125
- func (v * SQLiteTable ) Disconnect () error { return nil }
126
- func (v * SQLiteTable ) Destroy () error { return nil }
127
- func (v * SQLiteModule ) DestroyModule () {}
173
+ func (v * SQLiteTable ) Disconnect () error {
174
+ // We close the client
175
+ v .client .Client .Kill ()
176
+ return nil
177
+ }
178
+ func (v * SQLiteTable ) Destroy () error { return nil }
179
+ func (v * SQLiteModule ) DestroyModule () {}
128
180
129
181
// Column is called when a column is queried
130
182
//
131
183
// It should return the value of the column
132
184
func (c * SQLiteCursor ) Column (context * sqlite3.SQLiteContext , col int ) error { return nil }
133
185
134
186
// EOF is called after each row is queried to check if there are more rows
135
- func (c * SQLiteCursor ) EOF () bool { return false }
187
+ func (c * SQLiteCursor ) EOF () bool {
188
+ return c .noMoreRows && c .rows .Len () == 0
189
+ }
136
190
137
191
// Next is called to move the cursor to the next row
138
192
//
139
193
// If noMoreRows is set to false, and the cursor is at the end of the rows,
140
194
// Next will ask the plugin for more rows
141
195
//
142
196
// If noMoreRows is set to true, Next will set EOF to true
143
- func (c * SQLiteCursor ) Next () error { return nil }
197
+ func (c * SQLiteCursor ) Next () error {
198
+ // If the cursor is at the end of the rows
199
+ // we ask the plugin for more rows
200
+ if c .rows .Len () == 0 {
201
+ // If the plugin stated that there are no more rows, we return
202
+ if c .noMoreRows {
203
+ return nil
204
+ }
205
+ _ , err := c .requestRowsFromPlugin ()
206
+ if err != nil {
207
+ return errors .Join (errors .New ("could not request the rows from the plugin" ), err )
208
+ }
209
+ }
210
+ // We move the cursor to the next row
211
+ c .rows .PopFront ()
212
+
213
+ return nil
214
+ }
144
215
145
216
// RowID is called to get the row ID of the current row
146
- func (c * SQLiteCursor ) Rowid () (int64 , error ) { return 0 , nil }
217
+ func (c * SQLiteCursor ) Rowid () (int64 , error ) {
218
+ // If the table has no primary key, we return a random number
219
+ if c .schema .PrimaryKey == - 1 {
220
+ return rand .Int64 (), nil
221
+ }
222
+ // Otherwise, we find the column that is the primary key
223
+ // and return its value
224
+ // TODO: handle the case where the primary key is a string
225
+ columnID := c .schema .PrimaryKey
226
+ return c .rows .Front ()[columnID ].(int64 ), nil
227
+ }
147
228
148
229
func (c * SQLiteCursor ) Filter (idxNum int , idxStr string , vals []interface {}) error {
149
230
// Filter can be called several times with the same cursor
@@ -153,5 +234,211 @@ func (c *SQLiteCursor) Filter(idxNum int, idxStr string, vals []interface{}) err
153
234
// Moreover, for the sake of simplicity, we will create a new cursor on the plugin side,
154
235
// which means the cursorIndex must be incremented while not yelding any conflict
155
236
// How to fix this? We must have access to the parent struct (SQLiteTable).
237
+
238
+ // Reset the cursor to its initial state
239
+ resetCursor (c )
240
+
241
+ // We unmarshal the constraints from the IdxStr field
242
+ // and store them in the constraints field of the cursor
243
+ var err error
244
+ err = loadConstraintsFromJSON (idxStr , & c .constraints , vals )
245
+ if err != nil {
246
+ return errors .Join (errors .New ("could not load the constraints" ), err )
247
+ }
248
+
249
+ // We request the rows from the plugin
250
+ _ , err = c .requestRowsFromPlugin ()
251
+ if err != nil {
252
+ return errors .Join (errors .New ("could not request the rows from the plugin" ), err )
253
+ }
254
+
255
+ return nil
256
+ }
257
+
258
+ const maxRowsFetchingRetry = 16
259
+
260
+ // requestRowsFromPlugin requests more rows to the plugin
261
+ //
262
+ // It returns the number of rows returned
263
+ func (cursor * SQLiteCursor ) requestRowsFromPlugin () (int , error ) {
264
+ if cursor .noMoreRows {
265
+ return 0 , errors .New ("requestRowsFromPlugin was called but plugin has no more rows" )
266
+ }
267
+
268
+ // We request the rows from the plugin
269
+ rows , noMoreRows , err := cursor .client .Plugin .Query (cursor .tableIndex , cursor .cursorIndex , cursor .constraints )
270
+ if err != nil {
271
+ return 0 , errors .Join (errors .New ("could not request the rows from the plugin" ), err )
272
+ }
273
+ i := 0
274
+ for (! noMoreRows ) && (len (rows ) == 0 ) && (i < maxRowsFetchingRetry ) {
275
+ rows , noMoreRows , err = cursor .client .Plugin .Query (cursor .tableIndex , cursor .cursorIndex , cursor .constraints )
276
+ time .Sleep (10 * time .Millisecond )
277
+ if err != nil {
278
+ return 0 , errors .Join (errors .New ("could not request the rows from the plugin" ), err )
279
+ }
280
+ }
281
+ if i == maxRowsFetchingRetry {
282
+ return 0 , errors .New ("could not fetch any row from the plugin. Max retries reached" )
283
+ }
284
+ for _ , row := range rows {
285
+ cursor .rows .PushBack (row )
286
+ }
287
+
288
+ return len (rows ), nil
289
+ }
290
+
291
+ // parseConstraintsFromSQLite parses the constraints from SQLite and stores them in the QueryConstraint struct
292
+ //
293
+ // For the offset and limit constraints, we store their position in the vals field
294
+ // so that we can pass them to the plugin
295
+ //
296
+ // For the IS NULL, IS, IS NOT NULL and IS NOT operators, we convert them to the EQUAL and NOT EQUAL operators
297
+ // because
298
+ func parseConstraintsFromSQLite (cst []sqlite3.InfoConstraint , constraints QueryConstraint , used []bool , schema DatabaseSchema ) {
299
+ /*
300
+ Internal notes:
301
+ - The usable constraints are the ones that are used in the query
302
+ - Any IS NULL, IS, IS NOT NULL and IS NOT operators are converted to EQUAL and NOT EQUAL operators
303
+ - For the LIMIT and OFFSET constraints, we store their position in the vals field
304
+ and let the loader get the values
305
+ - -1 as a value means SQL NULL. The loader will convert it to nil
306
+ - nil as a value means we don't know the value yet. The loader will get it from the vals field
307
+
308
+ I know it looks like a mess, will probably refactor it later
309
+ But you know, nothing is more permanent than a temporary solution.
310
+ */
311
+
312
+ // We iterate over the constraints and store the usable ones
313
+ var tempOp Operator
314
+ j := 0 // Keep track of the number of constraints used (for marking the LIMIT and OFFSET cols)
315
+ for i , c := range cst {
316
+ if c .Usable {
317
+ tempOp = convertSQLiteOPtoOperator (c .Op )
318
+ switch tempOp {
319
+ case OperatorLimit :
320
+ // We note the position of the LIMIT constraint in vals
321
+ constraints .Limit = j
322
+ case OperatorOffset :
323
+ // We note the position of the OFFSET constraint in vals
324
+ constraints .Offset = j
325
+ // We check if the schema handles the OFFSET constraint
326
+ // If not, we don't include it in vals
327
+ // Furthermore, it will tell SQLite that it must handle the OFFSET itself
328
+ // See https://github.com/julien040/go-sqlite3-anyquery/commit/f32fe2011fdf482c1a3c2f3c15dc85fb0e965550
329
+ if ! schema .HandleOffset {
330
+ used [i ] = false
331
+ }
332
+ case OperatorIsNull :
333
+ // We convert the IS NULL operator to the EQUAL operator
334
+ constraints .Columns = append (constraints .Columns , ColumnConstraint {
335
+ ColumnID : c .Column , // The column index
336
+ Operator : OperatorEqual ,
337
+ Value : - 1 , // -1 means SQL NULL | the loader will convert it to nil
338
+ })
339
+ continue // To avoid setting used[i] to true
340
+ case OperatorIs :
341
+ // We convert the IS operator to the EQUAL operator
342
+ constraints .Columns = append (constraints .Columns , ColumnConstraint {
343
+ ColumnID : c .Column , // The column index
344
+ Operator : OperatorEqual ,
345
+ Value : nil , // We don't know the value yet
346
+ })
347
+ case OperatorIsNotNull :
348
+ // We convert the IS NOT NULL operator to the NOT EQUAL operator
349
+ constraints .Columns = append (constraints .Columns , ColumnConstraint {
350
+ ColumnID : c .Column , // The column index
351
+ Operator : OperatorNotEqual ,
352
+ Value : - 1 , // -1 means SQL NULL | the loader will convert it to nilt
353
+ })
354
+ continue
355
+ case OperatorIsNot :
356
+ // We convert the IS NOT operator to the NOT EQUAL operator
357
+ constraints .Columns = append (constraints .Columns , ColumnConstraint {
358
+ ColumnID : c .Column , // The column index
359
+ Operator : OperatorNotEqual ,
360
+ Value : nil , // We don't know the value yet
361
+ })
362
+
363
+ // In all the other cases, we don't know the value yet
364
+ // so we store the constraint as is
365
+ default :
366
+ constraints .Columns = append (constraints .Columns , ColumnConstraint {
367
+ ColumnID : c .Column , // The column index
368
+ Operator : tempOp , // We convert the SQLite operator to our own operator
369
+ Value : nil , // We don't know the value yet
370
+ })
371
+ }
372
+ used [i ] = true
373
+ j ++
374
+ }
375
+ }
376
+ }
377
+
378
+ // loadConstraintsFromJSON unmashals the JSON serialized constraints
379
+ // from the IdxStr field of the IndexResult
380
+ // and stores them in the constraints field of the cursor
381
+ //
382
+ // It also infer the type of the value and stores it in the constraints field
383
+ func loadConstraintsFromJSON (idxStr string , constraints * QueryConstraint , vals []interface {}) error {
384
+ err := json .Unmarshal ([]byte (idxStr ), & constraints )
385
+ if err != nil {
386
+ return errors .Join (errors .New ("could not unmarshal the constraints" ), err )
387
+ }
388
+ // We load the values from the vals field in the QueryConstraint struct
389
+
390
+ // J is the indice of the value in the vals field
391
+ // We keep it separate from the loop because we need to increment it only when the value is not nil
392
+ j := 0
393
+ for i , cst := range constraints .Columns {
394
+ switch cst .Operator {
395
+ case OperatorLike :
396
+ // We convert the LIKE string to a MATCH string
397
+ // and store it in the constraints field
398
+ constraints .Columns [i ].Value = convertLikeToMatchString (vals [j ].(string ))
399
+ constraints .Columns [i ].Operator = OperatorMatch
400
+ j ++
401
+
402
+ default :
403
+ // If the value is -1, it means SQL NULL
404
+ // so we fill it with nil
405
+ // In the other cases, we fill it with the value in vals
406
+ if constraints .Columns [i ].Value == nil {
407
+ constraints .Columns [i ].Value = vals [i ]
408
+ j ++
409
+ } else {
410
+ constraints .Columns [i ].Value = nil
411
+ }
412
+ }
413
+
414
+ }
156
415
return nil
157
416
}
417
+
418
+ // convertLikeToMatchString converts a LIKE string to a MATCH string
419
+ //
420
+ // LIKE follows the SQL syntax with % and _
421
+ //
422
+ // MATCH follows the UNIX glob syntax with * and ?
423
+ func convertLikeToMatchString (s string ) string {
424
+ // We replace the % with *
425
+ // and the _ with ?
426
+ // We also escape the * and ? with a backslash
427
+ // to avoid any conflict
428
+ return strings .ReplaceAll (strings .ReplaceAll (s , "%" , "*" ), "_" , "?" )
429
+ }
430
+
431
+ // resetCursor resets the cursor to its initial state
432
+ //
433
+ // It's useful when SQLite reuses the cursor
434
+ func resetCursor (c * SQLiteCursor ) {
435
+ c .noMoreRows = false
436
+ c .rows .Clear ()
437
+ c .cursorIndex = * c .nextCursor
438
+ * c .nextCursor ++
439
+
440
+ c .constraints = QueryConstraint {
441
+ Limit : - 1 ,
442
+ Offset : - 1 ,
443
+ }
444
+ }
0 commit comments