Skip to content

Commit

Permalink
Regular expression extension. (#114)
Browse files Browse the repository at this point in the history
  • Loading branch information
ncruces committed Jul 4, 2024
1 parent 806cc66 commit a1fae26
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 2 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,16 @@ Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ run
reads data [line-by-line](https://github.com/asg017/sqlite-lines).
- [`github.com/ncruces/go-sqlite3/ext/pivot`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/pivot)
creates [pivot tables](https://github.com/jakethaw/pivot_vtab).
- [`github.com/ncruces/go-sqlite3/ext/regexp`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/regexp)
provides regular expression functions.
- [`github.com/ncruces/go-sqlite3/ext/statement`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/statement)
creates [parameterized views](https://github.com/0x09/sqlite-statement-vtab).
- [`github.com/ncruces/go-sqlite3/ext/stats`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/stats)
provides [statistics](https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html) functions.
- [`github.com/ncruces/go-sqlite3/ext/unicode`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/unicode)
provides [Unicode aware](https://sqlite.org/src/dir/ext/icu) functions.
- [`github.com/ncruces/go-sqlite3/ext/uuid`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/uuid)
generates [UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier).
- [`github.com/ncruces/go-sqlite3/ext/zorder`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/zorder)
maps multidimensional data to one dimension.
- [`github.com/ncruces/go-sqlite3/vfs/adiantum`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/adiantum)
Expand Down
77 changes: 77 additions & 0 deletions ext/regexp/regexp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Package regexp provides additional regular expression functions.
//
// It provides the following Unicode aware functions:
// - regexp_like(),
// - regexp_substr(),
// - regexp_replace(),
// - and a REGEXP operator.
//
// The implementation uses Go [regexp/syntax] for regular expressions.
//
// https://github.com/nalgeon/sqlean/blob/main/docs/regexp.md
package regexp

import (
"regexp"

"github.com/ncruces/go-sqlite3"
)

// Register registers Unicode aware functions for a database connection.
func Register(db *sqlite3.Conn) {
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS

db.CreateFunction("regexp", 2, flags, regex)
db.CreateFunction("regexp_like", 2, flags, regexLike)
db.CreateFunction("regexp_substr", 2, flags, regexSubstr)
db.CreateFunction("regexp_replace", 3, flags, regexReplace)
}

func load(ctx sqlite3.Context, i int, expr string) (*regexp.Regexp, error) {
re, ok := ctx.GetAuxData(i).(*regexp.Regexp)
if !ok {
r, err := regexp.Compile(expr)
if err != nil {
return nil, err
}
re = r
ctx.SetAuxData(0, r)
}
return re, nil
}

func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
re, err := load(ctx, 0, arg[0].Text())
if err != nil {
ctx.ResultError(err)
} else {
ctx.ResultBool(re.Match(arg[1].RawText()))
}
}

func regexLike(ctx sqlite3.Context, arg ...sqlite3.Value) {
re, err := load(ctx, 1, arg[1].Text())
if err != nil {
ctx.ResultError(err)
} else {
ctx.ResultBool(re.Match(arg[0].RawText()))
}
}

func regexSubstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
re, err := load(ctx, 1, arg[1].Text())
if err != nil {
ctx.ResultError(err)
} else {
ctx.ResultRawText(re.Find(arg[0].RawText()))
}
}

func regexReplace(ctx sqlite3.Context, arg ...sqlite3.Value) {
re, err := load(ctx, 1, arg[1].Text())
if err != nil {
ctx.ResultError(err)
} else {
ctx.ResultRawText(re.ReplaceAll(arg[0].RawText(), arg[2].RawText()))
}
}
75 changes: 75 additions & 0 deletions ext/regexp/regexp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package regexp

import (
"testing"

"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
)

func TestRegister(t *testing.T) {
t.Parallel()

db, err := driver.Open(":memory:", func(conn *sqlite3.Conn) error {
Register(conn)
return nil
})
if err != nil {
t.Fatal(err)
}
defer db.Close()

tests := []struct {
test string
want string
}{
{`'Hello' REGEXP 'elo'`, "0"},
{`'Hello' REGEXP 'ell'`, "1"},
{`'Hello' REGEXP 'el.'`, "1"},
{`regexp_like('Hello', 'elo')`, "0"},
{`regexp_like('Hello', 'ell')`, "1"},
{`regexp_like('Hello', 'el.')`, "1"},
{`regexp_substr('Hello', 'el.')`, "ell"},
{`regexp_replace('Hello', 'llo', 'll')`, "Hell"},
}

for _, tt := range tests {
var got string
err := db.QueryRow(`SELECT ` + tt.test).Scan(&got)
if err != nil {
t.Fatal(err)
}
if got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
}
}

func TestRegister_errors(t *testing.T) {
t.Parallel()

db, err := driver.Open(":memory:", func(conn *sqlite3.Conn) error {
Register(conn)
return nil
})
if err != nil {
t.Fatal(err)
}
defer db.Close()

tests := []string{
`'' REGEXP ?`,
`regexp_like('', ?)`,
`regexp_substr('', ?)`,
`regexp_replace('', ?, '')`,
}

for _, tt := range tests {
err := db.QueryRow(`SELECT `+tt, `\`).Scan(nil)
if err == nil {
t.Fatal("want error")
}
}
}
2 changes: 1 addition & 1 deletion ext/unicode/unicode.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
return
}
re = r
ctx.SetAuxData(0, re)
ctx.SetAuxData(0, r)
}
ctx.ResultBool(re.Match(arg[1].RawText()))
}
Expand Down
4 changes: 4 additions & 0 deletions ext/uuid/uuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
)

func Test_generate(t *testing.T) {
t.Parallel()

db, err := driver.Open(":memory:", func(conn *sqlite3.Conn) error {
Register(conn)
return nil
Expand Down Expand Up @@ -130,6 +132,8 @@ func Test_generate(t *testing.T) {
}

func Test_convert(t *testing.T) {
t.Parallel()

db, err := driver.Open(":memory:", func(conn *sqlite3.Conn) error {
Register(conn)
return nil
Expand Down
2 changes: 1 addition & 1 deletion func_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ func ExampleContext_SetAuxData() {
ctx.ResultError(err)
return
}
ctx.SetAuxData(0, r)
re = r
ctx.SetAuxData(0, r)
}
ctx.ResultBool(re.Match(arg[1].RawText()))
})
Expand Down

0 comments on commit a1fae26

Please sign in to comment.