Skip to content

Commit

Permalink
Merge branch '15-vtable'
Browse files Browse the repository at this point in the history
Fixes #15
  • Loading branch information
zombiezen committed Mar 1, 2023
2 parents 0ed6699 + 13f43a9 commit d4dcb01
Show file tree
Hide file tree
Showing 10 changed files with 1,605 additions and 10 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Support user-defined collating sequences
([#21](https://github.com/zombiezen/go-sqlite/issues/21)).
- Support user-defined virtual tables
([#15](https://github.com/zombiezen/go-sqlite/issues/15)).
- New package `ext/generateseries` provides
an optional `generate_series` table-valued function extension.
- Exported the `regexp` function example as a new `ext/refunc` package.

### Changed
Expand Down
45 changes: 45 additions & 0 deletions ext/generateseries/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2023 Ross Light
// SPDX-License-Identifier: ISC

package generateseries_test

import (
"fmt"
"log"

"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/ext/generateseries"
"zombiezen.com/go/sqlite/sqlitex"
)

func Example() {
conn, err := sqlite.OpenConn(":memory:")
if err != nil {
log.Fatal(err)
}
defer conn.Close()

if err := generateseries.Register(conn); err != nil {
log.Fatal(err)
}
err = sqlitex.ExecuteTransient(
conn,
`SELECT * FROM generate_series(0, 20, 5);`,
&sqlitex.ExecOptions{
ResultFunc: func(stmt *sqlite.Stmt) error {
fmt.Printf("%2d\n", stmt.ColumnInt(0))
return nil
},
},
)
if err != nil {
log.Fatal(err)
}

// Output:
// 0
// 5
// 10
// 15
// 20
}
269 changes: 269 additions & 0 deletions ext/generateseries/generateseries.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
// Copyright 2023 Ross Light
// SPDX-License-Identifier: ISC

// Package generateseries provides a port of the [generate_series] table-valued function
// from the SQLite tree.
//
// [generate_series]: https://sqlite.org/src/file/ext/misc/series.c
package generateseries

import (
"fmt"

"zombiezen.com/go/sqlite"
)

// Module is a virtual table module that can be registered with [sqlite.Conn.SetModule].
var Module = &sqlite.Module{
Connect: connect,
}

// Register registers the "generate_series" table-valued function on the given connection.
func Register(c *sqlite.Conn) error {
return c.SetModule("generate_series", Module)
}

type vtab struct{}

const (
seriesColumnValue = iota
seriesColumnStart
seriesColumnStop
seriesColumnStep
)

func connect(c *sqlite.Conn, opts *sqlite.VTableConnectOptions) (sqlite.VTable, *sqlite.VTableConfig, error) {
vtab := new(vtab)
cfg := &sqlite.VTableConfig{
Declaration: "CREATE TABLE x(value,start hidden,stop hidden,step hidden)",
AllowIndirect: true,
}
return vtab, cfg, nil
}

// BestIndex looks for equality constraints against the hidden start, stop, and step columns,
// and if present, it uses those constraints to bound the sequence of generated values.
// If the equality constraints are missing, it uses 0 for start, 4294967295 for stop,
// and 1 for step.
// BestIndex returns a small cost when both start and stop are available,
// and a very large cost if either start or stop are unavailable.
// This encourages the query planner to order joins such that the bounds of the
// series are well-defined.
//
// SQLite will invoke this method one or more times
// while planning a query that uses the generate_series virtual table.
// This routine needs to create a query plan for each invocation
// and compute an estimated cost for that plan.
//
// In this implementation ID.Num is used to represent the query plan.
// ID.String is unused.
//
// The query plan is represented by bits in idxNum:
//
// (1) start = $value -- constraint exists
// (2) stop = $value -- constraint exists
// (4) step = $value -- constraint exists
// (8) output in descending order
func (vt *vtab) BestIndex(inputs *sqlite.IndexInputs) (*sqlite.IndexOutputs, error) {
var idxNum int32
startSeen := false
var unusableMask uint
aIdx := [3]int{-1, -1, -1}
for i, c := range inputs.Constraints {
if c.Column < seriesColumnStart {
continue
}
col := c.Column - seriesColumnStart // [0, 2]
mask := uint(1 << col)
if col == 0 {
startSeen = true
}
if !c.Usable {
unusableMask |= mask
continue
}
if c.Op == sqlite.IndexConstraintEq {
idxNum |= int32(mask)
aIdx[col] = i
}
}
outputs := &sqlite.IndexOutputs{
ID: sqlite.IndexID{Num: idxNum},
ConstraintUsage: make([]sqlite.IndexConstraintUsage, len(inputs.Constraints)),
}
nArg := 0
for _, j := range aIdx {
if j >= 0 {
nArg++
outputs.ConstraintUsage[j] = sqlite.IndexConstraintUsage{
ArgvIndex: nArg,
Omit: true,
}
}
}
if !startSeen {
return nil, fmt.Errorf("first argument to \"generate_series()\" missing or unusable")
}
if unusableMask&^uint(idxNum) != 0 {
// The start, stop, and step columns are inputs.
// Therefore if there are unusable constraints on any of start, stop, or step then
// this plan is unusable.
return nil, sqlite.ResultConstraint.ToError()
}
if idxNum&3 == 3 {
// Both start= and stop= boundaries are available.
// This is the preferred case.
if idxNum&4 != 0 {
outputs.EstimatedCost = 1
} else {
outputs.EstimatedCost = 2
}
outputs.EstimatedRows = 1000
if len(inputs.OrderBy) >= 1 && inputs.OrderBy[0].Column == 0 {
if inputs.OrderBy[0].Desc {
idxNum |= 8
} else {
idxNum |= 16
}
outputs.OrderByConsumed = true
}
} else {
// If either boundary is missing, we have to generate a huge span of numbers.
// Make this case very expensive so that the query planner will work hard to avoid it.
outputs.EstimatedRows = 2147483647
}
return outputs, nil
}

func (vt *vtab) Open() (sqlite.VTableCursor, error) {
return new(cursor), nil
}

func (vt *vtab) Disconnect() error {
return nil
}

func (vt *vtab) Destroy() error {
return nil
}

type cursor struct {
isDesc bool
rowid int64
value int64
mnValue int64
mxValue int64
step int64
}

// Filter is called to "rewind" the cursor object back to the first row of output.
// This method is always called at least once
// prior to any call to Column or RowID or EOF.
//
// The query plan selected by BestIndex is passed in the id parameter.
// (id.String is not used in this implementation.)
// id.Num is a bitmask showing which constraints are available:
//
// 1: start=VALUE
// 2: stop=VALUE
// 4: step=VALUE
//
// Also, if bit 8 is set, that means that the series should be output in descending order
// rather than in ascending order.
// If bit 16 is set, then output must appear in ascending order.
//
// This routine should initialize the cursor and position it
// so that it is pointing at the first row,
// or pointing off the end of the table (so that EOF will return true)
// if the table is empty.
func (cur *cursor) Filter(id sqlite.IndexID, argv []sqlite.Value) error {
i := 0
if id.Num&1 != 0 {
cur.mnValue = argv[i].Int64()
i++
} else {
cur.mnValue = 0
}
if id.Num&2 != 0 {
cur.mxValue = argv[i].Int64()
i++
} else {
cur.mxValue = 0xffffffff
}
if id.Num&4 != 0 {
cur.step = argv[i].Int64()
i++
if cur.step == 0 {
cur.step = 1
} else if cur.step < 0 {
cur.step = -cur.step
if id.Num&16 == 0 {
id.Num |= 8
}
}
} else {
cur.step = 1
}
for _, arg := range argv {
if arg.Type() == sqlite.TypeNull {
// If any of the constraints have a NULL value, then return no rows.
// See ticket https://www.sqlite.org/src/info/fac496b61722daf2
cur.mnValue = 1
cur.mxValue = 0
break
}
}
if id.Num&8 != 0 {
cur.isDesc = true
cur.value = cur.mxValue
if cur.step > 0 {
cur.value -= (cur.mxValue - cur.mnValue) % cur.step
}
} else {
cur.isDesc = false
cur.value = cur.mnValue
}
cur.rowid = 1
return nil
}

func (cur *cursor) Next() error {
if cur.isDesc {
cur.value -= cur.step
} else {
cur.value += cur.step
}
cur.rowid++
return nil
}

func (cur *cursor) Column(i int, noChange bool) (sqlite.Value, error) {
switch i {
case seriesColumnValue:
return sqlite.IntegerValue(cur.value), nil
case seriesColumnStart:
return sqlite.IntegerValue(cur.mnValue), nil
case seriesColumnStop:
return sqlite.IntegerValue(cur.mxValue), nil
case seriesColumnStep:
return sqlite.IntegerValue(cur.step), nil
default:
panic("unreachable")
}
}

func (cur *cursor) RowID() (int64, error) {
return cur.rowid, nil
}

func (cur *cursor) EOF() bool {
if cur.isDesc {
return cur.value < cur.mnValue
} else {
return cur.value > cur.mxValue
}
}

func (cur *cursor) Close() error {
return nil
}
31 changes: 27 additions & 4 deletions func.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,15 +173,16 @@ func (ctx Context) resultError(err error) {
lib.Xsqlite3_result_error_code(ctx.tls, ctx.ptr, int32(ErrCode(err)))
}

// Value represents a value that can be stored in a database table. The zero
// value is NULL. The accessor methods on Value may perform automatic
// conversions and thus methods on Value must not be called concurrently.
// Value represents a value that can be stored in a database table.
// The zero value is NULL.
// The accessor methods on Value may perform automatic conversions
// and thus methods on Value must not be called concurrently.
type Value struct {
tls *libc.TLS
ptrOrType uintptr // pointer to sqlite_value if tls != nil, ColumnType otherwise

s string
n int64
n int64 // if ptrOrType == 0 and n != 0, then indicates the "nochange" NULL.
}

// IntegerValue returns a new Value representing the given integer.
Expand All @@ -205,6 +206,12 @@ func BlobValue(b []byte) Value {
return Value{ptrOrType: uintptr(TypeBlob), s: string(b)}
}

// Unchanged returns a NULL Value for which [Value.NoChange] reports true.
// This is only significant as the return value for [VTableCursor.Column].
func Unchanged() Value {
return Value{n: 1}
}

// Type returns the data type of the value. The result of Type is undefined if
// an automatic type conversion has occurred due to calling one of the other
// accessor methods.
Expand Down Expand Up @@ -357,6 +364,22 @@ func (v Value) Blob() []byte {
return libc.GoBytes(ptr, int(lib.Xsqlite3_value_bytes(v.tls, v.ptrOrType)))
}

// NoChange reports whether a column
// corresponding to this value in a [VTable.Update] method
// is unchanged by the UPDATE operation
// that the VTable.Update method call was invoked to implement
// and if the prior [VTableCursor.Column] method call that was invoked
// to extract the value for that column returned [Unchanged].
func (v Value) NoChange() bool {
if v.ptrOrType == 0 {
return v.n != 0
}
if v.tls == nil {
return false
}
return lib.Xsqlite3_value_nochange(v.tls, v.ptrOrType) != 0
}

// FunctionImpl describes an [application-defined SQL function].
// Either Scalar or MakeAggregate must be set, but not both.
//
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ require (
crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797
github.com/chzyer/readline v1.5.0
github.com/google/go-cmp v0.5.9
modernc.org/libc v1.21.5
modernc.org/sqlite v1.20.0
modernc.org/libc v1.22.2
modernc.org/sqlite v1.20.5-0.20230220170856-13895386cf24
)

require (
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
Expand Down
Loading

0 comments on commit d4dcb01

Please sign in to comment.