#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" OUT_DIR="${DB_DIAGRAM_OUT_DIR:-${REPO_ROOT}/.artifacts/db-diagrams}" mkdir -p "$OUT_DIR" TABLES_TSV="${OUT_DIR}/tables.tsv" RELATIONS_TSV="${OUT_DIR}/relations.tsv" DOT_FILE="${OUT_DIR}/database-diagram.dot" SVG_FILE="${OUT_DIR}/database-diagram.svg" PNG_FILE="${OUT_DIR}/database-diagram.png" HTML_FILE="${OUT_DIR}/database-diagram.html" if ! command -v psql >/dev/null 2>&1; then echo "psql is required to generate the database diagram." >&2 exit 1 fi export PGCONNECT_TIMEOUT="${PGCONNECT_TIMEOUT:-5}" export PGHOST="${PGHOST:-localhost}" export PGPORT="${PGPORT:-5433}" export PGDATABASE="${PGDATABASE:-socialize}" export PGUSER="${PGUSER:-sa}" PSQL_ARGS=(-v ON_ERROR_STOP=1 -X -A -t -F $'\t') CONNECTION_LABEL="${PGHOST}:${PGPORT}/${PGDATABASE} as ${PGUSER}" if [[ -n "${DATABASE_URL:-}" ]]; then PSQL=(psql "${DATABASE_URL}" "${PSQL_ARGS[@]}") CONNECTION_LABEL="DATABASE_URL" elif docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "socialize-postgres"; then PSQL=(docker exec -i socialize-postgres psql -U "$PGUSER" -d "$PGDATABASE" "${PSQL_ARGS[@]}") CONNECTION_LABEL="Docker container socialize-postgres/${PGDATABASE} as ${PGUSER}" else PSQL=(psql "${PSQL_ARGS[@]}") fi echo "Reading schema from ${CONNECTION_LABEL}..." if ! "${PSQL[@]}" -c "select 1" >/dev/null 2>&1; then echo "Could not connect to PostgreSQL." >&2 echo "Start infrastructure first, then provide credentials with DATABASE_URL, PGPASSWORD, or ~/.pgpass." >&2 exit 1 fi "${PSQL[@]}" > "$TABLES_TSV" <<'SQL' SELECT c.table_schema || '.' || c.table_name AS table_id, c.column_name, c.udt_name, c.is_nullable, CASE WHEN pk.column_name IS NULL THEN '' ELSE 'PK' END AS key_type FROM information_schema.columns c LEFT JOIN ( SELECT kcu.table_schema, kcu.table_name, kcu.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON kcu.constraint_catalog = tc.constraint_catalog AND kcu.constraint_schema = tc.constraint_schema AND kcu.constraint_name = tc.constraint_name WHERE tc.constraint_type = 'PRIMARY KEY' ) pk ON pk.table_schema = c.table_schema AND pk.table_name = c.table_name AND pk.column_name = c.column_name WHERE c.table_schema = 'public' AND c.table_name <> '__EFMigrationsHistory' ORDER BY c.table_name, c.ordinal_position; SQL "${PSQL[@]}" > "$RELATIONS_TSV" <<'SQL' SELECT tc.constraint_name, kcu.table_schema || '.' || kcu.table_name AS child_table, kcu.column_name AS child_column, ccu.table_schema || '.' || ccu.table_name AS parent_table, ccu.column_name AS parent_column, col.is_nullable FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON kcu.constraint_catalog = tc.constraint_catalog AND kcu.constraint_schema = tc.constraint_schema AND kcu.constraint_name = tc.constraint_name JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_catalog = tc.constraint_catalog AND ccu.constraint_schema = tc.constraint_schema AND ccu.constraint_name = tc.constraint_name JOIN information_schema.columns col ON col.table_schema = kcu.table_schema AND col.table_name = kcu.table_name AND col.column_name = kcu.column_name WHERE tc.constraint_type = 'FOREIGN KEY' AND kcu.table_schema = 'public' AND kcu.table_name <> '__EFMigrationsHistory' AND ccu.table_schema = 'public' AND ccu.table_name <> '__EFMigrationsHistory' ORDER BY child_table, child_column, parent_table, parent_column; SQL html_escape() { local value="$1" value="${value//&/&}" value="${value///>}" value="${value//\"/"}" printf '%s' "$value" } node_id() { local value="$1" value="${value//./__}" value="${value//-/__}" printf '%s' "$value" } declare -A TABLE_COLUMNS declare -A TABLE_SEEN declare -a TABLE_ORDER while IFS=$'\t' read -r table_name column_name column_type nullable key_type; do [[ -n "$table_name" ]] || continue if [[ -z "${TABLE_SEEN[$table_name]:-}" ]]; then TABLE_SEEN["$table_name"]=1 TABLE_ORDER+=("$table_name") fi label="$(html_escape "$column_name")" type_label="$(html_escape "$column_type")" suffix="" [[ "$key_type" == "PK" ]] && suffix=" PK" [[ "$nullable" == "YES" ]] && suffix="${suffix} nullable" TABLE_COLUMNS["$table_name"]+=$' '"${label}"$''"${type_label}${suffix}"$'\n' done < "$TABLES_TSV" { cat <<'DOT' digraph database { graph [ rankdir=LR, bgcolor="white", pad=0.4, nodesep=0.6, ranksep=1.0, splines=ortho, overlap=false ]; node [ shape=plain, fontname="Arial" ]; edge [ color="#64748b", arrowsize=0.7, fontname="Arial", fontsize=10 ]; DOT for table_name in "${TABLE_ORDER[@]}"; do id="$(node_id "$table_name")" title="$(html_escape "${table_name#public.}")" printf ' %s [label=<\n' "$id" printf ' \n' printf ' \n' "$title" printf '%s' "${TABLE_COLUMNS[$table_name]}" printf '
%s
\n' printf ' >];\n\n' done while IFS=$'\t' read -r constraint_name child_table child_column parent_table parent_column nullable; do [[ -n "$constraint_name" ]] || continue child_id="$(node_id "$child_table")" parent_id="$(node_id "$parent_table")" label="$(html_escape "${child_column} -> ${parent_column}")" style="solid" [[ "$nullable" == "YES" ]] && style="dashed" printf ' %s -> %s [label="%s", style="%s"];\n' "$child_id" "$parent_id" "$label" "$style" done < "$RELATIONS_TSV" printf '}\n' } > "$DOT_FILE" if command -v dot >/dev/null 2>&1; then dot -Tsvg "$DOT_FILE" -o "$SVG_FILE" dot -Tpng "$DOT_FILE" -o "$PNG_FILE" else echo "Graphviz dot was not found; wrote only ${DOT_FILE}." >&2 fi cat > "$HTML_FILE" < Socialize Database Diagram

Socialize Database Diagram

SVG PNG DOT
Open database-diagram.svg or database-diagram.png from this folder.
HTML echo "Wrote:" echo " ${HTML_FILE}" [[ -f "$SVG_FILE" ]] && echo " ${SVG_FILE}" [[ -f "$PNG_FILE" ]] && echo " ${PNG_FILE}" echo " ${DOT_FILE}"