Files
jormun-db/project_context.txt
2026-02-15 08:55:22 -05:00

3338 lines
80 KiB
Plaintext

# 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 <key_hex>
```
### 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
```