diff --git a/README.md b/README.md index 4cd868b..8cf9e7a 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/ext/regexp/regexp.go b/ext/regexp/regexp.go new file mode 100644 index 0000000..b8315e0 --- /dev/null +++ b/ext/regexp/regexp.go @@ -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())) + } +} diff --git a/ext/regexp/regexp_test.go b/ext/regexp/regexp_test.go new file mode 100644 index 0000000..3cd9456 --- /dev/null +++ b/ext/regexp/regexp_test.go @@ -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") + } + } +} diff --git a/ext/unicode/unicode.go b/ext/unicode/unicode.go index d44a92f..2c8caee 100644 --- a/ext/unicode/unicode.go +++ b/ext/unicode/unicode.go @@ -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())) } diff --git a/ext/uuid/uuid_test.go b/ext/uuid/uuid_test.go index 25979aa..cb97c75 100644 --- a/ext/uuid/uuid_test.go +++ b/ext/uuid/uuid_test.go @@ -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 @@ -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 diff --git a/func_test.go b/func_test.go index 357be87..d86446d 100644 --- a/func_test.go +++ b/func_test.go @@ -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())) })