Ctrl K
ComunidadGuías…

filter/schema.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package filter

import (
	"fmt"

	"github.com/google/cel-go/cel"
	"github.com/google/cel-go/ext"
)

// DialectName enumerates supported SQL dialects.
type DialectName string

const (
	DialectSQLite   DialectName = "sqlite"
	DialectMySQL    DialectName = "mysql"
	DialectPostgres DialectName = "postgres"
)

// FieldType represents the logical type of a field.
type FieldType string

const (
	FieldTypeString    FieldType = "string"
	FieldTypeInt       FieldType = "int"
	FieldTypeBool      FieldType = "bool"
	FieldTypeTimestamp FieldType = "timestamp"
)

// FieldKind describes how a field is stored.
type FieldKind string

const (
	FieldKindScalar     FieldKind = "scalar"
	FieldKindBoolColumn FieldKind = "bool_column"
	FieldKindJSONBool   FieldKind = "json_bool"
	// FieldKindJSONExists represents a boolean derived from the presence of a non-null JSON value.
	FieldKindJSONExists   FieldKind = "json_exists"
	FieldKindJSONList     FieldKind = "json_list"
	FieldKindVirtualAlias FieldKind = "virtual_alias"
	// FieldKindExists represents a boolean derived from the presence of related
	// rows in another table, optionally OR-ed with a JSON boolean flag.
	FieldKindExists FieldKind = "exists"
)

// Column identifies the backing table column.
type Column struct {
	Table string
	Name  string
}

// RelatedRows identifies the rows of another table that belong to a record,
// for fields of kind FieldKindExists.
type RelatedRows struct {
	// Table is the related table.
	Table string
	// ForeignKey is the column of Table that holds the owner's key.
	ForeignKey string
	// Owner is the outer column ForeignKey refers to, such as memo.id.
	Owner Column
	// PrefixColumn and Prefix, when set, keep only related rows whose
	// PrefixColumn starts with Prefix, ignoring case. Prefix is a schema
	// constant and must not contain quotes or LIKE wildcards.
	PrefixColumn string
	Prefix       string
}

// Field captures the schema metadata for an exposed CEL identifier.
type Field struct {
	Name                 string
	Kind                 FieldKind
	Type                 FieldType
	Column               Column
	JSONPath             []string
	Related              *RelatedRows
	AliasFor             string
	SupportsContains     bool
	Expressions          map[DialectName]string
	AllowedComparisonOps map[ComparisonOperator]bool
}

// Schema collects CEL environment options and field metadata.
type Schema struct {
	Name       string
	Fields     map[string]Field
	EnvOptions []cel.EnvOption
}

// Field returns the field metadata if present.
func (s Schema) Field(name string) (Field, bool) {
	f, ok := s.Fields[name]
	return f, ok
}

// ResolveAlias resolves a virtual alias to its target field.
func (s Schema) ResolveAlias(name string) (Field, bool) {
	field, ok := s.Fields[name]
	if !ok {
		return Field{}, false
	}
	if field.Kind == FieldKindVirtualAlias {
		target, ok := s.Fields[field.AliasFor]
		if !ok {
			return Field{}, false
		}
		return target, true
	}
	return field, true
}

