Skip to content

Commit a1fae26

Browse files
authored
Regular expression extension. (#114)
1 parent 806cc66 commit a1fae26

File tree

6 files changed

+162
-2
lines changed

6 files changed

+162
-2
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,16 @@ Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ run
4545
reads data [line-by-line](https://github.com/asg017/sqlite-lines).
4646
- [`github.com/ncruces/go-sqlite3/ext/pivot`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/pivot)
4747
creates [pivot tables](https://github.com/jakethaw/pivot_vtab).
48+
- [`github.com/ncruces/go-sqlite3/ext/regexp`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/regexp)
49+
provides regular expression functions.
4850
- [`github.com/ncruces/go-sqlite3/ext/statement`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/statement)
4951
creates [parameterized views](https://github.com/0x09/sqlite-statement-vtab).
5052
- [`github.com/ncruces/go-sqlite3/ext/stats`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/stats)
5153
provides [statistics](https://www.oreilly.com/library/view/sql-in-a/9780596155322/ch04s02.html) functions.
5254
- [`github.com/ncruces/go-sqlite3/ext/unicode`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/unicode)
5355
provides [Unicode aware](https://sqlite.org/src/dir/ext/icu) functions.
56+
- [`github.com/ncruces/go-sqlite3/ext/uuid`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/uuid)
57+
generates [UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier).
5458
- [`github.com/ncruces/go-sqlite3/ext/zorder`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/zorder)
5559
maps multidimensional data to one dimension.
5660
- [`github.com/ncruces/go-sqlite3/vfs/adiantum`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/adiantum)

ext/regexp/regexp.go

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Package regexp provides additional regular expression functions.
2+
//
3+
// It provides the following Unicode aware functions:
4+
// - regexp_like(),
5+
// - regexp_substr(),
6+
// - regexp_replace(),
7+
// - and a REGEXP operator.
8+
//
9+
// The implementation uses Go [regexp/syntax] for regular expressions.
10+
//
11+
// https://github.com/nalgeon/sqlean/blob/main/docs/regexp.md
12+
package regexp
13+
14+
import (
15+
"regexp"
16+
17+
"github.com/ncruces/go-sqlite3"
18+
)
19+
20+
// Register registers Unicode aware functions for a database connection.
21+
func Register(db *sqlite3.Conn) {
22+
flags := sqlite3.DETERMINISTIC | sqlite3.INNOCUOUS
23+
24+
db.CreateFunction("regexp", 2, flags, regex)
25+
db.CreateFunction("regexp_like", 2, flags, regexLike)
26+
db.CreateFunction("regexp_substr", 2, flags, regexSubstr)
27+
db.CreateFunction("regexp_replace", 3, flags, regexReplace)
28+
}
29+
30+
func load(ctx sqlite3.Context, i int, expr string) (*regexp.Regexp, error) {
31+
re, ok := ctx.GetAuxData(i).(*regexp.Regexp)
32+
if !ok {
33+
r, err := regexp.Compile(expr)
34+
if err != nil {
35+
return nil, err
36+
}
37+
re = r
38+
ctx.SetAuxData(0, r)
39+
}
40+
return re, nil
41+
}
42+
43+
func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
44+
re, err := load(ctx, 0, arg[0].Text())
45+
if err != nil {
46+
ctx.ResultError(err)
47+
} else {
48+
ctx.ResultBool(re.Match(arg[1].RawText()))
49+
}
50+
}
51+
52+
func regexLike(ctx sqlite3.Context, arg ...sqlite3.Value) {
53+
re, err := load(ctx, 1, arg[1].Text())
54+
if err != nil {
55+
ctx.ResultError(err)
56+
} else {
57+
ctx.ResultBool(re.Match(arg[0].RawText()))
58+
}
59+
}
60+
61+
func regexSubstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
62+
re, err := load(ctx, 1, arg[1].Text())
63+
if err != nil {
64+
ctx.ResultError(err)
65+
} else {
66+
ctx.ResultRawText(re.Find(arg[0].RawText()))
67+
}
68+
}
69+
70+
func regexReplace(ctx sqlite3.Context, arg ...sqlite3.Value) {
71+
re, err := load(ctx, 1, arg[1].Text())
72+
if err != nil {
73+
ctx.ResultError(err)
74+
} else {
75+
ctx.ResultRawText(re.ReplaceAll(arg[0].RawText(), arg[2].RawText()))
76+
}
77+
}

ext/regexp/regexp_test.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package regexp
2+
3+
import (
4+
"testing"
5+
6+
"github.com/ncruces/go-sqlite3"
7+
"github.com/ncruces/go-sqlite3/driver"
8+
_ "github.com/ncruces/go-sqlite3/embed"
9+
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
10+
)
11+
12+
func TestRegister(t *testing.T) {
13+
t.Parallel()
14+
15+
db, err := driver.Open(":memory:", func(conn *sqlite3.Conn) error {
16+
Register(conn)
17+
return nil
18+
})
19+
if err != nil {
20+
t.Fatal(err)
21+
}
22+
defer db.Close()
23+
24+
tests := []struct {
25+
test string
26+
want string
27+
}{
28+
{`'Hello' REGEXP 'elo'`, "0"},
29+
{`'Hello' REGEXP 'ell'`, "1"},
30+
{`'Hello' REGEXP 'el.'`, "1"},
31+
{`regexp_like('Hello', 'elo')`, "0"},
32+
{`regexp_like('Hello', 'ell')`, "1"},
33+
{`regexp_like('Hello', 'el.')`, "1"},
34+
{`regexp_substr('Hello', 'el.')`, "ell"},
35+
{`regexp_replace('Hello', 'llo', 'll')`, "Hell"},
36+
}
37+
38+
for _, tt := range tests {
39+
var got string
40+
err := db.QueryRow(`SELECT ` + tt.test).Scan(&got)
41+
if err != nil {
42+
t.Fatal(err)
43+
}
44+
if got != tt.want {
45+
t.Errorf("got %q, want %q", got, tt.want)
46+
}
47+
}
48+
}
49+
50+
func TestRegister_errors(t *testing.T) {
51+
t.Parallel()
52+
53+
db, err := driver.Open(":memory:", func(conn *sqlite3.Conn) error {
54+
Register(conn)
55+
return nil
56+
})
57+
if err != nil {
58+
t.Fatal(err)
59+
}
60+
defer db.Close()
61+
62+
tests := []string{
63+
`'' REGEXP ?`,
64+
`regexp_like('', ?)`,
65+
`regexp_substr('', ?)`,
66+
`regexp_replace('', ?, '')`,
67+
}
68+
69+
for _, tt := range tests {
70+
err := db.QueryRow(`SELECT `+tt, `\`).Scan(nil)
71+
if err == nil {
72+
t.Fatal("want error")
73+
}
74+
}
75+
}

ext/unicode/unicode.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
111111
return
112112
}
113113
re = r
114-
ctx.SetAuxData(0, re)
114+
ctx.SetAuxData(0, r)
115115
}
116116
ctx.ResultBool(re.Match(arg[1].RawText()))
117117
}

ext/uuid/uuid_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
)
1111

1212
func Test_generate(t *testing.T) {
13+
t.Parallel()
14+
1315
db, err := driver.Open(":memory:", func(conn *sqlite3.Conn) error {
1416
Register(conn)
1517
return nil
@@ -130,6 +132,8 @@ func Test_generate(t *testing.T) {
130132
}
131133

132134
func Test_convert(t *testing.T) {
135+
t.Parallel()
136+
133137
db, err := driver.Open(":memory:", func(conn *sqlite3.Conn) error {
134138
Register(conn)
135139
return nil

func_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ func ExampleContext_SetAuxData() {
130130
ctx.ResultError(err)
131131
return
132132
}
133-
ctx.SetAuxData(0, r)
134133
re = r
134+
ctx.SetAuxData(0, r)
135135
}
136136
ctx.ResultBool(re.Match(arg[1].RawText()))
137137
})

0 commit comments

Comments
 (0)