mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
feat(deploy): add deploy-verify-permissions recipe + upload/run deploy-server.sh before verification + run migrations in deploy
This commit is contained in:
210
scripts/generate-schema.py
Normal file
210
scripts/generate-schema.py
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate a clean schema.sql from the fully-migrated local database."""
|
||||
|
||||
import sqlite3
|
||||
import re
|
||||
import sys
|
||||
|
||||
DB_PATH = sys.argv[1] if len(sys.argv) > 1 else 'app/storage/xamxam.db'
|
||||
|
||||
db = sqlite3.connect(DB_PATH)
|
||||
|
||||
# Get all objects
|
||||
tables = db.execute(
|
||||
"SELECT name, sql FROM sqlite_master WHERE type='table' "
|
||||
"AND name NOT LIKE 'sqlite_%' AND name != '_migrations' ORDER BY name"
|
||||
).fetchall()
|
||||
|
||||
indexes = db.execute(
|
||||
"SELECT sql FROM sqlite_master WHERE type='index' "
|
||||
"AND name NOT LIKE 'sqlite_autoindex%' AND sql IS NOT NULL ORDER BY name"
|
||||
).fetchall()
|
||||
|
||||
views = db.execute(
|
||||
"SELECT sql FROM sqlite_master WHERE type='view' ORDER BY name"
|
||||
).fetchall()
|
||||
|
||||
triggers = db.execute(
|
||||
"SELECT sql FROM sqlite_master WHERE type='trigger' ORDER BY name"
|
||||
).fetchall()
|
||||
|
||||
print("-- ============================================================================")
|
||||
print("-- XAMXAM Database Schema — complete, fully migrated")
|
||||
print(f"-- Generated from local database on {db.execute('SELECT date()').fetchone()[0]}")
|
||||
print("-- All 28 migrations merged into this single file.")
|
||||
print("-- ============================================================================")
|
||||
print()
|
||||
|
||||
# We define the ideal CREATE TABLE statements by querying PRAGMA table_info
|
||||
# and reconstructing clean DDL, because SQLite stores ALTER TABLE artifacts.
|
||||
|
||||
def build_clean_table_ddl(name):
|
||||
"""Build a clean CREATE TABLE statement from PRAGMA table_info."""
|
||||
cols = db.execute(f"PRAGMA table_info('{name}')").fetchall()
|
||||
pk_cols = [c[1] for c in cols if c[5] > 0]
|
||||
|
||||
lines = [f"CREATE TABLE IF NOT EXISTS {name} ("]
|
||||
col_defs = []
|
||||
for cid, col_name, col_type, not_null, default, is_pk in cols:
|
||||
part = f" {col_name} {col_type}"
|
||||
if is_pk:
|
||||
if len(pk_cols) == 1:
|
||||
part += " PRIMARY KEY"
|
||||
if col_type.upper() == 'INTEGER':
|
||||
part += " AUTOINCREMENT"
|
||||
if not_null:
|
||||
part += " NOT NULL"
|
||||
if default is not None:
|
||||
if default.upper() in ('CURRENT_TIMESTAMP', 'CURRENT_TIME', 'CURRENT_DATE'):
|
||||
part += f" DEFAULT {default}"
|
||||
elif any(default.lower().startswith(fn + '(') for fn in ('datetime', 'date', 'time', 'strftime')):
|
||||
part += f" DEFAULT ({default})"
|
||||
elif default.startswith("'") or default.startswith('"'):
|
||||
part += f" DEFAULT {default}"
|
||||
else:
|
||||
part += f" DEFAULT {default}"
|
||||
col_defs.append(part)
|
||||
|
||||
# Multi-column PK
|
||||
if len(pk_cols) > 1:
|
||||
col_defs.append(f" PRIMARY KEY ({', '.join(pk_cols)})")
|
||||
|
||||
# Foreign keys
|
||||
fks = db.execute(f"PRAGMA foreign_key_list('{name}')").fetchall()
|
||||
for fk in fks:
|
||||
fk_id, seq, ref_table, from_col, to_col, on_update, on_delete, match = fk
|
||||
if seq == 0:
|
||||
col_defs.append(
|
||||
f" FOREIGN KEY ({from_col}) REFERENCES {ref_table}({to_col})"
|
||||
+ (f" ON DELETE {on_delete}" if on_delete and on_delete != 'NO ACTION' else "")
|
||||
)
|
||||
|
||||
lines.append(",\n".join(col_defs))
|
||||
lines.append(");")
|
||||
|
||||
# Add CHECK constraints if any from original DDL
|
||||
orig = db.execute(
|
||||
"SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (name,)
|
||||
).fetchone()
|
||||
if orig and orig[0]:
|
||||
for m in re.finditer(r'CHECK\s*\([^)]+\)', orig[0]):
|
||||
constraint = m.group(0)
|
||||
# INSERT OR IGNORE into site_settings uses ON CONFLICT — keep that pattern
|
||||
pass # CHECK is already in the column definition from PRAGMA
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# Sort tables in dependency order (rough)
|
||||
table_order = [
|
||||
'orientations', 'ap_programs', 'finality_types', 'languages',
|
||||
'format_types', 'tags', 'access_types', 'license_types',
|
||||
'authors', 'supervisors',
|
||||
'theses',
|
||||
'thesis_authors', 'thesis_supervisors', 'thesis_languages',
|
||||
'thesis_formats', 'thesis_tags', 'thesis_files',
|
||||
'site_settings', 'system_cache', 'pages', 'share_links',
|
||||
'smtp_settings', 'apropos_contents',
|
||||
'file_access_requests', 'file_access_tokens', 'file_access_sessions',
|
||||
'file_access_audit', 'form_help_blocks', 'admin_audit_log',
|
||||
'peertube_settings', 'audit_log',
|
||||
]
|
||||
|
||||
seen = set()
|
||||
for name in table_order:
|
||||
if name in seen:
|
||||
continue
|
||||
seen.add(name)
|
||||
try:
|
||||
ddl = build_clean_table_ddl(name)
|
||||
print(ddl)
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f"-- ERROR building DDL for {name}: {e}", file=sys.stderr)
|
||||
|
||||
print("-- ============================================================================")
|
||||
print("-- INDEXES")
|
||||
print("-- ============================================================================")
|
||||
print()
|
||||
|
||||
for (sql,) in indexes:
|
||||
clean = sql.strip()
|
||||
if not clean.endswith(';'):
|
||||
clean += ';'
|
||||
print(clean)
|
||||
print()
|
||||
|
||||
print("-- ============================================================================")
|
||||
print("-- VIEWS")
|
||||
print("-- ============================================================================")
|
||||
print()
|
||||
|
||||
for (sql,) in views:
|
||||
clean = sql.strip()
|
||||
if not clean.endswith(';'):
|
||||
clean += ';'
|
||||
print(clean)
|
||||
print()
|
||||
|
||||
print("-- ============================================================================")
|
||||
print("-- TRIGGERS")
|
||||
print("-- ============================================================================")
|
||||
print()
|
||||
|
||||
for (sql,) in triggers:
|
||||
clean = sql.strip()
|
||||
if not clean.endswith(';'):
|
||||
clean += ';'
|
||||
print(clean)
|
||||
print()
|
||||
|
||||
print("-- ============================================================================")
|
||||
print("-- SEED DATA (reference data + initial settings)")
|
||||
print("-- ============================================================================")
|
||||
print()
|
||||
|
||||
# Extract seed data from the local database
|
||||
# (table, query, explicit_column_names)
|
||||
seed_tables = [
|
||||
('orientations', "SELECT name FROM orientations", ['name']),
|
||||
('ap_programs', "SELECT name, code FROM ap_programs", ['name', 'code']),
|
||||
('finality_types', "SELECT name FROM finality_types", ['name']),
|
||||
('languages', "SELECT name FROM languages WHERE deleted_at IS NULL", ['name']),
|
||||
('format_types', "SELECT name, sort_order FROM format_types", ['name', 'sort_order']),
|
||||
('access_types', "SELECT name, description FROM access_types", ['name', 'description']),
|
||||
('license_types', "SELECT name FROM license_types", ['name']),
|
||||
('site_settings', "SELECT key, value FROM site_settings WHERE key IN ('access_type_interdit_enabled', 'access_type_interne_enabled', 'access_type_libre_enabled', 'objet_these_enabled', 'objet_frart_enabled', 'restricted_files_enabled', 'peertube_upload_enabled')", ['key', 'value']),
|
||||
('pages', "SELECT slug, title, content, is_published FROM pages WHERE slug IN ('charte', 'about', 'licenses')", ['slug', 'title', 'content', 'is_published']),
|
||||
('form_help_blocks', "SELECT key, name, content, enabled, sort_order FROM form_help_blocks", ['key', 'name', 'content', 'enabled', 'sort_order']),
|
||||
('apropos_contents', "SELECT key, value FROM apropos_contents WHERE key = 'contacts'", ['key', 'value']),
|
||||
]
|
||||
|
||||
for table, query, col_names in seed_tables:
|
||||
rows = db.execute(query).fetchall()
|
||||
if not rows:
|
||||
continue
|
||||
|
||||
for row in rows:
|
||||
vals = []
|
||||
for i, val in enumerate(row):
|
||||
if val is None:
|
||||
vals.append('NULL')
|
||||
elif isinstance(val, bool):
|
||||
vals.append('1' if val else '0')
|
||||
elif isinstance(val, (int, float)):
|
||||
vals.append(str(val))
|
||||
else:
|
||||
escaped = str(val).replace("'", "''")
|
||||
vals.append(f"'{escaped}'")
|
||||
|
||||
print(f"INSERT OR IGNORE INTO {table} ({', '.join(col_names)}) VALUES ({', '.join(vals)});")
|
||||
print()
|
||||
|
||||
# Singleton table inserts
|
||||
print("-- Singleton table placeholders")
|
||||
print("INSERT OR IGNORE INTO smtp_settings (id) VALUES (1);")
|
||||
print("INSERT OR IGNORE INTO peertube_settings (id) VALUES (1);")
|
||||
print()
|
||||
print("-- ============================================================================")
|
||||
print("-- END OF SCHEMA")
|
||||
print("-- ============================================================================")
|
||||
@@ -7,9 +7,18 @@
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
APP_DIR="$REPO_ROOT/app"
|
||||
SCHEMA="$APP_DIR/storage/schema.sql"
|
||||
PROD_DB="$APP_DIR/storage/xamxam.db"
|
||||
|
||||
# Detect layout: local dev has app/ subdir, server has files at repo root
|
||||
if [ -d "$REPO_ROOT/app/storage" ]; then
|
||||
SCHEMA="$REPO_ROOT/app/storage/schema.sql"
|
||||
PROD_DB="$REPO_ROOT/app/storage/xamxam.db"
|
||||
elif [ -f "$REPO_ROOT/storage/schema.sql" ]; then
|
||||
SCHEMA="$REPO_ROOT/storage/schema.sql"
|
||||
PROD_DB="$REPO_ROOT/storage/xamxam.db"
|
||||
else
|
||||
echo "ERROR: cannot find storage/schema.sql" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
init_db() {
|
||||
local db="$1"
|
||||
|
||||
Reference in New Issue
Block a user