479 lines
19 KiB
Python
479 lines
19 KiB
Python
"""capture the operational schema baseline
|
|
|
|
Revision ID: 0006_operational_schema_baseline
|
|
Revises: 0005_miner_inventory
|
|
Create Date: 2026-07-27
|
|
|
|
This migration adopts tables that older MinerOps builds created at runtime.
|
|
The IF NOT EXISTS clauses allow it to run against both an existing deployment
|
|
and a new database.
|
|
"""
|
|
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
|
|
|
|
revision: str = "0006_operational_schema_baseline"
|
|
down_revision: Union[str, None] = "0005_miner_inventory"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.execute(
|
|
"""
|
|
ALTER TABLE miners
|
|
ADD COLUMN IF NOT EXISTS manual_details JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
ADD COLUMN IF NOT EXISTS manual_details_updated_at TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS manual_details_updated_by TEXT,
|
|
ADD COLUMN IF NOT EXISTS psu_model TEXT,
|
|
ADD COLUMN IF NOT EXISTS customer_sn TEXT,
|
|
ADD COLUMN IF NOT EXISTS api3_supported BOOLEAN,
|
|
ADD COLUMN IF NOT EXISTS api3_api_version TEXT,
|
|
ADD COLUMN IF NOT EXISTS api3_api_switch TEXT,
|
|
ADD COLUMN IF NOT EXISTS api3_last_seen_at TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS api3_last_error TEXT,
|
|
ADD COLUMN IF NOT EXISTS configured_performance_mode TEXT,
|
|
ADD COLUMN IF NOT EXISTS configured_performance_mode_source TEXT,
|
|
ADD COLUMN IF NOT EXISTS configured_performance_mode_updated_at TIMESTAMPTZ;
|
|
|
|
ALTER TABLE miner_status_snapshots
|
|
ADD COLUMN IF NOT EXISTS ip_address INET,
|
|
ADD COLUMN IF NOT EXISTS mac_address TEXT,
|
|
ADD COLUMN IF NOT EXISTS fan_speed_in DOUBLE PRECISION,
|
|
ADD COLUMN IF NOT EXISTS fan_speed_out DOUBLE PRECISION,
|
|
ADD COLUMN IF NOT EXISTS elapsed_seconds DOUBLE PRECISION;
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS miner_maintenance_events (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
miner_id UUID NOT NULL REFERENCES miners(id) ON DELETE CASCADE,
|
|
event_type TEXT NOT NULL,
|
|
event_status TEXT NOT NULL DEFAULT 'completed',
|
|
event_title TEXT NOT NULL,
|
|
description TEXT,
|
|
event_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
old_mac TEXT,
|
|
new_mac TEXT,
|
|
old_ip INET,
|
|
new_ip INET,
|
|
component_type TEXT,
|
|
component_slot TEXT,
|
|
component_serial TEXT,
|
|
cost_amount NUMERIC(12, 2),
|
|
downtime_minutes INTEGER,
|
|
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_miner_maintenance_events_miner_id
|
|
ON miner_maintenance_events(miner_id);
|
|
CREATE INDEX IF NOT EXISTS idx_miner_maintenance_events_event_at
|
|
ON miner_maintenance_events(event_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_miner_maintenance_events_event_type
|
|
ON miner_maintenance_events(event_type);
|
|
|
|
CREATE TABLE IF NOT EXISTS location_positions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
room_id UUID REFERENCES rooms(id) ON DELETE SET NULL,
|
|
room_name TEXT NOT NULL,
|
|
rack_name TEXT NOT NULL,
|
|
shelf_name TEXT NOT NULL,
|
|
shelf_index INTEGER,
|
|
position_index INTEGER NOT NULL,
|
|
position_label TEXT,
|
|
position_kind TEXT NOT NULL DEFAULT 'miner_slot',
|
|
position_status TEXT NOT NULL DEFAULT 'available',
|
|
expected_mac TEXT,
|
|
miner_id UUID REFERENCES miners(id) ON DELETE SET NULL,
|
|
equipment_label TEXT,
|
|
notes TEXT,
|
|
source_file TEXT,
|
|
source_sheet TEXT,
|
|
source_cell TEXT,
|
|
raw_value TEXT,
|
|
conflict_reason TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
DO $$
|
|
BEGIN
|
|
ALTER TABLE location_positions
|
|
ADD CONSTRAINT uq_location_positions_slot
|
|
UNIQUE (room_name, rack_name, shelf_name, position_index);
|
|
EXCEPTION
|
|
WHEN duplicate_object OR duplicate_table THEN NULL;
|
|
END $$;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_location_positions_room
|
|
ON location_positions(room_name);
|
|
CREATE INDEX IF NOT EXISTS idx_location_positions_rack
|
|
ON location_positions(room_name, rack_name);
|
|
CREATE INDEX IF NOT EXISTS idx_location_positions_expected_mac
|
|
ON location_positions(expected_mac);
|
|
CREATE INDEX IF NOT EXISTS idx_location_positions_miner_id
|
|
ON location_positions(miner_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS miner_location_states (
|
|
miner_id UUID PRIMARY KEY REFERENCES miners(id) ON DELETE CASCADE,
|
|
asset_state TEXT NOT NULL DEFAULT 'waiting_placement',
|
|
location_position_id UUID REFERENCES location_positions(id) ON DELETE SET NULL,
|
|
state_notes TEXT,
|
|
updated_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_miner_location_states_asset_state
|
|
ON miner_location_states(asset_state);
|
|
|
|
CREATE TABLE IF NOT EXISTS location_import_issues (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
import_batch_id UUID NOT NULL,
|
|
issue_type TEXT NOT NULL,
|
|
severity TEXT NOT NULL DEFAULT 'warning',
|
|
message TEXT NOT NULL,
|
|
mac_address TEXT,
|
|
room_name TEXT,
|
|
rack_name TEXT,
|
|
shelf_name TEXT,
|
|
position_index INTEGER,
|
|
source_file TEXT,
|
|
source_cell TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
resolved_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_location_import_issues_batch
|
|
ON location_import_issues(import_batch_id);
|
|
CREATE INDEX IF NOT EXISTS idx_location_import_issues_mac
|
|
ON location_import_issues(mac_address);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS miner_command_events (
|
|
id TEXT PRIMARY KEY,
|
|
miner_id TEXT NOT NULL,
|
|
command_type TEXT NOT NULL,
|
|
command_status TEXT NOT NULL,
|
|
dry_run BOOLEAN NOT NULL DEFAULT true,
|
|
requested_by_user_id TEXT,
|
|
requested_by_username TEXT,
|
|
target_ip TEXT,
|
|
target_mac TEXT,
|
|
request_payload JSONB,
|
|
response_payload JSONB,
|
|
error_message TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
completed_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_miner_command_events_miner_id_created
|
|
ON miner_command_events(miner_id, created_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS miner_api_credentials (
|
|
miner_id TEXT PRIMARY KEY,
|
|
api_account TEXT NOT NULL DEFAULT 'super',
|
|
api_password TEXT NOT NULL,
|
|
password_source TEXT NOT NULL DEFAULT 'minerops',
|
|
updated_by_username TEXT,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS minerops_site_agents (
|
|
agent_id TEXT PRIMARY KEY,
|
|
site_name TEXT NOT NULL,
|
|
hostname TEXT,
|
|
version TEXT,
|
|
ip_address TEXT,
|
|
last_checkin_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
last_payload JSONB NOT NULL DEFAULT '{}'::jsonb
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS minerops_agent_snapshots (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
agent_id TEXT NOT NULL,
|
|
site_name TEXT NOT NULL,
|
|
snapshot_kind TEXT NOT NULL DEFAULT 'miner_poll',
|
|
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
miner_count INTEGER NOT NULL DEFAULT 0,
|
|
payload JSONB NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_minerops_agent_snapshots_received
|
|
ON minerops_agent_snapshots(received_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS minerops_agent_command_results (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
agent_id TEXT NOT NULL,
|
|
site_name TEXT NOT NULL,
|
|
command_id TEXT,
|
|
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
status TEXT NOT NULL,
|
|
payload JSONB NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_minerops_agent_command_results_received
|
|
ON minerops_agent_command_results(received_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS minerops_agent_command_queue (
|
|
id TEXT PRIMARY KEY,
|
|
agent_id TEXT NOT NULL,
|
|
command_type TEXT NOT NULL,
|
|
command_status TEXT NOT NULL DEFAULT 'pending',
|
|
requested_by_user_id TEXT,
|
|
requested_by_username TEXT,
|
|
request_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
result_payload JSONB,
|
|
error_message TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
claimed_at TIMESTAMPTZ,
|
|
completed_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_minerops_agent_command_queue_agent_status
|
|
ON minerops_agent_command_queue(agent_id, command_status, created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS minerops_agent_discoveries (
|
|
id TEXT PRIMARY KEY,
|
|
agent_id TEXT NOT NULL,
|
|
site_name TEXT NOT NULL,
|
|
room_name TEXT,
|
|
ip_address INET,
|
|
mac_address TEXT,
|
|
model TEXT,
|
|
hostname TEXT,
|
|
miner_sn TEXT,
|
|
raw_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
discovery_status TEXT NOT NULL DEFAULT 'new',
|
|
matched_miner_id UUID,
|
|
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_minerops_agent_discoveries_mac
|
|
ON minerops_agent_discoveries(mac_address);
|
|
CREATE INDEX IF NOT EXISTS idx_minerops_agent_discoveries_ip
|
|
ON minerops_agent_discoveries(ip_address);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS minerops_scheduled_bulk_actions (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT,
|
|
target_type TEXT NOT NULL,
|
|
target_value TEXT,
|
|
action TEXT NOT NULL DEFAULT 'performance_mode',
|
|
performance_mode TEXT,
|
|
repeat_rule TEXT NOT NULL DEFAULT 'once',
|
|
scheduled_for TIMESTAMPTZ NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
agent_id TEXT NOT NULL DEFAULT 'plexus-windows-agent',
|
|
requested_by_user_id TEXT,
|
|
requested_by_username TEXT,
|
|
queued_command_count INTEGER NOT NULL DEFAULT 0,
|
|
last_queued_command_count INTEGER NOT NULL DEFAULT 0,
|
|
run_count INTEGER NOT NULL DEFAULT 0,
|
|
last_error TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
processed_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_minerops_scheduled_bulk_actions_due
|
|
ON minerops_scheduled_bulk_actions(status, scheduled_for);
|
|
CREATE INDEX IF NOT EXISTS idx_minerops_scheduled_bulk_actions_created
|
|
ON minerops_scheduled_bulk_actions(created_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS miner_pool_snapshots (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
miner_id UUID,
|
|
ip_address INET,
|
|
mac_address TEXT,
|
|
model TEXT,
|
|
pool_index INTEGER,
|
|
url TEXT,
|
|
worker_user TEXT,
|
|
pool_status TEXT,
|
|
stratum_active BOOLEAN,
|
|
accepted DOUBLE PRECISION,
|
|
rejected DOUBLE PRECISION,
|
|
stale DOUBLE PRECISION,
|
|
get_failures DOUBLE PRECISION,
|
|
remote_failures DOUBLE PRECISION,
|
|
pool_rejected_percent DOUBLE PRECISION,
|
|
pool_stale_percent DOUBLE PRECISION,
|
|
last_share_time_epoch DOUBLE PRECISION,
|
|
raw_pool_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
agent_id TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
ALTER TABLE miner_pool_snapshots
|
|
ADD COLUMN IF NOT EXISTS model TEXT,
|
|
ADD COLUMN IF NOT EXISTS pool_index INTEGER,
|
|
ADD COLUMN IF NOT EXISTS url TEXT,
|
|
ADD COLUMN IF NOT EXISTS worker_user TEXT,
|
|
ADD COLUMN IF NOT EXISTS pool_status TEXT,
|
|
ADD COLUMN IF NOT EXISTS stratum_active BOOLEAN,
|
|
ADD COLUMN IF NOT EXISTS accepted DOUBLE PRECISION,
|
|
ADD COLUMN IF NOT EXISTS rejected DOUBLE PRECISION,
|
|
ADD COLUMN IF NOT EXISTS stale DOUBLE PRECISION,
|
|
ADD COLUMN IF NOT EXISTS get_failures DOUBLE PRECISION,
|
|
ADD COLUMN IF NOT EXISTS remote_failures DOUBLE PRECISION,
|
|
ADD COLUMN IF NOT EXISTS pool_rejected_percent DOUBLE PRECISION,
|
|
ADD COLUMN IF NOT EXISTS pool_stale_percent DOUBLE PRECISION,
|
|
ADD COLUMN IF NOT EXISTS last_share_time_epoch DOUBLE PRECISION,
|
|
ADD COLUMN IF NOT EXISTS raw_pool_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
ADD COLUMN IF NOT EXISTS agent_id TEXT;
|
|
|
|
DO $$
|
|
DECLARE
|
|
id_type TEXT;
|
|
BEGIN
|
|
SELECT data_type INTO id_type
|
|
FROM information_schema.columns
|
|
WHERE table_schema = current_schema()
|
|
AND table_name = 'miner_pool_snapshots'
|
|
AND column_name = 'id';
|
|
|
|
IF id_type = 'text' THEN
|
|
ALTER TABLE miner_pool_snapshots
|
|
ALTER COLUMN id SET DEFAULT (gen_random_uuid())::text;
|
|
ELSIF id_type = 'uuid' THEN
|
|
ALTER TABLE miner_pool_snapshots
|
|
ALTER COLUMN id SET DEFAULT gen_random_uuid();
|
|
END IF;
|
|
END $$;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_miner_pool_snapshots_miner_created
|
|
ON miner_pool_snapshots(miner_id, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_miner_pool_snapshots_ip_created
|
|
ON miner_pool_snapshots(ip_address, created_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS miner_api3_enrichment_snapshots (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
miner_id UUID,
|
|
agent_id TEXT,
|
|
site_name TEXT,
|
|
ip_address INET,
|
|
mac_address TEXT,
|
|
api_supported BOOLEAN,
|
|
api_error TEXT,
|
|
api_version TEXT,
|
|
api_switch TEXT,
|
|
model TEXT,
|
|
miner_sn TEXT,
|
|
power_sn TEXT,
|
|
psu_model TEXT,
|
|
device_info JSONB,
|
|
miner_setting JSONB,
|
|
miner_status JSONB,
|
|
fan_setting JSONB,
|
|
custom_data JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_miner_api3_enrichment_miner_created
|
|
ON miner_api3_enrichment_snapshots(miner_id, created_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS miner_api3_hashboard_snapshots (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
miner_id UUID,
|
|
ip_address INET,
|
|
mac_address TEXT,
|
|
slot INTEGER,
|
|
pcb_sn TEXT,
|
|
hash_average_ths DOUBLE PRECISION,
|
|
factory_hash_ths DOUBLE PRECISION,
|
|
freq DOUBLE PRECISION,
|
|
effective_chips INTEGER,
|
|
chip_temp_min_c DOUBLE PRECISION,
|
|
chip_temp_avg_c DOUBLE PRECISION,
|
|
chip_temp_max_c DOUBLE PRECISION,
|
|
raw_hashboard_json JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_miner_api3_hashboards_miner_created
|
|
ON miner_api3_hashboard_snapshots(miner_id, created_at DESC);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS miner_status_hourly_rollups (
|
|
miner_id UUID NOT NULL REFERENCES miners(id) ON DELETE CASCADE,
|
|
bucket_at TIMESTAMPTZ NOT NULL,
|
|
sample_count BIGINT NOT NULL,
|
|
avg_mhs_av DOUBLE PRECISION,
|
|
max_mhs_av DOUBLE PRECISION,
|
|
avg_power_w DOUBLE PRECISION,
|
|
max_chip_temp_c DOUBLE PRECISION,
|
|
avg_chip_temp_c DOUBLE PRECISION,
|
|
avg_pool_rejected_percent DOUBLE PRECISION,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (miner_id, bucket_at)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_miner_status_hourly_bucket
|
|
ON miner_status_hourly_rollups(bucket_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS miner_status_daily_rollups (
|
|
miner_id UUID NOT NULL REFERENCES miners(id) ON DELETE CASCADE,
|
|
bucket_date DATE NOT NULL,
|
|
sample_count BIGINT NOT NULL,
|
|
avg_mhs_av DOUBLE PRECISION,
|
|
max_mhs_av DOUBLE PRECISION,
|
|
avg_power_w DOUBLE PRECISION,
|
|
max_chip_temp_c DOUBLE PRECISION,
|
|
avg_chip_temp_c DOUBLE PRECISION,
|
|
avg_pool_rejected_percent DOUBLE PRECISION,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (miner_id, bucket_date)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_miner_status_daily_bucket
|
|
ON miner_status_daily_rollups(bucket_date);
|
|
|
|
CREATE TABLE IF NOT EXISTS minerops_retention_runs (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
mode TEXT NOT NULL,
|
|
raw_status_retention_days INTEGER NOT NULL,
|
|
hourly_rollup_rows_before BIGINT NOT NULL DEFAULT 0,
|
|
hourly_rollup_rows_after BIGINT NOT NULL DEFAULT 0,
|
|
daily_rollup_rows_before BIGINT NOT NULL DEFAULT 0,
|
|
daily_rollup_rows_after BIGINT NOT NULL DEFAULT 0,
|
|
raw_status_rows_before BIGINT NOT NULL DEFAULT 0,
|
|
raw_status_rows_after BIGINT NOT NULL DEFAULT 0,
|
|
raw_status_rows_eligible_delete BIGINT NOT NULL DEFAULT 0,
|
|
raw_status_rows_deleted BIGINT NOT NULL DEFAULT 0,
|
|
started_at TIMESTAMPTZ NOT NULL,
|
|
finished_at TIMESTAMPTZ NOT NULL,
|
|
notes TEXT
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_minerops_retention_runs_started
|
|
ON minerops_retention_runs(started_at DESC);
|
|
"""
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
raise RuntimeError(
|
|
"0006 adopts pre-existing operational tables and is intentionally irreversible"
|
|
)
|