// NewSchema constructs the memo filter schema and CEL environment.
func NewSchema() Schema {
	fields := map[string]Field{
		"content": {
			Name:             "content",
			Kind:             FieldKindScalar,
			Type:             FieldTypeString,
			Column:           Column{Table: "memo", Name: "content"},
			SupportsContains: true,
			Expressions:      map[DialectName]string{},
		},
		"creator": {
			Name:   "creator",
			Kind:   FieldKindScalar,
			Type:   FieldTypeString,
			Column: Column{Table: "memo_creator", Name: "username"},
			Expressions: map[DialectName]string{
				DialectSQLite:   "('users/' || %s)",
				DialectMySQL:    "CONCAT('users/', %s)",
				DialectPostgres: "('users/' || %s)",
			},
			AllowedComparisonOps: map[ComparisonOperator]bool{
				CompareEq:  true,
				CompareNeq: true,
			},
		},
		"creator_id": {
			Name:        "creator_id",
			Kind:        FieldKindScalar,
			Type:        FieldTypeInt,
			Column:      Column{Table: "memo", Name: "creator_id"},
			Expressions: map[DialectName]string{},
			AllowedComparisonOps: map[ComparisonOperator]bool{
				CompareEq:  true,
				CompareNeq: true,
			},
		},
		"created_ts": {
			Name:   "created_ts",
			Kind:   FieldKindScalar,
			Type:   FieldTypeTimestamp,
			Column: Column{Table: "memo", Name: "created_ts"},
			Expressions: map[DialectName]string{
				// MySQL stores created_ts as TIMESTAMP, needs conversion to epoch
				DialectMySQL: "UNIX_TIMESTAMP(%s)",
				// PostgreSQL and SQLite store created_ts as BIGINT (epoch), no conversion needed
				DialectPostgres: "%s",
				DialectSQLite:   "%s",
			},
		},
		"updated_ts": {
			Name:   "updated_ts",
			Kind:   FieldKindScalar,
			Type:   FieldTypeTimestamp,
			Column: Column{Table: "memo", Name: "updated_ts"},
			Expressions: map[DialectName]string{
				// MySQL stores updated_ts as TIMESTAMP, needs conversion to epoch
				DialectMySQL: "UNIX_TIMESTAMP(%s)",
				// PostgreSQL and SQLite store updated_ts as BIGINT (epoch), no conversion needed
				DialectPostgres: "%s",
				DialectSQLite:   "%s",
			},
		},
		"pinned": {
			Name:        "pinned",
			Kind:        FieldKindBoolColumn,
			Type:        FieldTypeBool,
			Column:      Column{Table: "memo", Name: "pinned"},
			Expressions: map[DialectName]string{},
			AllowedComparisonOps: map[ComparisonOperator]bool{
				CompareEq:  true,
				CompareNeq: true,
			},
		},
		"visibility": {
			Name:        "visibility",
			Kind:        FieldKindScalar,
			Type:        FieldTypeString,
			Column:      Column{Table: "memo", Name: "visibility"},
			Expressions: map[DialectName]string{},
			AllowedComparisonOps: map[ComparisonOperator]bool{
				CompareEq:  true,
				CompareNeq: true,
			},
		},
		"space": {
			Name:   "space",
			Kind:   FieldKindScalar,
			Type:   FieldTypeString,
			Column: Column{Table: "memo_space", Name: "uid"},
			Expressions: map[DialectName]string{
				DialectSQLite:   "CASE WHEN `memo`.`space_id` IS NULL THEN NULL WHEN %[1]s IS NULL THEN '' ELSE ('spaces/' || %[1]s) END",
				DialectMySQL:    "CASE WHEN `memo`.`space_id` IS NULL THEN NULL WHEN %[1]s IS NULL THEN '' ELSE CONCAT('spaces/', %[1]s) END",
				DialectPostgres: "CASE WHEN memo.space_id IS NULL THEN NULL WHEN %[1]s IS NULL THEN '' ELSE ('spaces/' || %[1]s) END",
			},
			AllowedComparisonOps: map[ComparisonOperator]bool{
				CompareEq: true,
			},
		},
		"tags": {
			Name:     "tags",
			Kind:     FieldKindJSONList,
			Type:     FieldTypeString,
			Column:   Column{Table: "memo", Name: "payload"},
			JSONPath: []string{"tags"},
		},
		"tag": {
			Name:     "tag",
			Kind:     FieldKindVirtualAlias,
			Type:     FieldTypeString,
			AliasFor: "tags",
		},
		"has_task_list": {
			Name:     "has_task_list",
			Kind:     FieldKindJSONBool,
			Type:     FieldTypeBool,
			Column:   Column{Table: "memo", Name: "payload"},
			JSONPath: []string{"property", "hasTaskList"},
			AllowedComparisonOps: map[ComparisonOperator]bool{
				CompareEq:  true,
				CompareNeq: true,
			},
		},
		"has_link": {
			Name:     "has_link",
			Kind:     FieldKindJSONBool,
			Type:     FieldTypeBool,
			Column:   Column{Table: "memo", Name: "payload"},
			JSONPath: []string{"property", "hasLink"},
			AllowedComparisonOps: map[ComparisonOperator]bool{
				CompareEq:  true,
				CompareNeq: true,
			},
		},
		"has_code": {
			Name:     "has_code",
			Kind:     FieldKindJSONBool,
			Type:     FieldTypeBool,
			Column:   Column{Table: "memo", Name: "payload"},
			JSONPath: []string{"property", "hasCode"},
			AllowedComparisonOps: map[ComparisonOperator]bool{
				CompareEq:  true,
				CompareNeq: true,
			},
		},
		"has_incomplete_tasks": {
			Name:     "has_incomplete_tasks",
			Kind:     FieldKindJSONBool,
			Type:     FieldTypeBool,
			Column:   Column{Table: "memo", Name: "payload"},
			JSONPath: []string{"property", "hasIncompleteTasks"},
			AllowedComparisonOps: map[ComparisonOperator]bool{
				CompareEq:  true,
				CompareNeq: true,
			},
		},
		"has_location": {
			Name:     "has_location",
			Kind:     FieldKindJSONExists,
			Type:     FieldTypeBool,
			Column:   Column{Table: "memo", Name: "payload"},
			JSONPath: []string{"location"},
			AllowedComparisonOps: map[ComparisonOperator]bool{
				CompareEq:  true,
				CompareNeq: true,
			},
		},
		// Attachment binding lives in attachment.memo_id and changes without the
		// memo content changing, so it is read at query time, never copied into
		// the memo payload.
		"has_attachments": {
			Name: "has_attachments",
			Kind: FieldKindExists,
			Type: FieldTypeBool,
			Related: &RelatedRows{
				Table:      "attachment",
				ForeignKey: "memo_id",
				Owner:      Column{Table: "memo", Name: "id"},
			},
			AllowedComparisonOps: map[ComparisonOperator]bool{
				CompareEq:  true,
				CompareNeq: true,
			},
		},
		// has_image answers "does the memo show a picture": Markdown image syntax
		// in the content (the stored hasInlineImage flag) or an attached image
		// file that renders in the attachment strip. Both halves are needed; the
		// stored flag alone misses photos that are uploaded but not referenced.
		"has_image": {
			Name:     "has_image",
			Kind:     FieldKindExists,
			Type:     FieldTypeBool,
			Column:   Column{Table: "memo", Name: "payload"},
			JSONPath: []string{"property", "hasInlineImage"},
			Related: &RelatedRows{
				Table:        "attachment",
				ForeignKey:   "memo_id",
				Owner:        Column{Table: "memo", Name: "id"},
				PrefixColumn: "type",
				Prefix:       "image/",
			},
			AllowedComparisonOps: map[ComparisonOperator]bool{
				CompareEq:  true,
				CompareNeq: true,
			},
		},
	}

	envOptions := []cel.EnvOption{
		cel.Variable("content", cel.StringType),
		cel.Variable("creator", cel.StringType),
		cel.Variable("creator_id", cel.IntType),
		cel.Variable("created_ts", cel.TimestampType),
		cel.Variable("updated_ts", cel.TimestampType),
		cel.Variable("pinned", cel.BoolType),
		cel.Variable("tag", cel.StringType),
		cel.Variable("tags", cel.ListType(cel.StringType)),
		cel.Variable("visibility", cel.StringType),
		// Dyn permits the explicit null comparison used for memos without a space.
		cel.Variable("space", cel.DynType),
		cel.Variable("has_task_list", cel.BoolType),
		cel.Variable("has_link", cel.BoolType),
		cel.Variable("has_code", cel.BoolType),
		cel.Variable("has_incomplete_tasks", cel.BoolType),
		cel.Variable("has_location", cel.BoolType),
		cel.Variable("has_attachments", cel.BoolType),
		cel.Variable("has_image", cel.BoolType),
		cel.Variable("now", cel.TimestampType),
		ext.Sets(),
		cel.ASTValidators(cel.ValidateRegexLiterals()),
	}

	return Schema{
		Name:       "memo",
		Fields:     fields,
		EnvOptions: envOptions,
	}
}

