Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
b89d8f53
Commit
b89d8f53
authored
May 26, 2025
by
Steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: implement hasTaskList filter
parent
cbf5687d
Changes
7
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
174 additions
and
6 deletions
+174
-6
filter.go
plugin/filter/filter.go
+1
-0
memo_filter.go
store/db/mysql/memo_filter.go
+25
-2
memo_filter_test.go
store/db/mysql/memo_filter_test.go
+35
-0
memo_filter.go
store/db/postgres/memo_filter.go
+20
-2
memo_filter_test.go
store/db/postgres/memo_filter_test.go
+35
-0
memo_filter.go
store/db/sqlite/memo_filter.go
+23
-2
memo_filter_test.go
store/db/sqlite/memo_filter_test.go
+35
-0
No files found.
plugin/filter/filter.go
View file @
b89d8f53
...
@@ -17,6 +17,7 @@ var MemoFilterCELAttributes = []cel.EnvOption{
...
@@ -17,6 +17,7 @@ var MemoFilterCELAttributes = []cel.EnvOption{
cel
.
Variable
(
"tag"
,
cel
.
StringType
),
cel
.
Variable
(
"tag"
,
cel
.
StringType
),
cel
.
Variable
(
"update_time"
,
cel
.
StringType
),
cel
.
Variable
(
"update_time"
,
cel
.
StringType
),
cel
.
Variable
(
"visibility"
,
cel
.
StringType
),
cel
.
Variable
(
"visibility"
,
cel
.
StringType
),
cel
.
Variable
(
"has_task_list"
,
cel
.
BoolType
),
}
}
// Parse parses the filter string and returns the parsed expression.
// Parse parses the filter string and returns the parsed expression.
...
...
store/db/mysql/memo_filter.go
View file @
b89d8f53
...
@@ -59,7 +59,7 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
...
@@ -59,7 +59,7 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
if
err
!=
nil
{
if
err
!=
nil
{
return
err
return
err
}
}
if
!
slices
.
Contains
([]
string
{
"creator_id"
,
"create_time"
,
"update_time"
,
"visibility"
,
"content"
},
identifier
)
{
if
!
slices
.
Contains
([]
string
{
"creator_id"
,
"create_time"
,
"update_time"
,
"visibility"
,
"content"
,
"has_task_list"
},
identifier
)
{
return
errors
.
Errorf
(
"invalid identifier for %s"
,
v
.
CallExpr
.
Function
)
return
errors
.
Errorf
(
"invalid identifier for %s"
,
v
.
CallExpr
.
Function
)
}
}
value
,
err
:=
filter
.
GetConstValue
(
v
.
CallExpr
.
Args
[
1
])
value
,
err
:=
filter
.
GetConstValue
(
v
.
CallExpr
.
Args
[
1
])
...
@@ -138,6 +138,25 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
...
@@ -138,6 +138,25 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
return
err
return
err
}
}
ctx
.
Args
=
append
(
ctx
.
Args
,
valueInt
)
ctx
.
Args
=
append
(
ctx
.
Args
,
valueInt
)
}
else
if
identifier
==
"has_task_list"
{
if
operator
!=
"="
&&
operator
!=
"!="
{
return
errors
.
Errorf
(
"invalid operator for %s"
,
v
.
CallExpr
.
Function
)
}
valueBool
,
ok
:=
value
.
(
bool
)
if
!
ok
{
return
errors
.
New
(
"invalid boolean value for has_task_list"
)
}
// In MySQL, we can use JSON_EXTRACT to get the value and compare it to 'true' or 'false'
compareValue
:=
"false"
if
valueBool
{
compareValue
=
"true"
}
// MySQL uses -> as a shorthand for JSON_EXTRACT
if
_
,
err
:=
ctx
.
Buffer
.
WriteString
(
fmt
.
Sprintf
(
"JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') %s CAST('%s' AS JSON)"
,
operator
,
compareValue
));
err
!=
nil
{
return
err
}
}
}
case
"@in"
:
case
"@in"
:
if
len
(
v
.
CallExpr
.
Args
)
!=
2
{
if
len
(
v
.
CallExpr
.
Args
)
!=
2
{
...
@@ -207,13 +226,17 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
...
@@ -207,13 +226,17 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
}
}
}
else
if
v
,
ok
:=
expr
.
ExprKind
.
(
*
exprv1
.
Expr_IdentExpr
);
ok
{
}
else
if
v
,
ok
:=
expr
.
ExprKind
.
(
*
exprv1
.
Expr_IdentExpr
);
ok
{
identifier
:=
v
.
IdentExpr
.
GetName
()
identifier
:=
v
.
IdentExpr
.
GetName
()
if
!
slices
.
Contains
([]
string
{
"pinned"
},
identifier
)
{
if
!
slices
.
Contains
([]
string
{
"pinned"
,
"has_task_list"
},
identifier
)
{
return
errors
.
Errorf
(
"invalid identifier for %s"
,
identifier
)
return
errors
.
Errorf
(
"invalid identifier for %s"
,
identifier
)
}
}
if
identifier
==
"pinned"
{
if
identifier
==
"pinned"
{
if
_
,
err
:=
ctx
.
Buffer
.
WriteString
(
"`memo`.`pinned` IS TRUE"
);
err
!=
nil
{
if
_
,
err
:=
ctx
.
Buffer
.
WriteString
(
"`memo`.`pinned` IS TRUE"
);
err
!=
nil
{
return
err
return
err
}
}
}
else
if
identifier
==
"has_task_list"
{
if
_
,
err
:=
ctx
.
Buffer
.
WriteString
(
"JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON)"
);
err
!=
nil
{
return
err
}
}
}
}
}
return
nil
return
nil
...
...
store/db/mysql/memo_filter_test.go
View file @
b89d8f53
...
@@ -59,6 +59,41 @@ func TestConvertExprToSQL(t *testing.T) {
...
@@ -59,6 +59,41 @@ func TestConvertExprToSQL(t *testing.T) {
want
:
"`memo`.`pinned` IS TRUE"
,
want
:
"`memo`.`pinned` IS TRUE"
,
args
:
[]
any
{},
args
:
[]
any
{},
},
},
{
filter
:
`has_task_list`
,
want
:
"JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON)"
,
args
:
[]
any
{},
},
{
filter
:
`has_task_list == true`
,
want
:
"JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON)"
,
args
:
[]
any
{},
},
{
filter
:
`has_task_list != false`
,
want
:
"JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') != CAST('false' AS JSON)"
,
args
:
[]
any
{},
},
{
filter
:
`has_task_list == false`
,
want
:
"JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('false' AS JSON)"
,
args
:
[]
any
{},
},
{
filter
:
`!has_task_list`
,
want
:
"NOT (JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON))"
,
args
:
[]
any
{},
},
{
filter
:
`has_task_list && pinned`
,
want
:
"(JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON) AND `memo`.`pinned` IS TRUE)"
,
args
:
[]
any
{},
},
{
filter
:
`has_task_list && content.contains("todo")`
,
want
:
"(JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON) AND `memo`.`content` LIKE ?)"
,
args
:
[]
any
{
"%todo%"
},
},
}
}
for
_
,
tt
:=
range
tests
{
for
_
,
tt
:=
range
tests
{
...
...
store/db/postgres/memo_filter.go
View file @
b89d8f53
...
@@ -59,7 +59,7 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
...
@@ -59,7 +59,7 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
if
err
!=
nil
{
if
err
!=
nil
{
return
err
return
err
}
}
if
!
slices
.
Contains
([]
string
{
"creator_id"
,
"create_time"
,
"update_time"
,
"visibility"
,
"content"
},
identifier
)
{
if
!
slices
.
Contains
([]
string
{
"creator_id"
,
"create_time"
,
"update_time"
,
"visibility"
,
"content"
,
"has_task_list"
},
identifier
)
{
return
errors
.
Errorf
(
"invalid identifier for %s"
,
v
.
CallExpr
.
Function
)
return
errors
.
Errorf
(
"invalid identifier for %s"
,
v
.
CallExpr
.
Function
)
}
}
value
,
err
:=
filter
.
GetConstValue
(
v
.
CallExpr
.
Args
[
1
])
value
,
err
:=
filter
.
GetConstValue
(
v
.
CallExpr
.
Args
[
1
])
...
@@ -135,6 +135,20 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
...
@@ -135,6 +135,20 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
return
err
return
err
}
}
ctx
.
Args
=
append
(
ctx
.
Args
,
valueInt
)
ctx
.
Args
=
append
(
ctx
.
Args
,
valueInt
)
}
else
if
identifier
==
"has_task_list"
{
if
operator
!=
"="
&&
operator
!=
"!="
{
return
errors
.
Errorf
(
"invalid operator for %s"
,
v
.
CallExpr
.
Function
)
}
valueBool
,
ok
:=
value
.
(
bool
)
if
!
ok
{
return
errors
.
New
(
"invalid boolean value for has_task_list"
)
}
// In PostgreSQL, extract the boolean from the JSON and compare it
if
_
,
err
:=
ctx
.
Buffer
.
WriteString
(
fmt
.
Sprintf
(
"(memo.payload->'property'->>'hasTaskList')::boolean %s %s"
,
operator
,
placeholder
(
len
(
ctx
.
Args
)
+
ctx
.
ArgsOffset
+
1
)));
err
!=
nil
{
return
err
}
ctx
.
Args
=
append
(
ctx
.
Args
,
valueBool
)
}
}
case
"@in"
:
case
"@in"
:
if
len
(
v
.
CallExpr
.
Args
)
!=
2
{
if
len
(
v
.
CallExpr
.
Args
)
!=
2
{
...
@@ -204,13 +218,17 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
...
@@ -204,13 +218,17 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
}
}
}
else
if
v
,
ok
:=
expr
.
ExprKind
.
(
*
exprv1
.
Expr_IdentExpr
);
ok
{
}
else
if
v
,
ok
:=
expr
.
ExprKind
.
(
*
exprv1
.
Expr_IdentExpr
);
ok
{
identifier
:=
v
.
IdentExpr
.
GetName
()
identifier
:=
v
.
IdentExpr
.
GetName
()
if
!
slices
.
Contains
([]
string
{
"pinned"
},
identifier
)
{
if
!
slices
.
Contains
([]
string
{
"pinned"
,
"has_task_list"
},
identifier
)
{
return
errors
.
Errorf
(
"invalid identifier %s"
,
identifier
)
return
errors
.
Errorf
(
"invalid identifier %s"
,
identifier
)
}
}
if
identifier
==
"pinned"
{
if
identifier
==
"pinned"
{
if
_
,
err
:=
ctx
.
Buffer
.
WriteString
(
"memo.pinned IS TRUE"
);
err
!=
nil
{
if
_
,
err
:=
ctx
.
Buffer
.
WriteString
(
"memo.pinned IS TRUE"
);
err
!=
nil
{
return
err
return
err
}
}
}
else
if
identifier
==
"has_task_list"
{
if
_
,
err
:=
ctx
.
Buffer
.
WriteString
(
"(memo.payload->'property'->>'hasTaskList')::boolean IS TRUE"
);
err
!=
nil
{
return
err
}
}
}
}
}
return
nil
return
nil
...
...
store/db/postgres/memo_filter_test.go
View file @
b89d8f53
...
@@ -59,6 +59,41 @@ func TestRestoreExprToSQL(t *testing.T) {
...
@@ -59,6 +59,41 @@ func TestRestoreExprToSQL(t *testing.T) {
want
:
"memo.pinned IS TRUE"
,
want
:
"memo.pinned IS TRUE"
,
args
:
[]
any
{},
args
:
[]
any
{},
},
},
{
filter
:
`has_task_list`
,
want
:
"(memo.payload->'property'->>'hasTaskList')::boolean IS TRUE"
,
args
:
[]
any
{},
},
{
filter
:
`has_task_list == true`
,
want
:
"(memo.payload->'property'->>'hasTaskList')::boolean = $1"
,
args
:
[]
any
{
true
},
},
{
filter
:
`has_task_list != false`
,
want
:
"(memo.payload->'property'->>'hasTaskList')::boolean != $1"
,
args
:
[]
any
{
false
},
},
{
filter
:
`has_task_list == false`
,
want
:
"(memo.payload->'property'->>'hasTaskList')::boolean = $1"
,
args
:
[]
any
{
false
},
},
{
filter
:
`!has_task_list`
,
want
:
"NOT ((memo.payload->'property'->>'hasTaskList')::boolean IS TRUE)"
,
args
:
[]
any
{},
},
{
filter
:
`has_task_list && pinned`
,
want
:
"((memo.payload->'property'->>'hasTaskList')::boolean IS TRUE AND memo.pinned IS TRUE)"
,
args
:
[]
any
{},
},
{
filter
:
`has_task_list && content.contains("todo")`
,
want
:
"((memo.payload->'property'->>'hasTaskList')::boolean IS TRUE AND memo.content ILIKE $1)"
,
args
:
[]
any
{
"%todo%"
},
},
}
}
for
_
,
tt
:=
range
tests
{
for
_
,
tt
:=
range
tests
{
...
...
store/db/sqlite/memo_filter.go
View file @
b89d8f53
...
@@ -59,7 +59,7 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
...
@@ -59,7 +59,7 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
if
err
!=
nil
{
if
err
!=
nil
{
return
err
return
err
}
}
if
!
slices
.
Contains
([]
string
{
"creator_id"
,
"create_time"
,
"update_time"
,
"visibility"
,
"content"
},
identifier
)
{
if
!
slices
.
Contains
([]
string
{
"creator_id"
,
"create_time"
,
"update_time"
,
"visibility"
,
"content"
,
"has_task_list"
},
identifier
)
{
return
errors
.
Errorf
(
"invalid identifier for %s"
,
v
.
CallExpr
.
Function
)
return
errors
.
Errorf
(
"invalid identifier for %s"
,
v
.
CallExpr
.
Function
)
}
}
value
,
err
:=
filter
.
GetConstValue
(
v
.
CallExpr
.
Args
[
1
])
value
,
err
:=
filter
.
GetConstValue
(
v
.
CallExpr
.
Args
[
1
])
...
@@ -138,6 +138,22 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
...
@@ -138,6 +138,22 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
return
err
return
err
}
}
ctx
.
Args
=
append
(
ctx
.
Args
,
valueInt
)
ctx
.
Args
=
append
(
ctx
.
Args
,
valueInt
)
}
else
if
identifier
==
"has_task_list"
{
if
operator
!=
"="
&&
operator
!=
"!="
{
return
errors
.
Errorf
(
"invalid operator for %s"
,
v
.
CallExpr
.
Function
)
}
valueBool
,
ok
:=
value
.
(
bool
)
if
!
ok
{
return
errors
.
New
(
"invalid boolean value for has_task_list"
)
}
// In SQLite JSON boolean values are 1 for true and 0 for false
compareValue
:=
0
if
valueBool
{
compareValue
=
1
}
if
_
,
err
:=
ctx
.
Buffer
.
WriteString
(
fmt
.
Sprintf
(
"JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') %s %d"
,
operator
,
compareValue
));
err
!=
nil
{
return
err
}
}
}
case
"@in"
:
case
"@in"
:
if
len
(
v
.
CallExpr
.
Args
)
!=
2
{
if
len
(
v
.
CallExpr
.
Args
)
!=
2
{
...
@@ -207,13 +223,18 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
...
@@ -207,13 +223,18 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err
}
}
}
else
if
v
,
ok
:=
expr
.
ExprKind
.
(
*
exprv1
.
Expr_IdentExpr
);
ok
{
}
else
if
v
,
ok
:=
expr
.
ExprKind
.
(
*
exprv1
.
Expr_IdentExpr
);
ok
{
identifier
:=
v
.
IdentExpr
.
GetName
()
identifier
:=
v
.
IdentExpr
.
GetName
()
if
!
slices
.
Contains
([]
string
{
"pinned"
},
identifier
)
{
if
!
slices
.
Contains
([]
string
{
"pinned"
,
"has_task_list"
},
identifier
)
{
return
errors
.
Errorf
(
"invalid identifier %s"
,
identifier
)
return
errors
.
Errorf
(
"invalid identifier %s"
,
identifier
)
}
}
if
identifier
==
"pinned"
{
if
identifier
==
"pinned"
{
if
_
,
err
:=
ctx
.
Buffer
.
WriteString
(
"`memo`.`pinned` IS TRUE"
);
err
!=
nil
{
if
_
,
err
:=
ctx
.
Buffer
.
WriteString
(
"`memo`.`pinned` IS TRUE"
);
err
!=
nil
{
return
err
return
err
}
}
}
else
if
identifier
==
"has_task_list"
{
// Handle has_task_list as a standalone boolean identifier
if
_
,
err
:=
ctx
.
Buffer
.
WriteString
(
"JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE"
);
err
!=
nil
{
return
err
}
}
}
}
}
return
nil
return
nil
...
...
store/db/sqlite/memo_filter_test.go
View file @
b89d8f53
...
@@ -74,6 +74,41 @@ func TestConvertExprToSQL(t *testing.T) {
...
@@ -74,6 +74,41 @@ func TestConvertExprToSQL(t *testing.T) {
want
:
"(`memo`.`creator_id` = ? OR `memo`.`visibility` IN (?,?))"
,
want
:
"(`memo`.`creator_id` = ? OR `memo`.`visibility` IN (?,?))"
,
args
:
[]
any
{
int64
(
101
),
"PUBLIC"
,
"PRIVATE"
},
args
:
[]
any
{
int64
(
101
),
"PUBLIC"
,
"PRIVATE"
},
},
},
{
filter
:
`has_task_list`
,
want
:
"JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE"
,
args
:
[]
any
{},
},
{
filter
:
`has_task_list == true`
,
want
:
"JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = 1"
,
args
:
[]
any
{},
},
{
filter
:
`has_task_list != false`
,
want
:
"JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') != 0"
,
args
:
[]
any
{},
},
{
filter
:
`has_task_list == false`
,
want
:
"JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = 0"
,
args
:
[]
any
{},
},
{
filter
:
`!has_task_list`
,
want
:
"NOT (JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE)"
,
args
:
[]
any
{},
},
{
filter
:
`has_task_list && pinned`
,
want
:
"(JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE AND `memo`.`pinned` IS TRUE)"
,
args
:
[]
any
{},
},
{
filter
:
`has_task_list && content.contains("todo")`
,
want
:
"(JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE AND `memo`.`content` LIKE ?)"
,
args
:
[]
any
{
"%todo%"
},
},
}
}
for
_
,
tt
:=
range
tests
{
for
_
,
tt
:=
range
tests
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment