flesh out the query stuff
This commit is contained in:
12
TODO.md
12
TODO.md
@@ -39,20 +39,18 @@ This tracks the rewrite from Zig to Odin and remaining features.
|
|||||||
- scan, query with pagination
|
- scan, query with pagination
|
||||||
- Table-level RW locks
|
- Table-level RW locks
|
||||||
|
|
||||||
- [ ] **dynamodb/handler.odin** - HTTP request handlers
|
|
||||||
- Route X-Amz-Target to handler functions
|
|
||||||
- handle_create_table, handle_put_item, etc.
|
|
||||||
- Build responses with proper error handling
|
|
||||||
- Arena allocator integration
|
|
||||||
|
|
||||||
### HTTP Server
|
### HTTP Server
|
||||||
|
|
||||||
- [ ] **HTTP server implementation**
|
- [x] **HTTP server implementation (MOSTLY DONE CONSOLIDATED HANDLER INTO MAIN AND HTTO FILES. NO NEED FOR A STAND ALONE HANDLER LIKE WE DID IN ZIG! JUST PLEASE GO OVER WHAT IS THERE!!!)**
|
||||||
- Accept TCP connections
|
- Accept TCP connections
|
||||||
- Parse HTTP POST requests
|
- Parse HTTP POST requests
|
||||||
- Read JSON bodies
|
- Read JSON bodies
|
||||||
- Send HTTP responses with headers
|
- Send HTTP responses with headers
|
||||||
- Keep-alive support
|
- Keep-alive support
|
||||||
|
- Route X-Amz-Target functions (this was the handler in zig but no need for that crap in odin land)
|
||||||
|
- handle_create_table, handle_put_item, etc. (this was the handler in zig but no need for that crap in odin land)
|
||||||
|
- Build responses with proper error handling (this was the handler in zig but no need for that crap in odin land)
|
||||||
|
- Arena allocator integration
|
||||||
- Options (Why we haven't checked this off yet, we need to make sure we chose the right option as the project grows, might make more sense to impliment different option):
|
- Options (Why we haven't checked this off yet, we need to make sure we chose the right option as the project grows, might make more sense to impliment different option):
|
||||||
- Use `core:net` directly
|
- Use `core:net` directly
|
||||||
- Use C FFI with libmicrohttpd
|
- Use C FFI with libmicrohttpd
|
||||||
|
|||||||
490
dynamodb/expression.odin
Normal file
490
dynamodb/expression.odin
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
// DynamoDB Expression Parser
|
||||||
|
// Parses KeyConditionExpression with ExpressionAttributeNames and ExpressionAttributeValues
|
||||||
|
// Supports: pk = :pk, pk = :pk AND sk > :sk, begins_with(sk, :prefix), BETWEEN, etc.
|
||||||
|
package dynamodb
|
||||||
|
|
||||||
|
import "core:encoding/json"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Sort Key Condition Operators
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
Sort_Key_Operator :: enum {
|
||||||
|
EQ, // =
|
||||||
|
LT, // <
|
||||||
|
LE, // <=
|
||||||
|
GT, // >
|
||||||
|
GE, // >=
|
||||||
|
BETWEEN, // BETWEEN x AND y
|
||||||
|
BEGINS_WITH, // begins_with(sk, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Parsed Structures
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
Sort_Key_Condition :: struct {
|
||||||
|
sk_name: string,
|
||||||
|
operator: Sort_Key_Operator,
|
||||||
|
value: Attribute_Value,
|
||||||
|
value2: Maybe(Attribute_Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
sort_key_condition_destroy :: proc(skc: ^Sort_Key_Condition) {
|
||||||
|
attr_value_destroy(&skc.value)
|
||||||
|
if v2, ok := skc.value2.?; ok {
|
||||||
|
v2_copy := v2
|
||||||
|
attr_value_destroy(&v2_copy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Key_Condition :: struct {
|
||||||
|
pk_name: string,
|
||||||
|
pk_value: Attribute_Value,
|
||||||
|
sk_condition: Maybe(Sort_Key_Condition),
|
||||||
|
}
|
||||||
|
|
||||||
|
key_condition_destroy :: proc(kc: ^Key_Condition) {
|
||||||
|
attr_value_destroy(&kc.pk_value)
|
||||||
|
if skc, ok := kc.sk_condition.?; ok {
|
||||||
|
skc_copy := skc
|
||||||
|
sort_key_condition_destroy(&skc_copy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the raw partition key value bytes for building storage keys
|
||||||
|
key_condition_get_pk_bytes :: proc(kc: ^Key_Condition) -> ([]byte, bool) {
|
||||||
|
#partial switch v in kc.pk_value {
|
||||||
|
case String:
|
||||||
|
return transmute([]byte)string(v), true
|
||||||
|
case Number:
|
||||||
|
return transmute([]byte)string(v), true
|
||||||
|
case Binary:
|
||||||
|
return transmute([]byte)string(v), true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Tokenizer
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
Tokenizer :: struct {
|
||||||
|
input: string,
|
||||||
|
pos: int,
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenizer_init :: proc(input: string) -> Tokenizer {
|
||||||
|
return Tokenizer{input = input, pos = 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenizer_next :: proc(t: ^Tokenizer) -> Maybe(string) {
|
||||||
|
// Skip whitespace
|
||||||
|
for t.pos < len(t.input) && is_whitespace(t.input[t.pos]) {
|
||||||
|
t.pos += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.pos >= len(t.input) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
start := t.pos
|
||||||
|
c := t.input[t.pos]
|
||||||
|
|
||||||
|
// Single-character tokens
|
||||||
|
if c == '(' || c == ')' || c == ',' {
|
||||||
|
t.pos += 1
|
||||||
|
return t.input[start:t.pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two-character operators
|
||||||
|
if t.pos + 1 < len(t.input) {
|
||||||
|
two := t.input[t.pos:t.pos + 2]
|
||||||
|
if two == "<=" || two == ">=" || two == "<>" {
|
||||||
|
t.pos += 2
|
||||||
|
return two
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single-character operators
|
||||||
|
if c == '=' || c == '<' || c == '>' {
|
||||||
|
t.pos += 1
|
||||||
|
return t.input[start:t.pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identifier or keyword (includes :placeholder and #name)
|
||||||
|
for t.pos < len(t.input) && is_ident_char(t.input[t.pos]) {
|
||||||
|
t.pos += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.pos > start {
|
||||||
|
return t.input[start:t.pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown character, skip it
|
||||||
|
t.pos += 1
|
||||||
|
return tokenizer_next(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
is_whitespace :: proc(c: byte) -> bool {
|
||||||
|
return c == ' ' || c == '\t' || c == '\n' || c == '\r'
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
is_ident_char :: proc(c: byte) -> bool {
|
||||||
|
return (c >= 'a' && c <= 'z') ||
|
||||||
|
(c >= 'A' && c <= 'Z') ||
|
||||||
|
(c >= '0' && c <= '9') ||
|
||||||
|
c == '_' || c == ':' || c == '#' || c == '-' || c == '.'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helper: convert Maybe(string) tokens into (string, bool) so or_return works.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
next_token :: proc(t: ^Tokenizer) -> (tok: string, ok: bool) {
|
||||||
|
if v, has := tokenizer_next(t).?; has {
|
||||||
|
tok = v
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Expression Parsing
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
parse_key_condition_expression :: proc(
|
||||||
|
expression: string,
|
||||||
|
attribute_names: Maybe(map[string]string),
|
||||||
|
attribute_values: map[string]Attribute_Value,
|
||||||
|
) -> (kc: Key_Condition, ok: bool) {
|
||||||
|
t := tokenizer_init(expression)
|
||||||
|
|
||||||
|
pk_name_token := next_token(&t) or_return
|
||||||
|
pk_name := resolve_attribute_name(pk_name_token, attribute_names) or_return
|
||||||
|
|
||||||
|
eq_token := next_token(&t) or_return
|
||||||
|
if eq_token != "=" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pk_value_token := next_token(&t) or_return
|
||||||
|
pk_value, pk_ok := resolve_attribute_value(pk_value_token, attribute_values)
|
||||||
|
if !pk_ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sk_condition: Maybe(Sort_Key_Condition) = nil
|
||||||
|
|
||||||
|
// Optional "AND ..."
|
||||||
|
if and_token, has_and := tokenizer_next(&t).?; has_and {
|
||||||
|
if !strings.equal_fold(and_token, "AND") {
|
||||||
|
attr_value_destroy(&pk_value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
skc, skc_ok := parse_sort_key_condition(&t, attribute_names, attribute_values)
|
||||||
|
if !skc_ok {
|
||||||
|
attr_value_destroy(&pk_value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sk_condition = skc
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
kc = Key_Condition{
|
||||||
|
pk_name = pk_name,
|
||||||
|
pk_value = pk_value,
|
||||||
|
sk_condition = sk_condition,
|
||||||
|
}
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
parse_sort_key_condition :: proc(
|
||||||
|
t: ^Tokenizer,
|
||||||
|
attribute_names: Maybe(map[string]string),
|
||||||
|
attribute_values: map[string]Attribute_Value,
|
||||||
|
) -> (skc: Sort_Key_Condition, ok: bool) {
|
||||||
|
first_token := next_token(t) or_return
|
||||||
|
|
||||||
|
if strings.equal_fold(first_token, "begins_with") {
|
||||||
|
skc, ok = parse_begins_with(t, attribute_names, attribute_values)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sk_name := resolve_attribute_name(first_token, attribute_names) or_return
|
||||||
|
|
||||||
|
op_token := next_token(t) or_return
|
||||||
|
operator, op_ok := parse_operator(op_token)
|
||||||
|
if !op_ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
value_token := next_token(t) or_return
|
||||||
|
value, val_ok := resolve_attribute_value(value_token, attribute_values)
|
||||||
|
if !val_ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
value2: Maybe(Attribute_Value) = nil
|
||||||
|
if operator == .BETWEEN {
|
||||||
|
// IMPORTANT: after allocating `value`, do NOT use `or_return` without cleanup.
|
||||||
|
and_token, tok_ok := next_token(t)
|
||||||
|
if !tok_ok || !strings.equal_fold(and_token, "AND") {
|
||||||
|
attr_value_destroy(&value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
value2_token, tok2_ok := next_token(t)
|
||||||
|
if !tok2_ok {
|
||||||
|
attr_value_destroy(&value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
v2, v2_ok := resolve_attribute_value(value2_token, attribute_values)
|
||||||
|
if !v2_ok {
|
||||||
|
attr_value_destroy(&value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value2 = v2
|
||||||
|
}
|
||||||
|
|
||||||
|
skc = Sort_Key_Condition{
|
||||||
|
sk_name = sk_name,
|
||||||
|
operator = operator,
|
||||||
|
value = value,
|
||||||
|
value2 = value2,
|
||||||
|
}
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
parse_begins_with :: proc(
|
||||||
|
t: ^Tokenizer,
|
||||||
|
attribute_names: Maybe(map[string]string),
|
||||||
|
attribute_values: map[string]Attribute_Value,
|
||||||
|
) -> (skc: Sort_Key_Condition, ok: bool) {
|
||||||
|
lparen := next_token(t) or_return
|
||||||
|
if lparen != "(" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sk_name_token := next_token(t) or_return
|
||||||
|
sk_name := resolve_attribute_name(sk_name_token, attribute_names) or_return
|
||||||
|
|
||||||
|
comma := next_token(t) or_return
|
||||||
|
if comma != "," {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
value_token := next_token(t) or_return
|
||||||
|
value, val_ok := resolve_attribute_value(value_token, attribute_values)
|
||||||
|
if !val_ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// after allocating `value`, avoid `or_return` so we can clean up
|
||||||
|
rparen, tok_ok := next_token(t)
|
||||||
|
if !tok_ok || rparen != ")" {
|
||||||
|
attr_value_destroy(&value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
skc = Sort_Key_Condition{
|
||||||
|
sk_name = sk_name,
|
||||||
|
operator = .BEGINS_WITH,
|
||||||
|
value = value,
|
||||||
|
value2 = nil,
|
||||||
|
}
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
parse_operator :: proc(token: string) -> (Sort_Key_Operator, bool) {
|
||||||
|
if token == "=" do return .EQ, true
|
||||||
|
if token == "<" do return .LT, true
|
||||||
|
if token == "<=" do return .LE, true
|
||||||
|
if token == ">" do return .GT, true
|
||||||
|
if token == ">=" do return .GE, true
|
||||||
|
if strings.equal_fold(token, "BETWEEN") do return .BETWEEN, true
|
||||||
|
return .EQ, false
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
resolve_attribute_name :: proc(token: string, names: Maybe(map[string]string)) -> (string, bool) {
|
||||||
|
if len(token) > 0 && token[0] == '#' {
|
||||||
|
if n, has_names := names.?; has_names {
|
||||||
|
if resolved, found := n[token]; found {
|
||||||
|
return resolved, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return token, true
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
resolve_attribute_value :: proc(
|
||||||
|
token: string,
|
||||||
|
values: map[string]Attribute_Value,
|
||||||
|
) -> (Attribute_Value, bool) {
|
||||||
|
if len(token) > 0 && token[0] == ':' {
|
||||||
|
if original, found := values[token]; found {
|
||||||
|
return attr_value_deep_copy(original), true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Request Parsing Helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
parse_expression_attribute_names :: proc(request_body: []byte) -> Maybe(map[string]string) {
|
||||||
|
data, parse_err := json.parse(request_body, allocator = context.temp_allocator)
|
||||||
|
if parse_err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer json.destroy_value(data)
|
||||||
|
|
||||||
|
root, ok := data.(json.Object)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
names_val, found := root["ExpressionAttributeNames"]
|
||||||
|
if !found {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
names_obj, names_ok := names_val.(json.Object)
|
||||||
|
if !names_ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]string)
|
||||||
|
|
||||||
|
for key, val in names_obj {
|
||||||
|
str, str_ok := val.(json.String)
|
||||||
|
if !str_ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[strings.clone(key)] = strings.clone(string(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_expression_attribute_values :: proc(request_body: []byte) -> (map[string]Attribute_Value, bool) {
|
||||||
|
data, parse_err := json.parse(request_body, allocator = context.temp_allocator)
|
||||||
|
if parse_err != nil {
|
||||||
|
return make(map[string]Attribute_Value), true
|
||||||
|
}
|
||||||
|
defer json.destroy_value(data)
|
||||||
|
|
||||||
|
root, ok := data.(json.Object)
|
||||||
|
if !ok {
|
||||||
|
return make(map[string]Attribute_Value), true
|
||||||
|
}
|
||||||
|
|
||||||
|
values_val, found := root["ExpressionAttributeValues"]
|
||||||
|
if !found {
|
||||||
|
return make(map[string]Attribute_Value), true
|
||||||
|
}
|
||||||
|
|
||||||
|
values_obj, values_ok := values_val.(json.Object)
|
||||||
|
if !values_ok {
|
||||||
|
return make(map[string]Attribute_Value), true
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]Attribute_Value)
|
||||||
|
|
||||||
|
for key, val in values_obj {
|
||||||
|
attr, attr_ok := parse_attribute_value(val)
|
||||||
|
if !attr_ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[strings.clone(key)] = attr
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: changed from Maybe(string) -> (string, bool) so callers can use or_return.
|
||||||
|
parse_key_condition_expression_string :: proc(request_body: []byte) -> (expr: string, ok: bool) {
|
||||||
|
body_str := string(request_body)
|
||||||
|
|
||||||
|
marker :: "\"KeyConditionExpression\""
|
||||||
|
start_idx := strings.index(body_str, marker)
|
||||||
|
if start_idx < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
after_marker := body_str[start_idx + len(marker):]
|
||||||
|
colon_idx := strings.index(after_marker, ":")
|
||||||
|
if colon_idx < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rest := after_marker[colon_idx + 1:]
|
||||||
|
quote_start := strings.index(rest, "\"")
|
||||||
|
if quote_start < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
value_start := quote_start + 1
|
||||||
|
pos := value_start
|
||||||
|
for pos < len(rest) {
|
||||||
|
if rest[pos] == '"' && (pos == 0 || rest[pos - 1] != '\\') {
|
||||||
|
expr = rest[value_start:pos]
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pos += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience: parse a complete Query key condition from request body
|
||||||
|
parse_query_key_condition :: proc(request_body: []byte) -> (kc: Key_Condition, ok: bool) {
|
||||||
|
expression := parse_key_condition_expression_string(request_body) or_return
|
||||||
|
|
||||||
|
attr_names := parse_expression_attribute_names(request_body)
|
||||||
|
defer {
|
||||||
|
if names, has_names := attr_names.?; has_names {
|
||||||
|
for k, v in names {
|
||||||
|
delete(k)
|
||||||
|
delete(v)
|
||||||
|
}
|
||||||
|
names_copy := names
|
||||||
|
delete(names_copy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attr_values, vals_ok := parse_expression_attribute_values(request_body)
|
||||||
|
if !vals_ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer {
|
||||||
|
for k, v in attr_values {
|
||||||
|
delete(k)
|
||||||
|
v_copy := v
|
||||||
|
attr_value_destroy(&v_copy)
|
||||||
|
}
|
||||||
|
delete(attr_values)
|
||||||
|
}
|
||||||
|
|
||||||
|
kc, ok = parse_key_condition_expression(expression, attr_names, attr_values)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
package dynamodb
|
package dynamodb
|
||||||
|
|
||||||
import "core:c"
|
import "core:c"
|
||||||
|
import "core:encoding/json"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:mem"
|
import "core:mem"
|
||||||
import "core:slice"
|
import "core:slice"
|
||||||
|
import "core:strconv"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "core:sync"
|
import "core:sync"
|
||||||
import "core:time"
|
import "core:time"
|
||||||
@@ -95,6 +97,16 @@ table_metadata_get_partition_key_name :: proc(metadata: ^Table_Metadata) -> Mayb
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the attribute type for a given attribute name
|
||||||
|
table_metadata_get_attribute_type :: proc(metadata: ^Table_Metadata, attr_name: string) -> Maybe(Scalar_Attribute_Type) {
|
||||||
|
for ad in metadata.attribute_definitions {
|
||||||
|
if ad.attribute_name == attr_name {
|
||||||
|
return ad.attribute_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Get the sort key attribute name (if any)
|
// Get the sort key attribute name (if any)
|
||||||
table_metadata_get_sort_key_name :: proc(metadata: ^Table_Metadata) -> Maybe(string) {
|
table_metadata_get_sort_key_name :: proc(metadata: ^Table_Metadata) -> Maybe(string) {
|
||||||
for ks in metadata.key_schema {
|
for ks in metadata.key_schema {
|
||||||
@@ -229,12 +241,196 @@ deserialize_table_metadata :: proc(data: []byte, allocator: mem.Allocator) -> (T
|
|||||||
|
|
||||||
metadata: Table_Metadata
|
metadata: Table_Metadata
|
||||||
|
|
||||||
// TODO: Parse KeySchema and AttributeDefinitions from JSON strings
|
// Parse table status
|
||||||
// For now, return empty - this will be implemented when needed
|
if status_val, found := meta_item["TableStatus"]; found {
|
||||||
|
#partial switch v in status_val {
|
||||||
|
case String:
|
||||||
|
metadata.table_status = table_status_from_string(string(v))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
metadata.table_status = .ACTIVE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse creation date time
|
||||||
|
if time_val, found := meta_item["CreationDateTime"]; found {
|
||||||
|
#partial switch v in time_val {
|
||||||
|
case Number:
|
||||||
|
val, parse_ok := strconv.parse_i64(string(v))
|
||||||
|
metadata.creation_date_time = val if parse_ok else 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse KeySchema from embedded JSON string
|
||||||
|
if ks_val, found := meta_item["KeySchema"]; found {
|
||||||
|
#partial switch v in ks_val {
|
||||||
|
case String:
|
||||||
|
ks, ks_ok := parse_key_schema_json(string(v), allocator)
|
||||||
|
if ks_ok {
|
||||||
|
metadata.key_schema = ks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse AttributeDefinitions from embedded JSON string
|
||||||
|
if ad_val, found := meta_item["AttributeDefinitions"]; found {
|
||||||
|
#partial switch v in ad_val {
|
||||||
|
case String:
|
||||||
|
ad, ad_ok := parse_attr_defs_json(string(v), allocator)
|
||||||
|
if ad_ok {
|
||||||
|
metadata.attribute_definitions = ad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return metadata, true
|
return metadata, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse key schema from JSON string like [{"AttributeName":"id","KeyType":"HASH"}]
|
||||||
|
parse_key_schema_json :: proc(json_str: string, allocator: mem.Allocator) -> ([]Key_Schema_Element, bool) {
|
||||||
|
data, parse_err := json.parse(transmute([]byte)json_str, allocator = context.temp_allocator)
|
||||||
|
if parse_err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
defer json.destroy_value(data)
|
||||||
|
|
||||||
|
arr, ok := data.(json.Array)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]Key_Schema_Element, len(arr), allocator)
|
||||||
|
|
||||||
|
for elem, i in arr {
|
||||||
|
obj, obj_ok := elem.(json.Object)
|
||||||
|
if !obj_ok {
|
||||||
|
cleanup_key_schema(result[:i], allocator)
|
||||||
|
delete(result, allocator)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
attr_name_val, name_found := obj["AttributeName"]
|
||||||
|
if !name_found {
|
||||||
|
cleanup_key_schema(result[:i], allocator)
|
||||||
|
delete(result, allocator)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
attr_name, name_ok := attr_name_val.(json.String)
|
||||||
|
if !name_ok {
|
||||||
|
cleanup_key_schema(result[:i], allocator)
|
||||||
|
delete(result, allocator)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
key_type_val, type_found := obj["KeyType"]
|
||||||
|
if !type_found {
|
||||||
|
cleanup_key_schema(result[:i], allocator)
|
||||||
|
delete(result, allocator)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
key_type_str, type_ok := key_type_val.(json.String)
|
||||||
|
if !type_ok {
|
||||||
|
cleanup_key_schema(result[:i], allocator)
|
||||||
|
delete(result, allocator)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
kt, kt_ok := key_type_from_string(string(key_type_str))
|
||||||
|
if !kt_ok {
|
||||||
|
cleanup_key_schema(result[:i], allocator)
|
||||||
|
delete(result, allocator)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
result[i] = Key_Schema_Element{
|
||||||
|
attribute_name = strings.clone(string(attr_name), allocator),
|
||||||
|
key_type = kt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_key_schema :: proc(elems: []Key_Schema_Element, allocator: mem.Allocator) {
|
||||||
|
for ks in elems {
|
||||||
|
delete(ks.attribute_name, allocator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse attribute definitions from JSON string
|
||||||
|
parse_attr_defs_json :: proc(json_str: string, allocator: mem.Allocator) -> ([]Attribute_Definition, bool) {
|
||||||
|
data, parse_err := json.parse(transmute([]byte)json_str, allocator = context.temp_allocator)
|
||||||
|
if parse_err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
defer json.destroy_value(data)
|
||||||
|
|
||||||
|
arr, ok := data.(json.Array)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]Attribute_Definition, len(arr), allocator)
|
||||||
|
|
||||||
|
for elem, i in arr {
|
||||||
|
obj, obj_ok := elem.(json.Object)
|
||||||
|
if !obj_ok {
|
||||||
|
cleanup_attr_defs(result[:i], allocator)
|
||||||
|
delete(result, allocator)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
attr_name_val, name_found := obj["AttributeName"]
|
||||||
|
if !name_found {
|
||||||
|
cleanup_attr_defs(result[:i], allocator)
|
||||||
|
delete(result, allocator)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
attr_name, name_ok := attr_name_val.(json.String)
|
||||||
|
if !name_ok {
|
||||||
|
cleanup_attr_defs(result[:i], allocator)
|
||||||
|
delete(result, allocator)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
attr_type_val, type_found := obj["AttributeType"]
|
||||||
|
if !type_found {
|
||||||
|
cleanup_attr_defs(result[:i], allocator)
|
||||||
|
delete(result, allocator)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
attr_type_str, type_ok := attr_type_val.(json.String)
|
||||||
|
if !type_ok {
|
||||||
|
cleanup_attr_defs(result[:i], allocator)
|
||||||
|
delete(result, allocator)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
at, at_ok := scalar_type_from_string(string(attr_type_str))
|
||||||
|
if !at_ok {
|
||||||
|
cleanup_attr_defs(result[:i], allocator)
|
||||||
|
delete(result, allocator)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
result[i] = Attribute_Definition{
|
||||||
|
attribute_name = strings.clone(string(attr_name), allocator),
|
||||||
|
attribute_type = at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_attr_defs :: proc(elems: []Attribute_Definition, allocator: mem.Allocator) {
|
||||||
|
for ad in elems {
|
||||||
|
delete(ad.attribute_name, allocator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get table metadata
|
// Get table metadata
|
||||||
get_table_metadata :: proc(engine: ^Storage_Engine, table_name: string) -> (Table_Metadata, Storage_Error) {
|
get_table_metadata :: proc(engine: ^Storage_Engine, table_name: string) -> (Table_Metadata, Storage_Error) {
|
||||||
meta_key := build_meta_key(table_name)
|
meta_key := build_meta_key(table_name)
|
||||||
@@ -616,6 +812,93 @@ scan :: proc(
|
|||||||
}, .None
|
}, .None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Query items by partition key with optional pagination
|
||||||
|
query :: proc(
|
||||||
|
engine: ^Storage_Engine,
|
||||||
|
table_name: string,
|
||||||
|
partition_key_value: []byte,
|
||||||
|
exclusive_start_key: Maybe([]byte),
|
||||||
|
limit: int,
|
||||||
|
) -> (Query_Result, Storage_Error) {
|
||||||
|
table_lock := get_or_create_table_lock(engine, table_name)
|
||||||
|
sync.rw_mutex_shared_lock(table_lock)
|
||||||
|
defer sync.rw_mutex_shared_unlock(table_lock)
|
||||||
|
|
||||||
|
// Verify table exists
|
||||||
|
metadata, meta_err := get_table_metadata(engine, table_name)
|
||||||
|
if meta_err != .None {
|
||||||
|
return {}, meta_err
|
||||||
|
}
|
||||||
|
defer table_metadata_destroy(&metadata, engine.allocator)
|
||||||
|
|
||||||
|
// Build partition prefix
|
||||||
|
prefix := build_partition_prefix(table_name, partition_key_value)
|
||||||
|
defer delete(prefix)
|
||||||
|
|
||||||
|
iter, iter_err := rocksdb.iter_create(&engine.db)
|
||||||
|
if iter_err != .None {
|
||||||
|
return {}, .RocksDB_Error
|
||||||
|
}
|
||||||
|
defer rocksdb.iter_destroy(&iter)
|
||||||
|
|
||||||
|
max_items := limit if limit > 0 else 1_000_000
|
||||||
|
|
||||||
|
// Seek to start position
|
||||||
|
if start_key, has_start := exclusive_start_key.?; has_start {
|
||||||
|
if has_prefix(start_key, prefix) {
|
||||||
|
rocksdb.iter_seek(&iter, start_key)
|
||||||
|
if rocksdb.iter_valid(&iter) {
|
||||||
|
rocksdb.iter_next(&iter)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rocksdb.iter_seek(&iter, prefix)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rocksdb.iter_seek(&iter, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([dynamic]Item)
|
||||||
|
count := 0
|
||||||
|
last_key: Maybe([]byte) = nil
|
||||||
|
|
||||||
|
for rocksdb.iter_valid(&iter) {
|
||||||
|
key := rocksdb.iter_key(&iter)
|
||||||
|
if key == nil || !has_prefix(key, prefix) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hit limit — save this key as pagination token and stop
|
||||||
|
if count >= max_items {
|
||||||
|
last_key = slice.clone(key)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
value := rocksdb.iter_value(&iter)
|
||||||
|
if value == nil {
|
||||||
|
rocksdb.iter_next(&iter)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
item, decode_ok := decode(value)
|
||||||
|
if !decode_ok {
|
||||||
|
rocksdb.iter_next(&iter)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
append(&items, item)
|
||||||
|
count += 1
|
||||||
|
rocksdb.iter_next(&iter)
|
||||||
|
}
|
||||||
|
|
||||||
|
result_items := make([]Item, len(items))
|
||||||
|
copy(result_items, items[:])
|
||||||
|
|
||||||
|
return Query_Result{
|
||||||
|
items = result_items,
|
||||||
|
last_evaluated_key = last_key,
|
||||||
|
}, .None
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to check if a byte slice has a prefix
|
// Helper to check if a byte slice has a prefix
|
||||||
has_prefix :: proc(data: []byte, prefix: []byte) -> bool {
|
has_prefix :: proc(data: []byte, prefix: []byte) -> bool {
|
||||||
if len(data) < len(prefix) {
|
if len(data) < len(prefix) {
|
||||||
@@ -629,8 +912,39 @@ has_prefix :: proc(data: []byte, prefix: []byte) -> bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// List tables (simplified - returns empty list for now)
|
// List tables by iterating over meta keys in RocksDB
|
||||||
list_tables :: proc(engine: ^Storage_Engine) -> []string {
|
list_tables :: proc(engine: ^Storage_Engine) -> ([]string, Storage_Error) {
|
||||||
// TODO: Implement by iterating over meta keys
|
iter, iter_err := rocksdb.iter_create(&engine.db)
|
||||||
return {}
|
if iter_err != .None {
|
||||||
|
return nil, .RocksDB_Error
|
||||||
|
}
|
||||||
|
defer rocksdb.iter_destroy(&iter)
|
||||||
|
|
||||||
|
meta_prefix := []byte{u8(Entity_Type.Meta)}
|
||||||
|
rocksdb.iter_seek(&iter, meta_prefix)
|
||||||
|
|
||||||
|
tables := make([dynamic]string)
|
||||||
|
|
||||||
|
for rocksdb.iter_valid(&iter) {
|
||||||
|
key := rocksdb.iter_key(&iter)
|
||||||
|
if key == nil || len(key) == 0 || key[0] != u8(Entity_Type.Meta) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := Key_Decoder{data = key, pos = 0}
|
||||||
|
_, et_ok := decoder_read_entity_type(&decoder)
|
||||||
|
if !et_ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
tbl_name_bytes, seg_ok := decoder_read_segment_borrowed(&decoder)
|
||||||
|
if !seg_ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
append(&tables, strings.clone(string(tbl_name_bytes)))
|
||||||
|
rocksdb.iter_next(&iter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tables[:], .None
|
||||||
}
|
}
|
||||||
@@ -253,6 +253,18 @@ table_status_to_string :: proc(status: Table_Status) -> string {
|
|||||||
return "ACTIVE"
|
return "ACTIVE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table_status_from_string :: proc(s: string) -> Table_Status {
|
||||||
|
switch s {
|
||||||
|
case "CREATING": return .CREATING
|
||||||
|
case "UPDATING": return .UPDATING
|
||||||
|
case "DELETING": return .DELETING
|
||||||
|
case "ACTIVE": return .ACTIVE
|
||||||
|
case "ARCHIVING": return .ARCHIVING
|
||||||
|
case "ARCHIVED": return .ARCHIVED
|
||||||
|
}
|
||||||
|
return .ACTIVE
|
||||||
|
}
|
||||||
|
|
||||||
// Table description
|
// Table description
|
||||||
Table_Description :: struct {
|
Table_Description :: struct {
|
||||||
table_name: string,
|
table_name: string,
|
||||||
@@ -352,6 +364,17 @@ error_to_response :: proc(err_type: DynamoDB_Error_Type, message: string) -> str
|
|||||||
return fmt.aprintf(`{{"__type":"%s","message":"%s"}}`, type_str, message)
|
return fmt.aprintf(`{{"__type":"%s","message":"%s"}}`, type_str, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build an Attribute_Value with the correct scalar type from raw bytes
|
||||||
|
build_attribute_value_with_type :: proc(raw_bytes: []byte, attr_type: Scalar_Attribute_Type) -> Attribute_Value {
|
||||||
|
owned := strings.clone(string(raw_bytes))
|
||||||
|
switch attr_type {
|
||||||
|
case .S: return String(owned)
|
||||||
|
case .N: return Number(owned)
|
||||||
|
case .B: return Binary(owned)
|
||||||
|
}
|
||||||
|
return String(owned)
|
||||||
|
}
|
||||||
|
|
||||||
// Deep copy an attribute value
|
// Deep copy an attribute value
|
||||||
attr_value_deep_copy :: proc(attr: Attribute_Value) -> Attribute_Value {
|
attr_value_deep_copy :: proc(attr: Attribute_Value) -> Attribute_Value {
|
||||||
switch v in attr {
|
switch v in attr {
|
||||||
|
|||||||
87
main.odin
87
main.odin
@@ -258,12 +258,15 @@ handle_describe_table :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_R
|
|||||||
}
|
}
|
||||||
|
|
||||||
handle_list_tables :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, response: ^HTTP_Response) {
|
handle_list_tables :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, response: ^HTTP_Response) {
|
||||||
_ = request // Not using request body for ListTables
|
_ = request
|
||||||
|
|
||||||
tables := dynamodb.list_tables(engine)
|
tables, err := dynamodb.list_tables(engine)
|
||||||
// list_tables returns []string which may be empty, not an error
|
if err != .None {
|
||||||
|
make_error_response(response, .InternalServerError, "Failed to list tables")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// tables are owned by engine allocator — just read them, don't free
|
||||||
|
|
||||||
// Build response
|
|
||||||
builder := strings.builder_make()
|
builder := strings.builder_make()
|
||||||
strings.write_string(&builder, `{"TableNames":[`)
|
strings.write_string(&builder, `{"TableNames":[`)
|
||||||
|
|
||||||
@@ -390,11 +393,77 @@ handle_delete_item :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Requ
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
handle_query :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, response: ^HTTP_Response) {
|
handle_query :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, response: ^HTTP_Response) {
|
||||||
_ = engine
|
table_name, ok := dynamodb.parse_table_name(request.body)
|
||||||
_ = request
|
if !ok {
|
||||||
// For now, return not implemented
|
make_error_response(response, .ValidationException, "Invalid request or missing TableName")
|
||||||
// TODO: Implement KeyConditionExpression parsing and query logic
|
return
|
||||||
make_error_response(response, .ValidationException, "Query operation not yet implemented")
|
}
|
||||||
|
|
||||||
|
// Parse KeyConditionExpression
|
||||||
|
kc, kc_ok := dynamodb.parse_query_key_condition(request.body)
|
||||||
|
if !kc_ok {
|
||||||
|
make_error_response(response, .ValidationException, "Missing or invalid KeyConditionExpression")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dynamodb.key_condition_destroy(&kc)
|
||||||
|
|
||||||
|
// Extract partition key bytes
|
||||||
|
pk_bytes, pk_ok := dynamodb.key_condition_get_pk_bytes(&kc)
|
||||||
|
if !pk_ok {
|
||||||
|
make_error_response(response, .ValidationException, "Invalid partition key type")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone pk_bytes so it survives kc cleanup (kc borrows from the parsed value)
|
||||||
|
pk_owned := make([]byte, len(pk_bytes))
|
||||||
|
copy(pk_owned, pk_bytes)
|
||||||
|
defer delete(pk_owned)
|
||||||
|
|
||||||
|
// Parse Limit
|
||||||
|
limit := dynamodb.parse_limit(request.body)
|
||||||
|
if limit == 0 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Parse ExclusiveStartKey properly (requires metadata for type info)
|
||||||
|
exclusive_start_key: Maybe([]byte) = nil
|
||||||
|
|
||||||
|
result, err := dynamodb.query(engine, table_name, pk_owned, exclusive_start_key, limit)
|
||||||
|
if err != .None {
|
||||||
|
#partial switch err {
|
||||||
|
case .Table_Not_Found:
|
||||||
|
make_error_response(response, .ResourceNotFoundException, "Table not found")
|
||||||
|
case:
|
||||||
|
make_error_response(response, .InternalServerError, "Query failed")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dynamodb.query_result_destroy(&result)
|
||||||
|
|
||||||
|
// Build response
|
||||||
|
builder := strings.builder_make()
|
||||||
|
strings.write_string(&builder, `{"Items":[`)
|
||||||
|
|
||||||
|
for item, i in result.items {
|
||||||
|
if i > 0 do strings.write_string(&builder, ",")
|
||||||
|
item_json := dynamodb.serialize_item(item)
|
||||||
|
strings.write_string(&builder, item_json)
|
||||||
|
}
|
||||||
|
|
||||||
|
strings.write_string(&builder, `],"Count":`)
|
||||||
|
fmt.sbprintf(&builder, "%d", len(result.items))
|
||||||
|
strings.write_string(&builder, `,"ScannedCount":`)
|
||||||
|
fmt.sbprintf(&builder, "%d", len(result.items))
|
||||||
|
|
||||||
|
// TODO: Add LastEvaluatedKey when pagination is fully wired
|
||||||
|
if last_key, has_last := result.last_evaluated_key.?; has_last {
|
||||||
|
_ = last_key
|
||||||
|
}
|
||||||
|
|
||||||
|
strings.write_string(&builder, "}")
|
||||||
|
|
||||||
|
resp_body := strings.to_string(builder)
|
||||||
|
response_set_body(response, transmute([]byte)resp_body)
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_scan :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, response: ^HTTP_Response) {
|
handle_scan :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, response: ^HTTP_Response) {
|
||||||
|
|||||||
Reference in New Issue
Block a user