// NewAttachmentSchema constructs the attachment filter schema and CEL environment.
func NewAttachmentSchema() Schema {
	fields := map[string]Field{
		"creator": {
			Name:   "creator",
			Kind:   FieldKindScalar,
			Type:   FieldTypeString,
			Column: Column{Table: "memo", Name: "creator_id"},
			Expressions: map[DialectName]string{
				DialectSQLite:   "('users/' || (SELECT username FROM user WHERE id = CASE WHEN attachment.memo_id IS NULL THEN attachment.creator_id ELSE %[1]s END))",
				DialectMySQL:    "CONCAT('users/', (SELECT username FROM `user` WHERE id = CASE WHEN `attachment`.`memo_id` IS NULL THEN `attachment`.`creator_id` ELSE %[1]s END))",
				DialectPostgres: "('users/' || (SELECT username FROM \"user\" WHERE id = CASE WHEN attachment.memo_id IS NULL THEN attachment.creator_id ELSE %[1]s END))",
			},
			AllowedComparisonOps: map[ComparisonOperator]bool{
				CompareEq: true,
			},
		},
		"filename": {
			Name:             "filename",
			Kind:             FieldKindScalar,
			Type:             FieldTypeString,
			Column:           Column{Table: "attachment", Name: "filename"},
			SupportsContains: true,
			Expressions:      map[DialectName]string{},
		},
		"mime_type": {
			Name:             "mime_type",
			Kind:             FieldKindScalar,
			Type:             FieldTypeString,
			Column:           Column{Table: "attachment", Name: "type"},
			SupportsContains: true,
			Expressions:      map[DialectName]string{},
		},
		"create_time": {
			Name:   "create_time",
			Kind:   FieldKindScalar,
			Type:   FieldTypeTimestamp,
			Column: Column{Table: "attachment", Name: "created_ts"},
			Expressions: map[DialectName]string{
				// MySQL stores created_ts as TIMESTAMP, needs conversion to epoch
				DialectMySQL: "UNIX_TIMESTAMP(%s)",
				// PostgreSQL and SQLite store created_ts as BIGINT (epoch), no conversion needed
				DialectPostgres: "%s",
				DialectSQLite:   "%s",
			},
		},
		"memo_id": {
			Name:        "memo_id",
			Kind:        FieldKindScalar,
			Type:        FieldTypeInt,
			Column:      Column{Table: "attachment", Name: "memo_id"},
			Expressions: map[DialectName]string{},
			AllowedComparisonOps: map[ComparisonOperator]bool{
				CompareEq:  true,
				CompareNeq: true,
			},
		},
		"space": {
			Name:   "space",
			Kind:   FieldKindScalar,
			Type:   FieldTypeString,
			Column: Column{Table: "attachment_space", Name: "uid"},
			Expressions: map[DialectName]string{
				DialectSQLite:   "CASE WHEN `attachment`.`memo_id` IS NULL OR `memo`.`space_id` IS NULL THEN NULL WHEN %[1]s IS NULL THEN '' ELSE ('spaces/' || %[1]s) END",
				DialectMySQL:    "CASE WHEN `attachment`.`memo_id` IS NULL OR `memo`.`space_id` IS NULL THEN NULL WHEN %[1]s IS NULL THEN '' ELSE CONCAT('spaces/', %[1]s) END",
				DialectPostgres: "CASE WHEN attachment.memo_id IS NULL OR memo.space_id IS NULL THEN NULL WHEN %[1]s IS NULL THEN '' ELSE ('spaces/' || %[1]s) END",
			},
			AllowedComparisonOps: map[ComparisonOperator]bool{
				CompareEq: true,
			},
		},
	}

	envOptions := []cel.EnvOption{
		cel.Variable("creator", cel.StringType),
		cel.Variable("filename", cel.StringType),
		cel.Variable("mime_type", cel.StringType),
		cel.Variable("create_time", cel.TimestampType),
		cel.Variable("memo_id", cel.AnyType),
		cel.Variable("space", cel.DynType),
		cel.Variable("now", cel.TimestampType),
		cel.ASTValidators(cel.ValidateRegexLiterals()),
	}

	return Schema{
		Name:       "attachment",
		Fields:     fields,
		EnvOptions: envOptions,
	}
}

// columnExpr returns the field expression for the given dialect, applying
// any schema-specific overrides (e.g. UNIX timestamp conversions).
func (f Field) columnExpr(d DialectName) string {
	base := qualifyColumn(d, f.Column)
	if expr, ok := f.Expressions[d]; ok && expr != "" {
		return fmt.Sprintf(expr, base)
	}
	return base
}