mirror of
https://github.com/hanzoskill/supabase.git
synced 2026-10-30 21:05:56 +00:00
488 lines
12 KiB
Bash
488 lines
12 KiB
Bash
#!/usr/bin/env bash
|
|
# Supabase CLI - Database operations, vector search, and more
|
|
set -euo pipefail
|
|
|
|
# Check required env vars
|
|
if [[ -z "${SUPABASE_URL:-}" ]]; then
|
|
echo "Error: SUPABASE_URL not set" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "${SUPABASE_SERVICE_KEY:-}" ]]; then
|
|
echo "Error: SUPABASE_SERVICE_KEY not set" >&2
|
|
exit 1
|
|
fi
|
|
|
|
REST_URL="${SUPABASE_URL}/rest/v1"
|
|
RPC_URL="${SUPABASE_URL}/rest/v1/rpc"
|
|
|
|
usage() {
|
|
cat << 'EOF'
|
|
Usage: supabase.sh <command> [args] [options]
|
|
|
|
Commands:
|
|
query <sql> Run raw SQL query
|
|
select <table> Select from table with filters
|
|
insert <table> <json> Insert row(s)
|
|
update <table> <json> Update rows (requires --eq filter)
|
|
upsert <table> <json> Insert or update
|
|
delete <table> Delete rows (requires --eq filter)
|
|
vector-search <table> <query> Similarity search with pgvector
|
|
tables List all tables
|
|
describe <table> Show table schema
|
|
rpc <function> <json> Call RPC function
|
|
help Show this help
|
|
|
|
Select/Update/Delete Options:
|
|
--columns <cols> Columns to select (comma-separated)
|
|
--eq <col:val> Equal filter
|
|
--neq <col:val> Not equal filter
|
|
--gt <col:val> Greater than
|
|
--lt <col:val> Less than
|
|
--gte <col:val> Greater than or equal
|
|
--lte <col:val> Less than or equal
|
|
--like <col:val> Pattern match
|
|
--ilike <col:val> Case-insensitive pattern match
|
|
--is <col:val> IS (for null, true, false)
|
|
--in <col:vals> IN (comma-separated values)
|
|
--limit <n> Limit results
|
|
--offset <n> Offset results
|
|
--order <col> Order by column
|
|
--desc Descending order
|
|
|
|
Vector Search Options:
|
|
--match-fn <name> RPC function name (default: match_<table>)
|
|
--limit <n> Number of results (default: 5)
|
|
--threshold <n> Similarity threshold (default: 0.5)
|
|
|
|
Examples:
|
|
supabase.sh query "SELECT * FROM users LIMIT 5"
|
|
supabase.sh select users --eq "status:active" --limit 10
|
|
supabase.sh insert users '{"name": "John", "email": "[email protected]"}'
|
|
supabase.sh vector-search docs "authentication guide" --limit 5
|
|
EOF
|
|
}
|
|
|
|
# Make API request
|
|
api_request() {
|
|
local method="$1"
|
|
local endpoint="$2"
|
|
local data="${3:-}"
|
|
local extra_headers=("${@:4}")
|
|
|
|
local args=(
|
|
-s
|
|
-X "$method"
|
|
-H "apikey: ${SUPABASE_SERVICE_KEY}"
|
|
-H "Authorization: Bearer ${SUPABASE_SERVICE_KEY}"
|
|
-H "Content-Type: application/json"
|
|
-H "Prefer: return=representation"
|
|
)
|
|
|
|
for header in "${extra_headers[@]:-}"; do
|
|
[[ -n "$header" ]] && args+=(-H "$header")
|
|
done
|
|
|
|
if [[ -n "$data" ]]; then
|
|
args+=(-d "$data")
|
|
fi
|
|
|
|
curl "${args[@]}" "$endpoint"
|
|
}
|
|
|
|
# Run raw SQL
|
|
cmd_query() {
|
|
local sql="${1:-}"
|
|
if [[ -z "$sql" ]]; then
|
|
echo "Error: SQL query required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local response
|
|
response=$(api_request POST "${RPC_URL}/exec_sql" "{\"query\": $(printf '%s' "$sql" | jq -Rs .)}" 2>/dev/null || true)
|
|
|
|
# If exec_sql doesn't exist, try the query endpoint
|
|
if [[ "$response" == *"Could not find"* ]] || [[ -z "$response" ]]; then
|
|
# Fall back to direct query via PostgREST (limited)
|
|
echo "Note: Direct SQL requires exec_sql function. Using REST API..." >&2
|
|
echo "Create function: CREATE FUNCTION exec_sql(query text) RETURNS json AS \$\$ ... \$\$" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "$response" | jq .
|
|
}
|
|
|
|
# Build filter query string
|
|
build_filters() {
|
|
local filters=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--eq)
|
|
local col="${2%%:*}"
|
|
local val="${2#*:}"
|
|
filters+="&${col}=eq.${val}"
|
|
shift 2
|
|
;;
|
|
--neq)
|
|
local col="${2%%:*}"
|
|
local val="${2#*:}"
|
|
filters+="&${col}=neq.${val}"
|
|
shift 2
|
|
;;
|
|
--gt)
|
|
local col="${2%%:*}"
|
|
local val="${2#*:}"
|
|
filters+="&${col}=gt.${val}"
|
|
shift 2
|
|
;;
|
|
--lt)
|
|
local col="${2%%:*}"
|
|
local val="${2#*:}"
|
|
filters+="&${col}=lt.${val}"
|
|
shift 2
|
|
;;
|
|
--gte)
|
|
local col="${2%%:*}"
|
|
local val="${2#*:}"
|
|
filters+="&${col}=gte.${val}"
|
|
shift 2
|
|
;;
|
|
--lte)
|
|
local col="${2%%:*}"
|
|
local val="${2#*:}"
|
|
filters+="&${col}=lte.${val}"
|
|
shift 2
|
|
;;
|
|
--like)
|
|
local col="${2%%:*}"
|
|
local val="${2#*:}"
|
|
filters+="&${col}=like.${val}"
|
|
shift 2
|
|
;;
|
|
--ilike)
|
|
local col="${2%%:*}"
|
|
local val="${2#*:}"
|
|
filters+="&${col}=ilike.${val}"
|
|
shift 2
|
|
;;
|
|
--is)
|
|
local col="${2%%:*}"
|
|
local val="${2#*:}"
|
|
filters+="&${col}=is.${val}"
|
|
shift 2
|
|
;;
|
|
--in)
|
|
local col="${2%%:*}"
|
|
local val="${2#*:}"
|
|
filters+="&${col}=in.(${val})"
|
|
shift 2
|
|
;;
|
|
--limit)
|
|
filters+="&limit=${2}"
|
|
shift 2
|
|
;;
|
|
--offset)
|
|
filters+="&offset=${2}"
|
|
shift 2
|
|
;;
|
|
--order)
|
|
ORDER_COL="$2"
|
|
shift 2
|
|
;;
|
|
--desc)
|
|
ORDER_DESC=true
|
|
shift
|
|
;;
|
|
--columns)
|
|
SELECT_COLS="$2"
|
|
shift 2
|
|
;;
|
|
*)
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Add ordering
|
|
if [[ -n "${ORDER_COL:-}" ]]; then
|
|
if [[ "${ORDER_DESC:-false}" == true ]]; then
|
|
filters+="&order=${ORDER_COL}.desc"
|
|
else
|
|
filters+="&order=${ORDER_COL}"
|
|
fi
|
|
fi
|
|
|
|
echo "$filters"
|
|
}
|
|
|
|
# Select from table
|
|
cmd_select() {
|
|
local table="${1:-}"
|
|
shift || true
|
|
|
|
if [[ -z "$table" ]]; then
|
|
echo "Error: Table name required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
ORDER_COL=""
|
|
ORDER_DESC=false
|
|
SELECT_COLS="*"
|
|
|
|
local filters
|
|
filters=$(build_filters "$@")
|
|
|
|
local url="${REST_URL}/${table}?select=${SELECT_COLS}${filters}"
|
|
api_request GET "$url" | jq .
|
|
}
|
|
|
|
# Insert into table
|
|
cmd_insert() {
|
|
local table="${1:-}"
|
|
local data="${2:-}"
|
|
|
|
if [[ -z "$table" ]] || [[ -z "$data" ]]; then
|
|
echo "Error: Table and JSON data required" >&2
|
|
echo "Usage: supabase.sh insert <table> '<json>'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
api_request POST "${REST_URL}/${table}" "$data" | jq .
|
|
}
|
|
|
|
# Update table
|
|
cmd_update() {
|
|
local table="${1:-}"
|
|
local data="${2:-}"
|
|
shift 2 || true
|
|
|
|
if [[ -z "$table" ]] || [[ -z "$data" ]]; then
|
|
echo "Error: Table and JSON data required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
ORDER_COL=""
|
|
ORDER_DESC=false
|
|
SELECT_COLS=""
|
|
|
|
local filters
|
|
filters=$(build_filters "$@")
|
|
|
|
if [[ -z "$filters" ]]; then
|
|
echo "Error: At least one filter required for update (use --eq)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local url="${REST_URL}/${table}?${filters:1}"
|
|
api_request PATCH "$url" "$data" | jq .
|
|
}
|
|
|
|
# Upsert into table
|
|
cmd_upsert() {
|
|
local table="${1:-}"
|
|
local data="${2:-}"
|
|
|
|
if [[ -z "$table" ]] || [[ -z "$data" ]]; then
|
|
echo "Error: Table and JSON data required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
api_request POST "${REST_URL}/${table}" "$data" "Prefer: resolution=merge-duplicates" | jq .
|
|
}
|
|
|
|
# Delete from table
|
|
cmd_delete() {
|
|
local table="${1:-}"
|
|
shift || true
|
|
|
|
if [[ -z "$table" ]]; then
|
|
echo "Error: Table name required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
ORDER_COL=""
|
|
ORDER_DESC=false
|
|
SELECT_COLS=""
|
|
|
|
local filters
|
|
filters=$(build_filters "$@")
|
|
|
|
if [[ -z "$filters" ]]; then
|
|
echo "Error: At least one filter required for delete (use --eq)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local url="${REST_URL}/${table}?${filters:1}"
|
|
api_request DELETE "$url" | jq .
|
|
}
|
|
|
|
# Vector similarity search
|
|
cmd_vector_search() {
|
|
local table="${1:-}"
|
|
local query="${2:-}"
|
|
shift 2 || true
|
|
|
|
if [[ -z "$table" ]] || [[ -z "$query" ]]; then
|
|
echo "Error: Table and query required" >&2
|
|
echo "Usage: supabase.sh vector-search <table> \"<query>\" [options]" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local match_fn="match_${table}"
|
|
local limit=5
|
|
local threshold=0.5
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--match-fn)
|
|
match_fn="$2"
|
|
shift 2
|
|
;;
|
|
--limit)
|
|
limit="$2"
|
|
shift 2
|
|
;;
|
|
--threshold)
|
|
threshold="$2"
|
|
shift 2
|
|
;;
|
|
*)
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Generate embedding using OpenAI
|
|
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
|
|
echo "Error: OPENAI_API_KEY required for vector search" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local embedding
|
|
embedding=$(curl -s https://api.openai.com/v1/embeddings \
|
|
-H "Authorization: Bearer ${OPENAI_API_KEY}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"input\": $(printf '%s' "$query" | jq -Rs .), \"model\": \"text-embedding-ada-002\"}" \
|
|
| jq -c '.data[0].embedding')
|
|
|
|
if [[ "$embedding" == "null" ]] || [[ -z "$embedding" ]]; then
|
|
echo "Error: Failed to generate embedding" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Call RPC function
|
|
local params
|
|
params=$(jq -n \
|
|
--argjson embedding "$embedding" \
|
|
--argjson threshold "$threshold" \
|
|
--argjson limit "$limit" \
|
|
'{query_embedding: $embedding, match_threshold: $threshold, match_count: $limit}')
|
|
|
|
api_request POST "${RPC_URL}/${match_fn}" "$params" | jq .
|
|
}
|
|
|
|
# List tables
|
|
cmd_tables() {
|
|
# Query information_schema
|
|
local response
|
|
response=$(api_request GET "${REST_URL}/information_schema.tables?select=table_name&table_schema=eq.public" 2>/dev/null || true)
|
|
|
|
if [[ -z "$response" ]] || [[ "$response" == *"permission denied"* ]]; then
|
|
echo "Tables (from REST API):"
|
|
# Try to list from REST endpoint directly
|
|
curl -s -I -X GET "${REST_URL}/" \
|
|
-H "apikey: ${SUPABASE_SERVICE_KEY}" \
|
|
-H "Authorization: Bearer ${SUPABASE_SERVICE_KEY}" 2>/dev/null \
|
|
| grep -i "x-" | head -20
|
|
else
|
|
echo "$response" | jq -r '.[].table_name' 2>/dev/null || echo "$response"
|
|
fi
|
|
}
|
|
|
|
# Describe table
|
|
cmd_describe() {
|
|
local table="${1:-}"
|
|
|
|
if [[ -z "$table" ]]; then
|
|
echo "Error: Table name required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Get column info via HEAD request
|
|
echo "Table: $table"
|
|
echo "Columns:"
|
|
curl -s -I -X GET "${REST_URL}/${table}?limit=0" \
|
|
-H "apikey: ${SUPABASE_SERVICE_KEY}" \
|
|
-H "Authorization: Bearer ${SUPABASE_SERVICE_KEY}" 2>/dev/null \
|
|
| grep -i "content-profile\|x-" | head -10
|
|
|
|
# Try to get a sample row
|
|
echo ""
|
|
echo "Sample (1 row):"
|
|
api_request GET "${REST_URL}/${table}?limit=1" | jq .
|
|
}
|
|
|
|
# Call RPC function
|
|
cmd_rpc() {
|
|
local fn="${1:-}"
|
|
local params="${2:-"{}"}"
|
|
|
|
if [[ -z "$fn" ]]; then
|
|
echo "Error: Function name required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
api_request POST "${RPC_URL}/${fn}" "$params" | jq .
|
|
}
|
|
|
|
# Main
|
|
case "${1:-help}" in
|
|
query)
|
|
shift
|
|
cmd_query "$@"
|
|
;;
|
|
select)
|
|
shift
|
|
cmd_select "$@"
|
|
;;
|
|
insert)
|
|
shift
|
|
cmd_insert "$@"
|
|
;;
|
|
update)
|
|
shift
|
|
cmd_update "$@"
|
|
;;
|
|
upsert)
|
|
shift
|
|
cmd_upsert "$@"
|
|
;;
|
|
delete)
|
|
shift
|
|
cmd_delete "$@"
|
|
;;
|
|
vector-search|vs)
|
|
shift
|
|
cmd_vector_search "$@"
|
|
;;
|
|
tables)
|
|
cmd_tables
|
|
;;
|
|
describe)
|
|
shift
|
|
cmd_describe "$@"
|
|
;;
|
|
rpc)
|
|
shift
|
|
cmd_rpc "$@"
|
|
;;
|
|
help|--help|-h)
|
|
usage
|
|
;;
|
|
*)
|
|
echo "Unknown command: $1" >&2
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|