Files
jr-sql-ai/bin/sqlai

298 lines
9.3 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="${ROOT}/.env"
[[ -f "$ENV_FILE" ]] && source "$ENV_FILE"
: "${OLLAMA_URL:=http://127.0.0.1:11434}"
: "${EXPERT_MODEL:=jr-sql-expert}"
: "${BASE_MODEL:=}" # fallback
ts() { date -Is; }
log_dir="${ROOT}/logs"
mkdir -p "$log_dir"
log_file="${log_dir}/sqlai-$(date -I).log"
exec > >(tee -a "$log_file") 2>&1
usage() {
cat <<'EOF'
Usage:
sqlai <mode> [--file path] [--text "free text"] [--no-metrics]
Modes:
ask General SQL Server 2022 Q&A (free text)
analyze-tsql Analyze a T-SQL query
analyze-proc Analyze a stored procedure
analyze-view Analyze a view
analyze-plan Analyze an execution plan (Showplan XML or text)
utf8-migration Create a UTF-8 migration plan (SQL Server 2022)
Input options:
--text "..." Provide input text directly (no pipe needed)
--file path Read input from file
(no args) Reads from STDIN
Other options:
--no-metrics Do not print metrics line
Examples:
sqlai ask --text "How do I troubleshoot parameter sniffing in SQL Server 2022?"
echo "SELECT 1;" | sqlai analyze-tsql
sqlai analyze-plan --file showplan.xml
EOF
}
mode="${1:-}"
[[ -z "$mode" ]] && { usage; exit 1; }
shift || true
file=""
text=""
no_metrics="0"
while [[ $# -gt 0 ]]; do
case "$1" in
--file) file="${2:-}"; shift 2;;
--text) text="${2:-}"; shift 2;;
--no-metrics) no_metrics="1"; shift 1;;
-h|--help) usage; exit 0;;
*) echo "[$(ts)] ERROR: Unknown arg: $1"; usage; exit 2;;
esac
done
# Read input (priority: --text > --file > stdin)
input=""
input_src=""
if [[ -n "$text" ]]; then
input="$text"
input_src="text"
elif [[ -n "$file" ]]; then
[[ -f "$file" ]] || { echo "[$(ts)] ERROR: file not found: $file"; exit 3; }
input="$(cat "$file")"
input_src="file:${file}"
else
input="$(cat)"
input_src="stdin"
fi
if [[ -z "${input//[[:space:]]/}" ]]; then
echo "[$(ts)] ERROR: empty input (source=$input_src)"
exit 4
fi
case "$mode" in
ask)
instruction=$'Beantworte die Frage als SQL Server 2022 Experte.\n\nWichtig:\n- Wenn Kontext fehlt: keine Rückfragen stellen; stattdessen Annahmen offenlegen und Optionen (A/B/C) mit Vor-/Nachteilen geben.\n- Ergebnis immer strukturiert mit: Kurzfazit, Optionen, Risiken/Checks, Nächste Schritte.\n- Wenn sinnvoll: konkrete T-SQL/DDL Snippets und Checklisten liefern.'
;;
analyze-tsql)
instruction=$'Analysiere das folgende T-SQL (SQL Server 2022): Performance, SARGability, Datentypen, Joins/Predicates, Indizes/Stats, Parameter Sniffing. Gib konkrete Rewrite- und Index-Ideen.'
;;
analyze-proc)
instruction=$'Analysiere die folgende Stored Procedure (SQL Server 2022): Performance/Correctness, Transaktionen/Locking, Sniffing, Temp tables vs table variables, RBAR. Gib konkrete Refactorings.'
;;
analyze-view)
instruction=$'Analysiere die folgende View (SQL Server 2022): SARGability, Predicate Pushdown, Expand/Inlining, Aggregationen/Distinct/Union, UDF-Risiken. Gib konkrete Verbesserungen.'
;;
analyze-plan)
instruction=$'Analysiere den folgenden SQL Server Execution Plan (Showplan XML oder Text): Hotspots, Spills/Warnings, Memory Grants, Kardinalität, Fixes (Rewrite/Stats/Indexing vorsichtig/Sniffing Mitigation).'
;;
utf8-migration)
instruction=$'Erstelle einen Migrationsplan (SQL Server 2022) zur Umstellung auf UTF-8 (UTF-8 enabled collations _UTF8): Abhängigkeiten (PK/FK/Indexes/Computed/Triggers), Cutover, Rollback, Tests, konkrete DDL/Checklisten.'
;;
*)
echo "[$(ts)] ERROR: unknown mode: $mode"
usage
exit 5
;;
esac
req_id="$(date +%Y%m%d-%H%M%S)-$$-$RANDOM"
# --- helper: model existence via /api/tags ---
model_exists() {
# args: modelname
local m="$1"
local tags
tags="$(curl -sS "${OLLAMA_URL}/api/tags" 2>/dev/null || true)"
[[ -z "$tags" ]] && return 1
printf '%s' "$tags" | python -c '
import json,sys
m=sys.argv[1]
try:
obj=json.load(sys.stdin)
except Exception:
sys.exit(2)
names=set()
for it in obj.get("models", []):
n=it.get("name")
if n: names.add(n)
# Accept exact match, or ":latest" variants
ok = (m in names) or (m + ":latest" in names) or (m.endswith(":latest") and m[:-7] in names)
sys.exit(0 if ok else 1)
' "$m" >/dev/null 2>&1
}
pick_model() {
# Prefer expert, fallback to base
if model_exists "$EXPERT_MODEL"; then
printf '%s' "$EXPERT_MODEL"
return 0
fi
if [[ -n "${BASE_MODEL:-}" ]] && model_exists "$BASE_MODEL"; then
echo "[$(ts)] sqlai: WARN: expert model not found in /api/tags; falling back to BASE_MODEL=${BASE_MODEL}"
printf '%s' "$BASE_MODEL"
return 0
fi
echo "[$(ts)] sqlai: ERROR: neither EXPERT_MODEL=${EXPERT_MODEL} nor BASE_MODEL=${BASE_MODEL:-<empty>} found (api/tags)."
return 1
}
echo "================================================================================"
echo "[$(ts)] sqlai: REQUEST_START id=$req_id"
echo "[$(ts)] sqlai: MODE=$mode EXPERT_MODEL=$EXPERT_MODEL BASE_MODEL=${BASE_MODEL:-<empty>} OLLAMA_URL=$OLLAMA_URL"
echo "[$(ts)] sqlai: INPUT_SOURCE=$input_src INPUT_BYTES=$(printf "%s" "$input" | wc -c)"
prompt=$(cat <<EOF
${instruction}
---BEGIN INPUT (${mode})---
${input}
---END INPUT (${mode})---
EOF
)
payload_for_model() {
local model="$1"
printf '%s' "$prompt" | python -c '
import json, os, sys
model=sys.argv[1]
prompt=sys.stdin.read()
print(json.dumps({"model": model, "prompt": prompt, "stream": False}, ensure_ascii=False))
' "$model"
}
do_request() {
# args: model, attempt
local model="$1"
local attempt="$2"
local payload resp_file http_code resp resp_bytes extracted error_msg response_txt
payload="$(payload_for_model "$model")"
resp_file="$(mktemp)"
http_code="$(
curl -sS -o "$resp_file" -w "%{http_code}" \
-X POST "${OLLAMA_URL}/api/generate" \
-H 'Content-Type: application/json' \
--data-binary "$payload" \
|| true
)"
resp="$(cat "$resp_file")"
rm -f "$resp_file"
resp_bytes="$(printf "%s" "$resp" | wc -c)"
echo "[$(ts)] sqlai: attempt=$attempt model=$model HTTP_CODE=$http_code RESP_BYTES=$resp_bytes id=$req_id"
# Validate JSON
if ! printf '%s' "$resp" | python -c 'import json,sys; json.load(sys.stdin)' >/dev/null 2>&1; then
echo "[$(ts)] sqlai: ERROR: invalid JSON response attempt=$attempt id=$req_id"
echo "[$(ts)] sqlai: RAW_RESPONSE_BEGIN attempt=$attempt id=$req_id"
printf '%s\n' "$resp" | head -n 200
echo "[$(ts)] sqlai: RAW_RESPONSE_END attempt=$attempt id=$req_id"
return 90
fi
extracted="$(
printf '%s' "$resp" | python -c '
import json,sys
obj=json.load(sys.stdin)
out={
"error": obj.get("error"),
"response": obj.get("response",""),
"metrics": {
"total_duration": obj.get("total_duration"),
"load_duration": obj.get("load_duration"),
"prompt_eval_count": obj.get("prompt_eval_count"),
"prompt_eval_duration": obj.get("prompt_eval_duration"),
"eval_count": obj.get("eval_count"),
"eval_duration": obj.get("eval_duration"),
}
}
print(json.dumps(out, ensure_ascii=False))
'
)"
error_msg="$(printf '%s' "$extracted" | python -c 'import json,sys; print((json.load(sys.stdin).get("error") or "").strip())')"
response_txt="$(printf '%s' "$extracted" | python -c 'import json,sys; print(json.load(sys.stdin).get("response") or "")')"
# HTTP != 200 -> error
if [[ "$http_code" != "200" ]]; then
echo "[$(ts)] sqlai: ERROR: non-200 HTTP_CODE=$http_code attempt=$attempt id=$req_id"
[[ -n "$error_msg" ]] && echo "[$(ts)] sqlai: OLLAMA_ERROR=$error_msg attempt=$attempt id=$req_id"
return 91
fi
# API-level error
if [[ -n "$error_msg" ]]; then
echo "[$(ts)] sqlai: ERROR: OLLAMA_ERROR=$error_msg attempt=$attempt id=$req_id"
# special code for retry decisions
if printf '%s' "$error_msg" | grep -qiE 'model .*not found|not found'; then
return 42
fi
return 92
fi
# Print answer
printf "\n%s\n\n" "$(printf "%s" "$response_txt" | sed 's/[[:space:]]*$//')"
# Print metrics optionally
if [[ "$no_metrics" != "1" ]]; then
echo "METRICS=$(printf '%s' "$extracted" | python -c 'import json,sys; import json as j; print(j.dumps(json.load(sys.stdin)["metrics"], ensure_ascii=False))')"
fi
# Empty answer warning
if [[ -z "${response_txt//[[:space:]]/}" ]]; then
echo "[$(ts)] sqlai: WARN: empty response attempt=$attempt model=$model id=$req_id"
fi
return 0
}
model_primary="$(pick_model)"
echo "[$(ts)] sqlai: selected_model_primary=$model_primary id=$req_id"
# Attempt 1
set +e
do_request "$model_primary" "1"
rc=$?
set -e
# Retry with BASE_MODEL if "model not found" and we weren't already using it
if [[ "$rc" -eq 42 ]]; then
if [[ -n "${BASE_MODEL:-}" ]] && [[ "$model_primary" != "$BASE_MODEL" ]] && model_exists "$BASE_MODEL"; then
echo "[$(ts)] sqlai: WARN: retrying with BASE_MODEL=$BASE_MODEL (expert model not found) id=$req_id"
set +e
do_request "$BASE_MODEL" "2"
rc2=$?
set -e
rc="$rc2"
else
echo "[$(ts)] sqlai: ERROR: retry requested but BASE_MODEL unavailable id=$req_id"
rc=93
fi
fi
if [[ "$rc" -ne 0 ]]; then
echo "[$(ts)] sqlai: REQUEST_END id=$req_id status=error rc=$rc"
echo "================================================================================"
exit "$rc"
fi
echo "[$(ts)] sqlai: REQUEST_END id=$req_id status=ok"
echo "================================================================================"