global secondary indexes
This commit is contained in:
276
gsi_handlers.odin
Normal file
276
gsi_handlers.odin
Normal file
@@ -0,0 +1,276 @@
|
||||
// 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"
|
||||
}
|
||||
Reference in New Issue
Block a user