-
Notifications
You must be signed in to change notification settings - Fork 272
Expand file tree
/
Copy pathfields_virtual.go
More file actions
130 lines (118 loc) · 3.87 KB
/
Copy pathfields_virtual.go
File metadata and controls
130 lines (118 loc) · 3.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// Copyright (C) 2026 Homer Server Contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
package services
import (
"encoding/json"
"fmt"
"regexp"
"strings"
)
const (
// VirtualKindDataExtraJSON extracts a value from the DuckLake data_extra JSON column.
VirtualKindDataExtraJSON = "data_extra_json"
)
// VirtualMatchLike wraps the value with SQL LIKE '%value%' (escaped).
// VirtualMatchEquals uses exact equality after trim.
// VirtualMatchAbsent / VirtualMatchPresent are driven by filter.virtual_absent / virtual_present (checkboxes), not filter.virtual values.
const (
VirtualMatchLike = "like"
VirtualMatchEquals = "equals"
VirtualMatchAbsent = "absent"
VirtualMatchPresent = "present"
)
var virtualPathSegmentRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
// VirtualFieldRule is a validated rule derived from fields_mapping[].virtual.
type VirtualFieldRule struct {
Kind string
Path string // logical JSON path without leading $. (e.g. to_tag or nested foo_bar)
Match string // like | equals | absent | present
}
// fieldMappingVirtualRaw is the optional virtual block on a mapping field row.
type fieldMappingVirtualRaw struct {
Kind string `json:"kind"`
Path string `json:"path"`
Match string `json:"match,omitempty"`
}
// fieldMappingEntryRaw matches one element of fields_mapping JSON arrays.
type fieldMappingEntryRaw struct {
ID string `json:"id"`
Virtual *fieldMappingVirtualRaw `json:"virtual,omitempty"`
}
// VirtualRulesFromFieldsMapping parses fields_mapping JSON and returns virtual
// rules keyed by field id. Non-virtual rows are skipped.
func VirtualRulesFromFieldsMapping(raw json.RawMessage) (map[string]VirtualFieldRule, error) {
if len(raw) == 0 || string(raw) == "null" {
return nil, nil
}
var entries []fieldMappingEntryRaw
if err := json.Unmarshal(raw, &entries); err != nil {
return nil, fmt.Errorf("fields_mapping: %w", err)
}
out := make(map[string]VirtualFieldRule)
for _, e := range entries {
if e.Virtual == nil || strings.TrimSpace(e.ID) == "" {
continue
}
rule, err := validateVirtualRule(e.ID, e.Virtual)
if err != nil {
return nil, fmt.Errorf("field %q: %w", e.ID, err)
}
out[e.ID] = rule
}
if len(out) == 0 {
return nil, nil
}
return out, nil
}
func validateVirtualRule(fieldID string, v *fieldMappingVirtualRaw) (VirtualFieldRule, error) {
if v == nil {
return VirtualFieldRule{}, fmt.Errorf("virtual spec is nil")
}
kind := strings.TrimSpace(v.Kind)
if kind != VirtualKindDataExtraJSON {
return VirtualFieldRule{}, fmt.Errorf("unsupported virtual.kind %q", v.Kind)
}
path := strings.TrimSpace(v.Path)
if path == "" {
return VirtualFieldRule{}, fmt.Errorf("virtual.path is required")
}
if err := validateVirtualJSONPath(path); err != nil {
return VirtualFieldRule{}, err
}
match := strings.TrimSpace(v.Match)
if match == "" {
match = VirtualMatchLike
}
if match != VirtualMatchLike && match != VirtualMatchEquals && match != VirtualMatchAbsent && match != VirtualMatchPresent {
return VirtualFieldRule{}, fmt.Errorf("unsupported virtual.match %q", v.Match)
}
_ = fieldID // reserved for future aliasing
return VirtualFieldRule{Kind: kind, Path: path, Match: match}, nil
}
func validateVirtualJSONPath(path string) error {
parts := strings.Split(path, ".")
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
return fmt.Errorf("invalid virtual.path %q", path)
}
if !virtualPathSegmentRe.MatchString(p) {
return fmt.Errorf("invalid path segment %q in %q", p, path)
}
}
return nil
}
// DuckJSONPath returns a DuckDB JSON path literal like '$.to_tag' or '$.a.b'.
func DuckJSONPath(logicalPath string) string {
parts := strings.Split(logicalPath, ".")
escaped := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
escaped = append(escaped, p)
}
return "$." + strings.Join(escaped, ".")
}