Compare commits
2 Commits
972e6ece5e
...
06ed6a2c97
| Author | SHA1 | Date | |
|---|---|---|---|
| 06ed6a2c97 | |||
| 2b04e29331 |
12
Makefile
12
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: all build release run test clean fmt help install
|
.PHONY: all build release run test clean fmt help install sdk_test
|
||||||
|
|
||||||
# Project configuration
|
# Project configuration
|
||||||
PROJECT_NAME := jormundb
|
PROJECT_NAME := jormundb
|
||||||
@@ -16,6 +16,8 @@ CXX := g++
|
|||||||
AR := ar
|
AR := ar
|
||||||
CXXFLAGS := -O2 -fPIC -std=c++20 $(INCLUDE_PATH)
|
CXXFLAGS := -O2 -fPIC -std=c++20 $(INCLUDE_PATH)
|
||||||
|
|
||||||
|
# name of the docker compose file for the python tests
|
||||||
|
SDK_TEST_COMPOSE := docker-compose-python-sdk-test.yaml
|
||||||
|
|
||||||
# RocksDB and compression libraries
|
# RocksDB and compression libraries
|
||||||
ROCKSDB_LIBS := -lrocksdb -lstdc++ -lsnappy -llz4 -lzstd -lz -lbz2
|
ROCKSDB_LIBS := -lrocksdb -lstdc++ -lsnappy -llz4 -lzstd -lz -lbz2
|
||||||
@@ -174,6 +176,13 @@ aws-test: run &
|
|||||||
--table-name TestTable
|
--table-name TestTable
|
||||||
@echo "\n$(GREEN)✓ AWS CLI test complete$(NC)"
|
@echo "\n$(GREEN)✓ AWS CLI test complete$(NC)"
|
||||||
|
|
||||||
|
# Python SDK integration tests (requires JormunDB running on localhost)
|
||||||
|
sdk_test:
|
||||||
|
@echo "$(BLUE)Running Python SDK tests against localhost:$(PORT)...$(NC)"
|
||||||
|
@docker compose -f $(SDK_TEST_COMPOSE) down --remove-orphans 2>/dev/null || true
|
||||||
|
@JORMUN_PORT=$(PORT) docker compose -f $(SDK_TEST_COMPOSE) run --rm --build sdk-test
|
||||||
|
@docker compose -f $(SDK_TEST_COMPOSE) down --remove-orphans 2>/dev/null || true
|
||||||
|
|
||||||
# Development workflow
|
# Development workflow
|
||||||
dev: clean build run
|
dev: clean build run
|
||||||
|
|
||||||
@@ -199,6 +208,7 @@ help:
|
|||||||
@echo "$(GREEN)Test Commands:$(NC)"
|
@echo "$(GREEN)Test Commands:$(NC)"
|
||||||
@echo " make test - Run unit tests"
|
@echo " make test - Run unit tests"
|
||||||
@echo " make aws-test - Test with AWS CLI commands"
|
@echo " make aws-test - Test with AWS CLI commands"
|
||||||
|
@echo " make sdk_test - Run Python SDK integration tests (requires running server)"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "$(GREEN)Utility Commands:$(NC)"
|
@echo "$(GREEN)Utility Commands:$(NC)"
|
||||||
@echo " make fmt - Format source code"
|
@echo " make fmt - Format source code"
|
||||||
|
|||||||
5
TODO.md
5
TODO.md
@@ -60,9 +60,10 @@ Goal: "aws cli works reliably for CreateTable/ListTables/PutItem/GetItem/DeleteI
|
|||||||
- TLV decode failure cases (corrupt bytes)
|
- TLV decode failure cases (corrupt bytes)
|
||||||
|
|
||||||
### 7) Secondary indexes
|
### 7) Secondary indexes
|
||||||
- [ ] Global Secondary Indexes (GSI)
|
- [x] Global Secondary Indexes (GSI)
|
||||||
- [ ] Local Secondary Indexes (LSI)
|
- [ ] Local Secondary Indexes (LSI)
|
||||||
- [ ] Index backfill + write-path maintenance
|
- [ ] Index backfill (existing data when GSI added to populated table)
|
||||||
|
- [x] Write-path maintenance (GSI)
|
||||||
|
|
||||||
### 8) Performance / ops
|
### 8) Performance / ops
|
||||||
- [ ] Connection reuse / keep-alive tuning
|
- [ ] Connection reuse / keep-alive tuning
|
||||||
|
|||||||
14
docker-compose-python-sdk-test.yaml
Normal file
14
docker-compose-python-sdk-test.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
sdk-test:
|
||||||
|
image: python:3.12-slim
|
||||||
|
network_mode: host
|
||||||
|
working_dir: /tests
|
||||||
|
volumes:
|
||||||
|
- ./tests/sdk:/tests
|
||||||
|
environment:
|
||||||
|
- JORMUN_ENDPOINT=http://localhost:${JORMUN_PORT:-8002}
|
||||||
|
- AWS_ACCESS_KEY_ID=local
|
||||||
|
- AWS_SECRET_ACCESS_KEY=local
|
||||||
|
- AWS_DEFAULT_REGION=us-east-1
|
||||||
|
command: >
|
||||||
|
sh -c "pip install --quiet boto3 && python test_sdk.py"
|
||||||
@@ -96,7 +96,6 @@ evaluate_condition_expression :: proc(
|
|||||||
}
|
}
|
||||||
defer {
|
defer {
|
||||||
filter_node_destroy(filter_node)
|
filter_node_destroy(filter_node)
|
||||||
free(filter_node)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is no existing item, build an empty item for evaluation.
|
// If there is no existing item, build an empty item for evaluation.
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ Sort_Key_Condition :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sort_key_condition_destroy :: proc(skc: ^Sort_Key_Condition) {
|
sort_key_condition_destroy :: proc(skc: ^Sort_Key_Condition) {
|
||||||
|
delete(skc.sk_name) // Free the cloned string
|
||||||
attr_value_destroy(&skc.value)
|
attr_value_destroy(&skc.value)
|
||||||
if v2, ok := skc.value2.?; ok {
|
if v2, ok := skc.value2.?; ok {
|
||||||
v2_copy := v2
|
v2_copy := v2
|
||||||
@@ -46,6 +47,7 @@ Key_Condition :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
key_condition_destroy :: proc(kc: ^Key_Condition) {
|
key_condition_destroy :: proc(kc: ^Key_Condition) {
|
||||||
|
delete(kc.pk_name) // Free the cloned string
|
||||||
attr_value_destroy(&kc.pk_value)
|
attr_value_destroy(&kc.pk_value)
|
||||||
if skc, ok := kc.sk_condition.?; ok {
|
if skc, ok := kc.sk_condition.?; ok {
|
||||||
skc_copy := skc
|
skc_copy := skc
|
||||||
@@ -166,7 +168,8 @@ parse_key_condition_expression :: proc(
|
|||||||
t := tokenizer_init(expression)
|
t := tokenizer_init(expression)
|
||||||
|
|
||||||
pk_name_token := next_token(&t) or_return
|
pk_name_token := next_token(&t) or_return
|
||||||
pk_name := resolve_attribute_name(pk_name_token, attribute_names) or_return
|
pk_name_unowned := resolve_attribute_name(pk_name_token, attribute_names) or_return
|
||||||
|
pk_name := strings.clone(pk_name_unowned) // Clone for safe storage
|
||||||
|
|
||||||
eq_token := next_token(&t) or_return
|
eq_token := next_token(&t) or_return
|
||||||
if eq_token != "=" {
|
if eq_token != "=" {
|
||||||
@@ -219,7 +222,8 @@ parse_sort_key_condition :: proc(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sk_name := resolve_attribute_name(first_token, attribute_names) or_return
|
sk_name_unowned := resolve_attribute_name(first_token, attribute_names) or_return
|
||||||
|
sk_name := strings.clone(sk_name_unowned) // Clone for safe storage
|
||||||
|
|
||||||
op_token := next_token(t) or_return
|
op_token := next_token(t) or_return
|
||||||
operator, op_ok := parse_operator(op_token)
|
operator, op_ok := parse_operator(op_token)
|
||||||
@@ -278,7 +282,8 @@ parse_begins_with :: proc(
|
|||||||
}
|
}
|
||||||
|
|
||||||
sk_name_token := next_token(t) or_return
|
sk_name_token := next_token(t) or_return
|
||||||
sk_name := resolve_attribute_name(sk_name_token, attribute_names) or_return
|
sk_name_unowned := resolve_attribute_name(sk_name_token, attribute_names) or_return
|
||||||
|
sk_name := strings.clone(sk_name_unowned) // Clone for safe storage
|
||||||
|
|
||||||
comma := next_token(t) or_return
|
comma := next_token(t) or_return
|
||||||
if comma != "," {
|
if comma != "," {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ parse_projection_expression :: proc(
|
|||||||
delete(result)
|
delete(result)
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
append(&result, resolved)
|
append(&result, strings.clone(resolved)) // Clone for safe storage
|
||||||
}
|
}
|
||||||
|
|
||||||
return result[:], true
|
return result[:], true
|
||||||
@@ -160,15 +160,12 @@ filter_node_destroy :: proc(node: ^Filter_Node) {
|
|||||||
|
|
||||||
if node.left != nil {
|
if node.left != nil {
|
||||||
filter_node_destroy(node.left)
|
filter_node_destroy(node.left)
|
||||||
free(node.left)
|
|
||||||
}
|
}
|
||||||
if node.right != nil {
|
if node.right != nil {
|
||||||
filter_node_destroy(node.right)
|
filter_node_destroy(node.right)
|
||||||
free(node.right)
|
|
||||||
}
|
}
|
||||||
if node.child != nil {
|
if node.child != nil {
|
||||||
filter_node_destroy(node.child)
|
filter_node_destroy(node.child)
|
||||||
free(node.child)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +205,6 @@ parse_or_expr :: proc(
|
|||||||
right, right_ok := parse_and_expr(t, names, values)
|
right, right_ok := parse_and_expr(t, names, values)
|
||||||
if !right_ok {
|
if !right_ok {
|
||||||
filter_node_destroy(left)
|
filter_node_destroy(left)
|
||||||
free(left)
|
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +244,6 @@ parse_and_expr :: proc(
|
|||||||
right, right_ok := parse_not_expr(t, names, values)
|
right, right_ok := parse_not_expr(t, names, values)
|
||||||
if !right_ok {
|
if !right_ok {
|
||||||
filter_node_destroy(left)
|
filter_node_destroy(left)
|
||||||
free(left)
|
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +307,6 @@ parse_primary_expr :: proc(
|
|||||||
rparen, rp_ok := next_token(t)
|
rparen, rp_ok := next_token(t)
|
||||||
if !rp_ok || rparen != ")" {
|
if !rp_ok || rparen != ")" {
|
||||||
filter_node_destroy(inner)
|
filter_node_destroy(inner)
|
||||||
free(inner)
|
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return inner, true
|
return inner, true
|
||||||
|
|||||||
@@ -244,7 +244,6 @@ transact_write_items :: proc(
|
|||||||
}
|
}
|
||||||
defer {
|
defer {
|
||||||
filter_node_destroy(filter_node)
|
filter_node_destroy(filter_node)
|
||||||
free(filter_node)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
eval_item: Item
|
eval_item: Item
|
||||||
|
|||||||
37
main.odin
37
main.odin
@@ -1198,6 +1198,15 @@ handle_query :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, r
|
|||||||
|
|
||||||
// Apply ProjectionExpression
|
// Apply ProjectionExpression
|
||||||
projection, has_proj := dynamodb.parse_projection_expression(request.body, attr_names)
|
projection, has_proj := dynamodb.parse_projection_expression(request.body, attr_names)
|
||||||
|
defer { // This block just frees the cloned string and projection slice
|
||||||
|
if has_proj && len(projection) > 0 {
|
||||||
|
for path in projection {
|
||||||
|
delete(path) // Free each cloned string
|
||||||
|
}
|
||||||
|
delete(projection) // Free the slice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final_items: []dynamodb.Item
|
final_items: []dynamodb.Item
|
||||||
|
|
||||||
if has_proj && len(projection) > 0 {
|
if has_proj && len(projection) > 0 {
|
||||||
@@ -1237,6 +1246,15 @@ handle_query :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, r
|
|||||||
|
|
||||||
// ---- Apply ProjectionExpression ----
|
// ---- Apply ProjectionExpression ----
|
||||||
projection, has_proj := dynamodb.parse_projection_expression(request.body, attr_names)
|
projection, has_proj := dynamodb.parse_projection_expression(request.body, attr_names)
|
||||||
|
defer { // This block just frees the cloned string and projection slice
|
||||||
|
if has_proj && len(projection) > 0 {
|
||||||
|
for path in projection {
|
||||||
|
delete(path) // Free each cloned string
|
||||||
|
}
|
||||||
|
delete(projection) // Free the slice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final_items: []dynamodb.Item
|
final_items: []dynamodb.Item
|
||||||
|
|
||||||
if has_proj && len(projection) > 0 {
|
if has_proj && len(projection) > 0 {
|
||||||
@@ -1342,6 +1360,15 @@ handle_scan :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, re
|
|||||||
scanned_count := len(result.items)
|
scanned_count := len(result.items)
|
||||||
|
|
||||||
projection, has_proj := dynamodb.parse_projection_expression(request.body, attr_names)
|
projection, has_proj := dynamodb.parse_projection_expression(request.body, attr_names)
|
||||||
|
defer { // This block just frees the cloned string and projection slice
|
||||||
|
if has_proj && len(projection) > 0 {
|
||||||
|
for path in projection {
|
||||||
|
delete(path) // Free each cloned string
|
||||||
|
}
|
||||||
|
delete(projection) // Free the slice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final_items: []dynamodb.Item
|
final_items: []dynamodb.Item
|
||||||
|
|
||||||
if has_proj && len(projection) > 0 {
|
if has_proj && len(projection) > 0 {
|
||||||
@@ -1381,6 +1408,15 @@ handle_scan :: proc(engine: ^dynamodb.Storage_Engine, request: ^HTTP_Request, re
|
|||||||
|
|
||||||
// ---- Apply ProjectionExpression ----
|
// ---- Apply ProjectionExpression ----
|
||||||
projection, has_proj := dynamodb.parse_projection_expression(request.body, attr_names)
|
projection, has_proj := dynamodb.parse_projection_expression(request.body, attr_names)
|
||||||
|
defer { // This block just frees the cloned string and projection slice
|
||||||
|
if has_proj && len(projection) > 0 {
|
||||||
|
for path in projection {
|
||||||
|
delete(path) // Free each cloned string
|
||||||
|
}
|
||||||
|
delete(projection) // Free the slice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final_items: []dynamodb.Item
|
final_items: []dynamodb.Item
|
||||||
|
|
||||||
if has_proj && len(projection) > 0 {
|
if has_proj && len(projection) > 0 {
|
||||||
@@ -1427,7 +1463,6 @@ apply_filter_to_items :: proc(
|
|||||||
}
|
}
|
||||||
defer {
|
defer {
|
||||||
dynamodb.filter_node_destroy(filter_node)
|
dynamodb.filter_node_destroy(filter_node)
|
||||||
free(filter_node)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter items
|
// Filter items
|
||||||
|
|||||||
Reference in New Issue
Block a user