Repository: G-Research/fasttrackml
Pull request #171 (2023-07-11 18:29:16)

Add ability to use regular expressions in Aim search queries
Implement `match` function which is analog of `python` match:
- modified `query` object to keep current `dialector`.
- use `github.com/multiprocessio/go-sqlite3-stdlib` - it represents big set of extensions, because natively `sqllite` doesn't support `regexp`.  Probably `sqlite3-pcre` is something that could help but in case of `local` development question is still opened? Also there is - https://github.com/nalgeon/sqlean but let's discuss it.
- modified `unit` tests to test different cases for different `dialectors`.
- not fully sure how to apply the same tests for `integration` tests. Maybe we need some sort of `matrix` to run service with different configurations: `postgres`, `sqlite`, `s3`, etc.

P.S Im not sure that this is final solution but I've spent a lot of time and seems like there is no normal way to do that for `sqlite`.

jgiannuzzi: With regards to regexp support in SQLite, could we please benchmark a C regexp implementation vs a Go one?
Also, if we use a Go one, it would be enough to simply register a copy of this [trivial function](https://github.com/multiprocessio/go-sqlite3-stdlib/blob/6ac92baf2e8afd2c95e333d939edb744eea9ea5e/regexp.go#L7-L14) within a ConnectHook (example [here](https://github.com/multiprocessio/go-sqlite3-stdlib/blob/6ac92baf2e8afd2c95e333d939edb744eea9ea5e/stdlib.go#L18-L42)). I'm not very keen in overriding so many SQLite builtin functions just for this.
jgiannuzzi: here is a [complete working example](https://github.com/mattn/go-sqlite3/blob/64bb93539113de32d6ae3daef1e09ca8239c743d/_example/custom_func/main.go) of registering a function written in Go with SQLite
dsuhinin: > here is a [complete working example](https://github.com/mattn/go-sqlite3/blob/64bb93539113de32d6ae3daef1e09ca8239c743d/_example/custom_func/main.go) of registering a function written in Go with SQLite

yeesss. I also checked this stuff but it was a problem. When I tried to use `regexp` in `WHERE` statement, gorm always says that function is unregistered, that's why I just continued to move on. Ok. let me dive again.
jgiannuzzi: another thing to benchmark is whether having a LRU cache of compiled regexps in the custom Go function would make a difference — my guess is that it does and so we should implement it that way
dsuhinin: > here is a [complete working example](https://github.com/mattn/go-sqlite3/blob/64bb93539113de32d6ae3daef1e09ca8239c743d/_example/custom_func/main.go) of registering a function written in Go with SQLite

ahh, ok. now I remembered what is the problem with this solution. so when you registered `regexp` function such way, other functions that we use just stop working. probably when you we register like that -> https://github.com/mattn/go-sqlite3/blob/64bb93539113de32d6ae3daef1e09ca8239c743d/_example/custom_func/main.go it is no different with to use -> github.com/multiprocessio/go-sqlite3-stdlib. as a result when I registered new `driver`, other functions just stop working like for example `mod` and I think encryption also should stop working.
jgiannuzzi: I just tested it and it works fine with both encryption and MOD — please implement it this way!
jgiannuzzi: here are a few examples of queries using regexes that should work:

---

```python
re.match("(?:test|train)", run.name)
```
=>
```sql
SELECT * FROM "runs" WHERE "runs"."name" REGEXP '^(?:test|train)';
```

---

```python
re.search("(?:test|train)", run.name)
```
=>
```sql
SELECT * FROM "runs" WHERE "runs"."name" REGEXP '(?:test|train)';
```

---

```python
re.search("(?:test|train)", run.mode)
```
=>
```sql
SELECT * FROM "runs" LEFT JOIN params params_0 ON runs.run_uuid = params_0.run_uuid AND params_0.key = 'mode' WHERE "params_0"."value" REGEXP '(?:test|train)';
```
dsuhinin: > here are a few examples of queries using regexes that should work:
> 
> ```python
> re.match("(?:test|train)", run.name)
> ```
> 
> =>
> 
> ```sql
> SELECT * FROM "runs" WHERE "runs"."name" REGEXP '^(?:test|train)';
> ```
> 
>  ```python
> re.search("(?:test|train)", run.name)
> ```
> 
> =>
> 
> ```sql
> SELECT * FROM "runs" WHERE "runs"."name" REGEXP '(?:test|train)';
> ```
> 
>  ```python
> re.search("(?:test|train)", run.mode)
> ```
> 
> =>
> 
> ```sql
> SELECT * FROM "runs" LEFT JOIN params params_0 ON runs.run_uuid = params_0.run_uuid AND params_0.key = 'mode' WHERE "params_0"."value" REGEXP '(?:test|train)';
> ```

ok, thank you! Ill take a look how we can handle that.
dsuhinin: @jgiannuzzi oook, Im a bit refactored current functionality to support `match` and `search` functions to be like in `python`. As usually, the solution is not simple but it works and fully tested by `unit` and `integration` tests. please let me know what you think?

Patch:
diff --git a/go.mod b/go.mod
index e9e6add..56c7afe 100644
--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,9 @@ require (
 	github.com/go-python/gpython v0.2.0
 	github.com/gofiber/fiber/v2 v2.48.0
 	github.com/google/uuid v1.3.0
+	github.com/hashicorp/golang-lru/v2 v2.0.4
 	github.com/hetiansu5/urlquery v1.2.7
+	github.com/mattn/go-sqlite3 v1.14.16
 	github.com/pkg/errors v0.9.1
 	github.com/rotisserie/eris v0.5.4
 	github.com/sirupsen/logrus v1.9.3
@@ -50,7 +52,6 @@ require (
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.19 // indirect
 	github.com/mattn/go-runewidth v0.0.14 // indirect
-	github.com/mattn/go-sqlite3 v1.14.16 // indirect
 	github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect
 	github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
diff --git a/go.sum b/go.sum
index 7dd7f06..4ce0871 100644
--- a/go.sum
+++ b/go.sum
@@ -147,6 +147,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
 github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru/v2 v2.0.4 h1:7GHuZcgid37q8o5i3QI9KMT4nCWQQ3Kx3Ov6bb9MfK0=
+github.com/hashicorp/golang-lru/v2 v2.0.4/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hetiansu5/urlquery v1.2.7 h1:jn0h+9pIRqUziSPnRdK/gJK8S5TCnk+HZZx5fRHf8K0=
diff --git a/pkg/api/aim/query/clause.go b/pkg/api/aim/query/clause.go
new file mode 100644
index 0000000..1281acc
--- /dev/null
+++ b/pkg/api/aim/query/clause.go
@@ -0,0 +1,38 @@
+package query
+
+import (
+	"fmt"
+
+	"gorm.io/driver/postgres"
+	"gorm.io/gorm/clause"
+)
+
+// Regexp whether string matches regular expression
+type Regexp struct {
+	clause.Eq
+	Dialector string
+}
+
+// Build builds positive statement.
+func (regexp Regexp) Build(builder clause.Builder) {
+	builder.WriteQuoted(regexp.Column)
+	switch regexp.Dialector {
+	case postgres.Dialector{}.Name():
+		builder.WriteString(" ~ ")
+	default:
+		builder.WriteString(" regexp ")
+	}
+	builder.AddVar(builder, fmt.Sprintf("%s", regexp.Value))
+}
+
+// NegationBuild builds negative statement.
+func (regexp Regexp) NegationBuild(builder clause.Builder) {
+	builder.WriteQuoted(regexp.Column)
+	switch regexp.Dialector {
+	case postgres.Dialector{}.Name():
+		builder.WriteString(" !~ ")
+	default:
+		builder.WriteString(" NOT regexp ")
+	}
+	builder.AddVar(builder, regexp.Value)
+}
diff --git a/pkg/api/aim/query/query.go b/pkg/api/aim/query/query.go
index 16ff6b7..607bc29 100644
--- a/pkg/api/aim/query/query.go
+++ b/pkg/api/aim/query/query.go
@@ -16,21 +16,16 @@ import (
 	"github.com/G-Research/fasttrackml/pkg/api/mlflow/dao/models"
 )
 
-const (
-	OperationEndsWith   = "endswith"
-	OperationContains   = "contains"
-	OperationStartsWith = "startswith"
-)
-
 type DefaultExpression struct {
 	Contains   string
 	Expression string
 }
 
 type QueryParser struct {
-	Default  DefaultExpression
-	Tables   map[string]string
-	TzOffset int
+	Default   DefaultExpression
+	Tables    map[string]string
+	TzOffset  int
+	Dialector string
 }
 
 type ParsedQuery interface {
@@ -217,7 +212,7 @@ func (pq *parsedQuery) parseAttribute(node *ast.Attribute) (any, error) {
 		}
 		attribute := string(node.Attr)
 		switch strings.ToLower(attribute) {
-		case OperationEndsWith:
+		case "endswith":
 			return callable(func(args []ast.Expr) (any, error) {
 				if len(args) != 1 {
 					return nil, errors.New("`endwith` function support exactly one argument")
@@ -231,11 +226,15 @@ func (pq *parsedQuery) parseAttribute(node *ast.Attribute) (any, error) {
 				if !ok {
 					return nil, errors.New("unsupported argument type. has to be `string` only")
 				}
-				return clause.Expr{
-					SQL: fmt.Sprintf(`"%s"."%s" LIKE '%%%s'`, c.Table, c.Name, arg.S),
+				return clause.Like{
+					Value: fmt.Sprintf("%%%s", arg.S),
+					Column: clause.Column{
+						Table: c.Table,
+						Name:  c.Name,
+					},
 				}, nil
 			}), nil
-		case OperationContains:
+		case "contains":
 			return callable(func(args []ast.Expr) (any, error) {
 				if len(args) != 1 {
 					return nil, errors.New("`contains` function support exactly one argument")
@@ -249,11 +248,15 @@ func (pq *parsedQuery) parseAttribute(node *ast.Attribute) (any, error) {
 				if !ok {
 					return nil, errors.New("unsupported argument type. has to be `string` only")
 				}
-				return clause.Expr{
-					SQL: fmt.Sprintf(`"%s"."%s" LIKE '%%%s%%'`, c.Table, c.Name, arg.S),
+				return clause.Like{
+					Value: fmt.Sprintf("%%%s%%", arg.S),
+					Column: clause.Column{
+						Table: c.Table,
+						Name:  c.Name,
+					},
 				}, nil
 			}), nil
-		case OperationStartsWith:
+		case "startswith":
 			return callable(func(args []ast.Expr) (any, error) {
 				if len(args) != 1 {
 					return nil, errors.New("`startwith` function support exactly one argument")
@@ -267,8 +270,12 @@ func (pq *parsedQuery) parseAttribute(node *ast.Attribute) (any, error) {
 				if !ok {
 					return nil, errors.New("unsupported argument type. has to be `string` only")
 				}
-				return clause.Expr{
-					SQL: fmt.Sprintf(`"%s"."%s" LIKE '%s%%'`, c.Table, c.Name, arg.S),
+				return clause.Like{
+					Value: fmt.Sprintf("%s%%", arg.S),
+					Column: clause.Column{
+						Table: c.Table,
+						Name:  c.Name,
+					},
 				}, nil
 			}), nil
 		}
@@ -403,7 +410,7 @@ func (pq *parsedQuery) parseName(node *ast.Name) (any, error) {
 		case "run":
 			table, ok := pq.qp.Tables["runs"]
 			if !ok {
-				return nil, errors.New("unsupported name identifier \"run\"")
+				return nil, errors.New(`unsupported name identifier "run"`)
 			}
 			return attributeGetter(
 				func(attr string) (any, error) {
@@ -431,7 +438,7 @@ func (pq *parsedQuery) parseName(node *ast.Name) (any, error) {
 					case "experiment":
 						e, ok := pq.qp.Tables["experiments"]
 						if !ok {
-							return nil, errors.New("unsupported attribute \"experiment\"")
+							return nil, errors.New(`unsupported attribute "experiment"`)
 						}
 						return clause.Column{
 							Table: e,
@@ -554,7 +561,7 @@ func (pq *parsedQuery) parseName(node *ast.Name) (any, error) {
 		case "metric":
 			table, ok := pq.qp.Tables["metrics"]
 			if !ok {
-				return nil, errors.New("unsupported name identifier \"metric\"")
+				return nil, errors.New(`unsupported name identifier "metric"`)
 			}
 			return attributeGetter(
 				func(attr string) (any, error) {
@@ -581,6 +588,58 @@ func (pq *parsedQuery) parseName(node *ast.Name) (any, error) {
 					}
 				},
 			), nil
+		case "re":
+			return attributeGetter(
+				func(attr string) (any, error) {
+					switch attr {
+					case "match":
+						fallthrough
+					case "search":
+						return callable(
+							func(args []ast.Expr) (any, error) {
+								if len(args) != 2 {
+									return nil, errors.New("re.match function support exactly 2 arguments")
+								}
+
+								parsedNode, err := pq.parseNode(args[0])
+								if err != nil {
+									return nil, err
+								}
+								str, ok := parsedNode.(string)
+								if !ok {
+									return nil, errors.New("first argument type for re.match function has to be a string")
+								}
+
+								parsedNode, err = pq.parseNode(args[1])
+								if err != nil {
+									return nil, err
+								}
+								column, ok := parsedNode.(clause.Column)
+								if !ok {
+									return nil, errors.New(
+										"second argument type for re.match function has to be clause.Column",
+									)
+								}
+
+								// handle difference between `match` and `search`.
+								if attr == "match" {
+									str = fmt.Sprintf("^%s", str)
+								}
+
+								return Regexp{
+									Eq: clause.Eq{
+										Column: column,
+										Value:  str,
+									},
+									Dialector: pq.qp.Dialector,
+								}, nil
+							},
+						), nil
+					default:
+						return nil, fmt.Errorf("unsupported re function %s", attr)
+					}
+				},
+			), nil
 		case "datetime":
 			return callable(
 				func(args []ast.Expr) (any, error) {
diff --git a/pkg/api/aim/runs.go b/pkg/api/aim/runs.go
index 797e63e..b9f3afc 100644
--- a/pkg/api/aim/runs.go
+++ b/pkg/api/aim/runs.go
@@ -344,7 +344,8 @@ func SearchRuns(c *fiber.Ctx) error {
 			"runs":        "runs",
 			"experiments": "Experiment",
 		},
-		TzOffset: tzOffset,
+		TzOffset:  tzOffset,
+		Dialector: database.DB.Dialector.Name(),
 	}
 	qp, err := pq.Parse(q.Query)
 	if err != nil {
@@ -538,7 +539,8 @@ func SearchMetrics(c *fiber.Ctx) error {
 			"experiments": "experiments",
 			"metrics":     "latest_metrics",
 		},
-		TzOffset: tzOffset,
+		TzOffset:  tzOffset,
+		Dialector: database.DB.Dialector.Name(),
 	}
 	qp, err := pq.Parse(q.Query)
 	if err != nil {
diff --git a/pkg/database/database.go b/pkg/database/database.go
index 00e68fe..969f461 100644
--- a/pkg/database/database.go
+++ b/pkg/database/database.go
@@ -8,9 +8,14 @@ import (
 	glog "log"
 	"net/url"
 	"os"
+	"regexp"
 	"strings"
 	"time"
 
+	"github.com/rotisserie/eris"
+
+	lru "github.com/hashicorp/golang-lru/v2"
+	"github.com/mattn/go-sqlite3"
 	log "github.com/sirupsen/logrus"
 	"gorm.io/driver/postgres"
 	"gorm.io/driver/sqlite"
@@ -19,6 +24,10 @@ import (
 	"gorm.io/plugin/dbresolver"
 )
 
+const (
+	SQLiteCustomDriverName = "sqlite3_custom_driver"
+)
+
 type DbInstance struct {
 	*gorm.DB
 	dsn     string
@@ -75,7 +84,28 @@ func ConnectDB(
 			}
 		}
 
-		s, err := sql.Open(sqlite.DriverName, strings.Replace(dbURL.String(), "sqlite://", "file:", 1))
+		sql.Register(SQLiteCustomDriverName, &sqlite3.SQLiteDriver{
+			ConnectHook: func(conn *sqlite3.SQLiteConn) error {
+				// create LRU cache to cache regexp statements and results.
+				cache, err := lru.New[string, *regexp.Regexp](1000)
+				if err != nil {
+					return eris.Wrap(err, "error creating lru cache to cache regexp statements")
+				}
+				return conn.RegisterFunc("regexp", func(re, s string) bool {
+					result, ok := cache.Get(re)
+					if !ok {
+						result, err = regexp.Compile(re)
+						if err != nil {
+							return false
+						}
+						cache.Add(re, result)
+					}
+					return result.MatchString(s)
+				}, true)
+			},
+		})
+
+		s, err := sql.Open(SQLiteCustomDriverName, strings.Replace(dbURL.String(), "sqlite://", "file:", 1))
 		if err != nil {
 			return nil, fmt.Errorf("failed to connect to database: %w", err)
 		}
@@ -90,7 +120,7 @@ func ConnectDB(
 
 		q.Set("_query_only", "true")
 		dbURL.RawQuery = q.Encode()
-		r, err := sql.Open(sqlite.DriverName, strings.Replace(dbURL.String(), "sqlite://", "file:", 1))
+		r, err := sql.Open(SQLiteCustomDriverName, strings.Replace(dbURL.String(), "sqlite://", "file:", 1))
 		if err != nil {
 			DB.Close()
 			return nil, fmt.Errorf("failed to connect to database: %w", err)


Test patch:
diff --git a/pkg/api/aim/query/query_test.go b/pkg/api/aim/query/query_test.go
index 89a329c..86be249 100644
--- a/pkg/api/aim/query/query_test.go
+++ b/pkg/api/aim/query/query_test.go
@@ -7,6 +7,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/suite"
 	"gorm.io/driver/postgres"
+	"gorm.io/driver/sqlite"
 	"gorm.io/gorm"
 
 	"github.com/G-Research/fasttrackml/pkg/api/mlflow/dao/models"
@@ -24,6 +25,7 @@ func TestQueryTestSuite(t *testing.T) {
 func (s *QueryTestSuite) SetupTest() {
 	mockedDB, _, err := sqlmock.New()
 	assert.Nil(s.T(), err)
+
 	db, err := gorm.Open(postgres.New(postgres.Config{
 		Conn:       mockedDB,
 		DriverName: "postgres",
@@ -32,7 +34,7 @@ func (s *QueryTestSuite) SetupTest() {
 	s.db = db
 }
 
-func (s *QueryTestSuite) Test_Ok() {
+func (s *QueryTestSuite) TestPostgresDialector_Ok() {
 	tests := []struct {
 		name         string
 		query        string
@@ -48,20 +50,102 @@ func (s *QueryTestSuite) Test_Ok() {
 		{
 			name:         "TestRunNameWithContainsFunction",
 			query:        `(run.name.contains('run'))`,
-			expectedSQL:  `SELECT * FROM "runs" WHERE ("runs"."name" LIKE '%run%' AND "runs"."lifecycle_stage" <> $1) ORDER BY "runs"."run_uuid" LIMIT 1`,
-			expectedVars: []interface{}{models.LifecycleStageDeleted},
+			expectedSQL:  `SELECT * FROM "runs" WHERE ("runs"."name" LIKE $1 AND "runs"."lifecycle_stage" <> $2) ORDER BY "runs"."run_uuid" LIMIT 1`,
+			expectedVars: []interface{}{"%run%", models.LifecycleStageDeleted},
 		},
 		{
 			name:         "TestRunNameWithStartWithFunction",
 			query:        `(run.name.startswith('run'))`,
-			expectedSQL:  `SELECT * FROM "runs" WHERE ("runs"."name" LIKE 'run%' AND "runs"."lifecycle_stage" <> $1) ORDER BY "runs"."run_uuid" LIMIT 1`,
-			expectedVars: []interface{}{models.LifecycleStageDeleted},
+			expectedSQL:  `SELECT * FROM "runs" WHERE ("runs"."name" LIKE $1 AND "runs"."lifecycle_stage" <> $2) ORDER BY "runs"."run_uuid" LIMIT 1`,
+			expectedVars: []interface{}{"run%", models.LifecycleStageDeleted},
 		},
 		{
 			name:         "TestRunNameWithEndWithFunction",
 			query:        `(run.name.endswith('run'))`,
-			expectedSQL:  `SELECT * FROM "runs" WHERE ("runs"."name" LIKE '%run' AND "runs"."lifecycle_stage" <> $1) ORDER BY "runs"."run_uuid" LIMIT 1`,
-			expectedVars: []interface{}{models.LifecycleStageDeleted},
+			expectedSQL:  `SELECT * FROM "runs" WHERE ("runs"."name" LIKE $1 AND "runs"."lifecycle_stage" <> $2) ORDER BY "runs"."run_uuid" LIMIT 1`,
+			expectedVars: []interface{}{"%run", models.LifecycleStageDeleted},
+		},
+		{
+			name:         "TestRunNameWithRegexpMatchFunction",
+			query:        `(re.match('run', run.name))`,
+			expectedSQL:  `SELECT * FROM "runs" WHERE ("runs"."name" ~ $1 AND "runs"."lifecycle_stage" <> $2) ORDER BY "runs"."run_uuid" LIMIT 1`,
+			expectedVars: []interface{}{"^run", models.LifecycleStageDeleted},
+		},
+		{
+			name:         "TestRunNameWithRegexpSearchFunction",
+			query:        `(re.search('run', run.name))`,
+			expectedSQL:  `SELECT * FROM "runs" WHERE ("runs"."name" ~ $1 AND "runs"."lifecycle_stage" <> $2) ORDER BY "runs"."run_uuid" LIMIT 1`,
+			expectedVars: []interface{}{"run", models.LifecycleStageDeleted},
+		},
+	}
+
+	for _, tt := range tests {
+		s.T().Run(tt.name, func(T *testing.T) {
+			pq := QueryParser{
+				Default: DefaultExpression{
+					Contains:   "run.archived",
+					Expression: "not run.archived",
+				},
+				Tables: map[string]string{
+					"runs":        "runs",
+					"experiments": "Experiment",
+				},
+				Dialector: postgres.Dialector{}.Name(),
+			}
+			parsedQuery, err := pq.Parse(tt.query)
+			assert.Nil(s.T(), err)
+			result := parsedQuery.Filter(
+				s.db.Session(&gorm.Session{DryRun: true}).Model(models.Run{}),
+			).First(&models.Run{})
+			assert.Nil(s.T(), result.Error)
+			assert.Equal(s.T(), tt.expectedSQL, result.Statement.SQL.String())
+			assert.Equal(s.T(), tt.expectedVars, result.Statement.Vars)
+		})
+	}
+}
+
+func (s *QueryTestSuite) TestSqliteDialector_Ok() {
+	tests := []struct {
+		name         string
+		query        string
+		expectedSQL  string
+		expectedVars []interface{}
+	}{
+		{
+			name:         "TestRunNameWithoutFunction",
+			query:        `(run.name == 'run')`,
+			expectedSQL:  `SELECT * FROM "runs" WHERE ("runs"."name" = $1 AND "runs"."lifecycle_stage" <> $2) ORDER BY "runs"."run_uuid" LIMIT 1`,
+			expectedVars: []interface{}{"run", models.LifecycleStageDeleted},
+		},
+		{
+			name:         "TestRunNameWithContainsFunction",
+			query:        `(run.name.contains('run'))`,
+			expectedSQL:  `SELECT * FROM "runs" WHERE ("runs"."name" LIKE $1 AND "runs"."lifecycle_stage" <> $2) ORDER BY "runs"."run_uuid" LIMIT 1`,
+			expectedVars: []interface{}{"%run%", models.LifecycleStageDeleted},
+		},
+		{
+			name:         "TestRunNameWithStartWithFunction",
+			query:        `(run.name.startswith('run'))`,
+			expectedSQL:  `SELECT * FROM "runs" WHERE ("runs"."name" LIKE $1 AND "runs"."lifecycle_stage" <> $2) ORDER BY "runs"."run_uuid" LIMIT 1`,
+			expectedVars: []interface{}{"run%", models.LifecycleStageDeleted},
+		},
+		{
+			name:         "TestRunNameWithEndWithFunction",
+			query:        `(run.name.endswith('run'))`,
+			expectedSQL:  `SELECT * FROM "runs" WHERE ("runs"."name" LIKE $1 AND "runs"."lifecycle_stage" <> $2) ORDER BY "runs"."run_uuid" LIMIT 1`,
+			expectedVars: []interface{}{"%run", models.LifecycleStageDeleted},
+		},
+		{
+			name:         "TestRunNameWithRegexpMatchFunction",
+			query:        `(re.match('run', run.name))`,
+			expectedSQL:  `SELECT * FROM "runs" WHERE ("runs"."name" regexp $1 AND "runs"."lifecycle_stage" <> $2) ORDER BY "runs"."run_uuid" LIMIT 1`,
+			expectedVars: []interface{}{"^run", models.LifecycleStageDeleted},
+		},
+		{
+			name:         "TestRunNameWithRegexpSearchFunction",
+			query:        `(re.search('run', run.name))`,
+			expectedSQL:  `SELECT * FROM "runs" WHERE ("runs"."name" regexp $1 AND "runs"."lifecycle_stage" <> $2) ORDER BY "runs"."run_uuid" LIMIT 1`,
+			expectedVars: []interface{}{"run", models.LifecycleStageDeleted},
 		},
 	}
 
@@ -76,6 +160,7 @@ func (s *QueryTestSuite) Test_Ok() {
 					"runs":        "runs",
 					"experiments": "Experiment",
 				},
+				Dialector: sqlite.Dialector{}.Name(),
 			}
 			parsedQuery, err := pq.Parse(tt.query)
 			assert.Nil(s.T(), err)
diff --git a/tests/integration/golang/aim/metric/search_test.go b/tests/integration/golang/aim/metric/search_test.go
index 053e040..8fd291f 100644
--- a/tests/integration/golang/aim/metric/search_test.go
+++ b/tests/integration/golang/aim/metric/search_test.go
@@ -100,6 +100,14 @@ func (s *SearchMetricsTestSuite) Test_Ok() {
 			name:  "TestEndWithFunction",
 			query: `q=(metric.name=='key1' and run.name.endswith("run"))&p=500&report_progress=false`,
 		},
+		{
+			name:  "TestRegexpMatchFunction",
+			query: `q=(metric.name=='key1' and re.match("chill", run.name))&p=500&report_progress=false`,
+		},
+		{
+			name:  "TestRegexpSearchFunction",
+			query: `q=(metric.name=='key1' and re.search("run", run.name))&p=500&report_progress=false`,
+		},
 	}
 	for _, tt := range tests {
 		s.T().Run(tt.name, func(T *testing.T) {
diff --git a/tests/integration/golang/mlflow/run/delete_test.go b/tests/integration/golang/mlflow/run/delete_test.go
index 4e61cc7..d3050f0 100644
--- a/tests/integration/golang/mlflow/run/delete_test.go
+++ b/tests/integration/golang/mlflow/run/delete_test.go
@@ -56,7 +56,6 @@ func (s *DeleteRunTestSuite) SetupTest() {
 
 func (s *DeleteRunTestSuite) Test_Ok() {
 	defer func() {
-		assert.Nil(s.T(), s.runFixtures.UnloadFixtures())
 		assert.Nil(s.T(), s.experimentFixtures.UnloadFixtures())
 	}()
 	tests := []struct {