Merge Go frontend (apiserver + xcecli) into meson build
This commit is contained in:
parent
43dc12a36c
commit
c9dc8c4676
205 changed files with 30793 additions and 328 deletions
370
src/frontend/apiserver/store/query.go
Normal file
370
src/frontend/apiserver/store/query.go
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"apiserver/utils"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const PER_PAGE_LIMIT = 100
|
||||
|
||||
// ExecuteQuery runs the full filter → sort → paginate → select pipeline.
|
||||
func ExecuteQuery(rows []utils.OrderMap, opts *utils.QueryOptions, fields []string) ([]utils.OrderMap, string) {
|
||||
if opts == nil {
|
||||
return SelectFields(rows, fields), ""
|
||||
}
|
||||
|
||||
// 1. Filter
|
||||
if len(opts.Conditions) > 0 {
|
||||
rows = FilterRows(rows, opts.Conditions)
|
||||
}
|
||||
|
||||
// 2. Sort
|
||||
if len(opts.Orders) > 0 {
|
||||
SortRows(rows, opts.Orders)
|
||||
}
|
||||
|
||||
// 3. Paginate
|
||||
limit := opts.Limit
|
||||
if limit <= 0 {
|
||||
limit = PER_PAGE_LIMIT
|
||||
}
|
||||
rows, nextToken := PaginateRows(rows, limit, opts.Bookmark, opts.Orders)
|
||||
|
||||
// 4. Select fields
|
||||
rows = SelectFields(rows, fields)
|
||||
|
||||
return rows, nextToken
|
||||
}
|
||||
|
||||
// MatchesCondition evaluates a single filter condition against a row.
|
||||
func MatchesCondition(row utils.OrderMap, cond utils.Condition) bool {
|
||||
fieldVal := row.Data[cond.Column]
|
||||
|
||||
switch cond.Operator {
|
||||
case utils.OpEQ:
|
||||
return compareValues(fieldVal, cond.Value.Value) == 0
|
||||
case utils.OpNE:
|
||||
return compareValues(fieldVal, cond.Value.Value) != 0
|
||||
case utils.OpLT:
|
||||
return compareValues(fieldVal, cond.Value.Value) < 0
|
||||
case utils.OpLTE:
|
||||
return compareValues(fieldVal, cond.Value.Value) <= 0
|
||||
case utils.OpGT:
|
||||
return compareValues(fieldVal, cond.Value.Value) > 0
|
||||
case utils.OpGTE:
|
||||
return compareValues(fieldVal, cond.Value.Value) >= 0
|
||||
case utils.OpIN:
|
||||
return valueInList(fieldVal, cond.Value.Value)
|
||||
case utils.OpNOTIN:
|
||||
return !valueInList(fieldVal, cond.Value.Value)
|
||||
case utils.OpLIKE:
|
||||
return matchLike(fieldVal, cond.Value.Value)
|
||||
case utils.OpNOTLIKE:
|
||||
return !matchLike(fieldVal, cond.Value.Value)
|
||||
case utils.OpBETWEEN:
|
||||
return valueBetween(fieldVal, cond.Value.Value)
|
||||
case utils.OpISNULL:
|
||||
return fieldVal == nil
|
||||
case utils.OpNOTNULL:
|
||||
return fieldVal != nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MatchesBitmaskCondition handles bitmask fields (job_status, host_status).
|
||||
// For bitmask EQ: (val & mask) != 0 means the status is set.
|
||||
// For bitmask NE: (val & mask) == 0 means the status is NOT set.
|
||||
func MatchesBitmaskCondition(row utils.OrderMap, field string, bitmask int64, op utils.OperatorType) bool {
|
||||
val := toInt64(row.Data[field])
|
||||
result := val & bitmask
|
||||
switch op {
|
||||
case utils.OpNE: // bitmask "eq" is stored as OpNE (val & mask != 0)
|
||||
return result != 0
|
||||
case utils.OpEQ: // bitmask "ne" is stored as OpEQ (val & mask == 0)
|
||||
return result == 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsBitmaskField returns true for fields that use bitmask-based filtering.
|
||||
func IsBitmaskField(field string) bool {
|
||||
return field == "job_status" || field == "host_status"
|
||||
}
|
||||
|
||||
// FilterRows applies all conditions (AND logic) to rows.
|
||||
func FilterRows(rows []utils.OrderMap, conditions []utils.Condition) []utils.OrderMap {
|
||||
result := make([]utils.OrderMap, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
match := true
|
||||
for _, cond := range conditions {
|
||||
if IsBitmaskField(cond.Column) {
|
||||
bitmask, ok := cond.Value.Value.(int64)
|
||||
if !ok {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
if !MatchesBitmaskCondition(row, cond.Column, bitmask, cond.Operator) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if !MatchesCondition(row, cond) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if match {
|
||||
result = append(result, row)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SortRows sorts by multiple fields with asc/desc.
|
||||
func SortRows(rows []utils.OrderMap, orders []utils.SortOrder) {
|
||||
sort.SliceStable(rows, func(i, j int) bool {
|
||||
for _, ord := range orders {
|
||||
cmp := compareValues(rows[i].Data[ord.Column], rows[j].Data[ord.Column])
|
||||
if cmp == 0 {
|
||||
continue
|
||||
}
|
||||
if ord.Ascending {
|
||||
return cmp < 0
|
||||
}
|
||||
return cmp > 0
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
// PaginateRows applies limit + page_token (bookmark-based pagination).
|
||||
func PaginateRows(rows []utils.OrderMap, limit int, pageToken string, orders []utils.SortOrder) ([]utils.OrderMap, string) {
|
||||
startIdx := 0
|
||||
if pageToken != "" {
|
||||
startIdx = findPageTokenIndex(rows, pageToken, orders)
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = PER_PAGE_LIMIT
|
||||
}
|
||||
endIdx := startIdx + limit
|
||||
if endIdx > len(rows) {
|
||||
endIdx = len(rows)
|
||||
}
|
||||
if startIdx >= len(rows) {
|
||||
return []utils.OrderMap{}, ""
|
||||
}
|
||||
|
||||
result := rows[startIdx:endIdx]
|
||||
nextToken := ""
|
||||
if endIdx < len(rows) {
|
||||
nextToken = encodePageToken(result[len(result)-1], orders)
|
||||
}
|
||||
return result, nextToken
|
||||
}
|
||||
|
||||
// SelectFields projects only requested fields from rows.
|
||||
func SelectFields(rows []utils.OrderMap, fields []string) []utils.OrderMap {
|
||||
if len(fields) == 0 {
|
||||
return rows
|
||||
}
|
||||
result := make([]utils.OrderMap, len(rows))
|
||||
for i, row := range rows {
|
||||
newData := make(map[string]interface{})
|
||||
for _, f := range fields {
|
||||
if v, ok := row.Data[f]; ok {
|
||||
newData[f] = v
|
||||
}
|
||||
}
|
||||
result[i] = utils.OrderMap{Fields: fields, Data: newData}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// --- Helper functions ---
|
||||
|
||||
// compareValues does type-aware comparison. Returns -1, 0, or 1.
|
||||
func compareValues(a, b interface{}) int {
|
||||
if a == nil && b == nil {
|
||||
return 0
|
||||
}
|
||||
if a == nil {
|
||||
return -1
|
||||
}
|
||||
if b == nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Try int64 comparison
|
||||
aInt, aIsInt := toInt64Safe(a)
|
||||
bInt, bIsInt := toInt64Safe(b)
|
||||
if aIsInt && bIsInt {
|
||||
if aInt < bInt {
|
||||
return -1
|
||||
}
|
||||
if aInt > bInt {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Try float64 comparison
|
||||
aFloat, aIsFloat := toFloat64Safe(a)
|
||||
bFloat, bIsFloat := toFloat64Safe(b)
|
||||
if aIsFloat && bIsFloat {
|
||||
if aFloat < bFloat {
|
||||
return -1
|
||||
}
|
||||
if aFloat > bFloat {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// String comparison
|
||||
aStr := fmt.Sprintf("%v", a)
|
||||
bStr := fmt.Sprintf("%v", b)
|
||||
if aStr < bStr {
|
||||
return -1
|
||||
}
|
||||
if aStr > bStr {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func toInt64(v interface{}) int64 {
|
||||
val, _ := toInt64Safe(v)
|
||||
return val
|
||||
}
|
||||
|
||||
func toInt64Safe(v interface{}) (int64, bool) {
|
||||
switch val := v.(type) {
|
||||
case int64:
|
||||
return val, true
|
||||
case int:
|
||||
return int64(val), true
|
||||
case int32:
|
||||
return int64(val), true
|
||||
case uint64:
|
||||
return int64(val), true
|
||||
case float64:
|
||||
return int64(val), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func toFloat64Safe(v interface{}) (float64, bool) {
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
return val, true
|
||||
case float32:
|
||||
return float64(val), true
|
||||
case int64:
|
||||
return float64(val), true
|
||||
case int:
|
||||
return float64(val), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// valueInList checks if val is in a list of Values.
|
||||
func valueInList(val interface{}, list interface{}) bool {
|
||||
switch l := list.(type) {
|
||||
case []utils.Value:
|
||||
for _, item := range l {
|
||||
if compareValues(val, item.Value) == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for _, item := range l {
|
||||
if compareValues(val, item) == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchLike implements SQL LIKE with Go string matching.
|
||||
func matchLike(fieldVal, pattern interface{}) bool {
|
||||
fieldStr := fmt.Sprintf("%v", fieldVal)
|
||||
patternStr := fmt.Sprintf("%v", pattern)
|
||||
|
||||
// If pattern contains SQL wildcards, translate them
|
||||
if strings.Contains(patternStr, "%") {
|
||||
// Simple wildcard matching
|
||||
if strings.HasPrefix(patternStr, "%") && strings.HasSuffix(patternStr, "%") {
|
||||
return strings.Contains(strings.ToLower(fieldStr), strings.ToLower(strings.Trim(patternStr, "%")))
|
||||
}
|
||||
if strings.HasPrefix(patternStr, "%") {
|
||||
return strings.HasSuffix(strings.ToLower(fieldStr), strings.ToLower(strings.TrimPrefix(patternStr, "%")))
|
||||
}
|
||||
if strings.HasSuffix(patternStr, "%") {
|
||||
return strings.HasPrefix(strings.ToLower(fieldStr), strings.ToLower(strings.TrimSuffix(patternStr, "%")))
|
||||
}
|
||||
}
|
||||
|
||||
// Default: substring match (contains)
|
||||
return strings.Contains(strings.ToLower(fieldStr), strings.ToLower(patternStr))
|
||||
}
|
||||
|
||||
// valueBetween checks if val is between the first two elements of a Value array.
|
||||
func valueBetween(val interface{}, rangeVal interface{}) bool {
|
||||
switch r := rangeVal.(type) {
|
||||
case []utils.Value:
|
||||
if len(r) >= 2 {
|
||||
return compareValues(val, r[0].Value) >= 0 && compareValues(val, r[1].Value) <= 0
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Page token encoding/decoding using base64 JSON.
|
||||
func encodePageToken(lastRow utils.OrderMap, orders []utils.SortOrder) string {
|
||||
tokenData := make(map[string]interface{})
|
||||
for _, ord := range orders {
|
||||
if v, ok := lastRow.Data[ord.Column]; ok {
|
||||
tokenData[ord.Column] = v
|
||||
}
|
||||
}
|
||||
jsonBytes, err := json.Marshal(tokenData)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(jsonBytes)
|
||||
}
|
||||
|
||||
func findPageTokenIndex(rows []utils.OrderMap, pageToken string, orders []utils.SortOrder) int {
|
||||
jsonBytes, err := base64.URLEncoding.DecodeString(pageToken)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
var tokenData map[string]interface{}
|
||||
if err := json.Unmarshal(jsonBytes, &tokenData); err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
for i, row := range rows {
|
||||
match := true
|
||||
for _, ord := range orders {
|
||||
tokenVal, ok := tokenData[ord.Column]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if compareValues(row.Data[ord.Column], tokenVal) != 0 {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
return i + 1 // Start after the matched row
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue