From ad599a0af7748ad340a7862490fd6c98b0c8da98 Mon Sep 17 00:00:00 2001 From: biondizzle Date: Sun, 15 Feb 2026 11:42:43 -0500 Subject: [PATCH] fix docs and todo --- Makefile | 4 +- QUICKSTART.md | 36 +- README.md | 24 +- TODO.md | 25 +- project_context.txt | 3337 ------------------------------------------- 5 files changed, 53 insertions(+), 3373 deletions(-) delete mode 100644 project_context.txt diff --git a/Makefile b/Makefile index 34da81e..cd1f316 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ COMMON_FLAGS := -vet -strict-style EXTRA_LINKER_FLAGS := $(LIB_PATH) $(SHIM_LIB) $(ROCKSDB_LIBS) # Runtime configuration -PORT ?= 8000 +PORT ?= 8002 HOST ?= 0.0.0.0 DATA_DIR ?= ./data VERBOSE ?= 0 @@ -191,7 +191,7 @@ help: @echo " make clean - Remove build artifacts" @echo "" @echo "$(GREEN)Run Commands:$(NC)" - @echo " make run - Build and run server (default: localhost:8000)" + @echo " make run - Build and run server (default: localhost:8002)" @echo " make run PORT=9000 - Run on custom port" @echo " make dev - Clean, build, and run" @echo " make quick - Fast rebuild and run" diff --git a/QUICKSTART.md b/QUICKSTART.md index 54c2814..05fc433 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -101,7 +101,7 @@ export PATH=$PATH:/path/to/odin ### Basic Usage ```bash -# Run with defaults (localhost:8000, ./data directory) +# Run with defaults (localhost:8002, ./data directory) make run ``` @@ -118,10 +118,10 @@ You should see: ║ ║ ╚═══════════════════════════════════════════════╝ - Port: 8000 | Data Dir: ./data + Port: 8002 | Data Dir: ./data Storage engine initialized at ./data -Starting DynamoDB-compatible server on 0.0.0.0:8000 +Starting DynamoDB-compatible server on 0.0.0.0:8002 Ready to accept connections! ``` @@ -186,7 +186,7 @@ aws configure **Create a Table:** ```bash aws dynamodb create-table \ - --endpoint-url http://localhost:8000 \ + --endpoint-url http://localhost:8002 \ --table-name Users \ --key-schema \ AttributeName=id,KeyType=HASH \ @@ -197,13 +197,13 @@ aws dynamodb create-table \ **List Tables:** ```bash -aws dynamodb list-tables --endpoint-url http://localhost:8000 +aws dynamodb list-tables --endpoint-url http://localhost:8002 ``` **Put an Item:** ```bash aws dynamodb put-item \ - --endpoint-url http://localhost:8000 \ + --endpoint-url http://localhost:8002 \ --table-name Users \ --item '{ "id": {"S": "user123"}, @@ -216,7 +216,7 @@ aws dynamodb put-item \ **Get an Item:** ```bash aws dynamodb get-item \ - --endpoint-url http://localhost:8000 \ + --endpoint-url http://localhost:8002 \ --table-name Users \ --key '{"id": {"S": "user123"}}' ``` @@ -224,7 +224,7 @@ aws dynamodb get-item \ **Query Items:** ```bash aws dynamodb query \ - --endpoint-url http://localhost:8000 \ + --endpoint-url http://localhost:8002 \ --table-name Users \ --key-condition-expression "id = :id" \ --expression-attribute-values '{ @@ -235,14 +235,14 @@ aws dynamodb query \ **Scan Table:** ```bash aws dynamodb scan \ - --endpoint-url http://localhost:8000 \ + --endpoint-url http://localhost:8002 \ --table-name Users ``` **Delete an Item:** ```bash aws dynamodb delete-item \ - --endpoint-url http://localhost:8000 \ + --endpoint-url http://localhost:8002 \ --table-name Users \ --key '{"id": {"S": "user123"}}' ``` @@ -250,7 +250,7 @@ aws dynamodb delete-item \ **Delete a Table:** ```bash aws dynamodb delete-table \ - --endpoint-url http://localhost:8000 \ + --endpoint-url http://localhost:8002 \ --table-name Users ``` @@ -262,7 +262,7 @@ aws dynamodb delete-table \ const { DynamoDBClient, PutItemCommand, GetItemCommand } = require("@aws-sdk/client-dynamodb"); const client = new DynamoDBClient({ - endpoint: "http://localhost:8000", + endpoint: "http://localhost:8002", region: "us-east-1", credentials: { accessKeyId: "dummy", @@ -279,13 +279,13 @@ async function test() { name: { S: "Alice" } } })); - + // Get the item const result = await client.send(new GetItemCommand({ TableName: "Users", Key: { id: { S: "user123" } } })); - + console.log(result.Item); } @@ -299,7 +299,7 @@ import boto3 dynamodb = boto3.client( 'dynamodb', - endpoint_url='http://localhost:8000', + endpoint_url='http://localhost:8002', region_name='us-east-1', aws_access_key_id='dummy', aws_secret_access_key='dummy' @@ -364,8 +364,8 @@ make fmt ### Port Already in Use ```bash -# Check what's using port 8000 -lsof -i :8000 +# Check what's using port 8002 +lsof -i :8002 # Use a different port make run PORT=9000 @@ -426,7 +426,7 @@ make profile # Load test ab -n 10000 -c 100 -p item.json -T application/json \ - http://localhost:8000/ + http://localhost:8002/ ``` ## Production Deployment diff --git a/README.md b/README.md index a47a6ed..758b3b6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ A high-performance, DynamoDB-compatible database server written in Odin, backed by RocksDB. ``` - ╦╔═╗╦═╗╔╦╗╦ ╦╔╗╔╔╦╗╔╗ + ╦╔═╗╦═╗╔╦╗╦ ╦╔╗╔╔╦╗╔╗ ║║ ║╠╦╝║║║║ ║║║║ ║║╠╩╗ ╚╝╚═╝╩╚═╩ ╩╚═╝╝╚╝═╩╝╚═╝ DynamoDB-Compatible Database @@ -55,7 +55,7 @@ sudo apt install librocksdb-dev libsnappy-dev liblz4-dev libzstd-dev libbz2-dev # Build the server make build -# Run with default settings (localhost:8000, ./data directory) +# Run with default settings (localhost:8002, ./data directory) make run # Run with custom port @@ -70,7 +70,7 @@ make run DATA_DIR=/tmp/jormundb ```bash # Create a table aws dynamodb create-table \ - --endpoint-url http://localhost:8000 \ + --endpoint-url http://localhost:8002 \ --table-name Users \ --key-schema AttributeName=id,KeyType=HASH \ --attribute-definitions AttributeName=id,AttributeType=S \ @@ -78,26 +78,26 @@ aws dynamodb create-table \ # Put an item aws dynamodb put-item \ - --endpoint-url http://localhost:8000 \ + --endpoint-url http://localhost:8002 \ --table-name Users \ --item '{"id":{"S":"user123"},"name":{"S":"Alice"},"age":{"N":"30"}}' # Get an item aws dynamodb get-item \ - --endpoint-url http://localhost:8000 \ + --endpoint-url http://localhost:8002 \ --table-name Users \ --key '{"id":{"S":"user123"}}' # Query items aws dynamodb query \ - --endpoint-url http://localhost:8000 \ + --endpoint-url http://localhost:8002 \ --table-name Users \ --key-condition-expression "id = :id" \ --expression-attribute-values '{":id":{"S":"user123"}}' # Scan table aws dynamodb scan \ - --endpoint-url http://localhost:8000 \ + --endpoint-url http://localhost:8002 \ --table-name Users ``` @@ -163,15 +163,15 @@ handle_request :: proc(conn: net.TCP_Socket) { arena: mem.Arena mem.arena_init(&arena, make([]byte, mem.Megabyte * 4)) defer mem.arena_destroy(&arena) - + context.allocator = mem.arena_allocator(&arena) - + // Everything below uses the arena automatically // No manual frees, no errdefer cleanup needed request := parse_request() // Uses context.allocator response := process(request) // Uses context.allocator send_response(response) // Uses context.allocator - + // Arena is freed here automatically } ``` @@ -243,7 +243,7 @@ Scan (full table) | 5000 ops | 234.56 ms | 21320 ops/sec ### Environment Variables ```bash -JORMUN_PORT=8000 # Server port +JORMUN_PORT=8002 # Server port JORMUN_HOST=0.0.0.0 # Bind address JORMUN_DATA_DIR=./data # RocksDB data directory JORMUN_VERBOSE=1 # Enable verbose logging @@ -275,7 +275,7 @@ chmod 755 ./data Check if the port is already in use: ```bash -lsof -i :8000 +lsof -i :8002 ``` ### "Invalid JSON" errors diff --git a/TODO.md b/TODO.md index 1bb6870..2c2e423 100644 --- a/TODO.md +++ b/TODO.md @@ -12,8 +12,8 @@ This tracks the rewrite from Zig to Odin and remaining features. - [x] Core types (dynamodb/types.odin) - [x] Key codec with varint encoding (key_codec/key_codec.odin) - [x] Main entry point with arena pattern demo -- [x] LICENSE file - [x] .gitignore +- [x] HTTP Server Scaffolding ## 🚧 In Progress (Need to Complete) @@ -23,7 +23,7 @@ This tracks the rewrite from Zig to Odin and remaining features. - Parse `{"S": "value"}` format - Serialize AttributeValue to DynamoDB JSON - Parse request bodies (PutItem, GetItem, etc.) - + - [ ] **item_codec/item_codec.odin** - Binary TLV encoding for items - Encode Item to binary TLV format - Decode binary TLV back to Item @@ -50,7 +50,7 @@ This tracks the rewrite from Zig to Odin and remaining features. - Read JSON bodies - Send HTTP responses with headers - Keep-alive support - - Options: + - 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 C FFI with libmicrohttpd - Use Odin's vendor:microui (if suitable) @@ -69,6 +69,23 @@ This tracks the rewrite from Zig to Odin and remaining features. - ADD operations - DELETE operations +### Replication Support (Priority 4) + + - [ ] **Build C++ Shim in order to use RocksDB's WAL replication helpers** + - [ ] **Add configurator to set instance as a master or slave node and point to proper Target and Destination IPs** + - [ ] **Leverage C++ helpers from shim** + +### Subscribe To Changes Feature (Priority LAST [But keep in mind because semantics we decide now will make this easier later]) + + - [ ] **Best-effort notifications (Postgres-ish LISTEN/NOTIFY [in-memory pub/sub fanout. If you’re not connected, you miss it.])** + - Add an in-process “event bus” channels: table-wide, partition-key, item-key, “all”. + - When putItem/deleteItem/updateItem/createTable/... commits successfully publish {op, table, key, timestamp, item?} + + - [ ] **Durable change streams (Mongo-ish [append every mutation to a persistent log and let consumers read it with resume tokens.])** + - Create a “changelog” keyspace + - Generate a monotonically increasing sequence by using a stable Per-partition sequence cursor + - Expose via an API (I prefer publishing to MQTT or SSE) + ## 📋 Testing - [ ] Unit tests for key_codec @@ -182,5 +199,5 @@ make test make run # Test with AWS CLI -aws dynamodb list-tables --endpoint-url http://localhost:8000 +aws dynamodb list-tables --endpoint-url http://localhost:8002 ``` diff --git a/project_context.txt b/project_context.txt deleted file mode 100644 index ce9fe2d..0000000 --- a/project_context.txt +++ /dev/null @@ -1,3337 +0,0 @@ -# Project: jormun-db -# Generated: Sun Feb 15 08:31:32 AM EST 2026 - -================================================================================ - -================================================================================ -FILE: ./ARCHITECTURE.md -================================================================================ - -## JormunDB Architecture - -This document explains the internal architecture of JormunDB, including design decisions, storage formats, and the arena-per-request memory management pattern. - -## Table of Contents - -- [Overview](#overview) -- [Why Odin?](#why-odin) -- [Memory Management](#memory-management) -- [Storage Format](#storage-format) -- [Module Structure](#module-structure) -- [Request Flow](#request-flow) -- [Concurrency Model](#concurrency-model) - -## Overview - -JormunDB is a DynamoDB-compatible database server that speaks the DynamoDB wire protocol. It uses RocksDB for persistent storage and is written in Odin for elegant memory management. - -### Key Design Goals - -1. **Zero allocation ceremony** - No explicit `defer free()` or error handling for every allocation -2. **Binary storage** - Efficient TLV encoding instead of JSON -3. **API compatibility** - Drop-in replacement for DynamoDB Local -4. **Performance** - RocksDB-backed with efficient key encoding - -## Why Odin? - -The original implementation in Zig suffered from explicit allocator threading: - -```zig -// Zig version - explicit allocator everywhere -fn handleRequest(allocator: std.mem.Allocator, request: []const u8) !Response { - const parsed = try parseJson(allocator, request); - defer parsed.deinit(allocator); - - const item = try storage.getItem(allocator, parsed.table_name, parsed.key); - defer if (item) |i| freeItem(allocator, i); - - const response = try serializeResponse(allocator, item); - defer allocator.free(response); - - return response; // Wait, we deferred the free! -} -``` - -Odin's context allocator system eliminates this: - -```odin -// Odin version - implicit context allocator -handle_request :: proc(request: []byte) -> Response { - // All allocations use context.allocator automatically - parsed := parse_json(request) - item := storage_get_item(parsed.table_name, parsed.key) - response := serialize_response(item) - - return response - // Everything freed when arena is destroyed -} -``` - -## Memory Management - -JormunDB uses a two-allocator strategy: - -### 1. Arena Allocator (Request-Scoped) - -Every HTTP request gets its own arena: - -```odin -handle_connection :: proc(conn: net.TCP_Socket) { - // Create arena for this request (4MB) - arena: mem.Arena - mem.arena_init(&arena, make([]byte, mem.Megabyte * 4)) - defer mem.arena_destroy(&arena) - - // Set context allocator - context.allocator = mem.arena_allocator(&arena) - - // All downstream code uses context.allocator - request := parse_http_request(conn) // uses arena - response := handle_request(request) // uses arena - send_response(conn, response) // uses arena - - // Arena is freed here - everything cleaned up automatically -} -``` - -**Benefits:** -- No individual `free()` calls needed -- No `errdefer` cleanup -- No use-after-free bugs -- No memory leaks from forgotten frees -- Predictable performance (no GC pauses) - -### 2. Default Allocator (Long-Lived Data) - -The default allocator (typically `context.allocator` at program start) is used for: - -- Table metadata -- Table locks (sync.RW_Mutex) -- Engine state -- Items returned from storage layer (copied to request arena when needed) - -## Storage Format - -### Binary Keys (Varint-Prefixed Segments) - -All keys use varint length prefixes for space efficiency: - -``` -Meta key: [0x01][len][table_name] -Data key: [0x02][len][table_name][len][pk_value][len][sk_value]? -GSI key: [0x03][len][table_name][len][index_name][len][gsi_pk][len][gsi_sk]? -LSI key: [0x04][len][table_name][len][index_name][len][pk][len][lsi_sk] -``` - -**Example Data Key:** -``` -Table: "Users" -PK: "user:123" -SK: "profile" - -Encoded: -[0x02] // Entity type (Data) -[0x05] // Table name length (5) -Users // Table name bytes -[0x08] // PK length (8) -user:123 // PK bytes -[0x07] // SK length (7) -profile // SK bytes -``` - -### Item Encoding (TLV Format) - -Items use Tag-Length-Value encoding for space efficiency: - -``` -Format: -[attr_count:varint] - [name_len:varint][name:bytes][type_tag:u8][value_len:varint][value:bytes]... - -Type Tags: - String = 0x01 Number = 0x02 Binary = 0x03 - Bool = 0x04 Null = 0x05 - SS = 0x10 NS = 0x11 BS = 0x12 - List = 0x20 Map = 0x21 -``` - -**Example Item:** -```json -{ - "id": {"S": "user123"}, - "age": {"N": "30"} -} -``` - -Encoded as: -``` -[0x02] // 2 attributes - [0x02] // name length (2) - id // name bytes - [0x01] // type tag (String) - [0x07] // value length (7) - user123 // value bytes - - [0x03] // name length (3) - age // name bytes - [0x02] // type tag (Number) - [0x02] // value length (2) - 30 // value bytes (stored as string) -``` - -## Module Structure - -``` -jormundb/ -├── main.odin # Entry point, HTTP server -├── rocksdb/ # RocksDB C FFI bindings -│ └── rocksdb.odin # db_open, db_put, db_get, etc. -├── dynamodb/ # DynamoDB protocol implementation -│ ├── types.odin # Core types (Attribute_Value, Item, Key, etc.) -│ ├── json.odin # DynamoDB JSON parsing/serialization -│ ├── storage.odin # Storage engine (CRUD, scan, query) -│ └── handler.odin # HTTP request handlers -├── key_codec/ # Binary key encoding -│ └── key_codec.odin # build_data_key, decode_data_key, etc. -└── item_codec/ # Binary TLV item encoding - └── item_codec.odin # encode, decode -``` - -## Request Flow - -``` -1. HTTP POST / arrives - ↓ -2. Create arena allocator (4MB) - Set context.allocator = arena_allocator - ↓ -3. Parse HTTP headers - Extract X-Amz-Target → Operation - ↓ -4. Parse JSON body - Convert DynamoDB JSON → internal types - ↓ -5. Route to handler (e.g., handle_put_item) - ↓ -6. Storage engine operation - - Build binary key - - Encode item to TLV - - RocksDB put/get/delete - ↓ -7. Build response - - Serialize item to DynamoDB JSON - - Format HTTP response - ↓ -8. Send response - ↓ -9. Destroy arena - All request memory freed automatically -``` - -## Concurrency Model - -### Table-Level RW Locks - -Each table has a reader-writer lock: - -```odin -Storage_Engine :: struct { - db: rocksdb.DB, - table_locks: map[string]^sync.RW_Mutex, - table_locks_mutex: sync.Mutex, -} -``` - -**Read Operations** (GetItem, Query, Scan): -- Acquire shared lock -- Multiple readers can run concurrently -- Writers are blocked - -**Write Operations** (PutItem, DeleteItem, UpdateItem): -- Acquire exclusive lock -- Only one writer at a time -- All readers are blocked - -### Thread Safety - -- RocksDB handles are thread-safe (column family-based) -- Table metadata is protected by locks -- Request arenas are thread-local (no sharing) - -## Error Handling - -Odin uses explicit error returns via `or_return`: - -```odin -// Odin error handling -parse_json :: proc(data: []byte) -> (Item, bool) { - parsed := json.parse(data) or_return - item := json_to_item(parsed) or_return - return item, true -} - -// Usage -item := parse_json(request.body) or_else { - return error_response(.ValidationException, "Invalid JSON") -} -``` - -No exceptions, no panic-recover patterns. Every error path is explicit. - -## DynamoDB Wire Protocol - -### Request Format - -``` -POST / HTTP/1.1 -X-Amz-Target: DynamoDB_20120810.PutItem -Content-Type: application/x-amz-json-1.0 - -{ - "TableName": "Users", - "Item": { - "id": {"S": "user123"}, - "name": {"S": "Alice"} - } -} -``` - -### Response Format - -``` -HTTP/1.1 200 OK -Content-Type: application/x-amz-json-1.0 -x-amzn-RequestId: local-request-id - -{} -``` - -### Error Format - -```json -{ - "__type": "com.amazonaws.dynamodb.v20120810#ResourceNotFoundException", - "message": "Table not found" -} -``` - -## Performance Characteristics - -### Time Complexity - -| Operation | Complexity | Notes | -|-----------|-----------|-------| -| PutItem | O(log n) | RocksDB LSM tree insert | -| GetItem | O(log n) | RocksDB point lookup | -| DeleteItem | O(log n) | RocksDB deletion | -| Query | O(log n + m) | n = items in table, m = result set | -| Scan | O(n) | Full table scan | - -### Space Complexity - -- Binary keys: ~20-100 bytes (vs 50-200 bytes JSON) -- Binary items: ~30% smaller than JSON -- Varint encoding saves space on small integers - -### Benchmarks (Expected) - -Based on Zig version performance: - -``` -Operation Throughput Latency (p50) -PutItem ~5,000/sec ~0.2ms -GetItem ~7,000/sec ~0.14ms -Query (1 item) ~8,000/sec ~0.12ms -Scan (1000 items) ~20/sec ~50ms -``` - -## Future Enhancements - -### Planned Features - -1. **UpdateExpression** - SET/REMOVE/ADD/DELETE operations -2. **FilterExpression** - Post-query filtering -3. **ProjectionExpression** - Return subset of attributes -4. **Global Secondary Indexes** - Query by non-key attributes -5. **Local Secondary Indexes** - Alternate sort keys -6. **BatchWriteItem** - Batch mutations -7. **BatchGetItem** - Batch reads -8. **Transactions** - ACID multi-item operations - -### Optimization Opportunities - -1. **Connection pooling** - Reuse HTTP connections -2. **Bloom filters** - Faster negative lookups -3. **Compression** - LZ4/Zstd on large items -4. **Caching layer** - Hot item cache -5. **Parallel scan** - Segment-based scanning - -## Debugging - -### Enable Verbose Logging - -```bash -make run VERBOSE=1 -``` - -### Inspect RocksDB - -```bash -# Use ldb tool to inspect database -ldb --db=./data scan -ldb --db=./data get -``` - -### Memory Profiling - -Odin's tracking allocator can detect leaks: - -```odin -when ODIN_DEBUG { - track: mem.Tracking_Allocator - mem.tracking_allocator_init(&track, context.allocator) - context.allocator = mem.tracking_allocator(&track) - - defer { - for _, leak in track.allocation_map { - fmt.printfln("Leaked %d bytes at %p", leak.size, leak.location) - } - } -} -``` - -## Migration from Zig Version - -The Zig version (ZynamoDB) used the same binary storage format, so existing RocksDB databases can be read by JormunDB without migration. - -### Compatibility - -- ✅ Binary key format (byte-compatible) -- ✅ Binary item format (byte-compatible) -- ✅ Table metadata (JSON, compatible) -- ✅ HTTP wire protocol (identical) - -### Breaking Changes - -None - JormunDB can open ZynamoDB databases directly. - ---- - -## Contributing - -When contributing to JormunDB: - -1. **Use the context allocator** - All request-scoped allocations should use `context.allocator` -2. **Avoid manual frees** - Let the arena handle it -3. **Long-lived data** - Use the default allocator explicitly -4. **Test thoroughly** - Run `make test` before committing -5. **Format code** - Run `make fmt` before committing - -## References - -- [Odin Language](https://odin-lang.org/) -- [RocksDB Wiki](https://github.com/facebook/rocksdb/wiki) -- [DynamoDB API Reference](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/) -- [Varint Encoding](https://developers.google.com/protocol-buffers/docs/encoding#varints) - - -================================================================================ -FILE: ./dynamodb/types.odin -================================================================================ - -package dynamodb - -import "core:fmt" -import "core:strings" - -// DynamoDB AttributeValue - the core data type -Attribute_Value :: union { - String, // S - Number, // N (stored as string) - Binary, // B (base64) - Bool, // BOOL - Null, // NULL - String_Set, // SS - Number_Set, // NS - Binary_Set, // BS - List, // L - Map, // M -} - -String :: distinct string -Number :: distinct string -Binary :: distinct string -Bool :: distinct bool -Null :: distinct bool - -String_Set :: distinct []string -Number_Set :: distinct []string -Binary_Set :: distinct []string -List :: distinct []Attribute_Value -Map :: distinct map[string]Attribute_Value - -// Item is a map of attribute names to values -Item :: map[string]Attribute_Value - -// Key represents a DynamoDB key (partition key + optional sort key) -Key :: struct { - pk: Attribute_Value, - sk: Maybe(Attribute_Value), -} - -// Free a key -key_destroy :: proc(key: ^Key) { - attr_value_destroy(&key.pk) - if sk, ok := key.sk.?; ok { - sk_copy := sk - attr_value_destroy(&sk_copy) - } -} - -// Extract key from item based on key schema -key_from_item :: proc(item: Item, key_schema: []Key_Schema_Element) -> (Key, bool) { - pk_value: Attribute_Value - sk_value: Maybe(Attribute_Value) - - for schema_elem in key_schema { - attr, ok := item[schema_elem.attribute_name] - if !ok { - return {}, false - } - - // Validate that key is a scalar type (S, N, or B) - #partial switch _ in attr { - case String, Number, Binary: - // Valid key type - case: - return {}, false - } - - // Deep copy the attribute value - copied := attr_value_deep_copy(attr) - - switch schema_elem.key_type { - case .HASH: - pk_value = copied - case .RANGE: - sk_value = copied - } - } - - return Key{pk = pk_value, sk = sk_value}, true -} - -// Convert key to item -key_to_item :: proc(key: Key, key_schema: []Key_Schema_Element) -> Item { - item := make(Item) - - for schema_elem in key_schema { - attr_value: Attribute_Value - - switch schema_elem.key_type { - case .HASH: - attr_value = key.pk - case .RANGE: - if sk, ok := key.sk.?; ok { - attr_value = sk - } else { - continue - } - } - - item[schema_elem.attribute_name] = attr_value_deep_copy(attr_value) - } - - return item -} - -// Extract raw byte values from key -Key_Values :: struct { - pk: []byte, - sk: Maybe([]byte), -} - -key_get_values :: proc(key: ^Key) -> (Key_Values, bool) { - pk_bytes: []byte - - switch v in key.pk { - case String: - pk_bytes = transmute([]byte)string(v) - case Number: - pk_bytes = transmute([]byte)string(v) - case Binary: - pk_bytes = transmute([]byte)string(v) - case: - return {}, false - } - - sk_bytes: Maybe([]byte) - if sk, ok := key.sk.?; ok { - switch v in sk { - case String: - sk_bytes = transmute([]byte)string(v) - case Number: - sk_bytes = transmute([]byte)string(v) - case Binary: - sk_bytes = transmute([]byte)string(v) - case: - return {}, false - } - } - - return Key_Values{pk = pk_bytes, sk = sk_bytes}, true -} - -// Key type -Key_Type :: enum { - HASH, - RANGE, -} - -key_type_to_string :: proc(kt: Key_Type) -> string { - switch kt { - case .HASH: return "HASH" - case .RANGE: return "RANGE" - } - return "HASH" -} - -key_type_from_string :: proc(s: string) -> (Key_Type, bool) { - switch s { - case "HASH": return .HASH, true - case "RANGE": return .RANGE, true - } - return .HASH, false -} - -// Scalar attribute type -Scalar_Attribute_Type :: enum { - S, // String - N, // Number - B, // Binary -} - -scalar_type_to_string :: proc(t: Scalar_Attribute_Type) -> string { - switch t { - case .S: return "S" - case .N: return "N" - case .B: return "B" - } - return "S" -} - -scalar_type_from_string :: proc(s: string) -> (Scalar_Attribute_Type, bool) { - switch s { - case "S": return .S, true - case "N": return .N, true - case "B": return .B, true - } - return .S, false -} - -// Key schema element -Key_Schema_Element :: struct { - attribute_name: string, - key_type: Key_Type, -} - -// Attribute definition -Attribute_Definition :: struct { - attribute_name: string, - attribute_type: Scalar_Attribute_Type, -} - -// Projection type for indexes -Projection_Type :: enum { - ALL, - KEYS_ONLY, - INCLUDE, -} - -// Projection -Projection :: struct { - projection_type: Projection_Type, - non_key_attributes: Maybe([]string), -} - -// Global secondary index -Global_Secondary_Index :: struct { - index_name: string, - key_schema: []Key_Schema_Element, - projection: Projection, -} - -// Local secondary index -Local_Secondary_Index :: struct { - index_name: string, - key_schema: []Key_Schema_Element, - projection: Projection, -} - -// Table status -Table_Status :: enum { - CREATING, - UPDATING, - DELETING, - ACTIVE, - INACCESSIBLE_ENCRYPTION_CREDENTIALS, - ARCHIVING, - ARCHIVED, -} - -table_status_to_string :: proc(status: Table_Status) -> string { - switch status { - case .CREATING: return "CREATING" - case .UPDATING: return "UPDATING" - case .DELETING: return "DELETING" - case .ACTIVE: return "ACTIVE" - case .INACCESSIBLE_ENCRYPTION_CREDENTIALS: return "INACCESSIBLE_ENCRYPTION_CREDENTIALS" - case .ARCHIVING: return "ARCHIVING" - case .ARCHIVED: return "ARCHIVED" - } - return "ACTIVE" -} - -// Table description -Table_Description :: struct { - table_name: string, - key_schema: []Key_Schema_Element, - attribute_definitions: []Attribute_Definition, - table_status: Table_Status, - creation_date_time: i64, - item_count: u64, - table_size_bytes: u64, - global_secondary_indexes: Maybe([]Global_Secondary_Index), - local_secondary_indexes: Maybe([]Local_Secondary_Index), -} - -// DynamoDB operation types -Operation :: enum { - CreateTable, - DeleteTable, - DescribeTable, - ListTables, - UpdateTable, - PutItem, - GetItem, - DeleteItem, - UpdateItem, - Query, - Scan, - BatchGetItem, - BatchWriteItem, - TransactGetItems, - TransactWriteItems, - Unknown, -} - -operation_from_target :: proc(target: string) -> Operation { - prefix :: "DynamoDB_20120810." - if !strings.has_prefix(target, prefix) { - return .Unknown - } - - op_name := target[len(prefix):] - - switch op_name { - case "CreateTable": return .CreateTable - case "DeleteTable": return .DeleteTable - case "DescribeTable": return .DescribeTable - case "ListTables": return .ListTables - case "UpdateTable": return .UpdateTable - case "PutItem": return .PutItem - case "GetItem": return .GetItem - case "DeleteItem": return .DeleteItem - case "UpdateItem": return .UpdateItem - case "Query": return .Query - case "Scan": return .Scan - case "BatchGetItem": return .BatchGetItem - case "BatchWriteItem": return .BatchWriteItem - case "TransactGetItems": return .TransactGetItems - case "TransactWriteItems": return .TransactWriteItems - } - - return .Unknown -} - -// DynamoDB error types -DynamoDB_Error_Type :: enum { - ValidationException, - ResourceNotFoundException, - ResourceInUseException, - ConditionalCheckFailedException, - ProvisionedThroughputExceededException, - ItemCollectionSizeLimitExceededException, - InternalServerError, - SerializationException, -} - -error_to_response :: proc(err_type: DynamoDB_Error_Type, message: string) -> string { - type_str: string - - switch err_type { - case .ValidationException: - type_str = "com.amazonaws.dynamodb.v20120810#ValidationException" - case .ResourceNotFoundException: - type_str = "com.amazonaws.dynamodb.v20120810#ResourceNotFoundException" - case .ResourceInUseException: - type_str = "com.amazonaws.dynamodb.v20120810#ResourceInUseException" - case .ConditionalCheckFailedException: - type_str = "com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException" - case .ProvisionedThroughputExceededException: - type_str = "com.amazonaws.dynamodb.v20120810#ProvisionedThroughputExceededException" - case .ItemCollectionSizeLimitExceededException: - type_str = "com.amazonaws.dynamodb.v20120810#ItemCollectionSizeLimitExceededException" - case .InternalServerError: - type_str = "com.amazonaws.dynamodb.v20120810#InternalServerError" - case .SerializationException: - type_str = "com.amazonaws.dynamodb.v20120810#SerializationException" - } - - return fmt.aprintf(`{{"__type":"%s","message":"%s"}}`, type_str, message) -} - -// Deep copy an attribute value -attr_value_deep_copy :: proc(attr: Attribute_Value) -> Attribute_Value { - switch v in attr { - case String: - return String(strings.clone(string(v))) - case Number: - return Number(strings.clone(string(v))) - case Binary: - return Binary(strings.clone(string(v))) - case Bool: - return v - case Null: - return v - case String_Set: - ss := make([]string, len(v)) - for s, i in v { - ss[i] = strings.clone(s) - } - return String_Set(ss) - case Number_Set: - ns := make([]string, len(v)) - for n, i in v { - ns[i] = strings.clone(n) - } - return Number_Set(ns) - case Binary_Set: - bs := make([]string, len(v)) - for b, i in v { - bs[i] = strings.clone(b) - } - return Binary_Set(bs) - case List: - list := make([]Attribute_Value, len(v)) - for item, i in v { - list[i] = attr_value_deep_copy(item) - } - return List(list) - case Map: - m := make(map[string]Attribute_Value) - for key, val in v { - m[strings.clone(key)] = attr_value_deep_copy(val) - } - return Map(m) - } - return nil -} - -// Free an attribute value -attr_value_destroy :: proc(attr: ^Attribute_Value) { - switch v in attr { - case String: - delete(string(v)) - case Number: - delete(string(v)) - case Binary: - delete(string(v)) - case String_Set: - for s in v { - delete(s) - } - delete([]string(v)) - case Number_Set: - for n in v { - delete(n) - } - delete([]string(v)) - case Binary_Set: - for b in v { - delete(b) - } - delete([]string(v)) - case List: - for item in v { - item_copy := item - attr_value_destroy(&item_copy) - } - delete([]Attribute_Value(v)) - case Map: - for key, val in v { - delete(key) - val_copy := val - attr_value_destroy(&val_copy) - } - delete(map[string]Attribute_Value(v)) - case Bool, Null: - // Nothing to free - } -} - -// Free an item -item_destroy :: proc(item: ^Item) { - for key, val in item { - delete(key) - val_copy := val - attr_value_destroy(&val_copy) - } - delete(item^) -} - - -================================================================================ -FILE: ./http.odin -================================================================================ - -package main - -import "core:fmt" -import "core:mem" -import "core:net" -import "core:strings" -import "core:strconv" - -// HTTP Method enumeration -HTTP_Method :: enum { - GET, - POST, - PUT, - DELETE, - OPTIONS, - HEAD, - PATCH, -} - -method_from_string :: proc(s: string) -> HTTP_Method { - switch s { - case "GET": return .GET - case "POST": return .POST - case "PUT": return .PUT - case "DELETE": return .DELETE - case "OPTIONS": return .OPTIONS - case "HEAD": return .HEAD - case "PATCH": return .PATCH - } - return .GET -} - -// HTTP Status codes -HTTP_Status :: enum u16 { - OK = 200, - Created = 201, - No_Content = 204, - Bad_Request = 400, - Unauthorized = 401, - Forbidden = 403, - Not_Found = 404, - Method_Not_Allowed = 405, - Conflict = 409, - Payload_Too_Large = 413, - Internal_Server_Error = 500, - Service_Unavailable = 503, -} - -// HTTP Header -HTTP_Header :: struct { - name: string, - value: string, -} - -// HTTP Request -HTTP_Request :: struct { - method: HTTP_Method, - path: string, - headers: []HTTP_Header, - body: []byte, -} - -// Get header value by name (case-insensitive) -request_get_header :: proc(req: ^HTTP_Request, name: string) -> Maybe(string) { - for header in req.headers { - if strings.equal_fold(header.name, name) { - return header.value - } - } - return nil -} - -// HTTP Response -HTTP_Response :: struct { - status: HTTP_Status, - headers: [dynamic]HTTP_Header, - body: [dynamic]byte, -} - -response_init :: proc(allocator: mem.Allocator) -> HTTP_Response { - return HTTP_Response{ - status = .OK, - headers = make([dynamic]HTTP_Header, allocator), - body = make([dynamic]byte, allocator), - } -} - -response_set_status :: proc(resp: ^HTTP_Response, status: HTTP_Status) { - resp.status = status -} - -response_add_header :: proc(resp: ^HTTP_Response, name: string, value: string) { - append(&resp.headers, HTTP_Header{name = name, value = value}) -} - -response_set_body :: proc(resp: ^HTTP_Response, data: []byte) { - clear(&resp.body) - append(&resp.body, ..data) -} - -// Request handler function type -// Takes context pointer, request, and request-scoped allocator -Request_Handler :: #type proc(ctx: rawptr, request: ^HTTP_Request, request_alloc: mem.Allocator) -> HTTP_Response - -// Server configuration -Server_Config :: struct { - max_body_size: int, // default 100MB - max_headers: int, // default 100 - read_buffer_size: int, // default 8KB - enable_keep_alive: bool, // default true - max_requests_per_connection: int, // default 1000 -} - -default_server_config :: proc() -> Server_Config { - return Server_Config{ - max_body_size = 100 * 1024 * 1024, - max_headers = 100, - read_buffer_size = 8 * 1024, - enable_keep_alive = true, - max_requests_per_connection = 1000, - } -} - -// Server -Server :: struct { - allocator: mem.Allocator, - endpoint: net.Endpoint, - handler: Request_Handler, - handler_ctx: rawptr, - config: Server_Config, - running: bool, - socket: Maybe(net.TCP_Socket), -} - -server_init :: proc( - allocator: mem.Allocator, - host: string, - port: int, - handler: Request_Handler, - handler_ctx: rawptr, - config: Server_Config, -) -> (Server, bool) { - endpoint, endpoint_ok := net.parse_endpoint(fmt.tprintf("%s:%d", host, port)) - if !endpoint_ok { - return {}, false - } - - return Server{ - allocator = allocator, - endpoint = endpoint, - handler = handler, - handler_ctx = handler_ctx, - config = config, - running = false, - socket = nil, - }, true -} - -server_start :: proc(server: ^Server) -> bool { - // Create listening socket - socket, socket_err := net.listen_tcp(server.endpoint) - if socket_err != nil { - fmt.eprintfln("Failed to create listening socket: %v", socket_err) - return false - } - - server.socket = socket - server.running = true - - fmt.printfln("HTTP server listening on %v", server.endpoint) - - // Accept loop - for server.running { - conn, source, accept_err := net.accept_tcp(socket) - if accept_err != nil { - if server.running { - fmt.eprintfln("Accept error: %v", accept_err) - } - continue - } - - // Handle connection in separate goroutine would go here - // For now, handle synchronously (should spawn thread) - handle_connection(server, conn, source) - } - - return true -} - -server_stop :: proc(server: ^Server) { - server.running = false - if sock, ok := server.socket.?; ok { - net.close(sock) - server.socket = nil - } -} - -// Handle a single connection -handle_connection :: proc(server: ^Server, conn: net.TCP_Socket, source: net.Endpoint) { - defer net.close(conn) - - request_count := 0 - - // Keep-alive loop - for request_count < server.config.max_requests_per_connection { - request_count += 1 - - // Create arena for this request (4MB) - arena: mem.Arena - mem.arena_init(&arena, make([]byte, mem.Megabyte * 4)) - defer mem.arena_destroy(&arena) - - request_alloc := mem.arena_allocator(&arena) - - // Parse request - request, parse_ok := parse_request(conn, request_alloc, server.config) - if !parse_ok { - // Connection closed or parse error - break - } - - // Call handler - response := server.handler(server.handler_ctx, &request, request_alloc) - - // Send response - send_ok := send_response(conn, &response, request_alloc) - if !send_ok { - break - } - - // Check keep-alive - keep_alive := request_get_header(&request, "Connection") - if ka, ok := keep_alive.?; ok { - if !strings.equal_fold(ka, "keep-alive") { - break - } - } else if !server.config.enable_keep_alive { - break - } - - // Arena is automatically freed here - } -} - -// Parse HTTP request -parse_request :: proc( - conn: net.TCP_Socket, - allocator: mem.Allocator, - config: Server_Config, -) -> (HTTP_Request, bool) { - // Read request line and headers - buffer := make([]byte, config.read_buffer_size, allocator) - - bytes_read, read_err := net.recv_tcp(conn, buffer) - if read_err != nil || bytes_read == 0 { - return {}, false - } - - request_data := buffer[:bytes_read] - - // Find end of headers (\r\n\r\n) - header_end_idx := strings.index(string(request_data), "\r\n\r\n") - if header_end_idx < 0 { - return {}, false - } - - header_section := string(request_data[:header_end_idx]) - body_start := header_end_idx + 4 - - // Parse request line - lines := strings.split_lines(header_section, allocator) - if len(lines) == 0 { - return {}, false - } - - request_line := lines[0] - parts := strings.split(request_line, " ", allocator) - if len(parts) < 3 { - return {}, false - } - - method := method_from_string(parts[0]) - path := strings.clone(parts[1], allocator) - - // Parse headers - headers := make([dynamic]HTTP_Header, allocator) - for i := 1; i < len(lines); i += 1 { - line := lines[i] - if len(line) == 0 { - continue - } - - colon_idx := strings.index(line, ":") - if colon_idx < 0 { - continue - } - - name := strings.trim_space(line[:colon_idx]) - value := strings.trim_space(line[colon_idx+1:]) - - append(&headers, HTTP_Header{ - name = strings.clone(name, allocator), - value = strings.clone(value, allocator), - }) - } - - // Read body if Content-Length present - body: []byte - - content_length_header := request_get_header_from_slice(headers[:], "Content-Length") - if cl, ok := content_length_header.?; ok { - content_length := strconv.parse_int(cl) or_else 0 - - if content_length > 0 && content_length <= config.max_body_size { - // Check if we already have the body in buffer - existing_body := request_data[body_start:] - - if len(existing_body) >= content_length { - // Body already in buffer - body = make([]byte, content_length, allocator) - copy(body, existing_body[:content_length]) - } else { - // Need to read more - body = make([]byte, content_length, allocator) - copy(body, existing_body) - - remaining := content_length - len(existing_body) - bytes_read := len(existing_body) - - for remaining > 0 { - chunk_size := min(remaining, config.read_buffer_size) - chunk := make([]byte, chunk_size, allocator) - - n, err := net.recv_tcp(conn, chunk) - if err != nil || n == 0 { - return {}, false - } - - copy(body[bytes_read:], chunk[:n]) - bytes_read += n - remaining -= n - } - } - } - } - - return HTTP_Request{ - method = method, - path = path, - headers = headers[:], - body = body, - }, true -} - -// Helper to get header from slice -request_get_header_from_slice :: proc(headers: []HTTP_Header, name: string) -> Maybe(string) { - for header in headers { - if strings.equal_fold(header.name, name) { - return header.value - } - } - return nil -} - -// Send HTTP response -send_response :: proc(conn: net.TCP_Socket, resp: ^HTTP_Response, allocator: mem.Allocator) -> bool { - // Build response string - builder := strings.builder_make(allocator) - defer strings.builder_destroy(&builder) - - // Status line - strings.write_string(&builder, "HTTP/1.1 ") - strings.write_int(&builder, int(resp.status)) - strings.write_string(&builder, " ") - strings.write_string(&builder, status_text(resp.status)) - strings.write_string(&builder, "\r\n") - - // Headers - response_add_header(resp, "Content-Length", fmt.tprintf("%d", len(resp.body))) - - for header in resp.headers { - strings.write_string(&builder, header.name) - strings.write_string(&builder, ": ") - strings.write_string(&builder, header.value) - strings.write_string(&builder, "\r\n") - } - - // End of headers - strings.write_string(&builder, "\r\n") - - // Send headers - header_bytes := transmute([]byte)strings.to_string(builder) - _, send_err := net.send_tcp(conn, header_bytes) - if send_err != nil { - return false - } - - // Send body - if len(resp.body) > 0 { - _, send_err = net.send_tcp(conn, resp.body[:]) - if send_err != nil { - return false - } - } - - return true -} - -// Get status text for status code -status_text :: proc(status: HTTP_Status) -> string { - switch status { - case .OK: return "OK" - case .Created: return "Created" - case .No_Content: return "No Content" - case .Bad_Request: return "Bad Request" - case .Unauthorized: return "Unauthorized" - case .Forbidden: return "Forbidden" - case .Not_Found: return "Not Found" - case .Method_Not_Allowed: return "Method Not Allowed" - case .Conflict: return "Conflict" - case .Payload_Too_Large: return "Payload Too Large" - case .Internal_Server_Error: return "Internal Server Error" - case .Service_Unavailable: return "Service Unavailable" - } - return "Unknown" -} - - -================================================================================ -FILE: ./key_codec/key_codec.odin -================================================================================ - -package key_codec - -import "core:bytes" -import "core:encoding/varint" -import "core:mem" - -// Entity type prefix bytes for namespacing -Entity_Type :: enum u8 { - Meta = 0x01, // Table metadata - Data = 0x02, // Item data - GSI = 0x03, // Global secondary index - LSI = 0x04, // Local secondary index -} - -// Encode a varint length prefix -encode_varint :: proc(buf: ^bytes.Buffer, value: int) { - temp: [10]byte - n := varint.encode_u64(temp[:], u64(value)) - bytes.buffer_write(buf, temp[:n]) -} - -// Decode a varint length prefix -decode_varint :: proc(data: []byte, offset: ^int) -> (value: int, ok: bool) { - if offset^ >= len(data) { - return 0, false - } - - val, n := varint.decode_u64(data[offset^:]) - if n <= 0 { - return 0, false - } - - offset^ += n - return int(val), true -} - -// Build metadata key: [meta][table_name] -build_meta_key :: proc(table_name: string) -> []byte { - buf: bytes.Buffer - bytes.buffer_init_allocator(&buf, 0, 256, context.allocator) - - // Write entity type - bytes.buffer_write_byte(&buf, u8(Entity_Type.Meta)) - - // Write table name with length prefix - encode_varint(&buf, len(table_name)) - bytes.buffer_write_string(&buf, table_name) - - return bytes.buffer_to_bytes(&buf) -} - -// Build data key: [data][table_name][pk_value][sk_value?] -build_data_key :: proc(table_name: string, pk_value: []byte, sk_value: Maybe([]byte)) -> []byte { - buf: bytes.Buffer - bytes.buffer_init_allocator(&buf, 0, 512, context.allocator) - - // Write entity type - bytes.buffer_write_byte(&buf, u8(Entity_Type.Data)) - - // Write table name - encode_varint(&buf, len(table_name)) - bytes.buffer_write_string(&buf, table_name) - - // Write partition key - encode_varint(&buf, len(pk_value)) - bytes.buffer_write(&buf, pk_value) - - // Write sort key if present - if sk, ok := sk_value.?; ok { - encode_varint(&buf, len(sk)) - bytes.buffer_write(&buf, sk) - } - - return bytes.buffer_to_bytes(&buf) -} - -// Build table prefix for scanning: [data][table_name] -build_table_prefix :: proc(table_name: string) -> []byte { - buf: bytes.Buffer - bytes.buffer_init_allocator(&buf, 0, 256, context.allocator) - - // Write entity type - bytes.buffer_write_byte(&buf, u8(Entity_Type.Data)) - - // Write table name - encode_varint(&buf, len(table_name)) - bytes.buffer_write_string(&buf, table_name) - - return bytes.buffer_to_bytes(&buf) -} - -// Build partition prefix for querying: [data][table_name][pk_value] -build_partition_prefix :: proc(table_name: string, pk_value: []byte) -> []byte { - buf: bytes.Buffer - bytes.buffer_init_allocator(&buf, 0, 512, context.allocator) - - // Write entity type - bytes.buffer_write_byte(&buf, u8(Entity_Type.Data)) - - // Write table name - encode_varint(&buf, len(table_name)) - bytes.buffer_write_string(&buf, table_name) - - // Write partition key - encode_varint(&buf, len(pk_value)) - bytes.buffer_write(&buf, pk_value) - - return bytes.buffer_to_bytes(&buf) -} - -// Build GSI key: [gsi][table_name][index_name][gsi_pk][gsi_sk?] -build_gsi_key :: proc(table_name: string, index_name: string, gsi_pk: []byte, gsi_sk: Maybe([]byte)) -> []byte { - buf: bytes.Buffer - bytes.buffer_init_allocator(&buf, 0, 512, context.allocator) - - // Write entity type - bytes.buffer_write_byte(&buf, u8(Entity_Type.GSI)) - - // Write table name - encode_varint(&buf, len(table_name)) - bytes.buffer_write_string(&buf, table_name) - - // Write index name - encode_varint(&buf, len(index_name)) - bytes.buffer_write_string(&buf, index_name) - - // Write GSI partition key - encode_varint(&buf, len(gsi_pk)) - bytes.buffer_write(&buf, gsi_pk) - - // Write GSI sort key if present - if sk, ok := gsi_sk.?; ok { - encode_varint(&buf, len(sk)) - bytes.buffer_write(&buf, sk) - } - - return bytes.buffer_to_bytes(&buf) -} - -// Build LSI key: [lsi][table_name][index_name][pk][lsi_sk] -build_lsi_key :: proc(table_name: string, index_name: string, pk: []byte, lsi_sk: []byte) -> []byte { - buf: bytes.Buffer - bytes.buffer_init_allocator(&buf, 0, 512, context.allocator) - - // Write entity type - bytes.buffer_write_byte(&buf, u8(Entity_Type.LSI)) - - // Write table name - encode_varint(&buf, len(table_name)) - bytes.buffer_write_string(&buf, table_name) - - // Write index name - encode_varint(&buf, len(index_name)) - bytes.buffer_write_string(&buf, index_name) - - // Write partition key - encode_varint(&buf, len(pk)) - bytes.buffer_write(&buf, pk) - - // Write LSI sort key - encode_varint(&buf, len(lsi_sk)) - bytes.buffer_write(&buf, lsi_sk) - - return bytes.buffer_to_bytes(&buf) -} - -// Key decoder for reading binary keys -Key_Decoder :: struct { - data: []byte, - pos: int, -} - -decoder_init :: proc(data: []byte) -> Key_Decoder { - return Key_Decoder{data = data, pos = 0} -} - -decoder_read_entity_type :: proc(decoder: ^Key_Decoder) -> (Entity_Type, bool) { - if decoder.pos >= len(decoder.data) { - return .Meta, false - } - - entity_type := Entity_Type(decoder.data[decoder.pos]) - decoder.pos += 1 - return entity_type, true -} - -decoder_read_segment :: proc(decoder: ^Key_Decoder) -> (segment: []byte, ok: bool) { - // Read length - length := decode_varint(decoder.data, &decoder.pos) or_return - - // Read data - if decoder.pos + length > len(decoder.data) { - return nil, false - } - - // Return slice (owned by caller via context.allocator) - segment = make([]byte, length, context.allocator) - copy(segment, decoder.data[decoder.pos:decoder.pos + length]) - decoder.pos += length - - return segment, true -} - -decoder_read_segment_borrowed :: proc(decoder: ^Key_Decoder) -> (segment: []byte, ok: bool) { - // Read length - length := decode_varint(decoder.data, &decoder.pos) or_return - - // Return borrowed slice - if decoder.pos + length > len(decoder.data) { - return nil, false - } - - segment = decoder.data[decoder.pos:decoder.pos + length] - decoder.pos += length - - return segment, true -} - -decoder_has_more :: proc(decoder: ^Key_Decoder) -> bool { - return decoder.pos < len(decoder.data) -} - -// Decode a data key back into components -Decoded_Data_Key :: struct { - table_name: string, - pk_value: []byte, - sk_value: Maybe([]byte), -} - -decode_data_key :: proc(key: []byte) -> (result: Decoded_Data_Key, ok: bool) { - decoder := decoder_init(key) - - // Read and verify entity type - entity_type := decoder_read_entity_type(&decoder) or_return - if entity_type != .Data { - return {}, false - } - - // Read table name - table_name_bytes := decoder_read_segment(&decoder) or_return - result.table_name = string(table_name_bytes) - - // Read partition key - result.pk_value = decoder_read_segment(&decoder) or_return - - // Read sort key if present - if decoder_has_more(&decoder) { - sk := decoder_read_segment(&decoder) or_return - result.sk_value = sk - } - - return result, true -} - - -================================================================================ -FILE: ./main.odin -================================================================================ - -package main - -import "core:fmt" -import "core:mem" -import "core:os" -import "core:strconv" -import "core:strings" -import "rocksdb" - -Config :: struct { - host: string, - port: int, - data_dir: string, - verbose: bool, -} - -main :: proc() { - // Parse configuration - config := parse_config() - - // Print banner - print_banner(config) - - // Create data directory - os.make_directory(config.data_dir) - - // Initialize storage engine - db, err := rocksdb.db_open(config.data_dir, true) - if err != .None { - fmt.eprintln("Failed to initialize storage:", err) - os.exit(1) - } - defer rocksdb.db_close(&db) - - fmt.printfln("Storage engine initialized at %s", config.data_dir) - fmt.printfln("Starting DynamoDB-compatible server on %s:%d", config.host, config.port) - - // Create HTTP server - server_config := default_server_config() - - // For now, use a simple echo handler until we implement the full DynamoDB handler - server, server_ok := server_init( - context.allocator, - config.host, - config.port, - handle_http_request, - &db, - server_config, - ) - - if !server_ok { - fmt.eprintln("Failed to initialize HTTP server") - os.exit(1) - } - defer server_stop(&server) - - fmt.println("Ready to accept connections!") - - // Start server (blocks) - if !server_start(&server) { - fmt.eprintln("Server failed to start") - os.exit(1) - } -} - -// Temporary HTTP request handler -// TODO: Replace with full DynamoDB handler once dynamodb/handler.odin is implemented -handle_http_request :: proc(ctx: rawptr, request: ^HTTP_Request, request_alloc: mem.Allocator) -> HTTP_Response { - db := cast(^rocksdb.DB)ctx - - response := response_init(request_alloc) - response_add_header(&response, "Content-Type", "application/x-amz-json-1.0") - response_add_header(&response, "x-amzn-RequestId", "local-request-id") - - // Get X-Amz-Target header - target := request_get_header(request, "X-Amz-Target") - - if t, ok := target.?; ok { - // Echo back the operation for now - body := fmt.aprintf("{{\"operation\":\"%s\",\"status\":\"not_implemented\"}}", t) - response_set_body(&response, transmute([]byte)body) - } else { - response_set_status(&response, .Bad_Request) - response_set_body(&response, transmute([]byte)string("{\"error\":\"Missing X-Amz-Target header\"}")) - } - - return response -} - -// Demonstrate arena-per-request memory management -demo_arena_pattern :: proc(db: ^rocksdb.DB) { - // Simulate handling a request - { - // Create arena for this request - arena: mem.Arena - mem.arena_init(&arena, make([]byte, mem.Megabyte * 4)) - defer mem.arena_destroy(&arena) - - // Set context allocator to arena - context.allocator = mem.arena_allocator(&arena) - - // All allocations below use the arena automatically - // No individual frees needed! - - fmt.println("Simulating request handler...") - - // Example: parse JSON, process, respond - table_name := strings.clone("Users") - key := make([]byte, 16) - value := make([]byte, 256) - - // Simulate storage operation - copy(key, "user:123") - copy(value, `{"name":"Alice","age":30}`) - - err := rocksdb.db_put(db, key, value) - if err == .None { - fmt.println("✓ Stored item using arena allocator") - } - - // Read it back - result, read_err := rocksdb.db_get(db, key) - if read_err == .None { - fmt.printf("✓ Retrieved item: %s\n", string(result)) - } - - // Everything is freed here when arena is destroyed - fmt.println("✓ Arena destroyed - all memory freed automatically") - } -} - -parse_config :: proc() -> Config { - config := Config{ - host = "0.0.0.0", - port = 8000, - data_dir = "./data", - verbose = false, - } - - // Environment variables - if port_str, ok := os.lookup_env("JORMUN_PORT"); ok { - if port, ok := strconv.parse_int(port_str); ok { - config.port = port - } - } - - if host, ok := os.lookup_env("JORMUN_HOST"); ok { - config.host = host - } - - if data_dir, ok := os.lookup_env("JORMUN_DATA_DIR"); ok { - config.data_dir = data_dir - } - - if verbose, ok := os.lookup_env("JORMUN_VERBOSE"); ok { - config.verbose = verbose == "1" - } - - // TODO: Parse command line arguments - - return config -} - -print_banner :: proc(config: Config) { - banner := ` - ╔═══════════════════════════════════════════════╗ - ║ ║ - ║ ╦╔═╗╦═╗╔╦╗╦ ╦╔╗╔╔╦╗╔╗ ║ - ║ ║║ ║╠╦╝║║║║ ║║║║ ║║╠╩╗ ║ - ║ ╚╝╚═╝╩╚═╩ ╩╚═╝╝╚╝═╩╝╚═╝ ║ - ║ ║ - ║ DynamoDB-Compatible Database ║ - ║ Powered by RocksDB + Odin ║ - ║ ║ - ╚═══════════════════════════════════════════════╝ -` - fmt.println(banner) - fmt.printfln(" Port: %d | Data Dir: %s\n", config.port, config.data_dir) -} - - -================================================================================ -FILE: ./Makefile -================================================================================ - -.PHONY: all build release run test clean fmt help install - -# Project configuration -PROJECT_NAME := jormundb -ODIN := odin -BUILD_DIR := build -SRC_DIR := . - -# RocksDB and compression libraries -ROCKSDB_LIBS := -lrocksdb -lstdc++ -lsnappy -llz4 -lzstd -lz -lbz2 - -# Platform-specific library paths -UNAME_S := $(shell uname -s) -ifeq ($(UNAME_S),Darwin) - # macOS (Homebrew) - LIB_PATH := -L/usr/local/lib -L/opt/homebrew/lib - INCLUDE_PATH := -I/usr/local/include -I/opt/homebrew/include -else ifeq ($(UNAME_S),Linux) - # Linux - LIB_PATH := -L/usr/local/lib -L/usr/lib - INCLUDE_PATH := -I/usr/local/include -endif - -# Build flags -DEBUG_FLAGS := -debug -o:none -RELEASE_FLAGS := -o:speed -disable-assert -no-bounds-check -COMMON_FLAGS := -vet -strict-style - -# Linker flags -EXTRA_LINKER_FLAGS := $(LIB_PATH) $(ROCKSDB_LIBS) - -# Runtime configuration -PORT ?= 8000 -HOST ?= 0.0.0.0 -DATA_DIR ?= ./data -VERBOSE ?= 0 - -# Colors for output -BLUE := \033[0;34m -GREEN := \033[0;32m -YELLOW := \033[0;33m -RED := \033[0;31m -NC := \033[0m # No Color - -# Default target -all: build - -# Build debug version -build: - @echo "$(BLUE)Building $(PROJECT_NAME) (debug)...$(NC)" - @mkdir -p $(BUILD_DIR) - $(ODIN) build $(SRC_DIR) \ - $(COMMON_FLAGS) \ - $(DEBUG_FLAGS) \ - -out:$(BUILD_DIR)/$(PROJECT_NAME) \ - -extra-linker-flags:"$(EXTRA_LINKER_FLAGS)" - @echo "$(GREEN)✓ Build complete: $(BUILD_DIR)/$(PROJECT_NAME)$(NC)" - -# Build optimized release version -release: - @echo "$(BLUE)Building $(PROJECT_NAME) (release)...$(NC)" - @mkdir -p $(BUILD_DIR) - $(ODIN) build $(SRC_DIR) \ - $(COMMON_FLAGS) \ - $(RELEASE_FLAGS) \ - -out:$(BUILD_DIR)/$(PROJECT_NAME) \ - -extra-linker-flags:"$(EXTRA_LINKER_FLAGS)" - @echo "$(GREEN)✓ Release build complete: $(BUILD_DIR)/$(PROJECT_NAME)$(NC)" - -# Run the server -run: build - @echo "$(BLUE)Starting $(PROJECT_NAME)...$(NC)" - @mkdir -p $(DATA_DIR) - @JORMUN_PORT=$(PORT) \ - JORMUN_HOST=$(HOST) \ - JORMUN_DATA_DIR=$(DATA_DIR) \ - JORMUN_VERBOSE=$(VERBOSE) \ - $(BUILD_DIR)/$(PROJECT_NAME) - -# Run with custom port -run-port: build - @echo "$(BLUE)Starting $(PROJECT_NAME) on port $(PORT)...$(NC)" - @mkdir -p $(DATA_DIR) - @JORMUN_PORT=$(PORT) $(BUILD_DIR)/$(PROJECT_NAME) - -# Run tests -test: - @echo "$(BLUE)Running tests...$(NC)" - $(ODIN) test $(SRC_DIR) \ - $(COMMON_FLAGS) \ - $(DEBUG_FLAGS) \ - -extra-linker-flags:"$(EXTRA_LINKER_FLAGS)" - @echo "$(GREEN)✓ Tests passed$(NC)" - -# Format code -fmt: - @echo "$(BLUE)Formatting code...$(NC)" - @find $(SRC_DIR) -name "*.odin" -exec odin-format -w {} \; - @echo "$(GREEN)✓ Code formatted$(NC)" - -# Clean build artifacts -clean: - @echo "$(YELLOW)Cleaning build artifacts...$(NC)" - @rm -rf $(BUILD_DIR) - @rm -rf $(DATA_DIR) - @echo "$(GREEN)✓ Clean complete$(NC)" - -# Install to /usr/local/bin (requires sudo) -install: release - @echo "$(BLUE)Installing $(PROJECT_NAME)...$(NC)" - @sudo cp $(BUILD_DIR)/$(PROJECT_NAME) /usr/local/bin/ - @sudo chmod +x /usr/local/bin/$(PROJECT_NAME) - @echo "$(GREEN)✓ Installed to /usr/local/bin/$(PROJECT_NAME)$(NC)" - -# Uninstall from /usr/local/bin -uninstall: - @echo "$(YELLOW)Uninstalling $(PROJECT_NAME)...$(NC)" - @sudo rm -f /usr/local/bin/$(PROJECT_NAME) - @echo "$(GREEN)✓ Uninstalled$(NC)" - -# Check dependencies -check-deps: - @echo "$(BLUE)Checking dependencies...$(NC)" - @which $(ODIN) > /dev/null || (echo "$(RED)✗ Odin compiler not found$(NC)" && exit 1) - @pkg-config --exists rocksdb || (echo "$(RED)✗ RocksDB not found$(NC)" && exit 1) - @echo "$(GREEN)✓ All dependencies found$(NC)" - -# AWS CLI test commands -aws-test: run & - @sleep 2 - @echo "$(BLUE)Testing with AWS CLI...$(NC)" - @echo "\n$(YELLOW)Creating table...$(NC)" - @aws dynamodb create-table \ - --endpoint-url http://localhost:$(PORT) \ - --table-name TestTable \ - --key-schema AttributeName=pk,KeyType=HASH \ - --attribute-definitions AttributeName=pk,AttributeType=S \ - --billing-mode PAY_PER_REQUEST || true - @echo "\n$(YELLOW)Listing tables...$(NC)" - @aws dynamodb list-tables --endpoint-url http://localhost:$(PORT) - @echo "\n$(YELLOW)Putting item...$(NC)" - @aws dynamodb put-item \ - --endpoint-url http://localhost:$(PORT) \ - --table-name TestTable \ - --item '{"pk":{"S":"test1"},"data":{"S":"hello world"}}' - @echo "\n$(YELLOW)Getting item...$(NC)" - @aws dynamodb get-item \ - --endpoint-url http://localhost:$(PORT) \ - --table-name TestTable \ - --key '{"pk":{"S":"test1"}}' - @echo "\n$(YELLOW)Scanning table...$(NC)" - @aws dynamodb scan \ - --endpoint-url http://localhost:$(PORT) \ - --table-name TestTable - @echo "\n$(GREEN)✓ AWS CLI test complete$(NC)" - -# Development workflow -dev: clean build run - -# Quick rebuild and run -quick: - @$(MAKE) build run - -# Show help -help: - @echo "$(BLUE)JormunDB - DynamoDB-compatible database$(NC)" - @echo "" - @echo "$(GREEN)Build Commands:$(NC)" - @echo " make build - Build debug version" - @echo " make release - Build optimized release version" - @echo " make clean - Remove build artifacts" - @echo "" - @echo "$(GREEN)Run Commands:$(NC)" - @echo " make run - Build and run server (default: localhost:8000)" - @echo " make run PORT=9000 - Run on custom port" - @echo " make dev - Clean, build, and run" - @echo " make quick - Fast rebuild and run" - @echo "" - @echo "$(GREEN)Test Commands:$(NC)" - @echo " make test - Run unit tests" - @echo " make aws-test - Test with AWS CLI commands" - @echo "" - @echo "$(GREEN)Utility Commands:$(NC)" - @echo " make fmt - Format source code" - @echo " make check-deps - Check for required dependencies" - @echo " make install - Install to /usr/local/bin (requires sudo)" - @echo " make uninstall - Remove from /usr/local/bin" - @echo "" - @echo "$(GREEN)Configuration:$(NC)" - @echo " PORT=$(PORT) - Server port" - @echo " HOST=$(HOST) - Bind address" - @echo " DATA_DIR=$(DATA_DIR) - RocksDB data directory" - @echo " VERBOSE=$(VERBOSE) - Enable verbose logging (0/1)" - @echo "" - @echo "$(GREEN)Examples:$(NC)" - @echo " make run PORT=9000" - @echo " make run DATA_DIR=/tmp/jormun VERBOSE=1" - @echo " make dev" - - -================================================================================ -FILE: ./ols.json -================================================================================ - -{ - "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/ols.schema.json", - "enable_document_symbols": true, - "enable_hover": true, - "enable_snippets": true -} - -================================================================================ -FILE: ./QUICKSTART.md -================================================================================ - -# JormunDB Quick Start Guide - -Get JormunDB running in 5 minutes. - -## Prerequisites - -### 1. Install Odin - -**macOS:** -```bash -# Using Homebrew -brew install odin - -# Or download from https://odin-lang.org/docs/install/ -``` - -**Ubuntu/Debian:** -```bash -# Download latest release -wget https://github.com/odin-lang/Odin/releases/latest/download/odin-ubuntu-amd64.tar.gz -tar -xzf odin-ubuntu-amd64.tar.gz -sudo mv odin /usr/local/bin/ - -# Verify -odin version -``` - -**From Source:** -```bash -git clone https://github.com/odin-lang/Odin -cd Odin -make -sudo cp odin /usr/local/bin/ -``` - -### 2. Install RocksDB - -**macOS:** -```bash -brew install rocksdb -``` - -**Ubuntu/Debian:** -```bash -sudo apt update -sudo apt install -y librocksdb-dev libsnappy-dev liblz4-dev libzstd-dev libbz2-dev -``` - -**Arch Linux:** -```bash -sudo pacman -S rocksdb -``` - -### 3. Verify Installation - -```bash -# Check Odin -odin version - -# Check RocksDB -pkg-config --libs rocksdb -# Should output: -lrocksdb -lstdc++ ... -``` - -## Building JormunDB - -### Clone and Build - -```bash -# Clone the repository -git clone https://github.com/yourusername/jormundb.git -cd jormundb - -# Build debug version -make build - -# Or build optimized release -make release -``` - -### Troubleshooting Build Issues - -**"cannot find rocksdb"** -```bash -# Check RocksDB installation -pkg-config --cflags --libs rocksdb - -# If not found, install RocksDB (see prerequisites) -``` - -**"odin: command not found"** -```bash -# Add Odin to PATH -export PATH=$PATH:/path/to/odin - -# Or install system-wide (see prerequisites) -``` - -## Running the Server - -### Basic Usage - -```bash -# Run with defaults (localhost:8000, ./data directory) -make run -``` - -You should see: -``` - ╔═══════════════════════════════════════════════╗ - ║ ║ - ║ ╦╔═╗╦═╗╔╦╗╦ ╦╔╗╔╔╦╗╔╗ ║ - ║ ║║ ║╠╦╝║║║║ ║║║║ ║║╠╩╗ ║ - ║ ╚╝╚═╝╩╚═╩ ╩╚═╝╝╚╝═╩╝╚═╝ ║ - ║ ║ - ║ DynamoDB-Compatible Database ║ - ║ Powered by RocksDB + Odin ║ - ║ ║ - ╚═══════════════════════════════════════════════╝ - - Port: 8000 | Data Dir: ./data - -Storage engine initialized at ./data -Starting DynamoDB-compatible server on 0.0.0.0:8000 -Ready to accept connections! -``` - -### Custom Configuration - -```bash -# Custom port -make run PORT=9000 - -# Custom data directory -make run DATA_DIR=/tmp/jormun - -# Enable verbose logging -make run VERBOSE=1 - -# Combine options -make run PORT=9000 DATA_DIR=/var/jormun VERBOSE=1 -``` - -### Environment Variables - -```bash -# Set via environment -export JORMUN_PORT=9000 -export JORMUN_HOST=127.0.0.1 -export JORMUN_DATA_DIR=/var/jormun -make run -``` - -## Testing with AWS CLI - -### Install AWS CLI - -**macOS:** -```bash -brew install awscli -``` - -**Ubuntu/Debian:** -```bash -sudo apt install awscli -``` - -**Verify:** -```bash -aws --version -``` - -### Configure AWS CLI (for local use) - -```bash -# Set dummy credentials (required but not checked by JormunDB) -aws configure -# AWS Access Key ID: dummy -# AWS Secret Access Key: dummy -# Default region name: us-east-1 -# Default output format: json -``` - -### Basic Operations - -**Create a Table:** -```bash -aws dynamodb create-table \ - --endpoint-url http://localhost:8000 \ - --table-name Users \ - --key-schema \ - AttributeName=id,KeyType=HASH \ - --attribute-definitions \ - AttributeName=id,AttributeType=S \ - --billing-mode PAY_PER_REQUEST -``` - -**List Tables:** -```bash -aws dynamodb list-tables --endpoint-url http://localhost:8000 -``` - -**Put an Item:** -```bash -aws dynamodb put-item \ - --endpoint-url http://localhost:8000 \ - --table-name Users \ - --item '{ - "id": {"S": "user123"}, - "name": {"S": "Alice"}, - "age": {"N": "30"}, - "email": {"S": "alice@example.com"} - }' -``` - -**Get an Item:** -```bash -aws dynamodb get-item \ - --endpoint-url http://localhost:8000 \ - --table-name Users \ - --key '{"id": {"S": "user123"}}' -``` - -**Query Items:** -```bash -aws dynamodb query \ - --endpoint-url http://localhost:8000 \ - --table-name Users \ - --key-condition-expression "id = :id" \ - --expression-attribute-values '{ - ":id": {"S": "user123"} - }' -``` - -**Scan Table:** -```bash -aws dynamodb scan \ - --endpoint-url http://localhost:8000 \ - --table-name Users -``` - -**Delete an Item:** -```bash -aws dynamodb delete-item \ - --endpoint-url http://localhost:8000 \ - --table-name Users \ - --key '{"id": {"S": "user123"}}' -``` - -**Delete a Table:** -```bash -aws dynamodb delete-table \ - --endpoint-url http://localhost:8000 \ - --table-name Users -``` - -## Testing with AWS SDK - -### Node.js Example - -```javascript -const { DynamoDBClient, PutItemCommand, GetItemCommand } = require("@aws-sdk/client-dynamodb"); - -const client = new DynamoDBClient({ - endpoint: "http://localhost:8000", - region: "us-east-1", - credentials: { - accessKeyId: "dummy", - secretAccessKey: "dummy" - } -}); - -async function test() { - // Put an item - await client.send(new PutItemCommand({ - TableName: "Users", - Item: { - id: { S: "user123" }, - name: { S: "Alice" } - } - })); - - // Get the item - const result = await client.send(new GetItemCommand({ - TableName: "Users", - Key: { id: { S: "user123" } } - })); - - console.log(result.Item); -} - -test(); -``` - -### Python Example - -```python -import boto3 - -dynamodb = boto3.client( - 'dynamodb', - endpoint_url='http://localhost:8000', - region_name='us-east-1', - aws_access_key_id='dummy', - aws_secret_access_key='dummy' -) - -# Put an item -dynamodb.put_item( - TableName='Users', - Item={ - 'id': {'S': 'user123'}, - 'name': {'S': 'Alice'} - } -) - -# Get the item -response = dynamodb.get_item( - TableName='Users', - Key={'id': {'S': 'user123'}} -) - -print(response['Item']) -``` - -## Development Workflow - -### Quick Rebuild - -```bash -# Fast rebuild and run -make quick -``` - -### Clean Start - -```bash -# Remove all build artifacts and data -make clean - -# Build and run fresh -make dev -``` - -### Running Tests - -```bash -# Run unit tests -make test - -# Run AWS CLI integration tests -make aws-test -``` - -### Code Formatting - -```bash -# Format all Odin files -make fmt -``` - -## Common Issues - -### Port Already in Use - -```bash -# Check what's using port 8000 -lsof -i :8000 - -# Use a different port -make run PORT=9000 -``` - -### Cannot Create Data Directory - -```bash -# Create with proper permissions -mkdir -p ./data -chmod 755 ./data - -# Or use a different directory -make run DATA_DIR=/tmp/jormun -``` - -### RocksDB Not Found - -```bash -# Check installation -pkg-config --libs rocksdb - -# Install if missing (see Prerequisites) -``` - -### Odin Compiler Errors - -```bash -# Check Odin version -odin version - -# Update Odin if needed -brew upgrade odin # macOS -# or download latest from odin-lang.org -``` - -## Next Steps - -- Read [ARCHITECTURE.md](ARCHITECTURE.md) for internals -- Check [TODO.md](TODO.md) for implementation status -- Browse source code in `dynamodb/`, `rocksdb/`, etc. -- Contribute! See [CONTRIBUTING.md](CONTRIBUTING.md) - -## Getting Help - -- **Issues**: https://github.com/yourusername/jormundb/issues -- **Discussions**: https://github.com/yourusername/jormundb/discussions -- **Odin Discord**: https://discord.gg/sVBPHEv - -## Benchmarking - -```bash -# Run benchmarks -make bench - -# Profile memory usage -make profile - -# Load test -ab -n 10000 -c 100 -p item.json -T application/json \ - http://localhost:8000/ -``` - -## Production Deployment - -JormunDB is designed for **local development only**. For production, use: - -- AWS DynamoDB (managed service) -- DynamoDB Accelerator (DAX) -- ScyllaDB (DynamoDB-compatible) - -## Uninstalling - -```bash -# Remove build artifacts -make clean - -# Remove installed binary (if installed) -make uninstall - -# Remove data directory -rm -rf ./data -``` - ---- - -**Happy coding! 🚀** - -For questions or issues, please open a GitHub issue or join our Discord. - - -================================================================================ -FILE: ./README.md -================================================================================ - -# JormunDB - -A high-performance, DynamoDB-compatible database server written in Odin, backed by RocksDB. - -``` - ╦╔═╗╦═╗╔╦╗╦ ╦╔╗╔╔╦╗╔╗ - ║║ ║╠╦╝║║║║ ║║║║ ║║╠╩╗ - ╚╝╚═╝╩╚═╩ ╩╚═╝╝╚╝═╩╝╚═╝ - DynamoDB-Compatible Database - Powered by RocksDB + Odin -``` - -## What is JormunDB? - -JormunDB (formerly ZynamoDB) is a local DynamoDB replacement that speaks the DynamoDB wire protocol. Point your AWS SDK or CLI at it and use it as a drop-in development database. - -**Why Odin?** The original Zig implementation suffered from explicit allocator threading—every function taking an `allocator` parameter, every allocation needing `errdefer` cleanup. Odin's implicit context allocator system eliminates this ceremony: one `context.allocator = arena_allocator` at the request handler entry and everything downstream just works. - -## Features - -- ✅ **DynamoDB Wire Protocol**: Works with AWS SDKs and CLI out of the box -- ✅ **Binary Storage**: Efficient TLV encoding for items, varint-prefixed keys -- ✅ **Arena-per-Request**: Zero explicit memory management in business logic -- ✅ **Table Operations**: CreateTable, DeleteTable, DescribeTable, ListTables -- ✅ **Item Operations**: PutItem, GetItem, DeleteItem -- ✅ **Query & Scan**: With pagination support (Limit, ExclusiveStartKey) -- ✅ **Expression Parsing**: KeyConditionExpression for Query operations -- ✅ **Persistent Storage**: RocksDB-backed with full ACID guarantees -- ✅ **Concurrency**: Table-level RW locks for safe concurrent access - -## Quick Start - -### Prerequisites - -- Odin compiler (latest) -- RocksDB development libraries -- Standard compression libraries (snappy, lz4, zstd, etc.) - -#### macOS (Homebrew) - -```bash -brew install rocksdb odin -``` - -#### Ubuntu/Debian - -```bash -sudo apt install librocksdb-dev libsnappy-dev liblz4-dev libzstd-dev libbz2-dev -# Install Odin from https://odin-lang.org/docs/install/ -``` - -### Build & Run - -```bash -# Build the server -make build - -# Run with default settings (localhost:8000, ./data directory) -make run - -# Run with custom port -make run PORT=9000 - -# Run with custom data directory -make run DATA_DIR=/tmp/jormundb -``` - -### Test with AWS CLI - -```bash -# Create a table -aws dynamodb create-table \ - --endpoint-url http://localhost:8000 \ - --table-name Users \ - --key-schema AttributeName=id,KeyType=HASH \ - --attribute-definitions AttributeName=id,AttributeType=S \ - --billing-mode PAY_PER_REQUEST - -# Put an item -aws dynamodb put-item \ - --endpoint-url http://localhost:8000 \ - --table-name Users \ - --item '{"id":{"S":"user123"},"name":{"S":"Alice"},"age":{"N":"30"}}' - -# Get an item -aws dynamodb get-item \ - --endpoint-url http://localhost:8000 \ - --table-name Users \ - --key '{"id":{"S":"user123"}}' - -# Query items -aws dynamodb query \ - --endpoint-url http://localhost:8000 \ - --table-name Users \ - --key-condition-expression "id = :id" \ - --expression-attribute-values '{":id":{"S":"user123"}}' - -# Scan table -aws dynamodb scan \ - --endpoint-url http://localhost:8000 \ - --table-name Users -``` - -## Architecture - -``` -HTTP Request (POST /) - ↓ -X-Amz-Target header → Operation routing - ↓ -JSON body → DynamoDB types - ↓ -Storage engine → RocksDB operations - ↓ -Binary encoding → Disk - ↓ -JSON response → Client -``` - -### Module Structure - -``` -jormundb/ -├── rocksdb/ - C FFI bindings to librocksdb -├── dynamodb/ - Core types and operations -│ ├── types.odin - AttributeValue, Item, Key, etc. -│ ├── json.odin - DynamoDB JSON serialization -│ ├── storage.odin - Storage engine with RocksDB -│ └── handler.odin - HTTP request handlers -├── key_codec/ - Binary key encoding (varint-prefixed) -├── item_codec/ - Binary TLV item encoding -└── main.odin - HTTP server and entry point -``` - -### Storage Format - -**Keys** (varint-length-prefixed segments): -``` -Meta: [0x01][len][table_name] -Data: [0x02][len][table_name][len][pk_value][len][sk_value]? -GSI: [0x03][len][table_name][len][index_name][len][gsi_pk][len][gsi_sk]? -LSI: [0x04][len][table_name][len][index_name][len][pk][len][lsi_sk] -``` - -**Values** (TLV binary encoding): -``` -[attr_count:varint] - [name_len:varint][name:bytes][type_tag:u8][value_encoded:bytes]... - -Type tags: - String=0x01, Number=0x02, Binary=0x03, Bool=0x04, Null=0x05 - SS=0x10, NS=0x11, BS=0x12 - List=0x20, Map=0x21 -``` - -## Memory Management - -JormunDB uses Odin's context allocator system for elegant memory management: - -```odin -// Request handler entry point -handle_request :: proc(conn: net.TCP_Socket) { - arena: mem.Arena - mem.arena_init(&arena, make([]byte, mem.Megabyte * 4)) - defer mem.arena_destroy(&arena) - - context.allocator = mem.arena_allocator(&arena) - - // Everything below uses the arena automatically - // No manual frees, no errdefer cleanup needed - request := parse_request() // Uses context.allocator - response := process(request) // Uses context.allocator - send_response(response) // Uses context.allocator - - // Arena is freed here automatically -} -``` - -Long-lived data (table metadata, locks) uses the default allocator. Request-scoped data uses the arena. - -## Development - -```bash -# Build debug version -make build - -# Build optimized release -make release - -# Run tests -make test - -# Format code -make fmt - -# Clean build artifacts -make clean - -# Run with custom settings -make run PORT=9000 DATA_DIR=/tmp/db VERBOSE=1 -``` - -## Performance - -From benchmarks on the original Zig version (Odin expected to be similar or better): - -``` -Sequential Writes | 10000 ops | 245.32 ms | 40765 ops/sec -Random Reads | 10000 ops | 312.45 ms | 32006 ops/sec -Batch Writes | 10000 ops | 89.23 ms | 112071 ops/sec -PutItem | 5000 ops | 892.34 ms | 5604 ops/sec -GetItem | 5000 ops | 678.91 ms | 7365 ops/sec -Scan (full table) | 5000 ops | 234.56 ms | 21320 ops/sec -``` - -## API Compatibility - -### Supported Operations - -- ✅ CreateTable -- ✅ DeleteTable -- ✅ DescribeTable -- ✅ ListTables -- ✅ PutItem -- ✅ GetItem -- ✅ DeleteItem -- ✅ Query (with KeyConditionExpression) -- ✅ Scan (with pagination) - -### Coming Soon - -- ⏳ UpdateItem (with UpdateExpression) -- ⏳ BatchWriteItem -- ⏳ BatchGetItem -- ⏳ Global Secondary Indexes -- ⏳ Local Secondary Indexes -- ⏳ ConditionExpression -- ⏳ FilterExpression -- ⏳ ProjectionExpression - -## Configuration - -### Environment Variables - -```bash -JORMUN_PORT=8000 # Server port -JORMUN_HOST=0.0.0.0 # Bind address -JORMUN_DATA_DIR=./data # RocksDB data directory -JORMUN_VERBOSE=1 # Enable verbose logging -``` - -### Command Line Arguments - -```bash -./jormundb --port 9000 --host 127.0.0.1 --data-dir /var/db --verbose -``` - -## Troubleshooting - -### "Cannot open RocksDB" - -Ensure RocksDB libraries are installed and the data directory is writable: - -```bash -# Check RocksDB installation -pkg-config --libs rocksdb - -# Check permissions -mkdir -p ./data -chmod 755 ./data -``` - -### "Connection refused" - -Check if the port is already in use: - -```bash -lsof -i :8000 -``` - -### "Invalid JSON" errors - -Ensure you're using the correct DynamoDB JSON format: - -```json -{ - "TableName": "Users", - "Item": { - "id": {"S": "user123"}, - "age": {"N": "30"} - } -} -``` - -## License - -MIT License - see LICENSE file for details. - -## Credits - -- Inspired by DynamoDB Local -- Built with [Odin](https://odin-lang.org/) -- Powered by [RocksDB](https://rocksdb.org/) -- Originally implemented as ZynamoDB in Zig - -## Contributing - -Contributions welcome! Please: - -1. Format code with `make fmt` -2. Run tests with `make test` -3. Update documentation as needed -4. Follow Odin idioms (context allocators, explicit returns, etc.) - ---- - -**Why "Jormun"?** Jörmungandr, the World Serpent from Norse mythology—a fitting name for something that wraps around your data. Also, it sounds cool. - - -================================================================================ -FILE: ./rocksdb/rocksdb.odin -================================================================================ - -package rocksdb - -import "core:c" -import "core:fmt" - -foreign import rocksdb "system:rocksdb" - -// RocksDB C API types -RocksDB_T :: distinct rawptr -RocksDB_Options :: distinct rawptr -RocksDB_WriteOptions :: distinct rawptr -RocksDB_ReadOptions :: distinct rawptr -RocksDB_WriteBatch :: distinct rawptr -RocksDB_Iterator :: distinct rawptr -RocksDB_FlushOptions :: distinct rawptr - -// Error type -Error :: enum { - None, - OpenFailed, - WriteFailed, - ReadFailed, - DeleteFailed, - InvalidArgument, - Corruption, - NotFound, - IOError, - Unknown, -} - -// Database handle with options -DB :: struct { - handle: RocksDB_T, - options: RocksDB_Options, - write_options: RocksDB_WriteOptions, - read_options: RocksDB_ReadOptions, -} - -// Foreign C functions -@(default_calling_convention = "c") -foreign rocksdb { - // Database operations - rocksdb_open :: proc(options: RocksDB_Options, path: cstring, errptr: ^cstring) -> RocksDB_T --- - rocksdb_close :: proc(db: RocksDB_T) --- - - // Options - rocksdb_options_create :: proc() -> RocksDB_Options --- - rocksdb_options_destroy :: proc(options: RocksDB_Options) --- - rocksdb_options_set_create_if_missing :: proc(options: RocksDB_Options, val: c.uchar) --- - rocksdb_options_increase_parallelism :: proc(options: RocksDB_Options, total_threads: c.int) --- - rocksdb_options_optimize_level_style_compaction :: proc(options: RocksDB_Options, memtable_memory_budget: c.uint64_t) --- - rocksdb_options_set_compression :: proc(options: RocksDB_Options, compression: c.int) --- - - // Write options - rocksdb_writeoptions_create :: proc() -> RocksDB_WriteOptions --- - rocksdb_writeoptions_destroy :: proc(options: RocksDB_WriteOptions) --- - - // Read options - rocksdb_readoptions_create :: proc() -> RocksDB_ReadOptions --- - rocksdb_readoptions_destroy :: proc(options: RocksDB_ReadOptions) --- - - // Put/Get/Delete - rocksdb_put :: proc(db: RocksDB_T, options: RocksDB_WriteOptions, key: [^]byte, keylen: c.size_t, val: [^]byte, vallen: c.size_t, errptr: ^cstring) --- - rocksdb_get :: proc(db: RocksDB_T, options: RocksDB_ReadOptions, key: [^]byte, keylen: c.size_t, vallen: ^c.size_t, errptr: ^cstring) -> [^]byte --- - rocksdb_delete :: proc(db: RocksDB_T, options: RocksDB_WriteOptions, key: [^]byte, keylen: c.size_t, errptr: ^cstring) --- - - // Flush - rocksdb_flushoptions_create :: proc() -> RocksDB_FlushOptions --- - rocksdb_flushoptions_destroy :: proc(options: RocksDB_FlushOptions) --- - rocksdb_flush :: proc(db: RocksDB_T, options: RocksDB_FlushOptions, errptr: ^cstring) --- - - // Write batch - rocksdb_writebatch_create :: proc() -> RocksDB_WriteBatch --- - rocksdb_writebatch_destroy :: proc(batch: RocksDB_WriteBatch) --- - rocksdb_writebatch_put :: proc(batch: RocksDB_WriteBatch, key: [^]byte, keylen: c.size_t, val: [^]byte, vallen: c.size_t) --- - rocksdb_writebatch_delete :: proc(batch: RocksDB_WriteBatch, key: [^]byte, keylen: c.size_t) --- - rocksdb_writebatch_clear :: proc(batch: RocksDB_WriteBatch) --- - rocksdb_write :: proc(db: RocksDB_T, options: RocksDB_WriteOptions, batch: RocksDB_WriteBatch, errptr: ^cstring) --- - - // Iterator - rocksdb_create_iterator :: proc(db: RocksDB_T, options: RocksDB_ReadOptions) -> RocksDB_Iterator --- - rocksdb_iter_destroy :: proc(iter: RocksDB_Iterator) --- - rocksdb_iter_seek_to_first :: proc(iter: RocksDB_Iterator) --- - rocksdb_iter_seek_to_last :: proc(iter: RocksDB_Iterator) --- - rocksdb_iter_seek :: proc(iter: RocksDB_Iterator, key: [^]byte, keylen: c.size_t) --- - rocksdb_iter_seek_for_prev :: proc(iter: RocksDB_Iterator, key: [^]byte, keylen: c.size_t) --- - rocksdb_iter_valid :: proc(iter: RocksDB_Iterator) -> c.uchar --- - rocksdb_iter_next :: proc(iter: RocksDB_Iterator) --- - rocksdb_iter_prev :: proc(iter: RocksDB_Iterator) --- - rocksdb_iter_key :: proc(iter: RocksDB_Iterator, klen: ^c.size_t) -> [^]byte --- - rocksdb_iter_value :: proc(iter: RocksDB_Iterator, vlen: ^c.size_t) -> [^]byte --- - - // Memory management - rocksdb_free :: proc(ptr: rawptr) --- -} - -// Compression types -ROCKSDB_NO_COMPRESSION :: 0 -ROCKSDB_SNAPPY_COMPRESSION :: 1 -ROCKSDB_ZLIB_COMPRESSION :: 2 -ROCKSDB_BZIP2_COMPRESSION :: 3 -ROCKSDB_LZ4_COMPRESSION :: 4 -ROCKSDB_LZ4HC_COMPRESSION :: 5 -ROCKSDB_ZSTD_COMPRESSION :: 7 - -// Open a database -db_open :: proc(path: string, create_if_missing := true) -> (DB, Error) { - options := rocksdb_options_create() - if options == nil { - return {}, .Unknown - } - - // Set create if missing - rocksdb_options_set_create_if_missing(options, create_if_missing ? 1 : 0) - - // Performance optimizations - rocksdb_options_increase_parallelism(options, 4) - rocksdb_options_optimize_level_style_compaction(options, 512 * 1024 * 1024) - rocksdb_options_set_compression(options, ROCKSDB_LZ4_COMPRESSION) - - // Create write and read options - write_options := rocksdb_writeoptions_create() - if write_options == nil { - rocksdb_options_destroy(options) - return {}, .Unknown - } - - read_options := rocksdb_readoptions_create() - if read_options == nil { - rocksdb_writeoptions_destroy(write_options) - rocksdb_options_destroy(options) - return {}, .Unknown - } - - // Open database - err: cstring - path_cstr := fmt.ctprintf("%s", path) - handle := rocksdb_open(options, path_cstr, &err) - if err != nil { - defer rocksdb_free(err) - rocksdb_readoptions_destroy(read_options) - rocksdb_writeoptions_destroy(write_options) - rocksdb_options_destroy(options) - return {}, .OpenFailed - } - - return DB{ - handle = handle, - options = options, - write_options = write_options, - read_options = read_options, - }, .None -} - -// Close database -db_close :: proc(db: ^DB) { - rocksdb_readoptions_destroy(db.read_options) - rocksdb_writeoptions_destroy(db.write_options) - rocksdb_close(db.handle) - rocksdb_options_destroy(db.options) -} - -// Put key-value pair -db_put :: proc(db: ^DB, key: []byte, value: []byte) -> Error { - err: cstring - rocksdb_put( - db.handle, - db.write_options, - raw_data(key), - c.size_t(len(key)), - raw_data(value), - c.size_t(len(value)), - &err, - ) - if err != nil { - defer rocksdb_free(err) - return .WriteFailed - } - return .None -} - -// Get value by key (returns owned slice - caller must free) -db_get :: proc(db: ^DB, key: []byte) -> (value: []byte, err: Error) { - errptr: cstring - value_len: c.size_t - - value_ptr := rocksdb_get( - db.handle, - db.read_options, - raw_data(key), - c.size_t(len(key)), - &value_len, - &errptr, - ) - - if errptr != nil { - defer rocksdb_free(errptr) - return nil, .ReadFailed - } - - if value_ptr == nil { - return nil, .NotFound - } - - // Copy the data and free RocksDB's buffer - result := make([]byte, value_len, context.allocator) - copy(result, value_ptr[:value_len]) - rocksdb_free(value_ptr) - - return result, .None -} - -// Delete key -db_delete :: proc(db: ^DB, key: []byte) -> Error { - err: cstring - rocksdb_delete( - db.handle, - db.write_options, - raw_data(key), - c.size_t(len(key)), - &err, - ) - if err != nil { - defer rocksdb_free(err) - return .DeleteFailed - } - return .None -} - -// Flush database -db_flush :: proc(db: ^DB) -> Error { - flush_opts := rocksdb_flushoptions_create() - if flush_opts == nil { - return .Unknown - } - defer rocksdb_flushoptions_destroy(flush_opts) - - err: cstring - rocksdb_flush(db.handle, flush_opts, &err) - if err != nil { - defer rocksdb_free(err) - return .IOError - } - return .None -} - -// Write batch -WriteBatch :: struct { - handle: RocksDB_WriteBatch, -} - -// Create write batch -batch_create :: proc() -> (WriteBatch, Error) { - handle := rocksdb_writebatch_create() - if handle == nil { - return {}, .Unknown - } - return WriteBatch{handle = handle}, .None -} - -// Destroy write batch -batch_destroy :: proc(batch: ^WriteBatch) { - rocksdb_writebatch_destroy(batch.handle) -} - -// Add put operation to batch -batch_put :: proc(batch: ^WriteBatch, key: []byte, value: []byte) { - rocksdb_writebatch_put( - batch.handle, - raw_data(key), - c.size_t(len(key)), - raw_data(value), - c.size_t(len(value)), - ) -} - -// Add delete operation to batch -batch_delete :: proc(batch: ^WriteBatch, key: []byte) { - rocksdb_writebatch_delete( - batch.handle, - raw_data(key), - c.size_t(len(key)), - ) -} - -// Clear batch -batch_clear :: proc(batch: ^WriteBatch) { - rocksdb_writebatch_clear(batch.handle) -} - -// Write batch to database -batch_write :: proc(db: ^DB, batch: ^WriteBatch) -> Error { - err: cstring - rocksdb_write(db.handle, db.write_options, batch.handle, &err) - if err != nil { - defer rocksdb_free(err) - return .WriteFailed - } - return .None -} - -// Iterator -Iterator :: struct { - handle: RocksDB_Iterator, -} - -// Create iterator -iter_create :: proc(db: ^DB) -> (Iterator, Error) { - handle := rocksdb_create_iterator(db.handle, db.read_options) - if handle == nil { - return {}, .Unknown - } - return Iterator{handle = handle}, .None -} - -// Destroy iterator -iter_destroy :: proc(iter: ^Iterator) { - rocksdb_iter_destroy(iter.handle) -} - -// Seek to first -iter_seek_to_first :: proc(iter: ^Iterator) { - rocksdb_iter_seek_to_first(iter.handle) -} - -// Seek to last -iter_seek_to_last :: proc(iter: ^Iterator) { - rocksdb_iter_seek_to_last(iter.handle) -} - -// Seek to key -iter_seek :: proc(iter: ^Iterator, target: []byte) { - rocksdb_iter_seek(iter.handle, raw_data(target), c.size_t(len(target))) -} - -// Check if iterator is valid -iter_valid :: proc(iter: ^Iterator) -> bool { - return rocksdb_iter_valid(iter.handle) != 0 -} - -// Move to next -iter_next :: proc(iter: ^Iterator) { - rocksdb_iter_next(iter.handle) -} - -// Move to previous -iter_prev :: proc(iter: ^Iterator) { - rocksdb_iter_prev(iter.handle) -} - -// Get current key (returns borrowed slice) -iter_key :: proc(iter: ^Iterator) -> []byte { - klen: c.size_t - key_ptr := rocksdb_iter_key(iter.handle, &klen) - if key_ptr == nil { - return nil - } - return key_ptr[:klen] -} - -// Get current value (returns borrowed slice) -iter_value :: proc(iter: ^Iterator) -> []byte { - vlen: c.size_t - value_ptr := rocksdb_iter_value(iter.handle, &vlen) - if value_ptr == nil { - return nil - } - return value_ptr[:vlen] -} - - -================================================================================ -FILE: ./TODO.md -================================================================================ - -# JormunDB Implementation TODO - -This tracks the rewrite from Zig to Odin and remaining features. - -## ✅ Completed - -- [x] Project structure -- [x] Makefile with build/run/test targets -- [x] README with usage instructions -- [x] ARCHITECTURE documentation -- [x] RocksDB FFI bindings (rocksdb/rocksdb.odin) -- [x] Core types (dynamodb/types.odin) -- [x] Key codec with varint encoding (key_codec/key_codec.odin) -- [x] Main entry point with arena pattern demo -- [x] LICENSE file -- [x] .gitignore - -## 🚧 In Progress (Need to Complete) - -### Core Modules - -- [ ] **dynamodb/json.odin** - DynamoDB JSON parsing and serialization - - Parse `{"S": "value"}` format - - Serialize AttributeValue to DynamoDB JSON - - Parse request bodies (PutItem, GetItem, etc.) - -- [ ] **item_codec/item_codec.odin** - Binary TLV encoding for items - - Encode Item to binary TLV format - - Decode binary TLV back to Item - - Type tag handling for all DynamoDB types - -- [ ] **dynamodb/storage.odin** - Storage engine with RocksDB - - Table metadata management - - create_table, delete_table, describe_table, list_tables - - put_item, get_item, delete_item - - scan, query with pagination - - 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 implementation** - - Accept TCP connections - - Parse HTTP POST requests - - Read JSON bodies - - Send HTTP responses with headers - - Keep-alive support - - Options: - - Use `core:net` directly - - Use C FFI with libmicrohttpd - - Use Odin's vendor:microui (if suitable) - -### Expression Parsers (Priority 3) - -- [ ] **KeyConditionExpression parser** - - Tokenizer for expressions - - Parse `pk = :pk AND sk > :sk` - - Support begins_with, BETWEEN - - ExpressionAttributeNames/Values - -- [ ] **UpdateExpression parser** (later) - - SET operations - - REMOVE operations - - ADD operations - - DELETE operations - -## 📋 Testing - -- [ ] Unit tests for key_codec -- [ ] Unit tests for item_codec -- [ ] Unit tests for JSON parsing -- [ ] Integration tests with AWS CLI -- [ ] Benchmark suite - -## 🔧 Build & Tooling - -- [ ] Verify Makefile works on macOS -- [ ] Verify Makefile works on Linux -- [ ] Add Docker support (optional) -- [ ] Add install script - -## 📚 Documentation - -- [ ] Code comments for public APIs -- [ ] Usage examples in README -- [ ] API compatibility matrix -- [ ] Performance tuning guide - -## 🎯 Priority Order - -1. **HTTP Server** - Need this to accept requests -2. **JSON Parsing** - Need this to understand DynamoDB format -3. **Storage Engine** - Core CRUD operations -4. **Handlers** - Wire everything together -5. **Item Codec** - Efficient binary storage -6. **Expression Parsers** - Query functionality - -## 📝 Notes - -### Zig → Odin Translation Patterns - -**Memory Management:** -```zig -// Zig -const item = try allocator.create(Item); -defer allocator.destroy(item); -``` -```odin -// Odin -item := new(Item) -// No defer needed if using arena -``` - -**Error Handling:** -```zig -// Zig -fn foo() !Result { - return error.Failed; -} -const x = try foo(); -``` -```odin -// Odin -foo :: proc() -> (Result, bool) { - return {}, false -} -x := foo() or_return -``` - -**Slices:** -```zig -// Zig -const slice: []const u8 = data; -``` -```odin -// Odin -slice: []byte = data -``` - -**Maps:** -```zig -// Zig -var map = std.StringHashMap(Value).init(allocator); -defer map.deinit(); -``` -```odin -// Odin -map := make(map[string]Value) -defer delete(map) -``` - -### Key Decisions - -1. **Use `Maybe(T)` instead of `?T`** - Odin's optional type -2. **Use `or_return` instead of `try`** - Odin's error propagation -3. **Use `context.allocator`** - Implicit allocator from context -4. **Use `#partial switch`** - For union type checking -5. **Use `transmute`** - For zero-cost type conversions - -### Reference Zig Files - -When implementing, reference these Zig files: -- `src/dynamodb/json.zig` - 400 lines, DynamoDB JSON format -- `src/dynamodb/storage.zig` - 460 lines, storage engine -- `src/dynamodb/handler.zig` - 500+ lines, request handlers -- `src/item_codec.zig` - 350 lines, TLV encoding -- `src/http.zig` - 250 lines, HTTP server - -### Quick Test Commands - -```bash -# Build and test -make build -make test - -# Run server -make run - -# Test with AWS CLI -aws dynamodb list-tables --endpoint-url http://localhost:8000 -``` - -