Skip to content

Commit c3144ff

Browse files
committed
✨ Implement BestIndex, Disconnect, EOF, Filter, Next and RowId of Vtab
Implementation note To exchange the constraints between BestIndex and Filter, a struct is serialized with JSON BextIndex => parseConstraintsFromSQLite => JSON => loadConstraintsFromJSON => Filter Todo: RowId doesn't handle a string primary key
1 parent 74b46c7 commit c3144ff

File tree

2 files changed

+370
-18
lines changed

2 files changed

+370
-18
lines changed

plugin/module.go

+295-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package plugin
22

33
import (
4+
"encoding/json"
45
"errors"
6+
rand "math/rand/v2"
7+
"strings"
8+
"time"
59

610
"github.com/gammazero/deque"
711
"github.com/mattn/go-sqlite3"
@@ -43,6 +47,13 @@ type SQLiteCursor struct {
4347
noMoreRows bool
4448
rows *deque.Deque[[]interface{}] // A ring buffer to store the rows before sending them to SQLite
4549
nextCursor *int
50+
constraints QueryConstraint
51+
}
52+
53+
type constraintsFromQuery struct {
54+
columns []int
55+
op []Operator
56+
value []string
4657
}
4758

4859
// 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
93104
// However, we don't use it that way but only to serialize the constraints
94105
// for the Filter method
95106
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
97108
// 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+
99146
}
100147

101148
// Open is called when a new cursor is opened
@@ -110,6 +157,7 @@ func (t *SQLiteTable) Open() (sqlite3.VTabCursor, error) {
110157
false,
111158
deque.New[[]interface{}](preAllocatedCapacity, minimumCapacityRingBuffer),
112159
&t.nextCursor,
160+
QueryConstraint{},
113161
}
114162
// We increment the cursor id for the next cursor by 1
115163
// so that the next cursor will have a different id
@@ -122,28 +170,61 @@ func (t *SQLiteTable) Open() (sqlite3.VTabCursor, error) {
122170
func (c *SQLiteCursor) Close() error { return nil }
123171

124172
// 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() {}
128180

129181
// Column is called when a column is queried
130182
//
131183
// It should return the value of the column
132184
func (c *SQLiteCursor) Column(context *sqlite3.SQLiteContext, col int) error { return nil }
133185

134186
// 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+
}
136190

137191
// Next is called to move the cursor to the next row
138192
//
139193
// If noMoreRows is set to false, and the cursor is at the end of the rows,
140194
// Next will ask the plugin for more rows
141195
//
142196
// 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+
}
144215

145216
// 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+
}
147228

148229
func (c *SQLiteCursor) Filter(idxNum int, idxStr string, vals []interface{}) error {
149230
// 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
153234
// Moreover, for the sake of simplicity, we will create a new cursor on the plugin side,
154235
// which means the cursorIndex must be incremented while not yelding any conflict
155236
// 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+
}
156415
return nil
157416
}
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

Comments
 (0)