Created
July 18, 2025 02:53
-
-
Save hiendinhngoc/c94f96a590d177c08a947744202052e2 to your computer and use it in GitHub Desktop.
Export data
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require 'pg' | |
require 'fileutils' | |
require 'json' | |
require 'set' | |
RESERVED_WORDS = { | |
"column" => "\"column\"", | |
"default" => "\"default\"", | |
"type" => "\"type\"", | |
"not" => "\"not\"", | |
"null" => "\"null\"", | |
"check" => "\"check\"", | |
"unique" => "\"unique\"", | |
"primary" => "\"primary\"", | |
"authorization" => "\"authorization\"", | |
"group" => "\"group\"", | |
} | |
MODEL_DEFINITIONS = { | |
'organizations' => { | |
pk: 'id', | |
has_one: [ | |
{ related_table: 'custom_domain_setups', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'sportdata_suspensions', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'organization_settings', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'club_infos', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'containers', fk_in_related_table: 'menu_organization_id' }, | |
{ related_table: 'org_websites', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'game_sheet_apis', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'transient_org_themes', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'short_name_prefix_values', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'org_ui_settings', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'org_family_configuration_settings', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'athlete_global_ranking_configs', fk_in_related_table: 'organization_id' }, | |
], | |
has_many: [ | |
{ related_table: 'registrations', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'discount_codes', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'micro_service_requests', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'installment_plans', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'membership_registration_fees', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'membership_registration_fee_items', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'organization_people', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'chargebacks', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'season_delegations', fk_in_related_table: 'delegation_organization_id' }, | |
{ related_table: 'questions', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'sub_questions', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'organizations_sports', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'organization_social_networks', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'builder_variations', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'organization_type_definitions', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'events', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'organization_types', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'media_galleries', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'account_statement_exportable_questions', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'payments', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'installment_plan_subscriptions', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'rsportz_fee_invoices', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'multi_event_discount_schemes', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'membership_subscription_activations', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'organization_teams', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'locations', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'organization_locations', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'competitions', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'seasons', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'delegations', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'divisions', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'skill_levels', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'age_groups', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'orders', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'order_items', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'transactions', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'recurring_payments', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'suspension_rules', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'organization_certifications', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'organization_certification_providers', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'bulk_emails', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'ad_banners', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'fees', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'membership_options', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'membership_subscriptions', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'file_assets', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'page_inserts', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'sidebar_inserts', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'org_event_tags', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'org_groupings', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'org_group_items', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'competition_groupings', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'game_result_loaders', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'widget_inserts', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'layout_row_org_settings', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'page_layout_org_settings', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'stripe_notifications', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'membership_levels', fk_in_related_table: 'organization_id' }, | |
{ | |
related_table: 'metric_assignments', | |
fk_in_related_table: 'assignment_id', | |
polymorphic: { type_column: 'assignment_type', type_value: 'Organization' } | |
}, | |
{ related_table: 'org_pages', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'waivers', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'org_integrations', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'org_subscriptions', fk_in_related_table: 'organization_id'}, | |
# { related_table: 'stripe_payment_accounts', fk_in_related_table: 'owned_organization_id' }, | |
{ related_table: 'achievement_categories', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'organization_person_for_child_orgs', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'notification_assignments', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'account_statement_exportable_spec_attrs', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'org_api_users_organizations', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'org_fifa_duplicates', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'team_tags', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'proof_of_payments', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'org_sport_settings', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'custom_reports', fk_in_related_table: 'organization_id' }, | |
{ related_table: 'account_stripe_payments', fk_in_related_table: 'organization_id' }, | |
], | |
belongs_to: [ | |
{ related_table: 'membership_levels', fk_in_this_table: 'membership_level_id' }, | |
{ related_table: 'questions', fk_in_this_table: 'selected_question_id' }, | |
{ related_table: 'containers', fk_in_this_table: 'footer_container_id' }, | |
{ related_table: 'containers', fk_in_this_table: 'header_container_id' }, | |
{ related_table: 'containers', fk_in_this_table: 'page_insert_container_id' }, | |
{ related_table: 'locale_validate_formats', fk_in_this_table: 'locale_number_format_id' }, | |
{ related_table: 'locale_display_formats', fk_in_this_table: 'short_date_format_id' }, | |
{ related_table: 'locale_display_formats', fk_in_this_table: 'long_date_format_id' }, | |
{ related_table: 'stripe_payment_accounts', fk_in_this_table: 'stripe_payment_account_id' }, | |
{ related_table: 'organizations', fk_in_this_table: 'ancestor1_id' }, | |
{ related_table: 'organizations', fk_in_this_table: 'ancestor2_id' }, | |
{ related_table: 'organizations', fk_in_this_table: 'ancestor3_id' }, | |
{ related_table: 'organizations', fk_in_this_table: 'ancestor4_id' }, | |
{ related_table: 'organizations', fk_in_this_table: 'ancestor5_id' }, | |
], | |
}, | |
'metric_assignments' => { | |
pk: 'id', | |
}, | |
'people' => { | |
pk: 'id', | |
has_one: [ | |
{ related_table: 'iawa_registrations', fk_in_related_table: 'person_id' }, | |
{ related_table: 'accounts', fk_in_related_table: 'person_id' }, | |
{ related_table: 'schedule_views', fk_in_related_table: 'person_id' }, | |
{ related_table: 'super_admin_permissions', fk_in_related_table: 'person_id' }, | |
{ related_table: 'person_suspensions', fk_in_related_table: 'person_id' }, | |
{ related_table: 'removed_people', fk_in_related_table: 'person_id' }, | |
{ related_table: 'person_general_information', fk_in_related_table: 'person_id' }, | |
{ related_table: 'person_medicals', fk_in_related_table: 'person_id' }, | |
{ related_table: 'person_additional_contacts', fk_in_related_table: 'person_id' }, | |
{ related_table: 'person_emergency_contacts', fk_in_related_table: 'person_id' }, | |
{ related_table: 'person_team_contacts', fk_in_related_table: 'person_id' }, | |
{ related_table: 'person_equipment', fk_in_related_table: 'person_id' }, | |
{ related_table: 'person_identification', fk_in_related_table: 'person_id' }, | |
{ related_table: 'person_martial_arts', fk_in_related_table: 'person_id' }, | |
{ related_table: 'person_screening', fk_in_related_table: 'person_id' }, | |
{ related_table: 'person_skills_credentials', fk_in_related_table: 'person_id' }, | |
{ related_table: 'person_fifa_registration', fk_in_related_table: 'person_id' }, | |
], | |
has_many: [ | |
{ related_table: 'registration_groups', fk_in_related_table: 'person_id' }, | |
{ related_table: 'season_delegation_registrants', fk_in_related_table: 'person_id' }, | |
{ related_table: 'child_transfer_requests', fk_in_related_table: 'child_person_id' }, | |
{ related_table: 'division_people', fk_in_related_table: 'person_id' }, | |
{ related_table: 'attendees', fk_in_related_table: 'person_id' }, | |
{ related_table: 'player_statistics', fk_in_related_table: 'person_id' }, | |
{ related_table: 'game_actions', fk_in_related_table: 'person_id' }, | |
{ related_table: 'organization_alert_people', fk_in_related_table: 'person_id' }, | |
{ related_table: 'team_roster_invitations', fk_in_related_table: 'person_id' }, | |
{ related_table: 'profile_pages', fk_in_related_table: 'person_id' }, | |
{ related_table: 'people_placements', fk_in_related_table: 'person_id' }, | |
{ related_table: 'unverified_achievements', fk_in_related_table: 'person_id' }, | |
{ related_table: 'person_sport_details', fk_in_related_table: 'person_id' }, | |
{ related_table: 'person_profile_videos', fk_in_related_table: 'person_id' }, | |
{ related_table: 'delegation_officials', fk_in_related_table: 'person_id' }, | |
{ related_table: 'delegation_admins', fk_in_related_table: 'person_id' }, | |
{ related_table: 'scoring_actions', fk_in_related_table: 'person_id' }, | |
{ related_table: 'membership_certifications', fk_in_related_table: 'person_id' }, | |
{ related_table: 'bulk_member_invites', fk_in_related_table: 'person_id' }, | |
{ related_table: 'certification_expiration_update_logs', fk_in_related_table: 'person_id' }, | |
{ related_table: 'system_alert_dismissed_people', fk_in_related_table: 'person_id' }, | |
{ related_table: 'people_fifa_duplicates', fk_in_related_table: 'person_id' }, | |
{ related_table: 'ncsa_recruit_profiles', fk_in_related_table: 'person_id' }, | |
{ related_table: 'person_blocked_mobiles', fk_in_related_table: 'person_id' }, | |
{ related_table: 'organization_people', fk_in_related_table: 'person_id' }, | |
# Children relationships | |
{ related_table: 'people', fk_in_related_table: 'parent_id' }, | |
], | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'parent_id' }, | |
], | |
}, | |
'locale_display_formats' => { | |
pk: 'id', | |
}, | |
'locale_validate_formats' => { | |
pk: 'id', | |
}, | |
'custom_reports' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
}, | |
'org_sport_settings' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'sports', fk_in_this_table: 'sport_id' }, | |
], | |
has_many: [ | |
{ related_table: 'org_sport_achievement_levels', fk_in_related_table: 'org_sport_setting_id' }, | |
] | |
}, | |
'proof_of_payments' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'people', fk_in_this_table: 'person_id' }, | |
], | |
}, | |
'team_tags' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
has_many: [ | |
{ related_table: 'team_tag_standings', fk_in_related_table: 'team_tag_id' }, | |
] | |
}, | |
'org_fifa_duplicates' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
}, | |
'org_api_users_organizations' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'org_api_users', fk_in_this_table: 'org_api_user_id' }, | |
], | |
}, | |
'organizations_sports' => { | |
pk: 'organization_id,sport_id', # Composite primary key | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'sports', fk_in_this_table: 'sport_id' }, | |
], | |
}, | |
'account_statement_exportable_spec_attrs' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
}, | |
'notification_assignments' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'notifications', fk_in_this_table: 'notification_id' }, | |
], | |
}, | |
'organization_person_for_child_orgs' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
}, | |
'achievement_categories' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'sports', fk_in_this_table: 'sport_id' }, | |
], | |
has_many: [ | |
{ related_table: 'organization_person_achievement_levels', fk_in_related_table: 'category_id' }, | |
] | |
}, | |
'stripe_payment_accounts' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'owned_organization_id' }, | |
{ related_table: 'people', fk_in_this_table: 'current_org_admin_id' }, | |
], | |
has_many: [ | |
{ related_table: 'stripe_payment_owners', fk_in_related_table: 'stripe_payment_account_id' }, | |
{ related_table: 'account_statements', fk_in_related_table: 'stripe_payment_account_id' }, | |
{ | |
related_table: 'organization_alerts', | |
fk_in_related_table: 'reference_id', | |
polymorphic: { type_column: 'reference_type', type_value: 'StripePaymentAccount' } | |
}, | |
], | |
}, | |
'org_integrations' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
}, | |
'waivers' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
] | |
}, | |
'org_pages' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'containers', fk_in_this_table: 'body_container_id' }, | |
], | |
has_one: [] | |
}, | |
'membership_levels' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
has_many: [] | |
}, | |
'stripe_notifications' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'organization_alerts', fk_in_this_table: 'organization_alert_id' }, | |
], | |
}, | |
'page_layout_org_settings' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'page_layouts', fk_in_this_table: 'page_layout_id' }, | |
], | |
}, | |
'layout_row_org_settings' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'containers', fk_in_this_table: 'row_id' }, | |
], | |
}, | |
'widget_inserts' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'containers', fk_in_this_table: 'widget_container_id' }, | |
], | |
}, | |
'game_result_loaders' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
}, | |
'competition_groupings' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
has_many: [ | |
{ related_table: 'competition_groups', fk_in_related_table: 'grouping_id' }, | |
] | |
}, | |
'org_group_items' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'org_groups', fk_in_this_table: 'org_group_id' }, | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
}, | |
'org_groupings' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
has_many: [ | |
{ related_table: 'org_groups', fk_in_related_table: 'grouping_id' }, | |
] | |
}, | |
'org_event_tags' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
has_many: [ | |
{ related_table: 'event_taggings', fk_in_related_table: 'org_event_tag_id' }, | |
] | |
}, | |
'sidebar_inserts' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
}, | |
'page_inserts' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
}, | |
'file_assets' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'people', fk_in_this_table: 'author_id' }, | |
], | |
has_many: [ | |
{ related_table: 'widget_file_assets', fk_in_related_table: 'file_asset_id' }, | |
{ | |
related_table: 'media_gallery_assets', | |
fk_in_related_table: 'asset_id', | |
polymorphic: { type_column: 'asset_type', type_value: 'FileAsset' } | |
}, | |
] | |
}, | |
'membership_subscriptions' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'memberships', fk_in_this_table: 'membership_id' }, | |
], | |
has_many: [ | |
{ | |
related_table: 'waiver_registrations', | |
fk_in_related_table: 'registerable_id', | |
polymorphic: { type_column: 'registerable_type', type_value: 'MembershipSubscription' } | |
}, | |
{ | |
related_table: 'question_answers', | |
fk_in_related_table: 'resource_id', | |
polymorphic: { type_column: 'resource_type', type_value: 'MembershipSubscription' } | |
}, | |
{ | |
related_table: 'sub_question_answers', | |
fk_in_related_table: 'resource_id', | |
polymorphic: { type_column: 'resource_type', type_value: 'MembershipSubscription' } | |
}, | |
{ | |
related_table: 'activity_logs', | |
fk_in_related_table: 'resource_id', | |
polymorphic: { type_column: 'resource_type', type_value: 'MembershipSubscription' } | |
}, | |
{ | |
related_table: 'activity_logs', | |
fk_in_related_table: 'sub_resource_id', | |
polymorphic: { type_column: 'sub_resource_type', type_value: 'MembershipSubscription' } | |
}, | |
{ | |
related_table: 'stripe_checkouts', | |
fk_in_related_table: 'product_id', | |
polymorphic: { type_column: 'product_type', type_value: 'MembershipSubscription' } | |
}, | |
{ related_table: 'membership_certification_subscriptions', fk_in_related_table: 'membership_subscription_id' }, | |
{ related_table: 'alternate_owners', fk_in_related_table: 'membership_subscription_id' }, | |
{ related_table: 'aau_membership_details', fk_in_related_table: 'membership_subscription_id' } | |
] | |
}, | |
'membership_options' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
has_many: [ | |
{ related_table: 'membership_option_certifications', fk_in_related_table: 'membership_option_id' }, | |
{ related_table: 'membership_option_required_membership_options', fk_in_related_table: 'membership_option_id' }, | |
{ related_table: 'membership_option_organization_type_definitions', fk_in_related_table: 'membership_option_id' }, | |
{ related_table: 'membership_option_restricted_organization_type_definitions', fk_in_related_table: 'membership_option_id' }, | |
{ related_table: 'recurring_payment_plans', fk_in_related_table: 'membership_option_id' }, | |
# { related_table: 'colombian_receipt_membership_fields', fk_in_related_table: 'membership_option_id' }, | |
{ | |
related_table: 'activity_logs', | |
fk_in_related_table: 'resource_id', | |
polymorphic: { type_column: 'resource_type', type_value: 'MembershipOption' } | |
}, | |
{ | |
related_table: 'form_data_assignments', | |
fk_in_related_table: 'registerable_id', | |
polymorphic: { type_column: 'registerable_type', type_value: 'MembershipOption' } | |
}, | |
], | |
has_one: [ | |
{ related_table: 'membership_option_expiration_schemes', fk_in_related_table: 'membership_option_id' }, | |
{ related_table: 'fifa_registrations', fk_in_related_table: 'membership_option_id' }, | |
{ | |
related_table: 'text_instructions', | |
fk_in_related_table: 'textable_id', | |
polymorphic: { type_column: 'textable_type', type_value: 'MembershipOption' } | |
}, | |
] | |
}, | |
'fees' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
has_many: [ | |
{ related_table: 'question_options', fk_in_related_table: 'fee_id' } | |
] | |
}, | |
'ad_banners' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
] | |
}, | |
'bulk_emails' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
], | |
}, | |
'organization_certification_providers' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
] | |
}, | |
'organization_certifications' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'certifications', fk_in_this_table: 'certification_id' }, | |
], | |
has_many: [ | |
{ related_table: 'membership_option_certifications', fk_in_related_table: 'organization_certification_id' }, | |
], | |
has_one: [ | |
{ | |
related_table: 'custom_cards', | |
fk_in_related_table: 'resource_id', | |
polymorphic: { type_column: 'resource_type', type_value: 'OrganizationCertification' }, | |
}, | |
] | |
}, | |
'suspension_rules' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'game_rules', fk_in_this_table: 'game_rule_id' } | |
] | |
}, | |
'recurring_payments' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
], | |
}, | |
'transactions' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'billing_infos', fk_in_this_table: 'billing_info_id' }, | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
], | |
has_one: [ | |
{ related_table: 'charge_refunds', fk_in_related_table: 'refund_transaction_id' }, | |
], | |
has_many: [ | |
{ related_table: 'transaction_fees', fk_in_related_table: 'transaction_id' }, | |
{ related_table: 'charge_refunds', fk_in_related_table: 'charge_transaction_id' }, | |
{ related_table: 'account_statements', fk_in_related_table: 'transaction_id' }, | |
{ related_table: 'refunded_order_items', fk_in_related_table: 'transaction_id' } | |
] | |
}, | |
'order_items' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'question_answers', fk_in_this_table: 'question_answer_id' }, | |
{ related_table: 'people', fk_in_this_table: 'marked_by_id' }, | |
{ related_table: 'installment_plan_subscription_phases', fk_in_this_table: 'installment_plan_subscription_phase_id' }, | |
], | |
}, | |
'orders' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'organizations', fk_in_this_table: 'payer_org_id' }, | |
{ related_table: 'organizations', fk_in_this_table: 'parent_id' }, | |
], | |
has_many: [ | |
], | |
}, | |
'divisions' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'sports', fk_in_this_table: 'sport_id' }, | |
{ related_table: 'division_structs', fk_in_this_table: 'division_struct_id' }, | |
{ related_table: 'fee_generators', fk_in_this_table: 'fee_generator_id' }, | |
{ related_table: 'fee_generators', fk_in_this_table: 'team_fee_generator_id' }, | |
], | |
has_many: [ | |
{ related_table: 'division_teams', fk_in_related_table: 'division_id' }, | |
{ related_table: 'games', fk_in_related_table: 'division_id' }, | |
{ related_table: 'division_pool_names', fk_in_related_table: 'division_id' }, | |
], | |
has_one: [ | |
{ related_table: 'division_race_settings', fk_in_related_table: 'division_id' }, | |
{ related_table: 'division_race_standings', fk_in_related_table: 'division_id' }, | |
{ related_table: 'race_heat_configs', fk_in_related_table: 'division_id' }, | |
{ related_table: 'custom_podia', fk_in_related_table: 'division_id' }, | |
{ | |
related_table: 'text_instructions', | |
fk_in_related_table: 'textable_id', | |
polymorphic: { type_column: 'textable_type', type_value: 'Division' } | |
}, | |
] | |
}, | |
'org_subscriptions' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'org_subscription_plans', fk_in_this_table: 'stripe_plan_number' } | |
], | |
has_many: [ | |
] | |
}, | |
'age_groups' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
], | |
}, | |
'skill_levels' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
], | |
}, | |
'delegations' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
], | |
has_many: [ | |
{ | |
related_table: 'photos', | |
fk_in_related_table: 'photographed_id', | |
polymorphic: { type_column: 'photographed_type', type_value: 'Delegation' } | |
}, | |
] | |
}, | |
'seasons' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
has_many: [ | |
{ | |
related_table: 'form_data_assignments', | |
fk_in_related_table: 'registerable_id', | |
polymorphic: { type_column: 'registerable_type', type_value: 'Season' } | |
}, | |
{ | |
related_table: 'waiver_registrations', | |
fk_in_related_table: 'registerable_id', | |
polymorphic: { type_column: 'registerable_type', type_value: 'Season' } | |
}, | |
{ related_table: 'season_checkin_custom_slices', fk_in_related_table: 'season_id' }, | |
{ related_table: 'ossd_surface_auths', fk_in_related_table: 'season_id' }, | |
{ related_table: 'accreditation_data_sets', fk_in_related_table: 'season_id' }, | |
{ related_table: 'accreditation_one_offs', fk_in_related_table: 'season_id' }, | |
{ related_table: 'season_official_roles', fk_in_related_table: 'season_id' }, | |
{ related_table: 'accreditation_card_backgrounds', fk_in_related_table: 'season_id' }, | |
{ related_table: 'season_orders', fk_in_related_table: 'season_id' }, | |
{ related_table: 'tournament_permissions', fk_in_related_table: 'season_id' }, | |
{ related_table: 'event_video_feeds', fk_in_related_table: 'season_id' }, | |
{ related_table: 'facility_preferences', fk_in_related_table: 'season_id' }, | |
{ | |
related_table: 'news_page_tags', | |
fk_in_related_table: 'news_page_taggable_id', | |
polymorphic: { type_column: 'news_page_taggable_type', type_value: 'Season' } | |
}, | |
{ related_table: 'last_bulk_movements', fk_in_related_table: 'season_id' }, | |
{ related_table: 'season_schedule_runs', fk_in_related_table: 'season_id' } | |
], | |
has_one: [ | |
{ related_table: 'season_registrations', fk_in_related_table: 'season_id' }, | |
{ related_table: 'season_checkin_configurations', fk_in_related_table: 'season_id' }, | |
{ related_table: 'custom_cards', fk_in_related_table: 'resource_id', polymorphic: { type_column: 'resource_type', type_value: 'Season' } }, | |
{ related_table: 'accreditation_settings', fk_in_related_table: 'season_id' }, | |
{ related_table: 'race_medal_standings_configs', fk_in_related_table: 'season_id' }, | |
{ related_table: 'season_official_fee_calculations', fk_in_related_table: 'season_id' } | |
], | |
}, | |
'competitions' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'sports', fk_in_this_table: 'sport_id' } | |
], | |
has_many: [ | |
{ | |
related_table: 'competition_group_items', | |
fk_in_related_table: 'competition_id', | |
}, | |
{ | |
related_table: 'child_team_categories', | |
fk_in_related_table: 'competition_id', | |
}, | |
] | |
}, | |
'organization_locations' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
] | |
}, | |
'locations' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
has_many: [ | |
{ related_table: 'games', fk_in_related_table: 'facility_id' }, | |
{ related_table: 'scorekeeper_assignments', fk_in_related_table: 'location_id' } | |
] | |
}, | |
'organization_teams' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'teams', fk_in_this_table: 'team_id' } | |
] | |
}, | |
'membership_subscription_activations' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'recurring_payment_plans', fk_in_this_table: 'recurring_payment_plan_id' }, | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
], | |
has_many: [ | |
{ related_table: 'membership_subscription_activation_invoices', fk_in_related_table: 'membership_subscription_activation_id' }, | |
{ | |
related_table: 'activity_logs', | |
fk_in_related_table: 'resource_id', | |
polymorphic: { type_column: 'resource_type', type_value: 'MembershipSubscriptionActivation' }, | |
}, | |
{ | |
related_table: 'question_answers', | |
fk_in_related_table: 'sub_resource_id', | |
polymorphic: { type_column: 'sub_resource_type', type_value: 'MembershipSubscriptionActivation' } | |
}, | |
] | |
}, | |
'multi_event_discount_schemes' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
] | |
}, | |
'rsportz_fee_invoices' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'people', fk_in_this_table: 'marked_by_id' } | |
], | |
}, | |
'installment_plan_subscriptions' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
has_many: [ | |
{ related_table: 'installment_plan_subscription_phases', fk_in_related_table: 'installment_plan_subscription_id' }, | |
] | |
}, | |
'payments' => { | |
pk: 'id', | |
belongs_to: [ | |
], | |
}, | |
'account_statement_exportable_questions' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
}, | |
'media_galleries' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
], | |
has_many: [ | |
{ related_table: 'media_gallery_assets', fk_in_related_table: 'media_gallery_id' }, | |
{ related_table: 'media_gallery_widget_usages', fk_in_related_table: 'media_gallery_id' } | |
], | |
}, | |
'organization_types' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
}, | |
'events' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'facilities', fk_in_this_table: 'facility_id' }, | |
], | |
has_many: [ | |
{ related_table: 'event_taggings', fk_in_related_table: 'event_id' } | |
] | |
}, | |
'organization_type_definitions' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
], | |
has_many: [ | |
] | |
}, | |
'builder_variations' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'division_builders', fk_in_this_table: 'division_builder_id' } | |
], | |
has_many: [ | |
{ related_table: 'builder_segments', fk_in_related_table: 'builder_variation_id' } | |
] | |
}, | |
'organization_social_networks' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
], | |
has_many: [ | |
{ related_table: 'question_answers', fk_in_related_table: 'resource_id', polymorphic: { type_column: 'resource_type', type_value: 'OrganizationSocialNetwork' } } | |
] | |
}, | |
'sub_questions' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
], | |
has_many: [ | |
{ | |
related_table: 'sub_question_options', | |
fk_in_related_table: 'sub_question_id', | |
class_name: 'SubQuestionOption' | |
}, | |
{ | |
related_table: 'sub_question_answers', | |
fk_in_related_table: 'resource_id', | |
polymorphic: { type_column: 'resource_type', type_value: 'SubQuestion' } | |
} | |
] | |
}, | |
'questions' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
], | |
has_many: [ | |
{ related_table: 'question_options', fk_in_related_table: 'question_id' }, | |
{ | |
related_table: 'question_answers', | |
fk_in_related_table: 'resource_id', | |
polymorphic: { type_column: 'resource_type', type_value: 'Question' } | |
} | |
], | |
has_one: [ | |
{ related_table: 'question_answer_notifications', fk_in_related_table: 'question_id' } | |
] | |
}, | |
'season_delegations' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'delegation_organization_id', class_name: 'Organization' }, | |
], | |
has_many: [ | |
{ related_table: 'question_answers', fk_in_related_table: 'resource_id', polymorphic: { type_column: 'resource_type', type_value: 'SeasonDelegation' } }, | |
{ related_table: 'division_teams', fk_in_related_table: 'season_delegation_id' }, | |
{ related_table: 'season_delegation_registrants', fk_in_related_table: 'season_delegation_id' }, | |
{ related_table: 'sub_question_answers', fk_in_related_table: 'resource_id', polymorphic: { type_column: 'resource_type', type_value: 'SeasonDelegation' } }, | |
{ related_table: 'season_orders', fk_in_related_table: 'season_delegation_id' }, | |
{ related_table: 'season_delegation_standings', fk_in_related_table: 'season_delegation_id' }, | |
], | |
}, | |
'chargebacks' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
], | |
has_many: [ | |
{ related_table: 'chargeback_evidence_attachments', fk_in_related_table: 'chargeback_id' } | |
] | |
}, | |
'membership_registration_fee_items' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
] | |
}, | |
'membership_registration_fees' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
], | |
}, | |
'installment_plans' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
], | |
has_many: [ | |
{ related_table: 'installment_plan_phases', fk_in_related_table: 'installment_plan_id', class_name: 'InstallmentPlanPhase' }, | |
] | |
}, | |
'discount_codes' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
], | |
has_many: [ | |
{ related_table: 'redeemed_discount_codes', fk_in_related_table: 'discount_code_id' } | |
] | |
}, | |
'micro_service_requests' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
], | |
has_many: [ | |
{ related_table: 'micro_service_athletes', fk_in_related_table: 'micro_service_request_id' } | |
] | |
}, | |
'organization_people' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' }, | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'memberships', fk_in_this_table: 'membership_id'} | |
] | |
}, | |
'custom_domain_setups' => { pk: 'id', belongs_to: [{ related_table: 'organizations', fk_in_this_table: 'organization_id'}] }, | |
'sportdata_suspensions' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id'}, | |
{ related_table: 'people', fk_in_this_table: 'updated_by_id'} | |
] | |
}, | |
'organization_settings' => { pk: 'id', belongs_to: [{ related_table: 'organizations', fk_in_this_table: 'organization_id'}] }, | |
'club_infos' => { | |
pk: 'id', | |
has_many: [ | |
{ related_table: 'alternate_owners', fk_in_related_table: 'club_info_id' }, | |
{ related_table: 'club_info_groupings', fk_in_related_table: 'club_info_id' }, | |
{ related_table: 'question_answers', | |
fk_in_related_table: 'resource_id', | |
polymorphic: { | |
type_column: 'resource_type', | |
type_value: 'ClubInfo' | |
} | |
}, | |
{ related_table: 'sub_question_answers', | |
fk_in_related_table: 'resource_id', | |
polymorphic: { | |
type_column: 'resource_type', | |
type_value: 'ClubInfo' | |
} | |
} | |
], | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'org_subscription_plans', fk_in_this_table: 'org_subscription_plan_id' }, | |
{ related_table: 'alternate_owners', fk_in_this_table: 'main_alternate_owner_id' }, | |
{ related_table: 'organizations', fk_in_this_table: 'parent_organization_id' }, | |
], | |
has_one: [ | |
{ related_table: 'aau_infos', fk_in_related_table: 'club_info_id' } | |
] | |
}, | |
'containers' => { pk: 'id', belongs_to: [{ related_table: 'organizations', fk_in_this_table: 'menu_organization_id'}] }, # Note custom FK | |
'org_websites' => { pk: 'id', belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id'}, | |
]}, | |
'game_sheet_apis' => { pk: 'id', belongs_to: [{ related_table: 'organizations', fk_in_this_table: 'organization_id'}] }, | |
'transient_org_themes' => { | |
pk: 'id', | |
belongs_to: [{ related_table: 'organizations', fk_in_this_table: 'organization_id'}], | |
}, | |
'short_name_prefix_values' => { pk: 'id', belongs_to: [{ related_table: 'organizations', fk_in_this_table: 'organization_id'}] }, | |
'org_ui_settings' => { pk: 'id', belongs_to: [{ related_table: 'organizations', fk_in_this_table: 'organization_id'}] }, | |
'org_family_configuration_settings' => { pk: 'id', belongs_to: [{ related_table: 'organizations', fk_in_this_table: 'organization_id'}] }, | |
'athlete_global_ranking_configs' => { pk: 'id', belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id'}, | |
]}, | |
'registrations' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id'}, | |
{ related_table: 'people', fk_in_this_table: 'person_id'}, | |
{ related_table: 'teams', fk_in_this_table: 'team_id'}, | |
{ related_table: 'billing_infos', fk_in_this_table: 'billing_info_id'}, | |
], | |
has_many: [ | |
{ related_table: 'registration_cached_objects', fk_in_related_table: 'registration_id'}, | |
{ related_table: 'registration_group_registrations', fk_in_related_table: 'registration_id', dependent: :destroy}, | |
{ related_table: 'registration_discount_codes', fk_in_related_table: 'registration_id'}, | |
{ related_table: 'registration_log_items', fk_in_related_table: 'registration_object_id', dependent: :destroy}, | |
{ related_table: 'bank_wire_transfers', fk_in_related_table: 'registration_id'} | |
], | |
has_one: [ | |
{ related_table: 'registration_cached_objects', fk_in_related_table: 'registration_id'}, | |
{ related_table: 'bank_wire_transfers', fk_in_related_table: 'registration_id'} | |
], | |
}, | |
'text_inserts' => { | |
pk: 'id', | |
has_many: [ | |
{ related_table: 'form_data_assignments', fk_in_related_table: 'item_id' } | |
] | |
}, | |
'form_data_assignments' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'text_inserts', fk_in_this_table: 'item_id' }, | |
{ related_table: 'custom_sections', fk_in_this_table: 'item_id' }, | |
{ related_table: 'questions', fk_in_this_table: 'item_id' }, | |
{ related_table: 'question_placeholders', fk_in_this_table: 'item_id' }, | |
{ related_table: 'org_groupings', fk_in_this_table: 'item_id' } | |
] | |
}, | |
'question_placeholders' => { | |
pk: 'id', | |
has_many: [ | |
{ related_table: 'form_data_assignments', fk_in_related_table: 'item_id' } | |
] | |
}, | |
'custom_sections' => { | |
pk: 'id', | |
has_many: [ | |
{ related_table: 'form_data_assignments', fk_in_related_table: 'item_id' } | |
] | |
}, | |
'accounts' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'account_stripe_payments' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' }, | |
{ related_table: 'accounts', fk_in_this_table: 'account_id' } | |
] | |
}, | |
'division_people' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' }, | |
{ related_table: 'divisions', fk_in_this_table: 'division_id' } | |
] | |
}, | |
'memberships' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' }, | |
{ related_table: 'organizations', fk_in_this_table: 'organization_id' } | |
] | |
}, | |
'certifications' => { | |
pk: 'id' | |
}, | |
'sports' => { | |
pk: 'id', | |
has_many: [ | |
{ related_table: 'organizations_sports', fk_in_related_table: 'sport_id' }, | |
], | |
}, | |
'billing_infos' => { | |
pk: 'id' | |
}, | |
'teams' => { | |
pk: 'id' | |
}, | |
'facilities' => { | |
pk: 'id' | |
}, | |
'division_structs' => { | |
pk: 'id' | |
}, | |
'fee_generators' => { | |
pk: 'id' | |
}, | |
'game_rules' => { | |
pk: 'id' | |
}, | |
'division_builders' => { | |
pk: 'id' | |
}, | |
'page_layouts' => { | |
pk: 'id' | |
}, | |
'notifications' => { | |
pk: 'id' | |
}, | |
'org_api_users' => { | |
pk: 'id', | |
has_many: [ | |
{ related_table: 'org_api_users_organizations', fk_in_related_table: 'org_api_user_id' }, | |
], | |
}, | |
'org_subscription_plans' => { | |
pk: 'plan_id' | |
}, | |
'charge_refunds' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'transactions', fk_in_this_table: 'charge_transaction_id' }, | |
{ related_table: 'transactions', fk_in_this_table: 'refund_transaction_id' }, | |
] | |
}, | |
'widget_file_assets' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'file_assets', fk_in_this_table: 'file_asset_id' }, | |
] | |
}, | |
'organization_alerts' => { | |
pk: 'id' | |
}, | |
# People-related tables that were missing definitions | |
'attendees' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'player_statistics' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'game_actions' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'organization_alert_people' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'team_roster_invitations' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'profile_pages' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'people_placements' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'unverified_achievements' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'delegation_officials' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'delegation_admins' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'scoring_actions' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'membership_certifications' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'bulk_member_invites' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'certification_expiration_update_logs' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'system_alert_dismissed_people' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'people_fifa_duplicates' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'ncsa_recruit_profiles' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'person_blocked_mobiles' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'registration_groups' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'season_delegation_registrants' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'person_id' } | |
] | |
}, | |
'child_transfer_requests' => { | |
pk: 'id', | |
belongs_to: [ | |
{ related_table: 'people', fk_in_this_table: 'child_person_id' } | |
] | |
} | |
}.freeze | |
class OrganizationExporter | |
def initialize(org_id) | |
@db_user = ENV['DB_USER'] || 'deploy' | |
@db_name = ENV['DB_NAME'] || 'rsportz_dev' | |
@org_id = org_id | |
@db_password = ENV['DB_PASSWORD'] || 'password' | |
@host = ENV['DB_HOST'] || 'localhost' | |
@export_dir = "org_#{org_id}_export" | |
@export_file = File.join(@export_dir, "org_#{org_id}_complete_export.sql") | |
@count_file = File.join(@export_dir, "org_#{org_id}_count.sql") | |
@validation_file = File.join(@export_dir, "org_#{org_id}_validation.log") | |
@conn = nil | |
@export_order = [] | |
# Track record IDs to export per table to avoid duplicates | |
@records_to_export = Hash.new { |h, k| h[k] = Set.new } | |
@tables_processed = Set.new | |
# Track visited records to prevent infinite recursion | |
@visited_records = Set.new | |
end | |
def connect | |
@conn = PG.connect(dbname: @db_name, user: @db_user, password: @db_password, host: @host) | |
end | |
def disconnect | |
@conn.close if @conn | |
end | |
def export | |
start_time = Time.now | |
puts "From database: #{@db_name}, user: #{@db_user}, host: #{@host}" | |
puts "Exporting organization data for ID: #{@org_id}" | |
puts "Export started at: #{start_time.strftime('%Y-%m-%d %H:%M:%S')}" | |
# Show debugging options | |
if ENV['DEBUG_COLLECTION'] || ENV['DEBUG_SQL'] | |
puts "\n🐛 DEBUG MODE ENABLED:" | |
puts " DEBUG_COLLECTION=#{ENV['DEBUG_COLLECTION']}" if ENV['DEBUG_COLLECTION'] | |
puts " DEBUG_SQL=#{ENV['DEBUG_SQL']}" if ENV['DEBUG_SQL'] | |
else | |
puts "\n💡 Tip: Set DEBUG_COLLECTION=1 or DEBUG_SQL=1 for detailed logging" | |
end | |
puts "" | |
FileUtils.mkdir_p(@export_dir) | |
File.open(@export_file, 'w') do |f| | |
f.puts "BEGIN;" | |
f.puts "SET CONSTRAINTS ALL DEFERRED;" | |
end | |
# Phase 1: Collect all record IDs to export | |
puts "Phase 1: Collecting record IDs to export..." | |
collect_records_recursively('organizations', @org_id) | |
# Phase 1.5: Ensure all people and their relationships are collected | |
puts "Phase 1.5: Collecting all person relationships..." | |
collect_all_people_relationships | |
# Phase 1.6: Collect orphaned models that weren't reached through relationships | |
collect_orphaned_models | |
# Phase 2: Validate data integrity | |
puts "Phase 2: Validating data integrity..." | |
validate_data_integrity | |
# Phase 3: Export deduplicated data | |
puts "Phase 3: Exporting deduplicated data..." | |
export_collected_records | |
export_child_containers(@org_id) | |
export_widgets(@saved_ids) | |
File.open(@export_file, 'a') do |f| | |
f.puts "COMMIT;" | |
f.puts "SET CONSTRAINTS ALL IMMEDIATE;" | |
end | |
end_time = Time.now | |
duration = end_time - start_time | |
puts "\nExport complete! Created: #{@export_file}" | |
puts "Validation report: #{@validation_file}" | |
puts "\nExport order:" | |
@export_order.each_with_index do |table, index| | |
puts "#{index + 1}. #{table}" | |
end | |
puts "\n" | |
puts "Export finished at: #{end_time.strftime('%Y-%m-%d %H:%M:%S')}" | |
puts "Total duration: #{duration} seconds" | |
# Final recommendations | |
puts "\n📋 RECOMMENDATIONS:" | |
puts " 1. Check validation report: #{@validation_file}" | |
puts " 2. Review skipped relationships in the collection summary above" | |
puts " 3. If tables are missing, run with DEBUG_COLLECTION=1 to trace collection" | |
puts " 4. If SQL errors occur, run with DEBUG_SQL=1 to see queries" | |
end | |
private | |
def collect_all_people_relationships | |
puts " Collecting all people from organization_people..." | |
# Get all people referenced by organization_people for this org and related orgs | |
org_people_query = <<~SQL | |
SELECT DISTINCT op.person_id | |
FROM organization_people op | |
WHERE ( | |
op.organization_id = #{@org_id} OR | |
op.organization_id IN ( | |
SELECT id FROM organizations | |
WHERE ancestor1_id = #{@org_id} OR ancestor2_id = #{@org_id} OR ancestor3_id = #{@org_id} OR | |
ancestor4_id = #{@org_id} OR ancestor5_id = #{@org_id} | |
) | |
) | |
AND op.person_id IS NOT NULL | |
SQL | |
result = @conn.exec(org_people_query) | |
initial_person_ids = result.map { |row| row['person_id'].to_i }.compact | |
puts " Found #{initial_person_ids.size} people from organization_people" | |
# Get all people from division_people for divisions in this org | |
division_people_query = <<~SQL | |
SELECT DISTINCT dp.person_id | |
FROM division_people dp | |
INNER JOIN divisions d ON dp.division_id = d.id | |
WHERE ( | |
d.organization_id = #{@org_id} OR | |
d.organization_id IN ( | |
SELECT id FROM organizations | |
WHERE ancestor1_id = #{@org_id} OR ancestor2_id = #{@org_id} OR ancestor3_id = #{@org_id} OR | |
ancestor4_id = #{@org_id} OR ancestor5_id = #{@org_id} | |
) | |
) | |
AND dp.person_id IS NOT NULL | |
SQL | |
result = @conn.exec(division_people_query) | |
division_person_ids = result.map { |row| row['person_id'].to_i }.compact | |
puts " Found #{division_person_ids.size} people from division_people" | |
all_person_ids = (initial_person_ids + division_person_ids).uniq | |
if all_person_ids.any? | |
# Collect all family relationships (parents and children) recursively | |
family_query = <<~SQL | |
WITH RECURSIVE person_family AS ( | |
-- Base case: all people we've identified | |
SELECT id, parent_id, 1 as level | |
FROM people | |
WHERE id IN (#{all_person_ids.join(',')}) | |
UNION ALL | |
-- Recursive case: get parents and children | |
SELECT p.id, p.parent_id, pf.level + 1 | |
FROM people p | |
INNER JOIN person_family pf ON ( | |
p.id = pf.parent_id OR -- children of current people | |
p.parent_id = pf.id -- parents of current people | |
) | |
WHERE pf.level < 10 -- Prevent infinite loops | |
) | |
SELECT DISTINCT id FROM person_family | |
SQL | |
result = @conn.exec(family_query) | |
complete_person_ids = result.map { |row| row['id'].to_i }.compact | |
puts " Expanded to #{complete_person_ids.size} people (including family relationships)" | |
# Add all people to export list | |
@records_to_export['people'].merge(complete_person_ids) | |
@export_order << 'people' unless @export_order.include?('people') | |
# Also collect related models for these people | |
collect_person_related_models(complete_person_ids) | |
end | |
end | |
def collect_person_related_models(person_ids) | |
return if person_ids.empty? | |
puts " Collecting person-related models..." | |
# Collect accounts for these people | |
account_query = "SELECT DISTINCT id FROM accounts WHERE person_id IN (#{person_ids.join(',')})" | |
result = @conn.exec(account_query) | |
account_ids = result.map { |row| row['id'].to_i }.compact | |
if account_ids.any? | |
@records_to_export['accounts'].merge(account_ids) | |
@export_order << 'accounts' unless @export_order.include?('accounts') | |
puts " Added #{account_ids.size} accounts" | |
end | |
# Collect other person-related tables | |
person_tables = %w[ | |
person_general_information person_medicals person_additional_contacts | |
person_emergency_contacts person_team_contacts person_equipment | |
person_identification person_martial_arts person_screening | |
person_skills_credentials person_fifa_registration iawa_registrations | |
schedule_views super_admin_permissions person_suspensions | |
removed_people person_sport_details person_profile_videos | |
] | |
person_tables.each do |table| | |
begin | |
query = "SELECT DISTINCT id FROM #{table} WHERE person_id IN (#{person_ids.join(',')})" | |
result = @conn.exec(query) | |
ids = result.map { |row| row['id'].to_i }.compact | |
if ids.any? | |
@records_to_export[table].merge(ids) | |
@export_order << table unless @export_order.include?(table) | |
puts " Added #{ids.size} #{table} records" | |
end | |
rescue PG::UndefinedTable | |
# Table doesn't exist, skip it | |
next | |
rescue PG::Error => e | |
puts " Warning: Could not collect #{table}: #{e.message}" | |
next | |
end | |
end | |
end | |
def validate_data_integrity | |
validation_issues = [] | |
puts " Validating organization_people -> people relationships..." | |
# Check for organization_people with missing people | |
if @records_to_export['organization_people'].any? | |
op_ids = @records_to_export['organization_people'].to_a.join(',') | |
missing_people_query = <<~SQL | |
SELECT op.id as op_id, op.person_id, op.organization_id | |
FROM organization_people op | |
WHERE op.id IN (#{op_ids}) | |
AND op.person_id IS NOT NULL | |
AND op.person_id NOT IN (#{@records_to_export['people'].to_a.join(',') || '0'}) | |
SQL | |
result = @conn.exec(missing_people_query) | |
if result.any? | |
result.each do |row| | |
issue = "OrganizationPerson ID #{row['op_id']} references missing person_id #{row['person_id']} (org: #{row['organization_id']})" | |
validation_issues << issue | |
puts " ⚠️ #{issue}" | |
# Try to add the missing person | |
missing_person_id = row['person_id'].to_i | |
@records_to_export['people'].add(missing_person_id) | |
collect_person_related_models([missing_person_id]) | |
end | |
end | |
end | |
# Check for people with missing parents | |
if @records_to_export['people'].any? | |
people_ids = @records_to_export['people'].to_a.join(',') | |
missing_parents_query = <<~SQL | |
SELECT p.id, p.parent_id | |
FROM people p | |
WHERE p.id IN (#{people_ids}) | |
AND p.parent_id IS NOT NULL | |
AND p.parent_id NOT IN (#{people_ids}) | |
SQL | |
result = @conn.exec(missing_parents_query) | |
if result.any? | |
missing_parent_ids = [] | |
result.each do |row| | |
issue = "Person ID #{row['id']} references missing parent_id #{row['parent_id']}" | |
validation_issues << issue | |
puts " ⚠️ #{issue}" | |
missing_parent_ids << row['parent_id'].to_i | |
end | |
# Add missing parents and their related models | |
@records_to_export['people'].merge(missing_parent_ids) | |
collect_person_related_models(missing_parent_ids) | |
puts " ✅ Added #{missing_parent_ids.size} missing parents" | |
end | |
end | |
# Check for division_people with missing people | |
if @records_to_export['division_people'].any? | |
dp_ids = @records_to_export['division_people'].to_a.join(',') | |
missing_dp_people_query = <<~SQL | |
SELECT dp.id as dp_id, dp.person_id, dp.division_id | |
FROM division_people dp | |
WHERE dp.id IN (#{dp_ids}) | |
AND dp.person_id IS NOT NULL | |
AND dp.person_id NOT IN (#{@records_to_export['people'].to_a.join(',') || '0'}) | |
SQL | |
result = @conn.exec(missing_dp_people_query) | |
if result.any? | |
missing_dp_person_ids = [] | |
result.each do |row| | |
issue = "DivisionPerson ID #{row['dp_id']} references missing person_id #{row['person_id']} (division: #{row['division_id']})" | |
validation_issues << issue | |
puts " ⚠️ #{issue}" | |
missing_dp_person_ids << row['person_id'].to_i | |
end | |
# Add missing people and their related models | |
@records_to_export['people'].merge(missing_dp_person_ids) | |
collect_person_related_models(missing_dp_person_ids) | |
puts " ✅ Added #{missing_dp_person_ids.size} missing division people" | |
end | |
end | |
# Write validation report | |
File.open(@validation_file, 'w') do |f| | |
f.puts "Data Integrity Validation Report" | |
f.puts "Organization ID: #{@org_id}" | |
f.puts "Generated at: #{Time.now}" | |
f.puts "=" * 50 | |
f.puts | |
if validation_issues.empty? | |
f.puts "✅ No data integrity issues found!" | |
else | |
f.puts "⚠️ Found #{validation_issues.size} data integrity issues:" | |
f.puts | |
validation_issues.each_with_index do |issue, index| | |
f.puts "#{index + 1}. #{issue}" | |
end | |
end | |
f.puts | |
f.puts "Export Summary:" | |
@records_to_export.each do |table, ids| | |
f.puts " #{table}: #{ids.size} records" | |
end | |
end | |
if validation_issues.any? | |
puts " ⚠️ Found #{validation_issues.size} data integrity issues (see #{@validation_file})" | |
else | |
puts " ✅ Data integrity validation passed!" | |
end | |
end | |
def collect_records_recursively(model_name, record_id) | |
model_def = MODEL_DEFINITIONS[model_name] | |
return unless model_def | |
# Add cycle detection to prevent infinite recursion | |
record_key = "#{model_name}:#{record_id}" | |
if @visited_records.include?(record_key) | |
puts " Skipping already visited #{record_key}" if ENV['DEBUG_COLLECTION'] | |
return | |
end | |
@visited_records.add(record_key) | |
puts " Collecting #{model_name} record #{record_id}..." if ENV['DEBUG_COLLECTION'] | |
if model_name == 'organizations' | |
@records_to_export['organizations'].add(record_id) | |
@export_order << 'organizations' unless @export_order.include?('organizations') | |
puts "Collecting organization #{record_id}" | |
else | |
# For non-organization models, add the record to the export list | |
@records_to_export[model_name].add(record_id) | |
@export_order << model_name unless @export_order.include?(model_name) | |
end | |
# Collect has_one relationships | |
if model_def[:has_one] | |
model_def[:has_one].each do |rel| | |
related_table = rel[:related_table] | |
# Handle polymorphic relationships | |
if rel[:polymorphic] | |
where_condition = "#{rel[:fk_in_related_table]} = #{record_id} AND #{rel[:polymorphic][:type_column]} = '#{rel[:polymorphic][:type_value]}'" | |
else | |
where_condition = "#{rel[:fk_in_related_table]} = #{record_id}" | |
end | |
ids = get_related_record_ids(related_table, where_condition) | |
if ids.any? | |
@records_to_export[related_table].merge(ids) | |
@export_order << related_table unless @export_order.include?(related_table) | |
puts " Found #{ids.size} #{related_table} records (has_one)" if ENV['DEBUG_COLLECTION'] | |
# Recursively collect related model's relationships for each ID | |
ids.each do |related_id| | |
collect_records_recursively(related_table, related_id) | |
end | |
end | |
end | |
end | |
# Collect has_many relationships | |
if model_def[:has_many] | |
@skipped_relationships ||= [] | |
model_def[:has_many].each do |rel| | |
related_table = rel[:related_table] | |
# Handle polymorphic relationships | |
if rel[:polymorphic] | |
where_condition = "#{rel[:fk_in_related_table]} = #{record_id} AND #{rel[:polymorphic][:type_column]} = '#{rel[:polymorphic][:type_value]}'" | |
else | |
# For organizations, use the record_id directly (which is org_id) | |
if model_name == 'organizations' | |
where_condition = "#{rel[:fk_in_related_table]} = #{record_id}" | |
else | |
# For other models, we need to find records in related_table that reference the current record | |
# The current record_id should be used with the FK specified in the relationship | |
where_condition = "#{rel[:fk_in_related_table]} = #{record_id}" | |
end | |
end | |
if where_condition.blank? | |
@skipped_relationships << "#{model_name} -> #{related_table} (blank condition)" | |
next | |
end | |
ids = get_related_record_ids(related_table, where_condition) | |
if ids.any? | |
@records_to_export[related_table].merge(ids) | |
@export_order << related_table unless @export_order.include?(related_table) | |
puts " Found #{ids.size} #{related_table} records (has_many)" if ENV['DEBUG_COLLECTION'] | |
# Recursively collect related model's relationships for each ID | |
ids.each do |related_id| | |
collect_records_recursively(related_table, related_id) | |
end | |
else | |
# Track tables with zero records | |
@records_to_export[related_table] ||= Set.new | |
end | |
end | |
end | |
# Collect belongs_to relationships | |
if model_def[:belongs_to] | |
model_def[:belongs_to].each do |rel| | |
related_table = rel[:related_table] | |
next if related_table == 'organizations' && model_name != 'organizations' | |
fk_column = rel[:fk_in_this_table] | |
related_ids = get_related_ids_for_record(model_name, fk_column, record_id) | |
related_ids.each do |related_id| | |
next if related_id.blank? | |
@records_to_export[related_table].add(related_id) | |
@export_order << related_table unless @export_order.include?(related_table) | |
puts " Found #{related_table} record #{related_id} (belongs_to)" if ENV['DEBUG_COLLECTION'] | |
# Recursively collect related model's relationships | |
collect_records_recursively(related_table, related_id) | |
end | |
end | |
end | |
end | |
def get_related_record_ids(table_name, where_condition) | |
begin | |
# First check if table exists | |
table_check = @conn.exec("SELECT to_regclass('#{table_name}') as exists") | |
if table_check.first['exists'].nil? | |
puts " Table #{table_name} doesn't exist, skipping..." if ENV['DEBUG_COLLECTION'] | |
return [] | |
end | |
pk_column = MODEL_DEFINITIONS.dig(table_name, :pk) || 'id' | |
# Check if the primary key column actually exists | |
column_check = @conn.exec("SELECT column_name FROM information_schema.columns WHERE table_name = '#{table_name}' AND column_name = '#{pk_column}'") | |
if column_check.ntuples == 0 | |
# If specified pk doesn't exist, try to find the actual primary key | |
actual_pk_query = @conn.exec("SELECT a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = '#{table_name}'::regclass AND i.indisprimary") | |
if actual_pk_query.ntuples > 0 | |
pk_column = actual_pk_query.first['attname'] | |
else | |
puts "Warning: No primary key found for #{table_name}, using all columns" | |
# For tables without primary keys (like join tables), select all columns | |
columns = @conn.exec("SELECT column_name FROM information_schema.columns WHERE table_name = '#{table_name}' ORDER BY ordinal_position").map { |r| r['column_name'] } | |
pk_column = columns.join(', ') | |
end | |
end | |
query = "SELECT #{pk_column} FROM #{table_name} WHERE #{where_condition}" | |
puts " Query: #{query}" if ENV['DEBUG_SQL'] | |
result = @conn.exec(query) | |
if pk_column.include?(',') | |
# For composite keys, return a simple count or identifier | |
result.map.with_index { |row, idx| "#{table_name}_#{idx}" } | |
else | |
result.map { |row| row[pk_column] }.compact.select { |id| !id.to_s.empty? } | |
end | |
rescue PG::UndefinedTable => e | |
puts " Table #{table_name} doesn't exist: #{e.message}" if ENV['DEBUG_COLLECTION'] | |
[] | |
rescue PG::Error => e | |
puts "Database error while fetching record IDs for #{table_name}: #{e.message}" if ENV['DEBUG_COLLECTION'] | |
puts "Query attempted: #{query}" if defined?(query) && ENV['DEBUG_COLLECTION'] | |
puts "Where condition: #{where_condition}" if ENV['DEBUG_COLLECTION'] | |
[] | |
rescue StandardError => e | |
puts "Unexpected error while fetching record IDs for #{table_name}: #{e.message}" if ENV['DEBUG_COLLECTION'] | |
puts "Query attempted: #{query}" if defined?(query) && ENV['DEBUG_COLLECTION'] | |
[] | |
end | |
end | |
def export_collected_records | |
puts "\n=== COLLECTION SUMMARY ===" | |
total_tables_with_data = 0 | |
total_tables_checked = 0 | |
@records_to_export.each do |table_name, record_ids| | |
puts "#{table_name}: #{record_ids.size} records" | |
total_tables_with_data += 1 if record_ids.size > 0 | |
total_tables_checked += 1 | |
end | |
puts "========================" | |
puts "Total tables checked: #{total_tables_checked}" | |
puts "Total tables with data: #{total_tables_with_data}" | |
# Show missing models from MODEL_DEFINITIONS | |
missing_models = MODEL_DEFINITIONS.keys - @records_to_export.keys | |
if missing_models.any? | |
puts "\n⚠️ MISSING MODELS FROM MODEL_DEFINITIONS:" | |
missing_models.each_with_index do |model, index| | |
puts " #{index + 1}. #{model}" | |
end | |
else | |
puts "\n✅ All models from MODEL_DEFINITIONS were processed!" | |
end | |
# Show models with zero records | |
zero_record_models = @records_to_export.select { |table, ids| ids.empty? }.keys | |
if zero_record_models.any? | |
puts "\n📭 MODELS WITH ZERO RECORDS:" | |
zero_record_models.each_with_index do |model, index| | |
puts " #{index + 1}. #{model}" | |
end | |
end | |
if @skipped_relationships&.any? | |
puts "\nSKIPPED RELATIONSHIPS:" | |
@skipped_relationships.uniq.each_with_index do |skip, index| | |
puts " #{index + 1}. #{skip}" | |
end | |
end | |
puts "========================\n" | |
@records_to_export.each do |table_name, record_ids| | |
next if record_ids.empty? | |
# Check if table actually exists before trying to export | |
begin | |
table_check = @conn.exec("SELECT to_regclass('#{table_name}') as exists") | |
if table_check.first['exists'].nil? | |
puts "⚠️ Skipping #{table_name}: table doesn't exist in database (#{record_ids.size} records collected)" | |
next | |
end | |
rescue PG::Error => e | |
puts "⚠️ Skipping #{table_name}: error checking table existence - #{e.message}" | |
next | |
end | |
puts "Exporting #{record_ids.size} records from #{table_name}" | |
pk_column = MODEL_DEFINITIONS.dig(table_name, :pk) || 'id' | |
if table_name == 'org_subscription_plans' | |
where_clause = "WHERE plan_id IN ('#{record_ids.to_a.join("','")}')" | |
elsif pk_column.include?(',') | |
# Handle composite primary keys by filtering on known organization columns | |
if table_name == 'organizations_sports' | |
where_clause = "WHERE organization_id = #{@org_id}" | |
else | |
# For other composite key tables, try to find organization_id column | |
org_column_check = @conn.exec("SELECT column_name FROM information_schema.columns WHERE table_name = '#{table_name}' AND column_name = 'organization_id'") | |
if org_column_check.ntuples > 0 | |
where_clause = "WHERE organization_id = #{@org_id}" | |
puts "Filtering #{table_name} by organization_id for composite primary key" | |
else | |
where_clause = "" | |
puts "Warning: #{table_name} has composite primary key but no organization_id column, exporting all records" | |
end | |
end | |
else | |
where_clause = "WHERE #{pk_column} IN (#{record_ids.to_a.join(',')})" | |
end | |
export_table(table_name, where_clause) | |
end | |
generate_sql_statements_for_collected_records | |
end | |
def generate_sql_statements_for_collected_records | |
@records_to_export.each do |table_name, record_ids| | |
next if record_ids.empty? | |
# Check if table actually exists before generating SQL | |
begin | |
table_check = @conn.exec("SELECT to_regclass('#{table_name}') as exists") | |
if table_check.first['exists'].nil? | |
puts " Skipping SQL generation for #{table_name}: table doesn't exist" if ENV['DEBUG_COLLECTION'] | |
next | |
end | |
rescue PG::Error => e | |
puts " Skipping SQL generation for #{table_name}: error checking table - #{e.message}" if ENV['DEBUG_COLLECTION'] | |
next | |
end | |
pk_column = MODEL_DEFINITIONS.dig(table_name, :pk) || 'id' | |
if table_name == 'org_subscription_plans' | |
where_clause = "WHERE plan_id IN ('#{record_ids.to_a.join("','")}')" | |
elsif pk_column.include?(',') | |
# Handle composite primary keys by filtering on known organization columns | |
if table_name == 'organizations_sports' | |
where_clause = "WHERE organization_id = #{@org_id}" | |
else | |
# For other composite key tables, try to find organization_id column | |
org_column_check = @conn.exec("SELECT column_name FROM information_schema.columns WHERE table_name = '#{table_name}' AND column_name = 'organization_id'") | |
if org_column_check.ntuples > 0 | |
where_clause = "WHERE organization_id = #{@org_id}" | |
else | |
where_clause = "" | |
end | |
end | |
else | |
where_clause = "WHERE #{pk_column} IN (#{record_ids.to_a.join(',')})" | |
end | |
generate_table_sql(table_name, [where_clause]) | |
end | |
end | |
def generate_table_sql(table_name, where_clauses) | |
columns = @conn.exec("SELECT column_name FROM information_schema.columns WHERE table_name = '#{table_name}' ORDER BY ordinal_position").map { |r| r['column_name'] } | |
quoted_columns = columns.map { |c| RESERVED_WORDS[c] || c }.join(', ') | |
csv_file = File.join(@export_dir, "#{table_name}.csv") | |
File.open(@export_file, 'a') do |f| | |
f.puts "-- Exporting #{table_name}" | |
if File.exist?(csv_file) && File.size(csv_file) > 0 | |
where_clauses.each do |where_clause| | |
if where_clause.present? | |
f.puts "DELETE FROM #{table_name} #{where_clause};" | |
else | |
f.puts "DELETE FROM #{table_name};" | |
end | |
end | |
f.puts "\\COPY #{table_name} (#{quoted_columns}) FROM '#{csv_file}' WITH (FORMAT csv, NULL '\\N');" | |
end | |
f.puts "" | |
end | |
end | |
def find_parent_model_key(model_name, model_definitions) | |
model_def = model_definitions[model_name] | |
return nil unless model_def | |
model_def[:belongs_to]&.find { |rel| rel[:related_table] == 'organizations' }&.dig(:fk_in_this_table) | |
end | |
def has_organization_relationship?(model_name) | |
# Check if model has direct organization relationship | |
model_def = MODEL_DEFINITIONS[model_name] | |
return false unless model_def | |
# Check belongs_to relationships | |
has_direct_org_relation = model_def[:belongs_to]&.any? { |rel| rel[:related_table] == 'organizations' } | |
# Check if it's a known model with indirect relationships | |
indirect_models = %w[accounts division_people people] | |
has_direct_org_relation || indirect_models.include?(model_name) | |
end | |
def get_ids_for_model(model_name, fk_column, related_id) | |
begin | |
return [related_id] if model_name == 'organizations' && fk_column == 'organization_id' | |
# Handle case where fk_column is nil or empty | |
if fk_column.blank? | |
puts "Warning: No foreign key column found for #{model_name}, skipping..." | |
return [] | |
end | |
query = "SELECT id FROM #{model_name} WHERE #{fk_column} = #{related_id}" | |
result = @conn.exec(query) | |
result.map { |row| row['id'] }.compact | |
rescue PG::Error => e | |
puts "Database error while fetching IDs for #{model_name}: #{e.message}" | |
puts "Query attempted: SELECT id FROM #{model_name} WHERE #{fk_column} = #{related_id}" | |
[] # Return empty array as fallback | |
rescue StandardError => e | |
puts "Unexpected error while fetching IDs for #{model_name}: #{e.message}" | |
puts e.backtrace.join("\n") | |
[] # Return empty array as fallback | |
end | |
end | |
def get_related_ids_for_record(model_name, fk_column, record_id) | |
begin | |
# Handle case where fk_column is nil or empty | |
if fk_column.blank? | |
puts "Warning: No foreign key column specified for #{model_name}, skipping..." | |
return [] | |
end | |
# Skip synthetic record IDs from composite primary key tables | |
if record_id.to_s.include?("#{model_name}_") | |
puts " Skipping synthetic record ID #{record_id} for composite key table #{model_name}" if ENV['DEBUG_COLLECTION'] | |
return [] | |
end | |
# Check if the model has composite primary key | |
model_def = MODEL_DEFINITIONS[model_name] | |
if model_def && model_def[:pk]&.include?(',') | |
puts " Skipping #{model_name} with composite primary key for belongs_to relationship" if ENV['DEBUG_COLLECTION'] | |
return [] | |
end | |
# Check if the foreign key column exists in the table | |
column_check = @conn.exec("SELECT column_name FROM information_schema.columns WHERE table_name = '#{model_name}' AND column_name = '#{fk_column}'") | |
if column_check.ntuples == 0 | |
puts " Column #{fk_column} doesn't exist in #{model_name}, skipping..." if ENV['DEBUG_COLLECTION'] | |
return [] | |
end | |
# Check if 'id' column exists for WHERE clause | |
id_column_check = @conn.exec("SELECT column_name FROM information_schema.columns WHERE table_name = '#{model_name}' AND column_name = 'id'") | |
if id_column_check.ntuples == 0 | |
puts " No 'id' column in #{model_name}, skipping belongs_to relationship" if ENV['DEBUG_COLLECTION'] | |
return [] | |
end | |
query = "SELECT DISTINCT #{fk_column} FROM #{model_name} WHERE id = #{record_id}" | |
result = @conn.exec(query) | |
result.map { |row| row[fk_column] }.compact.select { |id| !id.to_s.empty? } | |
rescue PG::Error => e | |
puts "Database error while fetching related IDs for #{model_name}.#{fk_column} with record_id #{record_id}: #{e.message}" if ENV['DEBUG_COLLECTION'] | |
[] # Return empty array as fallback | |
rescue StandardError => e | |
puts "Unexpected error while fetching related IDs for #{model_name}.#{fk_column} with record_id #{record_id}: #{e.message}" if ENV['DEBUG_COLLECTION'] | |
[] # Return empty array as fallback | |
end | |
end | |
def collect_orphaned_models | |
puts "Phase 1.6: Collecting orphaned models with organization_id..." | |
# Check all models in MODEL_DEFINITIONS that weren't collected | |
orphaned_count = 0 | |
MODEL_DEFINITIONS.each do |model_name, model_def| | |
# Skip if already collected | |
next if @records_to_export[model_name]&.any? | |
# Skip if no primary key defined | |
next unless model_def[:pk] | |
begin | |
# Check if table has organization_id column | |
column_check = @conn.exec("SELECT column_name FROM information_schema.columns WHERE table_name = '#{model_name}' AND column_name = 'organization_id'") | |
if column_check.ntuples > 0 | |
# Get records for this organization | |
pk_column = model_def[:pk] | |
if pk_column.include?(',') | |
# Skip composite primary keys for now | |
puts " Skipping #{model_name} (composite primary key)" | |
next | |
end | |
query = "SELECT #{pk_column} FROM #{model_name} WHERE organization_id = #{@org_id}" | |
result = @conn.exec(query) | |
ids = result.map { |row| row[pk_column] }.compact | |
if ids.any? | |
@records_to_export[model_name].merge(ids) | |
@export_order << model_name unless @export_order.include?(model_name) | |
puts " Collected #{ids.size} orphaned records from #{model_name}" | |
orphaned_count += ids.size | |
# Recursively collect relationships for these orphaned records | |
ids.each do |record_id| | |
collect_records_recursively(model_name, record_id) | |
end | |
end | |
end | |
rescue PG::UndefinedTable | |
puts " Table #{model_name} doesn't exist, skipping..." | |
next | |
rescue PG::Error => e | |
puts " Database error checking #{model_name}: #{e.message}" | |
next | |
end | |
end | |
puts " Found #{orphaned_count} orphaned records across all models" | |
end | |
def export_child_containers(org_id) | |
# Get body containers | |
query_body_containers = @conn.exec("SELECT c.id FROM organizations o \ | |
INNER JOIN org_pages p ON p.organization_id = o.id \ | |
INNER JOIN containers c ON c.id = p.body_container_id WHERE o.id = #{org_id}" | |
) | |
body_container_ids = query_body_containers.map { |row| row['id'] } | |
# Get menu containers - this was missing! | |
query_menu_containers = @conn.exec("SELECT c.id FROM containers c \ | |
WHERE c.menu_organization_id = #{org_id} AND c.type = 'MenuContainer'" | |
) | |
menu_container_ids = query_menu_containers.map { |row| row['id'] } | |
# Combine both container types | |
all_container_ids = body_container_ids + menu_container_ids | |
puts "Found #{body_container_ids.size} body containers and #{menu_container_ids.size} menu containers" | |
puts "Total containers to export: #{all_container_ids.size}" | |
@saved_ids = all_container_ids.dup # Include root containers in saved_ids | |
# Skip if no containers to export | |
return if all_container_ids.empty? | |
def recursively_export_child_containers(container_ids) | |
return if container_ids.empty? | |
count_query = "SELECT COUNT(*) FROM containers WHERE parent_id IN (#{container_ids.join(',')});" | |
puts "export containers: #{count_query}" | |
count_result = @conn.exec(count_query) | |
count = count_result.first['count'] | |
puts "export containers: #{count}" | |
query = "SELECT * FROM containers WHERE parent_id IN (#{container_ids.join(',')})" | |
puts "export_child_containers query: #{query}" | |
query_child_containers = @conn.exec(query + ";") | |
@conn.exec("COPY (#{query}) TO STDOUT WITH (FORMAT csv, NULL '\\N')") | |
child_container_ids = query_child_containers.map { |row| row['id'] } | |
csv_file = File.join(@export_dir, "containers.csv") | |
# Add all child container IDs to saved_ids (not just the last level) | |
@saved_ids += child_container_ids if child_container_ids.any? | |
File.open(csv_file, 'ab') do |f| | |
while row = @conn.get_copy_data | |
f.write row | |
end | |
end | |
return if child_container_ids.empty? | |
recursively_export_child_containers(child_container_ids) | |
end | |
recursively_export_child_containers(all_container_ids) | |
puts "Total containers collected for widget export: #{@saved_ids.size}" | |
end | |
def export_widgets(container_ids) | |
columns = @conn | |
.exec("SELECT column_name FROM information_schema.columns WHERE table_name = 'widgets' ORDER BY ordinal_position") | |
.map { |r| r['column_name'] } | |
quoted_columns = columns.map { |c| RESERVED_WORDS[c] || c }.join(', ') | |
count_query = "SELECT COUNT(*) as count FROM widgets WHERE widget_container_id IN (#{container_ids.join(',')});" | |
count_result = @conn.exec(count_query) | |
count = count_result.first['count'] | |
puts "export widgets count: #{count}" | |
query = "SELECT * FROM widgets WHERE widget_container_id IN (#{container_ids.join(',')})" | |
query_widgets = @conn.exec(query + ";") | |
puts "export_widgets query: #{query}" | |
@conn.exec("COPY (#{query}) TO STDOUT WITH (FORMAT csv, NULL '\\N')") | |
csv_file = File.join(@export_dir, "widgets.csv") | |
File.open(csv_file, 'ab') do |f| | |
while row = @conn.get_copy_data | |
f.write row | |
end | |
end | |
File.open(@export_file, 'a') do |f| | |
f.puts "DELETE FROM widgets WHERE widget_container_id IN (#{container_ids.join(',')});" | |
f.puts "\\COPY widgets (#{quoted_columns}) FROM '#{csv_file}' WITH (FORMAT csv, NULL '\\N');" | |
end | |
end | |
def export_table(table, where_clause) | |
columns = @conn.exec("SELECT column_name FROM information_schema.columns WHERE table_name = '#{table}' ORDER BY ordinal_position").map { |r| r['column_name'] } | |
count_query = "SELECT '#{table}' as table, COUNT(*) as count FROM #{table} #{where_clause};" | |
count_result = @conn.exec(count_query) | |
count = count_result.first['count'] | |
puts "count #{table}: #{count}" | |
File.open(@count_file, 'a') do |f| | |
f.puts count_query | |
end | |
select_query = "SELECT #{columns.map { |c| RESERVED_WORDS[c] || c }.join(', ')} FROM #{table} #{where_clause}" | |
puts "export_table select_query: #{select_query}" | |
@conn.exec("COPY (#{select_query}) TO STDOUT WITH (FORMAT csv, NULL '\\N')") | |
return if count.to_i == 0 || count.nil? | |
csv_file = File.join(@export_dir, "#{table}.csv") | |
# Always overwrite the file since we're exporting each table only once | |
File.open(csv_file, 'wb') do |f| | |
while row = @conn.get_copy_data | |
f.write row | |
end | |
end | |
end | |
end | |
namespace :migrate do | |
desc 'Export data and related data for one org at a time' | |
task :org_export, [:org_id] => :environment do |t, args| | |
unless args[:org_id].present? | |
puts "Error: Please provide an organization ID" | |
puts "" | |
puts "Usage: rake migrate:org_export[org_id]" | |
puts "" | |
puts "Debugging options:" | |
puts " DEBUG_COLLECTION=1 rake migrate:org_export[org_id] # Show detailed collection tracing" | |
puts " DEBUG_SQL=1 rake migrate:org_export[org_id] # Show SQL queries being executed" | |
puts " DEBUG_COLLECTION=1 DEBUG_SQL=1 rake migrate:org_export[org_id] # Enable both" | |
puts "" | |
puts "Examples:" | |
puts " rake migrate:org_export[123]" | |
puts " DEBUG_COLLECTION=1 rake migrate:org_export[123]" | |
exit 1 | |
end | |
begin | |
exporter = OrganizationExporter.new(args[:org_id]) | |
exporter.connect | |
exporter.export | |
rescue => e | |
puts "Error: #{e.message}" | |
puts e.backtrace | |
exit 1 | |
ensure | |
exporter.disconnect if exporter | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment