963 lines
23 KiB
Odin
963 lines
23 KiB
Odin
// UpdateExpression Parser and Executor
|
|
// Supports: SET path = value [, path = value ...]
|
|
// REMOVE path [, path ...]
|
|
// ADD path value [, path value ...] (numeric add / set add)
|
|
// DELETE path value [, path value ...] (set remove)
|
|
//
|
|
// Values can be:
|
|
// :placeholder → resolved from ExpressionAttributeValues
|
|
// path + :placeholder → numeric addition
|
|
// path - :placeholder → numeric subtraction
|
|
// if_not_exists(path, :placeholder) → default value
|
|
// list_append(operand, operand) → list concatenation
|
|
package dynamodb
|
|
|
|
import "core:encoding/json"
|
|
import "core:strings"
|
|
|
|
// ============================================================================
|
|
// Update Plan — parsed representation of an UpdateExpression
|
|
// ============================================================================
|
|
|
|
Update_Action_Type :: enum {
|
|
SET,
|
|
REMOVE,
|
|
ADD,
|
|
DELETE,
|
|
}
|
|
|
|
Set_Value_Kind :: enum {
|
|
Direct, // SET x = :val
|
|
Plus, // SET x = x + :val or SET x = :val + x
|
|
Minus, // SET x = x - :val
|
|
If_Not_Exists, // SET x = if_not_exists(x, :val)
|
|
List_Append, // SET x = list_append(x, :val)
|
|
}
|
|
|
|
Set_Action :: struct {
|
|
path: string,
|
|
value_kind: Set_Value_Kind,
|
|
value: Attribute_Value, // primary value
|
|
source: string, // source path for Plus/Minus/If_Not_Exists/List_Append
|
|
value2: Maybe(Attribute_Value), // second operand for list_append where both are values
|
|
}
|
|
|
|
Remove_Action :: struct {
|
|
path: string,
|
|
}
|
|
|
|
Add_Action :: struct {
|
|
path: string,
|
|
value: Attribute_Value,
|
|
}
|
|
|
|
Delete_Action :: struct {
|
|
path: string,
|
|
value: Attribute_Value,
|
|
}
|
|
|
|
Update_Plan :: struct {
|
|
sets: [dynamic]Set_Action,
|
|
removes: [dynamic]Remove_Action,
|
|
adds: [dynamic]Add_Action,
|
|
deletes: [dynamic]Delete_Action,
|
|
}
|
|
|
|
update_plan_destroy :: proc(plan: ^Update_Plan) {
|
|
for &s in plan.sets {
|
|
attr_value_destroy(&s.value)
|
|
if v2, ok := s.value2.?; ok {
|
|
v2_copy := v2
|
|
attr_value_destroy(&v2_copy)
|
|
}
|
|
}
|
|
delete(plan.sets)
|
|
|
|
delete(plan.removes)
|
|
|
|
for &a in plan.adds {
|
|
attr_value_destroy(&a.value)
|
|
}
|
|
delete(plan.adds)
|
|
|
|
for &d in plan.deletes {
|
|
attr_value_destroy(&d.value)
|
|
}
|
|
delete(plan.deletes)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Parse UpdateExpression
|
|
//
|
|
// Grammar (simplified):
|
|
// update_expr = clause { clause }
|
|
// clause = "SET" set_list | "REMOVE" remove_list | "ADD" add_list | "DELETE" delete_list
|
|
// set_list = set_entry { "," set_entry }
|
|
// set_entry = path "=" value_expr
|
|
// value_expr = :placeholder
|
|
// | path "+" :placeholder
|
|
// | path "-" :placeholder
|
|
// | "if_not_exists" "(" path "," :placeholder ")"
|
|
// | "list_append" "(" operand "," operand ")"
|
|
// remove_list = path { "," path }
|
|
// add_list = add_entry { "," add_entry }
|
|
// add_entry = path :placeholder
|
|
// delete_list = delete_entry { "," delete_entry }
|
|
// delete_entry= path :placeholder
|
|
// ============================================================================
|
|
|
|
parse_update_expression :: proc(
|
|
expression: string,
|
|
attribute_names: Maybe(map[string]string),
|
|
attribute_values: map[string]Attribute_Value,
|
|
) -> (plan: Update_Plan, ok: bool) {
|
|
plan.sets = make([dynamic]Set_Action)
|
|
plan.removes = make([dynamic]Remove_Action)
|
|
plan.adds = make([dynamic]Add_Action)
|
|
plan.deletes = make([dynamic]Delete_Action)
|
|
|
|
t := tokenizer_init(expression)
|
|
|
|
for {
|
|
keyword_maybe := tokenizer_next(&t)
|
|
keyword_str, has_keyword := keyword_maybe.?
|
|
if !has_keyword {
|
|
break // done
|
|
}
|
|
|
|
if strings.equal_fold(keyword_str, "SET") {
|
|
if !parse_set_clause(&t, &plan, attribute_names, attribute_values) {
|
|
update_plan_destroy(&plan)
|
|
return {}, false
|
|
}
|
|
} else if strings.equal_fold(keyword_str, "REMOVE") {
|
|
if !parse_remove_clause(&t, &plan, attribute_names) {
|
|
update_plan_destroy(&plan)
|
|
return {}, false
|
|
}
|
|
} else if strings.equal_fold(keyword_str, "ADD") {
|
|
if !parse_add_clause(&t, &plan, attribute_names, attribute_values) {
|
|
update_plan_destroy(&plan)
|
|
return {}, false
|
|
}
|
|
} else if strings.equal_fold(keyword_str, "DELETE") {
|
|
if !parse_delete_clause(&t, &plan, attribute_names, attribute_values) {
|
|
update_plan_destroy(&plan)
|
|
return {}, false
|
|
}
|
|
} else {
|
|
update_plan_destroy(&plan)
|
|
return {}, false
|
|
}
|
|
}
|
|
|
|
return plan, true
|
|
}
|
|
|
|
// ============================================================================
|
|
// SET clause parsing
|
|
// ============================================================================
|
|
|
|
parse_set_clause :: proc(
|
|
t: ^Tokenizer,
|
|
plan: ^Update_Plan,
|
|
names: Maybe(map[string]string),
|
|
values: map[string]Attribute_Value,
|
|
) -> bool {
|
|
saved_pos: int
|
|
for {
|
|
// Save position before reading so we can rewind if it's a clause keyword
|
|
saved_pos = t.pos
|
|
|
|
// Path
|
|
path_tok, path_ok := next_token(t)
|
|
if !path_ok {
|
|
return false
|
|
}
|
|
|
|
// Check if this is actually a new clause keyword (SET/REMOVE/ADD/DELETE)
|
|
if is_clause_keyword(path_tok) {
|
|
t.pos = saved_pos
|
|
return true
|
|
}
|
|
|
|
path, path_resolved := resolve_attribute_name(path_tok, names)
|
|
if !path_resolved {
|
|
return false
|
|
}
|
|
|
|
// "="
|
|
eq_tok, eq_ok := next_token(t)
|
|
if !eq_ok || eq_tok != "=" {
|
|
return false
|
|
}
|
|
|
|
// Value expression
|
|
action, act_ok := parse_set_value_expr(t, path, names, values)
|
|
if !act_ok {
|
|
return false
|
|
}
|
|
|
|
append(&plan.sets, action)
|
|
|
|
// Check for comma (more entries) or end
|
|
saved_pos = t.pos
|
|
comma_maybe := tokenizer_next(t)
|
|
if comma, has := comma_maybe.?; has {
|
|
if comma == "," {
|
|
continue
|
|
}
|
|
// Not a comma — put it back
|
|
t.pos = saved_pos
|
|
}
|
|
break
|
|
}
|
|
return true
|
|
}
|
|
|
|
parse_set_value_expr :: proc(
|
|
t: ^Tokenizer,
|
|
path: string,
|
|
names: Maybe(map[string]string),
|
|
values: map[string]Attribute_Value,
|
|
) -> (action: Set_Action, ok: bool) {
|
|
first_tok, first_ok := next_token(t)
|
|
if !first_ok {
|
|
return {}, false
|
|
}
|
|
|
|
// Check for if_not_exists(...)
|
|
if strings.equal_fold(first_tok, "if_not_exists") {
|
|
action, ok = parse_if_not_exists(t, path, names, values)
|
|
return
|
|
}
|
|
|
|
// Check for list_append(...)
|
|
if strings.equal_fold(first_tok, "list_append") {
|
|
action, ok = parse_list_append(t, path, names, values)
|
|
return
|
|
}
|
|
|
|
peek_pos: int
|
|
|
|
// Check if first token is a :placeholder (direct value)
|
|
if len(first_tok) > 0 && first_tok[0] == ':' {
|
|
// Could be :val + path or :val - path or just :val
|
|
peek_pos = t.pos
|
|
op_maybe := tokenizer_next(t)
|
|
if op, has_op := op_maybe.?; has_op && (op == "+" || op == "-") {
|
|
// :val op path
|
|
second_tok, sec_ok := next_token(t)
|
|
if !sec_ok {
|
|
return {}, false
|
|
}
|
|
source, source_resolved := resolve_attribute_name(second_tok, names)
|
|
if !source_resolved {
|
|
return {}, false
|
|
}
|
|
val, val_ok := resolve_attribute_value(first_tok, values)
|
|
if !val_ok {
|
|
return {}, false
|
|
}
|
|
kind := Set_Value_Kind.Plus if op == "+" else Set_Value_Kind.Minus
|
|
return Set_Action{
|
|
path = path,
|
|
value_kind = kind,
|
|
value = val,
|
|
source = source,
|
|
}, true
|
|
}
|
|
// Just a direct value
|
|
t.pos = peek_pos
|
|
val, val_ok := resolve_attribute_value(first_tok, values)
|
|
if !val_ok {
|
|
return {}, false
|
|
}
|
|
return Set_Action{
|
|
path = path,
|
|
value_kind = .Direct,
|
|
value = val,
|
|
}, true
|
|
}
|
|
|
|
// First token is a path — check for path + :val or path - :val
|
|
source, source_resolved := resolve_attribute_name(first_tok, names)
|
|
if !source_resolved {
|
|
return {}, false
|
|
}
|
|
peek_pos = t.pos
|
|
op_maybe := tokenizer_next(t)
|
|
if op, has_op := op_maybe.?; has_op && (op == "+" || op == "-") {
|
|
val_tok, vt_ok := next_token(t)
|
|
if !vt_ok {
|
|
return {}, false
|
|
}
|
|
val, val_ok := resolve_attribute_value(val_tok, values)
|
|
if !val_ok {
|
|
return {}, false
|
|
}
|
|
kind := Set_Value_Kind.Plus if op == "+" else Set_Value_Kind.Minus
|
|
return Set_Action{
|
|
path = path,
|
|
value_kind = kind,
|
|
value = val,
|
|
source = source,
|
|
}, true
|
|
}
|
|
// Just a path reference — treat as direct copy (SET a = b)
|
|
t.pos = peek_pos
|
|
return {}, false
|
|
}
|
|
|
|
parse_if_not_exists :: proc(
|
|
t: ^Tokenizer,
|
|
path: string,
|
|
names: Maybe(map[string]string),
|
|
values: map[string]Attribute_Value,
|
|
) -> (action: Set_Action, ok: bool) {
|
|
lparen, lp_ok := next_token(t)
|
|
if !lp_ok || lparen != "(" {
|
|
return {}, false
|
|
}
|
|
|
|
src_tok, src_ok := next_token(t)
|
|
if !src_ok {
|
|
return {}, false
|
|
}
|
|
source, source_resolved := resolve_attribute_name(src_tok, names)
|
|
if !source_resolved {
|
|
return {}, false
|
|
}
|
|
|
|
comma, comma_ok := next_token(t)
|
|
if !comma_ok || comma != "," {
|
|
return {}, false
|
|
}
|
|
|
|
val_tok, vt_ok := next_token(t)
|
|
if !vt_ok {
|
|
return {}, false
|
|
}
|
|
val, val_ok := resolve_attribute_value(val_tok, values)
|
|
if !val_ok {
|
|
return {}, false
|
|
}
|
|
|
|
rparen, rp_ok := next_token(t)
|
|
if !rp_ok || rparen != ")" {
|
|
attr_value_destroy(&val)
|
|
return {}, false
|
|
}
|
|
|
|
return Set_Action{
|
|
path = path,
|
|
value_kind = .If_Not_Exists,
|
|
value = val,
|
|
source = source,
|
|
}, true
|
|
}
|
|
|
|
parse_list_append :: proc(
|
|
t: ^Tokenizer,
|
|
path: string,
|
|
names: Maybe(map[string]string),
|
|
values: map[string]Attribute_Value,
|
|
) -> (action: Set_Action, ok: bool) {
|
|
lparen, lp_ok := next_token(t)
|
|
if !lp_ok || lparen != "(" {
|
|
return {}, false
|
|
}
|
|
|
|
// First operand — could be :val or path
|
|
first_tok, first_ok := next_token(t)
|
|
if !first_ok {
|
|
return {}, false
|
|
}
|
|
|
|
comma, comma_ok := next_token(t)
|
|
if !comma_ok || comma != "," {
|
|
return {}, false
|
|
}
|
|
|
|
// Second operand
|
|
second_tok, second_ok := next_token(t)
|
|
if !second_ok {
|
|
return {}, false
|
|
}
|
|
|
|
rparen, rp_ok := next_token(t)
|
|
if !rp_ok || rparen != ")" {
|
|
return {}, false
|
|
}
|
|
|
|
// Determine which is the path and which is the value
|
|
// Common patterns: list_append(path, :val) or list_append(:val, path)
|
|
source: string
|
|
val: Attribute_Value
|
|
resolved: bool
|
|
|
|
if len(first_tok) > 0 && first_tok[0] == ':' {
|
|
// list_append(:val, path)
|
|
v, v_ok := resolve_attribute_value(first_tok, values)
|
|
if !v_ok {
|
|
return {}, false
|
|
}
|
|
val = v
|
|
source, resolved = resolve_attribute_name(second_tok, names)
|
|
if !resolved {
|
|
return {}, false
|
|
}
|
|
} else if len(second_tok) > 0 && second_tok[0] == ':' {
|
|
// list_append(path, :val)
|
|
source, resolved = resolve_attribute_name(first_tok, names)
|
|
if !resolved {
|
|
return {}, false
|
|
}
|
|
v, v_ok := resolve_attribute_value(second_tok, values)
|
|
if !v_ok {
|
|
return {}, false
|
|
}
|
|
val = v
|
|
} else {
|
|
return {}, false
|
|
}
|
|
|
|
return Set_Action{
|
|
path = path,
|
|
value_kind = .List_Append,
|
|
value = val,
|
|
source = source,
|
|
}, true
|
|
}
|
|
|
|
// ============================================================================
|
|
// REMOVE clause parsing
|
|
// ============================================================================
|
|
|
|
parse_remove_clause :: proc(
|
|
t: ^Tokenizer,
|
|
plan: ^Update_Plan,
|
|
names: Maybe(map[string]string),
|
|
) -> bool {
|
|
saved_pos: int
|
|
for {
|
|
saved_pos = t.pos
|
|
|
|
path_tok, path_ok := next_token(t)
|
|
if !path_ok {
|
|
return false
|
|
}
|
|
|
|
if is_clause_keyword(path_tok) {
|
|
t.pos = saved_pos
|
|
return true
|
|
}
|
|
|
|
path, path_resolved := resolve_attribute_name(path_tok, names)
|
|
if !path_resolved {
|
|
return false
|
|
}
|
|
|
|
append(&plan.removes, Remove_Action{path = path})
|
|
|
|
saved_pos = t.pos
|
|
comma_maybe := tokenizer_next(t)
|
|
if comma, has := comma_maybe.?; has {
|
|
if comma == "," {
|
|
continue
|
|
}
|
|
t.pos = saved_pos
|
|
}
|
|
break
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// ADD clause parsing
|
|
// ============================================================================
|
|
|
|
parse_add_clause :: proc(
|
|
t: ^Tokenizer,
|
|
plan: ^Update_Plan,
|
|
names: Maybe(map[string]string),
|
|
values: map[string]Attribute_Value,
|
|
) -> bool {
|
|
saved_pos: int
|
|
for {
|
|
saved_pos = t.pos
|
|
|
|
path_tok, path_ok := next_token(t)
|
|
if !path_ok {
|
|
return false
|
|
}
|
|
|
|
if is_clause_keyword(path_tok) {
|
|
t.pos = saved_pos
|
|
return true
|
|
}
|
|
|
|
path, path_resolved := resolve_attribute_name(path_tok, names)
|
|
if !path_resolved {
|
|
return false
|
|
}
|
|
|
|
val_tok, vt_ok := next_token(t)
|
|
if !vt_ok {
|
|
return false
|
|
}
|
|
val, val_ok := resolve_attribute_value(val_tok, values)
|
|
if !val_ok {
|
|
return false
|
|
}
|
|
|
|
append(&plan.adds, Add_Action{path = path, value = val})
|
|
|
|
saved_pos = t.pos
|
|
comma_maybe := tokenizer_next(t)
|
|
if comma, has := comma_maybe.?; has {
|
|
if comma == "," {
|
|
continue
|
|
}
|
|
t.pos = saved_pos
|
|
}
|
|
break
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// DELETE clause parsing
|
|
// ============================================================================
|
|
|
|
parse_delete_clause :: proc(
|
|
t: ^Tokenizer,
|
|
plan: ^Update_Plan,
|
|
names: Maybe(map[string]string),
|
|
values: map[string]Attribute_Value,
|
|
) -> bool {
|
|
saved_pos: int
|
|
for {
|
|
saved_pos = t.pos
|
|
|
|
path_tok, path_ok := next_token(t)
|
|
if !path_ok {
|
|
return false
|
|
}
|
|
|
|
if is_clause_keyword(path_tok) {
|
|
t.pos = saved_pos
|
|
return true
|
|
}
|
|
|
|
path, path_resolved := resolve_attribute_name(path_tok, names)
|
|
if !path_resolved {
|
|
return false
|
|
}
|
|
|
|
val_tok, vt_ok := next_token(t)
|
|
if !vt_ok {
|
|
return false
|
|
}
|
|
val, val_ok := resolve_attribute_value(val_tok, values)
|
|
if !val_ok {
|
|
return false
|
|
}
|
|
|
|
append(&plan.deletes, Delete_Action{path = path, value = val})
|
|
|
|
saved_pos = t.pos
|
|
comma_maybe := tokenizer_next(t)
|
|
if comma, has := comma_maybe.?; has {
|
|
if comma == "," {
|
|
continue
|
|
}
|
|
t.pos = saved_pos
|
|
}
|
|
break
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helpers
|
|
// ============================================================================
|
|
|
|
is_clause_keyword :: proc(tok: string) -> bool {
|
|
return strings.equal_fold(tok, "SET") ||
|
|
strings.equal_fold(tok, "REMOVE") ||
|
|
strings.equal_fold(tok, "ADD") ||
|
|
strings.equal_fold(tok, "DELETE")
|
|
}
|
|
|
|
// ============================================================================
|
|
// Execute Update Plan — apply mutations to an Item (in-place)
|
|
// ============================================================================
|
|
|
|
execute_update_plan :: proc(item: ^Item, plan: ^Update_Plan) -> bool {
|
|
// Execute SET actions
|
|
for &action in plan.sets {
|
|
switch action.value_kind {
|
|
case .Direct:
|
|
// Remove old value if exists
|
|
if old, found := item[action.path]; found {
|
|
old_copy := old
|
|
attr_value_destroy(&old_copy)
|
|
delete_key(item, action.path)
|
|
}
|
|
item[strings.clone(action.path)] = attr_value_deep_copy(action.value)
|
|
|
|
case .Plus:
|
|
// Numeric addition: path = source + value or path = value + source
|
|
existing: Attribute_Value
|
|
if src, found := item[action.source]; found {
|
|
existing = src
|
|
} else {
|
|
return false // source attribute not found
|
|
}
|
|
result, add_ok := numeric_add(existing, action.value)
|
|
if !add_ok {
|
|
return false
|
|
}
|
|
if old, found := item[action.path]; found {
|
|
old_copy := old
|
|
attr_value_destroy(&old_copy)
|
|
delete_key(item, action.path)
|
|
}
|
|
item[strings.clone(action.path)] = result
|
|
|
|
case .Minus:
|
|
// Numeric subtraction: path = source - value
|
|
existing: Attribute_Value
|
|
if src, found := item[action.source]; found {
|
|
existing = src
|
|
} else {
|
|
return false
|
|
}
|
|
result, sub_ok := numeric_subtract(existing, action.value)
|
|
if !sub_ok {
|
|
return false
|
|
}
|
|
if old, found := item[action.path]; found {
|
|
old_copy := old
|
|
attr_value_destroy(&old_copy)
|
|
delete_key(item, action.path)
|
|
}
|
|
item[strings.clone(action.path)] = result
|
|
|
|
case .If_Not_Exists:
|
|
// Only set if attribute doesn't exist
|
|
if _, found := item[action.source]; !found {
|
|
if old, found2 := item[action.path]; found2 {
|
|
old_copy := old
|
|
attr_value_destroy(&old_copy)
|
|
delete_key(item, action.path)
|
|
}
|
|
item[strings.clone(action.path)] = attr_value_deep_copy(action.value)
|
|
}
|
|
// If attribute exists, do nothing (keep current value)
|
|
|
|
case .List_Append:
|
|
// Append to list
|
|
existing_list: []Attribute_Value
|
|
if src, found := item[action.source]; found {
|
|
if l, is_list := src.(List); is_list {
|
|
existing_list = ([]Attribute_Value)(l)
|
|
} else {
|
|
return false
|
|
}
|
|
} else {
|
|
existing_list = {}
|
|
}
|
|
|
|
append_list: []Attribute_Value
|
|
if l, is_list := action.value.(List); is_list {
|
|
append_list = ([]Attribute_Value)(l)
|
|
} else {
|
|
return false
|
|
}
|
|
|
|
new_list := make([]Attribute_Value, len(existing_list) + len(append_list))
|
|
for item_val, i in existing_list {
|
|
new_list[i] = attr_value_deep_copy(item_val)
|
|
}
|
|
for item_val, i in append_list {
|
|
new_list[len(existing_list) + i] = attr_value_deep_copy(item_val)
|
|
}
|
|
|
|
if old, found := item[action.path]; found {
|
|
old_copy := old
|
|
attr_value_destroy(&old_copy)
|
|
delete_key(item, action.path)
|
|
}
|
|
item[strings.clone(action.path)] = List(new_list)
|
|
}
|
|
}
|
|
|
|
// Execute REMOVE actions
|
|
for &action in plan.removes {
|
|
if old, found := item[action.path]; found {
|
|
old_copy := old
|
|
attr_value_destroy(&old_copy)
|
|
delete_key(item, action.path)
|
|
}
|
|
}
|
|
|
|
// Execute ADD actions
|
|
for &action in plan.adds {
|
|
if existing, found := item[action.path]; found {
|
|
// If existing is a number, add numerically
|
|
#partial switch v in existing {
|
|
case DDB_Number:
|
|
result, add_ok := numeric_add(existing, action.value)
|
|
if !add_ok {
|
|
return false
|
|
}
|
|
old_copy := existing
|
|
attr_value_destroy(&old_copy)
|
|
delete_key(item, action.path)
|
|
item[strings.clone(action.path)] = result
|
|
|
|
case String_Set:
|
|
// Add elements to string set
|
|
if new_ss, is_ss := action.value.(String_Set); is_ss {
|
|
merged := set_union_strings(([]string)(v), ([]string)(new_ss))
|
|
old_copy := existing
|
|
attr_value_destroy(&old_copy)
|
|
delete_key(item, action.path)
|
|
item[strings.clone(action.path)] = String_Set(merged)
|
|
} else {
|
|
return false
|
|
}
|
|
|
|
case DDB_Number_Set:
|
|
if new_ns, is_ns := action.value.(DDB_Number_Set); is_ns {
|
|
merged := set_union_ddb_numbers(([]DDB_Number)(v), ([]DDB_Number)(new_ns))
|
|
old_copy := existing
|
|
attr_value_destroy(&old_copy)
|
|
delete_key(item, action.path)
|
|
item[strings.clone(action.path)] = DDB_Number_Set(merged)
|
|
} else {
|
|
return false
|
|
}
|
|
|
|
case:
|
|
return false
|
|
}
|
|
} else {
|
|
// Attribute doesn't exist — create it
|
|
item[strings.clone(action.path)] = attr_value_deep_copy(action.value)
|
|
}
|
|
}
|
|
|
|
// Execute DELETE actions (remove elements from sets)
|
|
for &action in plan.deletes {
|
|
if existing, found := item[action.path]; found {
|
|
#partial switch v in existing {
|
|
case String_Set:
|
|
if del_ss, is_ss := action.value.(String_Set); is_ss {
|
|
result := set_difference_strings(([]string)(v), ([]string)(del_ss))
|
|
old_copy := existing
|
|
attr_value_destroy(&old_copy)
|
|
delete_key(item, action.path)
|
|
if len(result) > 0 {
|
|
item[strings.clone(action.path)] = String_Set(result)
|
|
} else {
|
|
delete(result)
|
|
}
|
|
}
|
|
|
|
case DDB_Number_Set:
|
|
if del_ns, is_ns := action.value.(DDB_Number_Set); is_ns {
|
|
result := set_difference_ddb_numbers(([]DDB_Number)(v), ([]DDB_Number)(del_ns))
|
|
old_copy := existing
|
|
attr_value_destroy(&old_copy)
|
|
delete_key(item, action.path)
|
|
if len(result) > 0 {
|
|
item[strings.clone(action.path)] = DDB_Number_Set(result)
|
|
} else {
|
|
delete(result)
|
|
}
|
|
}
|
|
|
|
case:
|
|
// DELETE on non-set type is a no-op in DynamoDB
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Numeric helpers
|
|
// ============================================================================
|
|
|
|
numeric_add :: proc(a: Attribute_Value, b: Attribute_Value) -> (Attribute_Value, bool) {
|
|
a_num, a_ok := a.(DDB_Number)
|
|
b_num, b_ok := b.(DDB_Number)
|
|
if !a_ok || !b_ok {
|
|
return nil, false
|
|
}
|
|
|
|
result, result_ok := add_ddb_numbers(a_num, b_num)
|
|
if !result_ok {
|
|
return nil, false
|
|
}
|
|
return result, true
|
|
}
|
|
|
|
numeric_subtract :: proc(a: Attribute_Value, b: Attribute_Value) -> (Attribute_Value, bool) {
|
|
a_num, a_ok := a.(DDB_Number)
|
|
b_num, b_ok := b.(DDB_Number)
|
|
if !a_ok || !b_ok {
|
|
return nil, false
|
|
}
|
|
|
|
result, result_ok := subtract_ddb_numbers(a_num, b_num)
|
|
if !result_ok {
|
|
return nil, false
|
|
}
|
|
return result, true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Set helpers
|
|
// ============================================================================
|
|
|
|
set_union_strings :: proc(a: []string, b: []string) -> []string {
|
|
seen := make(map[string]bool, allocator = context.temp_allocator)
|
|
for s in a {
|
|
seen[s] = true
|
|
}
|
|
for s in b {
|
|
seen[s] = true
|
|
}
|
|
|
|
result := make([]string, len(seen))
|
|
i := 0
|
|
for s in seen {
|
|
result[i] = strings.clone(s)
|
|
i += 1
|
|
}
|
|
return result
|
|
}
|
|
|
|
set_difference_strings :: proc(a: []string, b: []string) -> []string {
|
|
to_remove := make(map[string]bool, allocator = context.temp_allocator)
|
|
for s in b {
|
|
to_remove[s] = true
|
|
}
|
|
|
|
result := make([dynamic]string)
|
|
for s in a {
|
|
if !(s in to_remove) {
|
|
append(&result, strings.clone(s))
|
|
}
|
|
}
|
|
return result[:]
|
|
}
|
|
|
|
// Union of two DDB_Number slices (dedup by numeric equality)
|
|
set_union_ddb_numbers :: proc(a: []DDB_Number, b: []DDB_Number) -> []DDB_Number {
|
|
result := make([dynamic]DDB_Number)
|
|
|
|
// Add all from a
|
|
for num in a {
|
|
append(&result, clone_ddb_number(num))
|
|
}
|
|
|
|
// Add from b if not already present
|
|
for num in b {
|
|
found := false
|
|
for existing in result {
|
|
if compare_ddb_numbers(existing, num) == 0 {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
append(&result, clone_ddb_number(num))
|
|
}
|
|
}
|
|
|
|
return result[:]
|
|
}
|
|
|
|
// Difference: elements in a that are NOT in b
|
|
set_difference_ddb_numbers :: proc(a: []DDB_Number, b: []DDB_Number) -> []DDB_Number {
|
|
result := make([dynamic]DDB_Number)
|
|
|
|
for num in a {
|
|
in_b := false
|
|
for del in b {
|
|
if compare_ddb_numbers(num, del) == 0 {
|
|
in_b = true
|
|
break
|
|
}
|
|
}
|
|
if !in_b {
|
|
append(&result, clone_ddb_number(num))
|
|
}
|
|
}
|
|
|
|
return result[:]
|
|
}
|
|
|
|
// ============================================================================
|
|
// Request Parsing Helper
|
|
// ============================================================================
|
|
|
|
parse_update_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
|
|
}
|
|
|
|
ue_val, found := root["UpdateExpression"]
|
|
if !found {
|
|
return
|
|
}
|
|
|
|
ue_str, str_ok := ue_val.(json.String)
|
|
if !str_ok {
|
|
return
|
|
}
|
|
|
|
expr = strings.clone(string(ue_str))
|
|
ok = true
|
|
return
|
|
}
|
|
|
|
// Parse ReturnValues from request body
|
|
parse_return_values :: proc(request_body: []byte) -> string {
|
|
data, parse_err := json.parse(request_body, allocator = context.temp_allocator)
|
|
if parse_err != nil {
|
|
return strings.clone("NONE")
|
|
}
|
|
defer json.destroy_value(data)
|
|
|
|
root, root_ok := data.(json.Object)
|
|
if !root_ok {
|
|
return strings.clone("NONE")
|
|
}
|
|
|
|
rv_val, found := root["ReturnValues"]
|
|
if !found {
|
|
return strings.clone("NONE")
|
|
}
|
|
|
|
rv_str, str_ok := rv_val.(json.String)
|
|
if !str_ok {
|
|
return strings.clone("NONE")
|
|
}
|
|
|
|
return strings.clone(string(rv_str))
|
|
}
|