#!/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