130 lines
3.5 KiB
Bash
130 lines
3.5 KiB
Bash
#!/bin/bash
|
|
set -e
|
|
|
|
DUCKDB_PORT="${DUCKDB_PORT:-8432}"
|
|
DUCKDB_DATABASE="${DUCKDB_DATABASE:-:memory:}"
|
|
DUCKDB_DATA_DIR="${DUCKDB_DATA_DIR:-/data/duckdb}"
|
|
DUCKDB_READ_ONLY="${DUCKDB_READ_ONLY:-false}"
|
|
|
|
# Create query handler script
|
|
create_handler() {
|
|
cat > /tmp/duckdb_handler.sh << 'HANDLER'
|
|
#!/bin/bash
|
|
|
|
DUCKDB_DATABASE="${DUCKDB_DATABASE:-:memory:}"
|
|
DUCKDB_READ_ONLY="${DUCKDB_READ_ONLY:-false}"
|
|
|
|
# Read HTTP request
|
|
read -r request_line
|
|
method=$(echo "$request_line" | cut -d' ' -f1)
|
|
path=$(echo "$request_line" | cut -d' ' -f2)
|
|
|
|
# Read headers
|
|
content_length=0
|
|
while read -r header; do
|
|
header=$(echo "$header" | tr -d '\r')
|
|
[ -z "$header" ] && break
|
|
if [[ "$header" =~ ^[Cc]ontent-[Ll]ength:\ *([0-9]+) ]]; then
|
|
content_length="${BASH_REMATCH[1]}"
|
|
fi
|
|
done
|
|
|
|
# Read body
|
|
body=""
|
|
if [ "$content_length" -gt 0 ]; then
|
|
body=$(head -c "$content_length")
|
|
fi
|
|
|
|
# Route request
|
|
send_response() {
|
|
local status="$1"
|
|
local content_type="$2"
|
|
local body="$3"
|
|
local body_length=${#body}
|
|
|
|
printf "HTTP/1.1 %s\r\n" "$status"
|
|
printf "Content-Type: %s\r\n" "$content_type"
|
|
printf "Content-Length: %d\r\n" "$body_length"
|
|
printf "Connection: close\r\n"
|
|
printf "\r\n"
|
|
printf "%s" "$body"
|
|
}
|
|
|
|
# Health check
|
|
if [ "$path" = "/health" ]; then
|
|
send_response "200 OK" "application/json" '{"status":"ok"}'
|
|
exit 0
|
|
fi
|
|
|
|
# Query endpoint
|
|
if [ "$path" = "/query" ] && [ "$method" = "POST" ]; then
|
|
# Extract SQL from JSON body
|
|
sql=$(echo "$body" | jq -r '.sql // empty')
|
|
|
|
if [ -z "$sql" ]; then
|
|
send_response "400 Bad Request" "application/json" '{"error":"Missing sql field"}'
|
|
exit 0
|
|
fi
|
|
|
|
# Build duckdb command
|
|
duckdb_args=()
|
|
if [ "$DUCKDB_READ_ONLY" = "true" ]; then
|
|
duckdb_args+=("-readonly")
|
|
fi
|
|
duckdb_args+=("-json")
|
|
duckdb_args+=("$DUCKDB_DATABASE")
|
|
|
|
# Execute query
|
|
start_time=$(date +%s%3N)
|
|
result=$(echo "$sql" | duckdb "${duckdb_args[@]}" 2>&1) || {
|
|
error_msg=$(echo "$result" | jq -Rs '.')
|
|
send_response "400 Bad Request" "application/json" "{\"success\":false,\"error\":$error_msg}"
|
|
exit 0
|
|
}
|
|
end_time=$(date +%s%3N)
|
|
time_ms=$((end_time - start_time))
|
|
|
|
# Parse result
|
|
if [ -z "$result" ] || [ "$result" = "[]" ]; then
|
|
send_response "200 OK" "application/json" "{\"success\":true,\"rows\":[],\"row_count\":0,\"time_ms\":$time_ms}"
|
|
else
|
|
row_count=$(echo "$result" | jq 'length')
|
|
columns=$(echo "$result" | jq -c '.[0] | keys')
|
|
rows=$(echo "$result" | jq -c '[.[] | [.[]]]')
|
|
|
|
response=$(jq -n \
|
|
--argjson columns "$columns" \
|
|
--argjson rows "$rows" \
|
|
--argjson row_count "$row_count" \
|
|
--argjson time_ms "$time_ms" \
|
|
'{success:true, columns:$columns, rows:$rows, row_count:$row_count, time_ms:$time_ms}')
|
|
|
|
send_response "200 OK" "application/json" "$response"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# Not found
|
|
send_response "404 Not Found" "application/json" '{"error":"Not found. Use POST /query"}'
|
|
HANDLER
|
|
|
|
chmod +x /tmp/duckdb_handler.sh
|
|
|
|
# Export env vars for handler
|
|
export DUCKDB_DATABASE
|
|
export DUCKDB_READ_ONLY
|
|
}
|
|
|
|
# Serve HTTP API
|
|
serve_api() {
|
|
echo "Starting DuckDB HTTP API on port $DUCKDB_PORT..."
|
|
echo "Database: $DUCKDB_DATABASE"
|
|
echo "Data directory: $DUCKDB_DATA_DIR"
|
|
[ "$DUCKDB_READ_ONLY" = "true" ] && echo "Mode: read-only"
|
|
|
|
exec socat TCP-LISTEN:$DUCKDB_PORT,reuseaddr,fork EXEC:"/tmp/duckdb_handler.sh"
|
|
}
|
|
|
|
create_handler
|
|
serve_api
|