#!/bin/bash # Script to check ClickHouse migrations for proper ON CLUSTER clause usage # This script validates that all DDL operations in ClickHouse migrations include the ON CLUSTER '{cluster}' clause # Reference: https://clickhouse.com/docs/sql-reference/distributed-ddl # # Usage: # ./check_clickhouse_migrations_cluster.sh # Check all migration files # ./check_clickhouse_migrations_cluster.sh file1.sql file2.sql # Check specific files set -uo pipefail # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Configuration MIGRATION_DIR="apps/opik-backend/src/main/resources/liquibase/db-app-analytics/migrations" EXIT_CODE=0 # DDL patterns that require ON CLUSTER clause # Reference: https://clickhouse.com/docs/sql-reference/distributed-ddl # EXCHANGE covers the Distributed-layer cutover shape (EXCHANGE TABLES foo_local AND foo_shadow_local) # used from Hyperscale Slice 2 onward. The check is table-name agnostic: both _local shadow tables # and their unsuffixed Distributed wrappers are validated the same way. DDL_COMMANDS_REGEX="(CREATE|DROP|ALTER|RENAME|EXCHANGE)" # Combined pattern for detecting DDL statements that need ON CLUSTER DDL_DETECTION_REGEX="^\s*${DDL_COMMANDS_REGEX}\s+" # Exact pattern for ON CLUSTER clause validation (project-specific) ON_CLUSTER_REGEX="ON\s+CLUSTER\s+['\"]\\{cluster\\}['\"]" # Pattern for detecting rollback comments in migration files ROLLBACK_COMMENT_REGEX="^[[:space:]]*--[[:space:]]*rollback[[:space:]]+" # Pattern for extracting DDL from rollback comments (for sed substitution) ROLLBACK_EXTRACT_REGEX="s/^[[:space:]]*--[[:space:]]*rollback[[:space:]]+//i" echo "🔍 Checking ClickHouse migrations for ON CLUSTER clause usage..." echo "📁 Migration directory: ${MIGRATION_DIR}" echo # Function to check if a DDL statement has ON CLUSTER clause check_ddl_statement() { local file="$1" local line_num="$2" local line="$3" local ddl_type="$4" # Skip if line is a comment (but rollback DDL should already be extracted by the caller) if echo "$line" | grep -qE "^\s*--"; then return 0 fi # Check if the DDL statement has ON CLUSTER clause using centralized regex # Required pattern: ON CLUSTER '{cluster}' (exact match for this project) if ! echo "$line" | grep -qiE "$ON_CLUSTER_REGEX"; then echo -e "${RED}❌ ERROR: Missing ON CLUSTER clause${NC}" echo -e " 📄 File: ${file}" echo -e " 📍 Line ${line_num}: ${line}" echo -e " 🔧 Expected: ${ddl_type} ... ON CLUSTER '{cluster}'" echo return 1 fi return 0 } # Function to validate a single migration file validate_migration_file() { local file="$1" local file_errors=0 echo "📋 Checking: $(basename "$file")" echo "🔍 DEBUG: Full file path: $file" echo "🔍 DEBUG: File exists: $(test -f "$file" && echo "YES" || echo "NO")" echo "🔍 DEBUG: File readable: $(test -r "$file" && echo "YES" || echo "NO")" # Read file line by line local line_num=0 while IFS= read -r line; do ((line_num++)) # Remove leading/trailing whitespace (but preserve quotes) line="${line#"${line%%[![:space:]]*}"}" # Remove leading whitespace line="${line%"${line##*[![:space:]]}"}" # Remove trailing whitespace # Skip empty lines and regular comments (but not rollback statements) if [[ -z "$line" ]]; then continue fi # Handle rollback statements specially if echo "$line" | grep -qiE "$ROLLBACK_COMMENT_REGEX"; then # Extract the DDL statement from the rollback comment local rollback_ddl=$(echo "$line" | sed -E "$ROLLBACK_EXTRACT_REGEX") # Check if the rollback contains DDL statements using centralized regex if echo "$rollback_ddl" | grep -qiE "$DDL_DETECTION_REGEX"; then # Extract DDL type using centralized regex local ddl_type=$(echo "$rollback_ddl" | sed -nE "s/$DDL_DETECTION_REGEX.*/\1/Ip") if ! check_ddl_statement "$file" "$line_num" "$rollback_ddl" "$ddl_type"; then ((file_errors++)) fi fi continue fi # Skip other comments if [[ "$line" =~ ^[[:space:]]*-- ]]; then continue fi # Check for DDL statements using centralized regex (case insensitive) if echo "$line" | grep -qiE "$DDL_DETECTION_REGEX"; then # Extract DDL type using centralized regex local ddl_type=$(echo "$line" | sed -nE "s/$DDL_DETECTION_REGEX.*/\1/Ip") if ! check_ddl_statement "$file" "$line_num" "$line" "$ddl_type"; then ((file_errors++)) fi fi done < "$file" if [[ $file_errors -eq 0 ]]; then echo -e " ✅ ${GREEN}All DDL statements have proper ON CLUSTER clause${NC}" else echo -e " ❌ ${RED}Found $file_errors DDL statement(s) missing ON CLUSTER clause${NC}" fi echo return $file_errors } # Main validation logic main() { local total_errors=0 local total_files=0 local migration_files=() # Check if specific files were provided as arguments if [[ $# -gt 0 ]]; then # Use provided file arguments migration_files=("$@") echo "🎯 Validating specific migration files provided as arguments..." # Validate that all provided files exist and are SQL files for file in "${migration_files[@]}"; do if [[ ! -f "$file" ]]; then echo -e "${RED}❌ ERROR: File not found: $file${NC}" exit 1 fi if [[ ! "$file" =~ \.sql$ ]]; then echo -e "${YELLOW}⚠️ WARNING: Skipping non-SQL file: $file${NC}" continue fi done else # Check if migration directory exists if [[ ! -d "$MIGRATION_DIR" ]]; then echo -e "${RED}❌ ERROR: Migration directory not found: $MIGRATION_DIR${NC}" exit 1 fi # Find all SQL migration files in the directory migration_files=($(find "$MIGRATION_DIR" -name "*.sql" | sort)) echo "📊 Validating all migration files in directory..." fi # Filter out non-SQL files and files that don't exist local valid_files=() for file in "${migration_files[@]}"; do if [[ -f "$file" && "$file" =~ \.sql$ ]]; then valid_files+=("$file") fi done if [[ ${#valid_files[@]} -eq 0 ]]; then echo -e "${YELLOW}⚠️ WARNING: No valid SQL migration files to check${NC}" exit 0 fi echo "📊 Found ${#valid_files[@]} migration file(s) to check" echo "=" $(printf '=%.0s' {1..50}) echo # Validate each migration file for file in "${valid_files[@]}"; do ((total_files++)) echo "🔍 DEBUG: About to validate file: $file" if validate_migration_file "$file"; then echo "🔍 DEBUG: File validation succeeded: $file" else echo "🔍 DEBUG: File validation failed: $file" ((total_errors++)) fi done # Summary echo "=" $(printf '=%.0s' {1..50}) echo "📈 SUMMARY:" echo " 📁 Total files checked: $total_files" if [[ $total_errors -eq 0 ]]; then echo -e " ✅ ${GREEN}All migration files are valid!${NC}" echo -e " 🎉 ${GREEN}All DDL operations include proper ON CLUSTER clause${NC}" else echo -e " ❌ ${RED}Found $total_errors file(s) with missing ON CLUSTER clauses${NC}" echo -e " 🛠️ ${YELLOW}Please add 'ON CLUSTER '\"'\"'{cluster}'\"'\"'' to all CREATE, DROP, ALTER, RENAME, and EXCHANGE statements${NC}" echo echo -e "${YELLOW}📖 Reference: https://clickhouse.com/docs/sql-reference/distributed-ddl${NC}" echo -e "${YELLOW}🔧 Example: CREATE TABLE my_table ON CLUSTER '{cluster}' (...);${NC}" EXIT_CODE=1 fi echo } # Run main function main "$@" exit $EXIT_CODE