// 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 { delete(result) return nil, false } append(&result, resolved) } 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) free(node.left) } if node.right != nil { filter_node_destroy(node.right) free(node.right) } if node.child != nil { filter_node_destroy(node.child) free(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) return } 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) free(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) free(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) free(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) 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 }