Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Regular expression extension. #114

Merged
merged 3 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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