This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/scripts/db-validate.sh
Jeffrey Smith 680ec3b897
All checks were successful
CI/CD / detect-changes (push) Successful in 3s
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Successful in 2m34s
CI/CD / test-sqlite (push) Successful in 2m46s
CI/CD / build-and-deploy (push) Successful in 1m55s
Feat rebrand armature (#43)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-03-31 23:25:37 +00:00

373 lines
13 KiB
Bash

#!/bin/bash
# ============================================
# Armature - Schema Validation (v0.16)
# ============================================
# Verifies the database schema is correct after
# migration. Checks expected tables, key columns,
# indexes, triggers, and confirms dropped artifacts.
# Exits non-zero on any failure.
#
# Required env vars:
# PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE
#
# Usage:
# scripts/db-validate.sh
# ============================================
set -euo pipefail
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
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
echo " ✓ table: ${table}"
else
echo " ✗ MISSING table: ${table}"
ERRORS=$((ERRORS + 1))
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
echo " ✓ column: ${table}.${column}"
else
echo " ✗ MISSING column: ${table}.${column}"
ERRORS=$((ERRORS + 1))
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
echo " ✓ extension: ${ext}"
else
echo " ✗ MISSING extension: ${ext}"
ERRORS=$((ERRORS + 1))
fi
}
# ═══════════════════════════════════════════
# CHECKS
# ═══════════════════════════════════════════
# ── Extensions ────────────────────────────
echo ""
echo "PostgreSQL Extensions:"
check_extension "pgcrypto"
check_extension "vector"
# ── Migration tracking ───────────────────
echo ""
echo "Migration tracking:"
check_table "schema_migrations"
MIGRATION_COUNT=$(psql -tAc "SELECT COUNT(*) FROM schema_migrations;" 2>/dev/null || echo "0")
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}"
# ── Core tables ──────────────────────────
echo ""
echo "Core tables:"
check_table "users"
check_table "refresh_tokens"
check_table "teams"
check_table "team_members"
check_table "groups"
check_table "group_members"
check_table "provider_configs"
check_table "model_catalog"
check_table "personas"
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_participants"
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 "files"
check_table "knowledge_bases"
check_table "kb_documents"
check_table "kb_chunks"
check_table "channel_knowledge_bases"
# ── Projects (v0.19.0) ───────────────────
echo ""
echo "Projects (v0.19.0):"
check_table "projects"
check_table "project_channels"
check_table "project_knowledge_bases"
check_table "project_notes"
# ── Notifications (v0.20.0) ──────────────
echo ""
echo "Notifications (v0.20.0):"
check_table "notifications"
# ── 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"
echo ""
echo "Provider Proxy (v0.23.0):"
check_column "provider_configs" "proxy_mode"
check_column "provider_configs" "proxy_url"
echo ""
echo "Personas (v0.23.0):"
check_column "personas" "handle"
check_table "persona_groups"
check_table "persona_group_members"
# ── 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 "channels" "project_id"
check_column "messages" "parent_id"
check_column "messages" "sibling_index"
check_column "messages" "tool_calls"
check_column "messages" "metadata"
# ── Notes ────────────────────────────────
echo ""
echo "Notes:"
check_column "notes" "search_vector"
check_column "notes" "embedding"
check_column "notes" "team_id"
check_index "idx_notes_search"
# ── Extensions ───────────────────────────
echo ""
echo "Extensions:"
check_column "extensions" "ext_id"
check_column "extensions" "tier"
check_column "extensions" "manifest"
check_column "extensions" "is_system"
check_column "extensions" "scope"
# ── Files ──────────────────────────
echo ""
echo "Files:"
check_column "files" "channel_id"
check_column "files" "storage_key"
check_column "files" "extracted_text"
check_column "files" "origin"
check_column "files" "display_hint"
check_column "files" "updated_at"
check_index "idx_files_channel"
check_index "idx_files_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
# ═══════════════════════════════════════════
# ── Summary ──────────────────────────────────
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [[ ${ERRORS} -eq 0 ]]; then
echo "✅ Schema validation passed: ${CHECKS}/${CHECKS} checks OK"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
else
echo "❌ Schema validation FAILED: ${ERRORS}/${CHECKS} checks failed"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
exit 1
fi