Files
jormun-db/gsi_handlers.odin
2026-02-16 02:15:15 -05:00

277 lines
7.0 KiB
Odin

// gsi_handlers.odin — GSI-related HTTP handler helpers
//
// This file lives in the main package alongside main.odin.
// It provides:
// 1. parse_global_secondary_indexes — parse GSI definitions from CreateTable request
// 2. parse_index_name — extract IndexName from Query/Scan requests
// 3. Projection type helper for response building
package main
import "core:encoding/json"
import "core:strings"
import "dynamodb"
// ============================================================================
// Parse GlobalSecondaryIndexes from CreateTable request body
//
// DynamoDB CreateTable request format for GSIs:
// {
// "GlobalSecondaryIndexes": [
// {
// "IndexName": "email-index",
// "KeySchema": [
// { "AttributeName": "email", "KeyType": "HASH" },
// { "AttributeName": "timestamp", "KeyType": "RANGE" }
// ],
// "Projection": {
// "ProjectionType": "ALL" | "KEYS_ONLY" | "INCLUDE",
// "NonKeyAttributes": ["attr1", "attr2"] // only for INCLUDE
// }
// }
// ]
// }
//
// Returns nil if no GSI definitions are present (valid — GSIs are optional).
// ============================================================================
parse_global_secondary_indexes :: proc(
root: json.Object,
attr_defs: []dynamodb.Attribute_Definition,
) -> Maybe([]dynamodb.Global_Secondary_Index) {
gsi_val, found := root["GlobalSecondaryIndexes"]
if !found {
return nil
}
gsi_arr, ok := gsi_val.(json.Array)
if !ok || len(gsi_arr) == 0 {
return nil
}
gsis := make([]dynamodb.Global_Secondary_Index, len(gsi_arr))
for elem, i in gsi_arr {
elem_obj, elem_ok := elem.(json.Object)
if !elem_ok {
cleanup_parsed_gsis(gsis[:i])
delete(gsis)
return nil
}
gsi, gsi_ok := parse_single_gsi(elem_obj, attr_defs)
if !gsi_ok {
cleanup_parsed_gsis(gsis[:i])
delete(gsis)
return nil
}
gsis[i] = gsi
}
return gsis
}
@(private = "file")
parse_single_gsi :: proc(
obj: json.Object,
attr_defs: []dynamodb.Attribute_Definition,
) -> (dynamodb.Global_Secondary_Index, bool) {
gsi: dynamodb.Global_Secondary_Index
// IndexName (required)
idx_val, idx_found := obj["IndexName"]
if !idx_found {
return {}, false
}
idx_str, idx_ok := idx_val.(json.String)
if !idx_ok {
return {}, false
}
gsi.index_name = strings.clone(string(idx_str))
// KeySchema (required)
ks_val, ks_found := obj["KeySchema"]
if !ks_found {
delete(gsi.index_name)
return {}, false
}
ks_arr, ks_ok := ks_val.(json.Array)
if !ks_ok || len(ks_arr) == 0 || len(ks_arr) > 2 {
delete(gsi.index_name)
return {}, false
}
key_schema := make([]dynamodb.Key_Schema_Element, len(ks_arr))
hash_count := 0
for ks_elem, j in ks_arr {
ks_obj, kobj_ok := ks_elem.(json.Object)
if !kobj_ok {
for k in 0..<j { delete(key_schema[k].attribute_name) }
delete(key_schema)
delete(gsi.index_name)
return {}, false
}
an_val, an_found := ks_obj["AttributeName"]
if !an_found {
for k in 0..<j { delete(key_schema[k].attribute_name) }
delete(key_schema)
delete(gsi.index_name)
return {}, false
}
an_str, an_ok := an_val.(json.String)
if !an_ok {
for k in 0..<j { delete(key_schema[k].attribute_name) }
delete(key_schema)
delete(gsi.index_name)
return {}, false
}
kt_val, kt_found := ks_obj["KeyType"]
if !kt_found {
for k in 0..<j { delete(key_schema[k].attribute_name) }
delete(key_schema)
delete(gsi.index_name)
return {}, false
}
kt_str, kt_ok := kt_val.(json.String)
if !kt_ok {
for k in 0..<j { delete(key_schema[k].attribute_name) }
delete(key_schema)
delete(gsi.index_name)
return {}, false
}
kt, kt_parse_ok := dynamodb.key_type_from_string(string(kt_str))
if !kt_parse_ok {
for k in 0..<j { delete(key_schema[k].attribute_name) }
delete(key_schema)
delete(gsi.index_name)
return {}, false
}
if kt == .HASH {
hash_count += 1
}
// Validate that the GSI key attribute is in AttributeDefinitions
attr_defined := false
for ad in attr_defs {
if ad.attribute_name == string(an_str) {
attr_defined = true
break
}
}
if !attr_defined {
for k in 0..<j { delete(key_schema[k].attribute_name) }
delete(key_schema)
delete(gsi.index_name)
return {}, false
}
key_schema[j] = dynamodb.Key_Schema_Element{
attribute_name = strings.clone(string(an_str)),
key_type = kt,
}
}
// Must have exactly one HASH key
if hash_count != 1 {
for ks in key_schema { delete(ks.attribute_name) }
delete(key_schema)
delete(gsi.index_name)
return {}, false
}
gsi.key_schema = key_schema
// Projection (optional — defaults to ALL)
gsi.projection.projection_type = .ALL
if proj_val, proj_found := obj["Projection"]; proj_found {
if proj_obj, proj_ok := proj_val.(json.Object); proj_ok {
if pt_val, pt_found := proj_obj["ProjectionType"]; pt_found {
if pt_str, pt_ok := pt_val.(json.String); pt_ok {
switch string(pt_str) {
case "ALL": gsi.projection.projection_type = .ALL
case "KEYS_ONLY": gsi.projection.projection_type = .KEYS_ONLY
case "INCLUDE": gsi.projection.projection_type = .INCLUDE
}
}
}
// NonKeyAttributes (only valid for INCLUDE projection)
if nka_val, nka_found := proj_obj["NonKeyAttributes"]; nka_found {
if nka_arr, nka_ok := nka_val.(json.Array); nka_ok && len(nka_arr) > 0 {
nka := make([]string, len(nka_arr))
for attr_val, k in nka_arr {
if attr_str, attr_ok := attr_val.(json.String); attr_ok {
nka[k] = strings.clone(string(attr_str))
}
}
gsi.projection.non_key_attributes = nka
}
}
}
}
return gsi, true
}
@(private = "file")
cleanup_parsed_gsis :: proc(gsis: []dynamodb.Global_Secondary_Index) {
for gsi in gsis {
delete(gsi.index_name)
for ks in gsi.key_schema {
delete(ks.attribute_name)
}
delete(gsi.key_schema)
if nka, has_nka := gsi.projection.non_key_attributes.?; has_nka {
for attr in nka { delete(attr) }
delete(nka)
}
}
}
// ============================================================================
// Parse IndexName from Query/Scan request
// ============================================================================
parse_index_name :: proc(request_body: []byte) -> Maybe(string) {
data, parse_err := json.parse(request_body, allocator = context.temp_allocator)
if parse_err != nil {
return nil
}
defer json.destroy_value(data)
root, root_ok := data.(json.Object)
if !root_ok {
return nil
}
idx_val, found := root["IndexName"]
if !found {
return nil
}
idx_str, ok := idx_val.(json.String)
if !ok {
return nil
}
return string(idx_str)
}
// ============================================================================
// Projection type to string for DescribeTable response
// ============================================================================
projection_type_to_string :: proc(pt: dynamodb.Projection_Type) -> string {
switch pt {
case .ALL: return "ALL"
case .KEYS_ONLY: return "KEYS_ONLY"
case .INCLUDE: return "INCLUDE"
}
return "ALL"
}