828 lines
18 KiB
Odin
828 lines
18 KiB
Odin
// FilterExpression and ProjectionExpression support
|
|
// FilterExpression: post-retrieval filter applied to Scan/Query results
|
|
// ProjectionExpression: return only specified attributes from items
|
|
package dynamodb
|
|
|
|
import "core:encoding/json"
|
|
import "core:strconv"
|
|
import "core:strings"
|
|
|
|
// ============================================================================
|
|
// ProjectionExpression
|
|
//
|
|
// A comma-separated list of attribute names (with optional #name substitution)
|
|
// that specifies which attributes to return.
|
|
// ============================================================================
|
|
|
|
parse_projection_expression :: proc(
|
|
request_body: []byte,
|
|
attribute_names: Maybe(map[string]string),
|
|
) -> (paths: []string, ok: bool) {
|
|
data, parse_err := json.parse(request_body, allocator = context.temp_allocator)
|
|
if parse_err != nil {
|
|
return nil, false
|
|
}
|
|
defer json.destroy_value(data)
|
|
|
|
root, root_ok := data.(json.Object)
|
|
if !root_ok {
|
|
return nil, false
|
|
}
|
|
|
|
pe_val, found := root["ProjectionExpression"]
|
|
if !found {
|
|
return nil, false // absent is not an error, caller should check
|
|
}
|
|
|
|
pe_str, str_ok := pe_val.(json.String)
|
|
if !str_ok {
|
|
return nil, false
|
|
}
|
|
|
|
// Split by comma and resolve names
|
|
parts := strings.split(string(pe_str), ",")
|
|
result := make([dynamic]string)
|
|
|
|
for part in parts {
|
|
trimmed := strings.trim_space(part)
|
|
if len(trimmed) == 0 {
|
|
continue
|
|
}
|
|
|
|
resolved, res_ok := resolve_attribute_name(trimmed, attribute_names)
|
|
if !res_ok {
|
|
// Cleanup previously cloned strings
|
|
for path in result {
|
|
delete(path)
|
|
}
|
|
delete(result)
|
|
return nil, false
|
|
}
|
|
append(&result, strings.clone(resolved)) // Clone for safe storage
|
|
}
|
|
|
|
return result[:], true
|
|
}
|
|
|
|
// Apply projection to a single item — returns a new item with only the specified attributes
|
|
apply_projection :: proc(item: Item, projection: []string) -> Item {
|
|
if len(projection) == 0 {
|
|
// No projection — return a deep copy of the full item
|
|
return item_deep_copy(item)
|
|
}
|
|
|
|
projected := make(Item)
|
|
for path in projection {
|
|
if val, found := item[path]; found {
|
|
projected[strings.clone(path)] = attr_value_deep_copy(val)
|
|
}
|
|
}
|
|
return projected
|
|
}
|
|
|
|
// Deep copy an entire item
|
|
item_deep_copy :: proc(item: Item) -> Item {
|
|
result := make(Item)
|
|
for key, val in item {
|
|
result[strings.clone(key)] = attr_value_deep_copy(val)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ============================================================================
|
|
// FilterExpression
|
|
//
|
|
// A condition expression applied post-retrieval. Supports:
|
|
// - Comparisons: path = :val, path <> :val, path < :val, etc.
|
|
// - BETWEEN: path BETWEEN :lo AND :hi
|
|
// - IN: path IN (:v1, :v2, :v3)
|
|
// - begins_with: begins_with(path, :prefix)
|
|
// - contains: contains(path, :substr)
|
|
// - attribute_exists(path)
|
|
// - attribute_not_exists(path)
|
|
// - AND / OR / NOT combinators
|
|
//
|
|
// This is a recursive-descent parser for condition expressions.
|
|
// ============================================================================
|
|
|
|
// Parsed filter node (expression tree)
|
|
Filter_Node_Type :: enum {
|
|
Comparison, // path op value
|
|
Between, // path BETWEEN lo AND hi
|
|
In, // path IN (v1, v2, ...)
|
|
Begins_With, // begins_with(path, value)
|
|
Contains, // contains(path, value)
|
|
Attribute_Exists, // attribute_exists(path)
|
|
Attribute_Not_Exists, // attribute_not_exists(path)
|
|
And, // left AND right
|
|
Or, // left OR right
|
|
Not, // NOT child
|
|
}
|
|
|
|
Comparison_Op :: enum {
|
|
EQ, // =
|
|
NE, // <>
|
|
LT, // <
|
|
LE, // <=
|
|
GT, // >
|
|
GE, // >=
|
|
}
|
|
|
|
Filter_Node :: struct {
|
|
type: Filter_Node_Type,
|
|
// For Comparison
|
|
path: string,
|
|
comp_op: Comparison_Op,
|
|
value: Attribute_Value,
|
|
// For Between
|
|
value2: Maybe(Attribute_Value),
|
|
// For In
|
|
in_values: []Attribute_Value,
|
|
// For And/Or
|
|
left: ^Filter_Node,
|
|
right: ^Filter_Node,
|
|
// For Not
|
|
child: ^Filter_Node,
|
|
}
|
|
|
|
filter_node_destroy :: proc(node: ^Filter_Node) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
|
|
attr_value_destroy(&node.value)
|
|
if v2, ok := node.value2.?; ok {
|
|
v2_copy := v2
|
|
attr_value_destroy(&v2_copy)
|
|
}
|
|
for &iv in node.in_values {
|
|
attr_value_destroy(&iv)
|
|
}
|
|
if node.in_values != nil {
|
|
delete(node.in_values)
|
|
}
|
|
|
|
if node.left != nil {
|
|
filter_node_destroy(node.left)
|
|
}
|
|
if node.right != nil {
|
|
filter_node_destroy(node.right)
|
|
}
|
|
if node.child != nil {
|
|
filter_node_destroy(node.child)
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Filter Expression Parser
|
|
// ============================================================================
|
|
|
|
parse_filter_expression :: proc(
|
|
expression: string,
|
|
attribute_names: Maybe(map[string]string),
|
|
attribute_values: map[string]Attribute_Value,
|
|
) -> (node: ^Filter_Node, ok: bool) {
|
|
t := tokenizer_init(expression)
|
|
node, ok = parse_or_expr(&t, attribute_names, attribute_values)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
|
|
// Verify all tokens were consumed (no trailing garbage)
|
|
if trailing := tokenizer_next(&t); trailing != nil {
|
|
filter_node_destroy(node)
|
|
return nil, false
|
|
}
|
|
|
|
return node, true
|
|
}
|
|
|
|
parse_or_expr :: proc(
|
|
t: ^Tokenizer,
|
|
names: Maybe(map[string]string),
|
|
values: map[string]Attribute_Value,
|
|
) -> (^Filter_Node, bool) {
|
|
left, left_ok := parse_and_expr(t, names, values)
|
|
if !left_ok {
|
|
return nil, false
|
|
}
|
|
|
|
for {
|
|
saved_pos := t.pos
|
|
tok_maybe := tokenizer_next(t)
|
|
tok, has := tok_maybe.?
|
|
if !has {
|
|
break
|
|
}
|
|
|
|
if strings.equal_fold(tok, "OR") {
|
|
right, right_ok := parse_and_expr(t, names, values)
|
|
if !right_ok {
|
|
filter_node_destroy(left)
|
|
return nil, false
|
|
}
|
|
|
|
parent := new(Filter_Node)
|
|
parent.type = .Or
|
|
parent.left = left
|
|
parent.right = right
|
|
left = parent
|
|
} else {
|
|
t.pos = saved_pos
|
|
break
|
|
}
|
|
}
|
|
|
|
return left, true
|
|
}
|
|
|
|
parse_and_expr :: proc(
|
|
t: ^Tokenizer,
|
|
names: Maybe(map[string]string),
|
|
values: map[string]Attribute_Value,
|
|
) -> (^Filter_Node, bool) {
|
|
left, left_ok := parse_not_expr(t, names, values)
|
|
if !left_ok {
|
|
return nil, false
|
|
}
|
|
|
|
for {
|
|
saved_pos := t.pos
|
|
tok_maybe := tokenizer_next(t)
|
|
tok, has := tok_maybe.?
|
|
if !has {
|
|
break
|
|
}
|
|
|
|
if strings.equal_fold(tok, "AND") {
|
|
right, right_ok := parse_not_expr(t, names, values)
|
|
if !right_ok {
|
|
filter_node_destroy(left)
|
|
return nil, false
|
|
}
|
|
|
|
parent := new(Filter_Node)
|
|
parent.type = .And
|
|
parent.left = left
|
|
parent.right = right
|
|
left = parent
|
|
} else {
|
|
t.pos = saved_pos
|
|
break
|
|
}
|
|
}
|
|
|
|
return left, true
|
|
}
|
|
|
|
parse_not_expr :: proc(
|
|
t: ^Tokenizer,
|
|
names: Maybe(map[string]string),
|
|
values: map[string]Attribute_Value,
|
|
) -> (^Filter_Node, bool) {
|
|
saved_pos := t.pos
|
|
tok_maybe := tokenizer_next(t)
|
|
tok, has := tok_maybe.?
|
|
if !has {
|
|
return nil, false
|
|
}
|
|
|
|
if strings.equal_fold(tok, "NOT") {
|
|
child, child_ok := parse_primary_expr(t, names, values)
|
|
if !child_ok {
|
|
return nil, false
|
|
}
|
|
node := new(Filter_Node)
|
|
node.type = .Not
|
|
node.child = child
|
|
return node, true
|
|
}
|
|
|
|
t.pos = saved_pos
|
|
return parse_primary_expr(t, names, values)
|
|
}
|
|
|
|
parse_primary_expr :: proc(
|
|
t: ^Tokenizer,
|
|
names: Maybe(map[string]string),
|
|
values: map[string]Attribute_Value,
|
|
) -> (^Filter_Node, bool) {
|
|
first_tok, first_ok := next_token(t)
|
|
if !first_ok {
|
|
return nil, false
|
|
}
|
|
|
|
// Parenthesized expression
|
|
if first_tok == "(" {
|
|
inner, inner_ok := parse_or_expr(t, names, values)
|
|
if !inner_ok {
|
|
return nil, false
|
|
}
|
|
rparen, rp_ok := next_token(t)
|
|
if !rp_ok || rparen != ")" {
|
|
filter_node_destroy(inner)
|
|
return nil, false
|
|
}
|
|
return inner, true
|
|
}
|
|
|
|
// Function-style: begins_with, contains, attribute_exists, attribute_not_exists
|
|
if strings.equal_fold(first_tok, "begins_with") {
|
|
return parse_filter_begins_with(t, names, values)
|
|
}
|
|
if strings.equal_fold(first_tok, "contains") {
|
|
return parse_filter_contains(t, names, values)
|
|
}
|
|
if strings.equal_fold(first_tok, "attribute_exists") {
|
|
return parse_filter_attr_exists(t, names, true)
|
|
}
|
|
if strings.equal_fold(first_tok, "attribute_not_exists") {
|
|
return parse_filter_attr_exists(t, names, false)
|
|
}
|
|
|
|
// Comparison, BETWEEN, or IN: path op value
|
|
path, path_ok := resolve_attribute_name(first_tok, names)
|
|
if !path_ok {
|
|
return nil, false
|
|
}
|
|
|
|
op_tok, op_ok := next_token(t)
|
|
if !op_ok {
|
|
return nil, false
|
|
}
|
|
|
|
// BETWEEN
|
|
if strings.equal_fold(op_tok, "BETWEEN") {
|
|
return parse_filter_between(t, path, names, values)
|
|
}
|
|
|
|
// IN
|
|
if strings.equal_fold(op_tok, "IN") {
|
|
return parse_filter_in(t, path, names, values)
|
|
}
|
|
|
|
// Comparison operators
|
|
comp_op: Comparison_Op
|
|
if op_tok == "=" {
|
|
comp_op = .EQ
|
|
} else if op_tok == "<>" {
|
|
comp_op = .NE
|
|
} else if op_tok == "<" {
|
|
comp_op = .LT
|
|
} else if op_tok == "<=" {
|
|
comp_op = .LE
|
|
} else if op_tok == ">" {
|
|
comp_op = .GT
|
|
} else if op_tok == ">=" {
|
|
comp_op = .GE
|
|
} else {
|
|
return nil, false
|
|
}
|
|
|
|
val_tok, vt_ok := next_token(t)
|
|
if !vt_ok {
|
|
return nil, false
|
|
}
|
|
val, val_ok := resolve_attribute_value(val_tok, values)
|
|
if !val_ok {
|
|
return nil, false
|
|
}
|
|
|
|
node := new(Filter_Node)
|
|
node.type = .Comparison
|
|
node.path = path
|
|
node.comp_op = comp_op
|
|
node.value = val
|
|
return node, true
|
|
}
|
|
|
|
parse_filter_begins_with :: proc(
|
|
t: ^Tokenizer,
|
|
names: Maybe(map[string]string),
|
|
values: map[string]Attribute_Value,
|
|
) -> (^Filter_Node, bool) {
|
|
lparen, lp_ok := next_token(t)
|
|
if !lp_ok || lparen != "(" {
|
|
return nil, false
|
|
}
|
|
|
|
path_tok, path_ok := next_token(t)
|
|
if !path_ok {
|
|
return nil, false
|
|
}
|
|
path, path_resolved := resolve_attribute_name(path_tok, names)
|
|
if !path_resolved {
|
|
return nil, false
|
|
}
|
|
|
|
comma, comma_ok := next_token(t)
|
|
if !comma_ok || comma != "," {
|
|
return nil, false
|
|
}
|
|
|
|
val_tok, vt_ok := next_token(t)
|
|
if !vt_ok {
|
|
return nil, false
|
|
}
|
|
val, val_ok := resolve_attribute_value(val_tok, values)
|
|
if !val_ok {
|
|
return nil, false
|
|
}
|
|
|
|
rparen, rp_ok := next_token(t)
|
|
if !rp_ok || rparen != ")" {
|
|
attr_value_destroy(&val)
|
|
return nil, false
|
|
}
|
|
|
|
node := new(Filter_Node)
|
|
node.type = .Begins_With
|
|
node.path = path
|
|
node.value = val
|
|
return node, true
|
|
}
|
|
|
|
parse_filter_contains :: proc(
|
|
t: ^Tokenizer,
|
|
names: Maybe(map[string]string),
|
|
values: map[string]Attribute_Value,
|
|
) -> (^Filter_Node, bool) {
|
|
lparen, lp_ok := next_token(t)
|
|
if !lp_ok || lparen != "(" {
|
|
return nil, false
|
|
}
|
|
|
|
path_tok, path_ok := next_token(t)
|
|
if !path_ok {
|
|
return nil, false
|
|
}
|
|
path, path_resolved := resolve_attribute_name(path_tok, names)
|
|
if !path_resolved {
|
|
return nil, false
|
|
}
|
|
|
|
comma, comma_ok := next_token(t)
|
|
if !comma_ok || comma != "," {
|
|
return nil, false
|
|
}
|
|
|
|
val_tok, vt_ok := next_token(t)
|
|
if !vt_ok {
|
|
return nil, false
|
|
}
|
|
val, val_ok := resolve_attribute_value(val_tok, values)
|
|
if !val_ok {
|
|
return nil, false
|
|
}
|
|
|
|
rparen, rp_ok := next_token(t)
|
|
if !rp_ok || rparen != ")" {
|
|
attr_value_destroy(&val)
|
|
return nil, false
|
|
}
|
|
|
|
node := new(Filter_Node)
|
|
node.type = .Contains
|
|
node.path = path
|
|
node.value = val
|
|
return node, true
|
|
}
|
|
|
|
parse_filter_attr_exists :: proc(
|
|
t: ^Tokenizer,
|
|
names: Maybe(map[string]string),
|
|
exists: bool,
|
|
) -> (^Filter_Node, bool) {
|
|
lparen, lp_ok := next_token(t)
|
|
if !lp_ok || lparen != "(" {
|
|
return nil, false
|
|
}
|
|
|
|
path_tok, path_ok := next_token(t)
|
|
if !path_ok {
|
|
return nil, false
|
|
}
|
|
path, path_resolved := resolve_attribute_name(path_tok, names)
|
|
if !path_resolved {
|
|
return nil, false
|
|
}
|
|
|
|
rparen, rp_ok := next_token(t)
|
|
if !rp_ok || rparen != ")" {
|
|
return nil, false
|
|
}
|
|
|
|
node := new(Filter_Node)
|
|
node.type = .Attribute_Exists if exists else .Attribute_Not_Exists
|
|
node.path = path
|
|
return node, true
|
|
}
|
|
|
|
parse_filter_between :: proc(
|
|
t: ^Tokenizer,
|
|
path: string,
|
|
names: Maybe(map[string]string),
|
|
values: map[string]Attribute_Value,
|
|
) -> (^Filter_Node, bool) {
|
|
lo_tok, lo_ok := next_token(t)
|
|
if !lo_ok {
|
|
return nil, false
|
|
}
|
|
lo_val, lo_val_ok := resolve_attribute_value(lo_tok, values)
|
|
if !lo_val_ok {
|
|
return nil, false
|
|
}
|
|
|
|
and_tok, and_ok := next_token(t)
|
|
if !and_ok || !strings.equal_fold(and_tok, "AND") {
|
|
attr_value_destroy(&lo_val)
|
|
return nil, false
|
|
}
|
|
|
|
hi_tok, hi_ok := next_token(t)
|
|
if !hi_ok {
|
|
attr_value_destroy(&lo_val)
|
|
return nil, false
|
|
}
|
|
hi_val, hi_val_ok := resolve_attribute_value(hi_tok, values)
|
|
if !hi_val_ok {
|
|
attr_value_destroy(&lo_val)
|
|
return nil, false
|
|
}
|
|
|
|
node := new(Filter_Node)
|
|
node.type = .Between
|
|
node.path = path
|
|
node.value = lo_val
|
|
node.value2 = hi_val
|
|
return node, true
|
|
}
|
|
|
|
parse_filter_in :: proc(
|
|
t: ^Tokenizer,
|
|
path: string,
|
|
names: Maybe(map[string]string),
|
|
values: map[string]Attribute_Value,
|
|
) -> (^Filter_Node, bool) {
|
|
lparen, lp_ok := next_token(t)
|
|
if !lp_ok || lparen != "(" {
|
|
return nil, false
|
|
}
|
|
|
|
in_vals := make([dynamic]Attribute_Value)
|
|
|
|
for {
|
|
val_tok, vt_ok := next_token(t)
|
|
if !vt_ok {
|
|
for &v in in_vals {
|
|
attr_value_destroy(&v)
|
|
}
|
|
delete(in_vals)
|
|
return nil, false
|
|
}
|
|
|
|
val, val_ok := resolve_attribute_value(val_tok, values)
|
|
if !val_ok {
|
|
for &v in in_vals {
|
|
attr_value_destroy(&v)
|
|
}
|
|
delete(in_vals)
|
|
return nil, false
|
|
}
|
|
append(&in_vals, val)
|
|
|
|
sep_tok, sep_ok := next_token(t)
|
|
if !sep_ok {
|
|
for &v in in_vals {
|
|
attr_value_destroy(&v)
|
|
}
|
|
delete(in_vals)
|
|
return nil, false
|
|
}
|
|
if sep_tok == ")" {
|
|
break
|
|
}
|
|
if sep_tok != "," {
|
|
for &v in in_vals {
|
|
attr_value_destroy(&v)
|
|
}
|
|
delete(in_vals)
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
node := new(Filter_Node)
|
|
node.type = .In
|
|
node.path = path
|
|
node.in_values = in_vals[:]
|
|
return node, true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Filter Expression Evaluation
|
|
// ============================================================================
|
|
|
|
evaluate_filter :: proc(item: Item, node: ^Filter_Node) -> bool {
|
|
if node == nil {
|
|
return true
|
|
}
|
|
|
|
switch node.type {
|
|
case .Comparison:
|
|
attr, found := item[node.path]
|
|
if !found {
|
|
return false
|
|
}
|
|
return evaluate_comparison(attr, node.comp_op, node.value)
|
|
|
|
case .Between:
|
|
attr, found := item[node.path]
|
|
if !found {
|
|
return false
|
|
}
|
|
lo_cmp := compare_attribute_values(attr, node.value)
|
|
if v2, ok := node.value2.?; ok {
|
|
hi_cmp := compare_attribute_values(attr, v2)
|
|
return lo_cmp >= 0 && hi_cmp <= 0
|
|
}
|
|
return false
|
|
|
|
case .In:
|
|
attr, found := item[node.path]
|
|
if !found {
|
|
return false
|
|
}
|
|
for in_val in node.in_values {
|
|
if compare_attribute_values(attr, in_val) == 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
|
|
case .Begins_With:
|
|
attr, found := item[node.path]
|
|
if !found {
|
|
return false
|
|
}
|
|
attr_str, attr_ok := attr_value_to_string_for_compare(attr)
|
|
val_str, val_ok := attr_value_to_string_for_compare(node.value)
|
|
if !attr_ok || !val_ok {
|
|
return false
|
|
}
|
|
return strings.has_prefix(attr_str, val_str)
|
|
|
|
case .Contains:
|
|
attr, found := item[node.path]
|
|
if !found {
|
|
return false
|
|
}
|
|
return evaluate_contains(attr, node.value)
|
|
|
|
case .Attribute_Exists:
|
|
_, found := item[node.path]
|
|
return found
|
|
|
|
case .Attribute_Not_Exists:
|
|
_, found := item[node.path]
|
|
return !found
|
|
|
|
case .And:
|
|
return evaluate_filter(item, node.left) && evaluate_filter(item, node.right)
|
|
|
|
case .Or:
|
|
return evaluate_filter(item, node.left) || evaluate_filter(item, node.right)
|
|
|
|
case .Not:
|
|
return !evaluate_filter(item, node.child)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
evaluate_comparison :: proc(attr: Attribute_Value, op: Comparison_Op, val: Attribute_Value) -> bool {
|
|
cmp := compare_attribute_values(attr, val)
|
|
|
|
// -2 means types are incomparable - all comparisons return false
|
|
// (matches DynamoDB behavior: mixed-type comparisons always fail)
|
|
if cmp == -2 {
|
|
return false
|
|
}
|
|
|
|
switch op {
|
|
case .EQ: return cmp == 0
|
|
case .NE: return cmp != 0
|
|
case .LT: return cmp < 0
|
|
case .LE: return cmp <= 0
|
|
case .GT: return cmp > 0
|
|
case .GE: return cmp >= 0
|
|
}
|
|
return false
|
|
}
|
|
|
|
evaluate_contains :: proc(attr: Attribute_Value, val: Attribute_Value) -> bool {
|
|
// For strings: substring check
|
|
#partial switch a in attr {
|
|
case String:
|
|
if v, ok := val.(String); ok {
|
|
return strings.contains(string(a), string(v))
|
|
}
|
|
|
|
case String_Set:
|
|
if v, ok := val.(String); ok {
|
|
for s in a {
|
|
if s == string(v) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
case Number_Set:
|
|
if v, ok := val.(Number); ok {
|
|
for n in a {
|
|
if n == string(v) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
case List:
|
|
for item in a {
|
|
if compare_attribute_values(item, val) == 0 {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Compare two AttributeValues. Returns <0, 0, or >0.
|
|
// For mixed types, returns -2 (not comparable).
|
|
compare_attribute_values :: proc(a: Attribute_Value, b: Attribute_Value) -> int {
|
|
a_str, a_ok := attr_value_to_string_for_compare(a)
|
|
b_str, b_ok := attr_value_to_string_for_compare(b)
|
|
|
|
if !a_ok || !b_ok {
|
|
// Try bool comparison
|
|
a_bool, a_is_bool := a.(Bool)
|
|
b_bool, b_is_bool := b.(Bool)
|
|
if a_is_bool && b_is_bool {
|
|
if bool(a_bool) == bool(b_bool) {
|
|
return 0
|
|
}
|
|
return -2
|
|
}
|
|
return -2
|
|
}
|
|
|
|
// For Numbers, do numeric comparison
|
|
_, a_is_num := a.(Number)
|
|
_, b_is_num := b.(Number)
|
|
if a_is_num && b_is_num {
|
|
a_val, a_parse := strconv.parse_f64(a_str)
|
|
b_val, b_parse := strconv.parse_f64(b_str)
|
|
if a_parse && b_parse {
|
|
if a_val < b_val {
|
|
return -1
|
|
}
|
|
if a_val > b_val {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
}
|
|
|
|
return strings.compare(a_str, b_str)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Request parsing helpers for FilterExpression
|
|
// ============================================================================
|
|
|
|
parse_filter_expression_string :: proc(request_body: []byte) -> (expr: string, ok: bool) {
|
|
data, parse_err := json.parse(request_body, allocator = context.temp_allocator)
|
|
if parse_err != nil {
|
|
return
|
|
}
|
|
defer json.destroy_value(data)
|
|
|
|
root, root_ok := data.(json.Object)
|
|
if !root_ok {
|
|
return
|
|
}
|
|
|
|
fe_val, found := root["FilterExpression"]
|
|
if !found {
|
|
return
|
|
}
|
|
|
|
fe_str, str_ok := fe_val.(json.String)
|
|
if !str_ok {
|
|
return
|
|
}
|
|
|
|
expr = string(fe_str)
|
|
ok = true
|
|
return
|
|
}
|