Changeset 0.16.0 (#74)
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
#!/bin/bash
|
||||
# ============================================
|
||||
# Chat Switchboard - Schema Validation (v0.9)
|
||||
# Chat Switchboard - Schema Validation (v0.16)
|
||||
# ============================================
|
||||
# Verifies the database schema is correct after
|
||||
# migration. Checks expected tables, key columns,
|
||||
# and constraints. Exits non-zero on any failure.
|
||||
# indexes, triggers, and confirms dropped artifacts.
|
||||
# Exits non-zero on any failure.
|
||||
#
|
||||
# Required env vars:
|
||||
# PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE
|
||||
@@ -20,10 +21,12 @@ echo "Schema validation: ${PGDATABASE}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
ERRORS=0
|
||||
CHECKS=0
|
||||
|
||||
# ── Helper: check table exists ───────────────
|
||||
check_table() {
|
||||
local table="$1"
|
||||
CHECKS=$((CHECKS + 1))
|
||||
local exists
|
||||
exists=$(psql -tAc "SELECT 1 FROM information_schema.tables WHERE table_schema='public' AND table_name='${table}';")
|
||||
if [[ "${exists}" == "1" ]]; then
|
||||
@@ -34,10 +37,25 @@ check_table() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Helper: check table does NOT exist ───────
|
||||
check_table_absent() {
|
||||
local table="$1"
|
||||
CHECKS=$((CHECKS + 1))
|
||||
local exists
|
||||
exists=$(psql -tAc "SELECT 1 FROM information_schema.tables WHERE table_schema='public' AND table_name='${table}';")
|
||||
if [[ "${exists}" == "1" ]]; then
|
||||
echo " ✗ UNEXPECTED table: ${table} (should have been dropped)"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo " ✓ absent: ${table}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Helper: check column exists ──────────────
|
||||
check_column() {
|
||||
local table="$1"
|
||||
local column="$2"
|
||||
CHECKS=$((CHECKS + 1))
|
||||
local exists
|
||||
exists=$(psql -tAc "SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='${table}' AND column_name='${column}';")
|
||||
if [[ "${exists}" == "1" ]]; then
|
||||
@@ -48,9 +66,70 @@ check_column() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Helper: check column does NOT exist ──────
|
||||
check_column_absent() {
|
||||
local table="$1"
|
||||
local column="$2"
|
||||
CHECKS=$((CHECKS + 1))
|
||||
local exists
|
||||
exists=$(psql -tAc "SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='${table}' AND column_name='${column}';")
|
||||
if [[ "${exists}" == "1" ]]; then
|
||||
echo " ✗ UNEXPECTED column: ${table}.${column} (should have been dropped)"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo " ✓ absent: ${table}.${column}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Helper: check column type ────────────────
|
||||
check_column_type() {
|
||||
local table="$1"
|
||||
local column="$2"
|
||||
local expected_type="$3"
|
||||
CHECKS=$((CHECKS + 1))
|
||||
local actual_type
|
||||
actual_type=$(psql -tAc "SELECT data_type FROM information_schema.columns WHERE table_schema='public' AND table_name='${table}' AND column_name='${column}';")
|
||||
if [[ "${actual_type}" == "${expected_type}" ]]; then
|
||||
echo " ✓ type: ${table}.${column} = ${expected_type}"
|
||||
else
|
||||
echo " ✗ WRONG type: ${table}.${column} = ${actual_type} (expected ${expected_type})"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Helper: check index exists ───────────────
|
||||
check_index() {
|
||||
local index="$1"
|
||||
CHECKS=$((CHECKS + 1))
|
||||
local exists
|
||||
exists=$(psql -tAc "SELECT 1 FROM pg_indexes WHERE schemaname='public' AND indexname='${index}';")
|
||||
if [[ "${exists}" == "1" ]]; then
|
||||
echo " ✓ index: ${index}"
|
||||
else
|
||||
echo " ✗ MISSING index: ${index}"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Helper: check trigger exists ─────────────
|
||||
check_trigger() {
|
||||
local trigger="$1"
|
||||
local table="$2"
|
||||
CHECKS=$((CHECKS + 1))
|
||||
local exists
|
||||
exists=$(psql -tAc "SELECT 1 FROM pg_trigger t JOIN pg_class c ON t.tgrelid = c.oid WHERE t.tgname='${trigger}' AND c.relname='${table}' AND NOT t.tgisinternal;")
|
||||
if [[ "${exists}" == "1" ]]; then
|
||||
echo " ✓ trigger: ${trigger} on ${table}"
|
||||
else
|
||||
echo " ✗ MISSING trigger: ${trigger} on ${table}"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Helper: check extension ──────────────────
|
||||
check_extension() {
|
||||
local ext="$1"
|
||||
CHECKS=$((CHECKS + 1))
|
||||
local exists
|
||||
exists=$(psql -tAc "SELECT 1 FROM pg_extension WHERE extname='${ext}';")
|
||||
if [[ "${exists}" == "1" ]]; then
|
||||
@@ -61,12 +140,17 @@ check_extension() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ── 1. Extensions ────────────────────────────
|
||||
echo ""
|
||||
echo "Extensions:"
|
||||
check_extension "pgcrypto"
|
||||
# ═══════════════════════════════════════════
|
||||
# CHECKS
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
# ── 2. Migration tracking ───────────────────
|
||||
# ── Extensions ────────────────────────────
|
||||
echo ""
|
||||
echo "PostgreSQL Extensions:"
|
||||
check_extension "pgcrypto"
|
||||
check_extension "vector"
|
||||
|
||||
# ── Migration tracking ───────────────────
|
||||
echo ""
|
||||
echo "Migration tracking:"
|
||||
check_table "schema_migrations"
|
||||
@@ -76,111 +160,177 @@ echo " ✓ ${MIGRATION_COUNT} migrations applied"
|
||||
LATEST=$(psql -tAc "SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1;" 2>/dev/null || echo "none")
|
||||
echo " ✓ latest: ${LATEST}"
|
||||
|
||||
# ── 3. Core tables ──────────────────────────
|
||||
# ── Core tables ──────────────────────────
|
||||
echo ""
|
||||
echo "Core tables:"
|
||||
check_table "users"
|
||||
check_table "provider_configs"
|
||||
check_table "channels"
|
||||
check_table "messages"
|
||||
check_table "refresh_tokens"
|
||||
check_table "teams"
|
||||
check_table "team_members"
|
||||
check_table "refresh_tokens"
|
||||
check_table "global_settings"
|
||||
|
||||
# ── 4. Users ────────────────────────────────
|
||||
echo ""
|
||||
echo "Users:"
|
||||
check_column "users" "id"
|
||||
check_column "users" "username"
|
||||
check_column "users" "email"
|
||||
check_column "users" "role"
|
||||
check_column "users" "is_active"
|
||||
check_column "users" "avatar_url"
|
||||
check_column "users" "display_name"
|
||||
check_column "users" "settings"
|
||||
|
||||
# ── 5. Provider Configs (replaces api_configs) ─
|
||||
echo ""
|
||||
echo "Provider Configs:"
|
||||
check_column "provider_configs" "scope"
|
||||
check_column "provider_configs" "owner_id"
|
||||
check_column "provider_configs" "provider"
|
||||
check_column "provider_configs" "endpoint"
|
||||
check_column "provider_configs" "api_key_enc"
|
||||
check_column "provider_configs" "headers"
|
||||
check_column "provider_configs" "settings"
|
||||
check_column "provider_configs" "is_active"
|
||||
|
||||
# ── 6. Model Catalog (replaces model_configs) ─
|
||||
echo ""
|
||||
echo "Model Catalog:"
|
||||
check_table "groups"
|
||||
check_table "group_members"
|
||||
check_table "provider_configs"
|
||||
check_table "model_catalog"
|
||||
check_column "model_catalog" "provider_config_id"
|
||||
check_column "model_catalog" "model_id"
|
||||
check_column "model_catalog" "display_name"
|
||||
check_column "model_catalog" "capabilities"
|
||||
check_column "model_catalog" "pricing"
|
||||
check_column "model_catalog" "visibility"
|
||||
|
||||
# ── 7. Personas (replaces model_presets) ────
|
||||
echo ""
|
||||
echo "Personas:"
|
||||
check_table "personas"
|
||||
check_column "personas" "scope"
|
||||
check_column "personas" "owner_id"
|
||||
check_column "personas" "name"
|
||||
check_column "personas" "base_model_id"
|
||||
check_column "personas" "provider_config_id"
|
||||
check_column "personas" "system_prompt"
|
||||
check_column "personas" "created_by"
|
||||
|
||||
# ── 8. Channels ─────────────────────────────
|
||||
echo ""
|
||||
echo "Channels:"
|
||||
check_column "channels" "user_id"
|
||||
check_column "channels" "type"
|
||||
check_column "channels" "provider_config_id"
|
||||
check_column "channels" "team_id"
|
||||
check_column "channels" "settings"
|
||||
check_table "persona_grants"
|
||||
check_table "resource_grants"
|
||||
check_table "platform_policies"
|
||||
check_table "global_settings"
|
||||
check_table "user_model_settings"
|
||||
check_table "channels"
|
||||
check_table "messages"
|
||||
check_table "channel_members"
|
||||
check_table "channel_models"
|
||||
check_table "channel_cursors"
|
||||
check_table "folders"
|
||||
check_table "notes"
|
||||
check_table "audit_log"
|
||||
check_table "usage_log"
|
||||
check_table "model_pricing"
|
||||
check_table "extensions"
|
||||
check_table "extension_user_settings"
|
||||
check_table "attachments"
|
||||
check_table "knowledge_bases"
|
||||
check_table "kb_documents"
|
||||
check_table "kb_chunks"
|
||||
check_table "channel_knowledge_bases"
|
||||
|
||||
# ── 9. Messages ─────────────────────────────
|
||||
# ── Dropped tables (v0.16.0 cleanup) ────
|
||||
echo ""
|
||||
echo "Messages:"
|
||||
check_column "messages" "channel_id"
|
||||
check_column "messages" "role"
|
||||
echo "Dropped tables (v0.16.0 cleanup):"
|
||||
check_table_absent "projects"
|
||||
check_table_absent "project_channels"
|
||||
|
||||
# ── Users (vault) ────────────────────────
|
||||
echo ""
|
||||
echo "Users (vault):"
|
||||
check_column "users" "encrypted_uek"
|
||||
check_column "users" "uek_salt"
|
||||
check_column "users" "uek_nonce"
|
||||
check_column "users" "vault_set"
|
||||
|
||||
# ── Provider Configs (encryption) ────────
|
||||
echo ""
|
||||
echo "Provider Configs (encryption):"
|
||||
check_column "provider_configs" "api_key_enc"
|
||||
check_column_type "provider_configs" "api_key_enc" "bytea"
|
||||
check_column "provider_configs" "key_nonce"
|
||||
check_column_type "provider_configs" "key_nonce" "bytea"
|
||||
check_column "provider_configs" "key_scope"
|
||||
check_column_absent "provider_configs" "api_key_plain"
|
||||
|
||||
# ── Model Catalog ────────────────────────
|
||||
echo ""
|
||||
echo "Model Catalog:"
|
||||
check_column "model_catalog" "model_type"
|
||||
check_column "model_catalog" "visibility"
|
||||
check_column "model_catalog" "capabilities"
|
||||
|
||||
# ── Groups (v0.16.0) ────────────────────
|
||||
echo ""
|
||||
echo "Groups (v0.16.0):"
|
||||
check_column "groups" "name"
|
||||
check_column "groups" "scope"
|
||||
check_column "groups" "team_id"
|
||||
check_column "groups" "created_by"
|
||||
check_column "group_members" "group_id"
|
||||
check_column "group_members" "user_id"
|
||||
check_column "group_members" "added_by"
|
||||
check_index "idx_group_members_user"
|
||||
check_index "idx_group_members_group"
|
||||
check_index "idx_groups_name_scope"
|
||||
|
||||
# ── Resource Grants (v0.16.0) ───────────
|
||||
echo ""
|
||||
echo "Resource Grants (v0.16.0):"
|
||||
check_column "resource_grants" "resource_type"
|
||||
check_column "resource_grants" "resource_id"
|
||||
check_column "resource_grants" "grant_scope"
|
||||
check_column "resource_grants" "granted_groups"
|
||||
check_column "resource_grants" "created_by"
|
||||
check_index "idx_resource_grants_resource"
|
||||
check_index "idx_resource_grants_groups"
|
||||
|
||||
# ── Personas ─────────────────────────────
|
||||
echo ""
|
||||
echo "Personas:"
|
||||
check_column "personas" "scope"
|
||||
check_column "personas" "owner_id"
|
||||
check_column "personas" "base_model_id"
|
||||
check_column "personas" "thinking_budget"
|
||||
check_column "personas" "is_shared"
|
||||
|
||||
# ── Channels & Messages ──────────────────
|
||||
echo ""
|
||||
echo "Channels & Messages:"
|
||||
check_column "channels" "settings"
|
||||
check_column "channels" "folder_id"
|
||||
check_column "channels" "team_id"
|
||||
check_column "messages" "parent_id"
|
||||
check_column "messages" "sibling_index"
|
||||
check_column "messages" "deleted_at"
|
||||
check_column "messages" "participant_type"
|
||||
check_column "messages" "tool_calls"
|
||||
check_column "messages" "metadata"
|
||||
|
||||
# ── 10. Organization ────────────────────────
|
||||
# ── Notes ────────────────────────────────
|
||||
echo ""
|
||||
echo "Organization:"
|
||||
check_table "folders"
|
||||
check_table "projects"
|
||||
check_table "notes"
|
||||
echo "Notes:"
|
||||
check_column "notes" "search_vector"
|
||||
check_column "notes" "embedding"
|
||||
check_column "notes" "team_id"
|
||||
check_index "idx_notes_search"
|
||||
|
||||
# ── 11. User Model Settings ────────────────
|
||||
# ── Extensions ───────────────────────────
|
||||
echo ""
|
||||
echo "User Model Settings:"
|
||||
check_table "user_model_settings"
|
||||
check_column "user_model_settings" "user_id"
|
||||
check_column "user_model_settings" "model_id"
|
||||
check_column "user_model_settings" "hidden"
|
||||
check_column "user_model_settings" "sort_order"
|
||||
echo "Extensions:"
|
||||
check_column "extensions" "ext_id"
|
||||
check_column "extensions" "tier"
|
||||
check_column "extensions" "manifest"
|
||||
check_column "extensions" "is_system"
|
||||
check_column "extensions" "scope"
|
||||
|
||||
# ── 12. Audit Log ───────────────────────────
|
||||
# ── Attachments ──────────────────────────
|
||||
echo ""
|
||||
echo "Audit Log:"
|
||||
check_table "audit_log"
|
||||
check_column "audit_log" "actor_id"
|
||||
check_column "audit_log" "action"
|
||||
check_column "audit_log" "resource_type"
|
||||
echo "Attachments:"
|
||||
check_column "attachments" "channel_id"
|
||||
check_column "attachments" "storage_key"
|
||||
check_column "attachments" "extracted_text"
|
||||
check_index "idx_attachments_channel"
|
||||
check_index "idx_attachments_orphan"
|
||||
|
||||
# ── Knowledge Bases ──────────────────────
|
||||
echo ""
|
||||
echo "Knowledge Bases:"
|
||||
check_column "knowledge_bases" "scope"
|
||||
check_column "knowledge_bases" "embedding_config"
|
||||
check_column "knowledge_bases" "document_count"
|
||||
check_table "kb_documents"
|
||||
check_table "kb_chunks"
|
||||
check_column "kb_chunks" "embedding"
|
||||
check_table "channel_knowledge_bases"
|
||||
|
||||
# ── Usage & Pricing ──────────────────────
|
||||
echo ""
|
||||
echo "Usage & Pricing:"
|
||||
check_column "usage_log" "provider_scope"
|
||||
check_column "usage_log" "role"
|
||||
check_column "usage_log" "cache_creation_tokens"
|
||||
check_column "model_pricing" "cache_create_per_m"
|
||||
|
||||
# ── Key triggers ─────────────────────────
|
||||
echo ""
|
||||
echo "Key triggers:"
|
||||
check_trigger "users_updated_at" "users"
|
||||
check_trigger "channels_updated_at" "channels"
|
||||
check_trigger "personas_updated_at" "personas"
|
||||
check_trigger "groups_updated_at" "groups"
|
||||
check_trigger "resource_grants_updated_at" "resource_grants"
|
||||
check_trigger "notes_search_update" "notes"
|
||||
|
||||
# ── CI Username Indexes ──────────────────
|
||||
echo ""
|
||||
echo "CI Username Indexes:"
|
||||
check_index "users_username_ci"
|
||||
check_index "users_email_ci"
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# ADD NEW MIGRATION CHECKS ABOVE THIS LINE
|
||||
@@ -190,11 +340,11 @@ check_column "audit_log" "resource_type"
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
if [[ ${ERRORS} -eq 0 ]]; then
|
||||
echo "✅ Schema validation passed (${MIGRATION_COUNT} migrations)"
|
||||
echo "✅ Schema validation passed: ${CHECKS}/${CHECKS} checks OK"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
exit 0
|
||||
else
|
||||
echo "❌ Schema validation FAILED: ${ERRORS} errors"
|
||||
echo "❌ Schema validation FAILED: ${ERRORS}/${CHECKS} checks failed"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user