mirror of
				https://github.com/usememos/memos.git
				synced 2025-11-01 01:06:04 +08:00 
			
		
		
		
	feat: implement hasTaskList filter
This commit is contained in:
		
							parent
							
								
									cbf5687dd9
								
							
						
					
					
						commit
						b89d8f5342
					
				
					 7 changed files with 174 additions and 6 deletions
				
			
		|  | @ -17,6 +17,7 @@ var MemoFilterCELAttributes = []cel.EnvOption{ | |||
| 	cel.Variable("tag", cel.StringType), | ||||
| 	cel.Variable("update_time", cel.StringType), | ||||
| 	cel.Variable("visibility", cel.StringType), | ||||
| 	cel.Variable("has_task_list", cel.BoolType), | ||||
| } | ||||
| 
 | ||||
| // Parse parses the filter string and returns the parsed expression. | ||||
|  |  | |||
|  | @ -59,7 +59,7 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err | |||
| 			if err != nil { | ||||
| 				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) | ||||
| 			} | ||||
| 			value, err := filter.GetConstValue(v.CallExpr.Args[1]) | ||||
|  | @ -138,6 +138,25 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err | |||
| 					return err | ||||
| 				} | ||||
| 				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": | ||||
| 			if len(v.CallExpr.Args) != 2 { | ||||
|  | @ -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 { | ||||
| 		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) | ||||
| 		} | ||||
| 		if identifier == "pinned" { | ||||
| 			if _, err := ctx.Buffer.WriteString("`memo`.`pinned` IS TRUE"); err != nil { | ||||
| 				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 | ||||
|  |  | |||
|  | @ -59,6 +59,41 @@ func TestConvertExprToSQL(t *testing.T) { | |||
| 			want:   "`memo`.`pinned` IS TRUE", | ||||
| 			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 { | ||||
|  |  | |||
|  | @ -59,7 +59,7 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err | |||
| 			if err != nil { | ||||
| 				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) | ||||
| 			} | ||||
| 			value, err := filter.GetConstValue(v.CallExpr.Args[1]) | ||||
|  | @ -135,6 +135,20 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err | |||
| 					return err | ||||
| 				} | ||||
| 				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": | ||||
| 			if len(v.CallExpr.Args) != 2 { | ||||
|  | @ -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 { | ||||
| 		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) | ||||
| 		} | ||||
| 		if identifier == "pinned" { | ||||
| 			if _, err := ctx.Buffer.WriteString("memo.pinned IS TRUE"); err != nil { | ||||
| 				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 | ||||
|  |  | |||
|  | @ -59,6 +59,41 @@ func TestRestoreExprToSQL(t *testing.T) { | |||
| 			want:   "memo.pinned IS TRUE", | ||||
| 			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 { | ||||
|  |  | |||
|  | @ -59,7 +59,7 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err | |||
| 			if err != nil { | ||||
| 				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) | ||||
| 			} | ||||
| 			value, err := filter.GetConstValue(v.CallExpr.Args[1]) | ||||
|  | @ -138,6 +138,22 @@ func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) err | |||
| 					return err | ||||
| 				} | ||||
| 				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": | ||||
| 			if len(v.CallExpr.Args) != 2 { | ||||
|  | @ -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 { | ||||
| 		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) | ||||
| 		} | ||||
| 		if identifier == "pinned" { | ||||
| 			if _, err := ctx.Buffer.WriteString("`memo`.`pinned` IS TRUE"); err != nil { | ||||
| 				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 | ||||
|  |  | |||
|  | @ -74,6 +74,41 @@ func TestConvertExprToSQL(t *testing.T) { | |||
| 			want:   "(`memo`.`creator_id` = ? OR `memo`.`visibility` IN (?,?))", | ||||
| 			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 { | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue