fix: memo filter for sqlite

This commit is contained in:
Steven 2025-07-22 23:39:52 +08:00
parent ac386c218d
commit 1a75d19a89
8 changed files with 23 additions and 153 deletions

View file

@ -559,7 +559,7 @@ func (s *APIV1Service) RenameMemoTag(ctx context.Context, request *v1pb.RenameMe
memoFind := &store.FindMemo{
CreatorID: &user.ID,
PayloadFind: &store.FindMemoPayload{TagSearch: []string{request.OldTag}},
Filters: []string{fmt.Sprintf("tag in [\"%s\"]", request.OldTag)},
ExcludeComments: true,
}
if (request.Parent) != "memos/-" {
@ -609,7 +609,7 @@ func (s *APIV1Service) DeleteMemoTag(ctx context.Context, request *v1pb.DeleteMe
memoFind := &store.FindMemo{
CreatorID: &user.ID,
PayloadFind: &store.FindMemoPayload{TagSearch: []string{request.Tag}},
Filters: []string{fmt.Sprintf("tag in [\"%s\"]", request.Tag)},
ExcludeContent: true,
ExcludeComments: true,
}

View file

@ -80,23 +80,6 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
if v := find.RowStatus; v != nil {
where, args = append(where, "`memo`.`row_status` = ?"), append(args, *v)
}
if v := find.CreatedTsBefore; v != nil {
where, args = append(where, "UNIX_TIMESTAMP(`memo`.`created_ts`) < ?"), append(args, *v)
}
if v := find.CreatedTsAfter; v != nil {
where, args = append(where, "UNIX_TIMESTAMP(`memo`.`created_ts`) > ?"), append(args, *v)
}
if v := find.UpdatedTsBefore; v != nil {
where, args = append(where, "UNIX_TIMESTAMP(`memo`.`updated_ts`) < ?"), append(args, *v)
}
if v := find.UpdatedTsAfter; v != nil {
where, args = append(where, "UNIX_TIMESTAMP(`memo`.`updated_ts`) > ?"), append(args, *v)
}
if v := find.ContentSearch; len(v) != 0 {
for _, s := range v {
where, args = append(where, "`memo`.`content` LIKE ?"), append(args, "%"+s+"%")
}
}
if v := find.VisibilityList; len(v) != 0 {
placeholder := []string{}
for _, visibility := range v {
@ -105,31 +88,6 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
}
where = append(where, fmt.Sprintf("`memo`.`visibility` in (%s)", strings.Join(placeholder, ",")))
}
if v := find.Pinned; v != nil {
where, args = append(where, "`memo`.`pinned` = ?"), append(args, *v)
}
if v := find.PayloadFind; v != nil {
if v.Raw != nil {
where, args = append(where, "`memo`.`payload` = ?"), append(args, *v.Raw)
}
if len(v.TagSearch) != 0 {
for _, tag := range v.TagSearch {
where, args = append(where, "(JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.tags'), ?) OR JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.tags'), ?))"), append(args, fmt.Sprintf(`"%s"`, tag), fmt.Sprintf(`"%s/"`, tag))
}
}
if v.HasLink {
where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasLink') IS TRUE")
}
if v.HasTaskList {
where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE")
}
if v.HasCode {
where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasCode') IS TRUE")
}
if v.HasIncompleteTasks {
where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasIncompleteTasks') IS TRUE")
}
}
if find.ExcludeComments {
having = append(having, "`parent_id` IS NULL")
}

View file

@ -72,23 +72,6 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
if v := find.RowStatus; v != nil {
where, args = append(where, "memo.row_status = "+placeholder(len(args)+1)), append(args, *v)
}
if v := find.CreatedTsBefore; v != nil {
where, args = append(where, "memo.created_ts < "+placeholder(len(args)+1)), append(args, *v)
}
if v := find.CreatedTsAfter; v != nil {
where, args = append(where, "memo.created_ts > "+placeholder(len(args)+1)), append(args, *v)
}
if v := find.UpdatedTsBefore; v != nil {
where, args = append(where, "memo.updated_ts < "+placeholder(len(args)+1)), append(args, *v)
}
if v := find.UpdatedTsAfter; v != nil {
where, args = append(where, "memo.updated_ts > "+placeholder(len(args)+1)), append(args, *v)
}
if v := find.ContentSearch; len(v) != 0 {
for _, s := range v {
where, args = append(where, "memo.content ILIKE "+placeholder(len(args)+1)), append(args, fmt.Sprintf("%%%s%%", s))
}
}
if v := find.VisibilityList; len(v) != 0 {
holders := []string{}
for _, visibility := range v {
@ -97,31 +80,6 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
}
where = append(where, fmt.Sprintf("memo.visibility in (%s)", strings.Join(holders, ", ")))
}
if v := find.Pinned; v != nil {
where, args = append(where, "memo.pinned = "+placeholder(len(args)+1)), append(args, *v)
}
if v := find.PayloadFind; v != nil {
if v.Raw != nil {
where, args = append(where, "memo.payload = "+placeholder(len(args)+1)), append(args, *v.Raw)
}
if len(v.TagSearch) != 0 {
for _, tag := range v.TagSearch {
where, args = append(where, "EXISTS (SELECT 1 FROM jsonb_array_elements(memo.payload->'tags') AS tag WHERE tag::text = "+placeholder(len(args)+1)+" OR tag::text LIKE "+placeholder(len(args)+2)+")"), append(args, fmt.Sprintf(`"%s"`, tag), fmt.Sprintf(`"%s/%%"`, tag))
}
}
if v.HasLink {
where = append(where, "(memo.payload->'property'->>'hasLink')::BOOLEAN IS TRUE")
}
if v.HasTaskList {
where = append(where, "(memo.payload->'property'->>'hasTaskList')::BOOLEAN IS TRUE")
}
if v.HasCode {
where = append(where, "(memo.payload->'property'->>'hasCode')::BOOLEAN IS TRUE")
}
if v.HasIncompleteTasks {
where = append(where, "(memo.payload->'property'->>'hasIncompleteTasks')::BOOLEAN IS TRUE")
}
}
if find.ExcludeComments {
where = append(where, "memo_relation.related_memo_id IS NULL")
}

View file

@ -72,23 +72,6 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
if v := find.RowStatus; v != nil {
where, args = append(where, "`memo`.`row_status` = ?"), append(args, *v)
}
if v := find.CreatedTsBefore; v != nil {
where, args = append(where, "`memo`.`created_ts` < ?"), append(args, *v)
}
if v := find.CreatedTsAfter; v != nil {
where, args = append(where, "`memo`.`created_ts` > ?"), append(args, *v)
}
if v := find.UpdatedTsBefore; v != nil {
where, args = append(where, "`memo`.`updated_ts` < ?"), append(args, *v)
}
if v := find.UpdatedTsAfter; v != nil {
where, args = append(where, "`memo`.`updated_ts` > ?"), append(args, *v)
}
if v := find.ContentSearch; len(v) != 0 {
for _, s := range v {
where, args = append(where, "`memo`.`content` LIKE ?"), append(args, fmt.Sprintf("%%%s%%", s))
}
}
if v := find.VisibilityList; len(v) != 0 {
placeholder := []string{}
for _, visibility := range v {
@ -97,31 +80,6 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
}
where = append(where, fmt.Sprintf("`memo`.`visibility` IN (%s)", strings.Join(placeholder, ",")))
}
if v := find.Pinned; v != nil {
where, args = append(where, "`memo`.`pinned` = ?"), append(args, *v)
}
if v := find.PayloadFind; v != nil {
if v.Raw != nil {
where, args = append(where, "`memo`.`payload` = ?"), append(args, *v.Raw)
}
if len(v.TagSearch) != 0 {
for _, tag := range v.TagSearch {
where, args = append(where, "(JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ? OR JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ?)"), append(args, fmt.Sprintf(`%%"%s"%%`, tag), fmt.Sprintf(`%%"%s/%%`, tag))
}
}
if v.HasLink {
where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasLink') IS TRUE")
}
if v.HasTaskList {
where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE")
}
if v.HasCode {
where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasCode') IS TRUE")
}
if v.HasIncompleteTasks {
where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasIncompleteTasks') IS TRUE")
}
}
if find.ExcludeComments {
where = append(where, "`parent_id` IS NULL")
}

View file

@ -200,15 +200,15 @@ func (d *DB) convertWithTemplates(ctx *filter.ConvertContext, expr *exprv1.Expr)
var sqlTemplate string
if operator == "=" {
if valueBool {
sqlTemplate = fmt.Sprintf("JSON_EXTRACT(`memo`.`payload`, '%s') = JSON('true')", jsonPath)
sqlTemplate = fmt.Sprintf("JSON_EXTRACT(`memo`.`payload`, '%s') IS TRUE", jsonPath)
} else {
sqlTemplate = fmt.Sprintf("JSON_EXTRACT(`memo`.`payload`, '%s') = JSON('false')", jsonPath)
sqlTemplate = fmt.Sprintf("NOT(JSON_EXTRACT(`memo`.`payload`, '%s') IS TRUE)", jsonPath)
}
} else { // operator == "!="
if valueBool {
sqlTemplate = fmt.Sprintf("JSON_EXTRACT(`memo`.`payload`, '%s') != JSON('true')", jsonPath)
sqlTemplate = fmt.Sprintf("NOT(JSON_EXTRACT(`memo`.`payload`, '%s') IS TRUE)", jsonPath)
} else {
sqlTemplate = fmt.Sprintf("JSON_EXTRACT(`memo`.`payload`, '%s') != JSON('false')", jsonPath)
sqlTemplate = fmt.Sprintf("JSON_EXTRACT(`memo`.`payload`, '%s') IS TRUE", jsonPath)
}
}
if _, err := ctx.Buffer.WriteString(sqlTemplate); err != nil {
@ -319,17 +319,17 @@ func (d *DB) convertWithTemplates(ctx *filter.ConvertContext, expr *exprv1.Expr)
}
} else if identifier == "has_link" {
// Handle has_link as a standalone boolean identifier
if _, err := ctx.Buffer.WriteString("JSON_EXTRACT(`memo`.`payload`, '$.property.hasLink') = JSON('true')"); err != nil {
if _, err := ctx.Buffer.WriteString("JSON_EXTRACT(`memo`.`payload`, '$.property.hasLink') IS TRUE"); err != nil {
return err
}
} else if identifier == "has_code" {
// Handle has_code as a standalone boolean identifier
if _, err := ctx.Buffer.WriteString("JSON_EXTRACT(`memo`.`payload`, '$.property.hasCode') = JSON('true')"); err != nil {
if _, err := ctx.Buffer.WriteString("JSON_EXTRACT(`memo`.`payload`, '$.property.hasCode') IS TRUE"); err != nil {
return err
}
} else if identifier == "has_incomplete_tasks" {
// Handle has_incomplete_tasks as a standalone boolean identifier
if _, err := ctx.Buffer.WriteString("JSON_EXTRACT(`memo`.`payload`, '$.property.hasIncompleteTasks') = JSON('true')"); err != nil {
if _, err := ctx.Buffer.WriteString("JSON_EXTRACT(`memo`.`payload`, '$.property.hasIncompleteTasks') IS TRUE"); err != nil {
return err
}
}

View file

@ -60,6 +60,11 @@ func TestConvertExprToSQL(t *testing.T) {
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE",
args: []any{},
},
{
filter: `has_code`,
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasCode') IS TRUE",
args: []any{},
},
{
filter: `has_task_list == true`,
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = 1",
@ -117,32 +122,32 @@ func TestConvertExprToSQL(t *testing.T) {
},
{
filter: `has_link == true`,
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasLink') = JSON('true')",
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasLink') IS TRUE",
args: []any{},
},
{
filter: `has_code == false`,
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasCode') = JSON('false')",
want: "NOT(JSON_EXTRACT(`memo`.`payload`, '$.property.hasCode') IS TRUE)",
args: []any{},
},
{
filter: `has_incomplete_tasks != false`,
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasIncompleteTasks') != JSON('false')",
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasIncompleteTasks') IS TRUE",
args: []any{},
},
{
filter: `has_link`,
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasLink') = JSON('true')",
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasLink') IS TRUE",
args: []any{},
},
{
filter: `has_code`,
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasCode') = JSON('true')",
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasCode') IS TRUE",
args: []any{},
},
{
filter: `has_incomplete_tasks`,
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasIncompleteTasks') = JSON('true')",
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasIncompleteTasks') IS TRUE",
args: []any{},
},
}

View file

@ -60,18 +60,11 @@ type FindMemo struct {
UID *string
// Standard fields
RowStatus *RowStatus
CreatorID *int32
CreatedTsAfter *int64
CreatedTsBefore *int64
UpdatedTsAfter *int64
UpdatedTsBefore *int64
RowStatus *RowStatus
CreatorID *int32
// Domain specific fields
ContentSearch []string
VisibilityList []Visibility
Pinned *bool
PayloadFind *FindMemoPayload
ExcludeContent bool
ExcludeComments bool
Filters []string

View file

@ -86,9 +86,7 @@ func TestMemoListByTags(t *testing.T) {
require.NotNil(t, memo)
memoList, err := ts.ListMemos(ctx, &store.FindMemo{
PayloadFind: &store.FindMemoPayload{
TagSearch: []string{"test_tag"},
},
Filters: []string{"tag in [\"test_tag\"]"},
})
require.NoError(t, err)
require.Equal(t, 1, len(memoList))