diff --git a/delete_zombie_constraint.py b/delete_zombie_constraint.py new file mode 100644 index 0000000..70e5b15 --- /dev/null +++ b/delete_zombie_constraint.py @@ -0,0 +1,86 @@ +import sys +import os +import argparse +import logging + +def main(): + parser = argparse.ArgumentParser(description="Drop Zombie Constraint 'product_pricelist_res_config_settin_res_config_settings_id_fkey'") + parser.add_argument("odoo_bin_path", help="Path to odoo-bin executable (e.g. odoo/odoo-bin)") + parser.add_argument("conf_path", help="Path to odoo.conf") + parser.add_argument("db_name", help="Database name") + + args = parser.parse_args() + + odoo_bin_path = os.path.abspath(args.odoo_bin_path) + conf_path = os.path.abspath(args.conf_path) + db_name = args.db_name + + # Add Odoo to sys.path + odoo_root = os.path.dirname(odoo_bin_path) + if odoo_root not in sys.path: + sys.path.append(odoo_root) + + # Change CWD to config directory to handle relative paths in config + os.chdir(os.path.dirname(conf_path)) + + try: + import odoo + from odoo import api, SUPERUSER_ID + except ImportError: + print(f"Error: Could not import 'odoo' module from {odoo_root}. Make sure odoo-bin path is correct.") + sys.exit(1) + + print(f"Initializing Odoo Environment for database: {db_name}...") + try: + odoo.tools.config.parse_config(['-c', conf_path]) + registry = odoo.registry(db_name) + except Exception as e: + print(f"Error initializing Odoo: {e}") + return + + with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + print("Connected to Odoo.") + + # Define the problematic constraint connection + constraint_name = 'product_pricelist_res_config_settin_res_config_settings_id_fkey' + + # Check if exists + cr.execute(""" + SELECT conrelid::regclass::text + FROM pg_constraint + WHERE conname = %s + """, (constraint_name,)) + result = cr.fetchone() + + if result: + table_name = result[0] + print(f"Found constraint '{constraint_name}' on table '{table_name}'.") + try: + print(f"Dropping table '{table_name}'...") + cr.execute(f"DROP TABLE IF EXISTS {table_name} CASCADE") + print("Table dropped.") + + # Double check constraint + cr.execute("SELECT count(*) FROM pg_constraint WHERE conname = %s", (constraint_name,)) + if cr.fetchone()[0] == 0: + print("Constraint successfully removed.") + cr.commit() + else: + print("WARNING: Constraint still exists! Rolled back.") + cr.rollback() + except Exception as e: + print(f"Error dropping table/constraint: {e}") + cr.rollback() + else: + print(f"Constraint '{constraint_name}' not found. Database is likely clean.") + # Also invoke the hook logic just in case the name varies? + # The hook used 'DROP TABLE IF EXISTS product_pricelist_res_config_settings_rel CASCADE' + # Let's try that too. + print("Attempting to drop 'product_pricelist_res_config_settings_rel' blindly just in case...") + cr.execute("DROP TABLE IF EXISTS product_pricelist_res_config_settings_rel CASCADE") + print("Done.") + cr.commit() + +if __name__ == "__main__": + main() diff --git a/delete_zombie_constraint_direct.py b/delete_zombie_constraint_direct.py new file mode 100644 index 0000000..2812074 --- /dev/null +++ b/delete_zombie_constraint_direct.py @@ -0,0 +1,87 @@ +import sys +import os +import argparse +import configparser +import psycopg2 + +def main(): + parser = argparse.ArgumentParser(description="Drop Zombie Constraint directly via psycopg2") + parser.add_argument("conf_path", help="Path to odoo.conf") + parser.add_argument("db_name", help="Database name") + + args = parser.parse_args() + conf_path = os.path.abspath(args.conf_path) + db_name = args.db_name + + if not os.path.exists(conf_path): + print(f"Error: Config file not found at {conf_path}") + sys.exit(1) + + # Parse config + config = configparser.ConfigParser() + config.read(conf_path) + + if 'options' not in config: + print("Error: 'options' section not found in config file.") + sys.exit(1) + + options = config['options'] + db_host = options.get('db_host', 'localhost') + db_port = options.get('db_port', '5432') + db_user = options.get('db_user', 'odoo') + db_password = options.get('db_password', '') + + print(f"Connecting to database '{db_name}' on {db_host}:{db_port} as user '{db_user}'...") + + try: + conn = psycopg2.connect( + dbname=db_name, + user=db_user, + password=db_password, + host=db_host, + port=db_port + ) + conn.autocommit = False # Use transaction + cur = conn.cursor() + print("Connected successfully.") + + constraint_name = 'product_pricelist_res_config_settin_res_config_settings_id_fkey' + + # Check for constraint existence + cur.execute("SELECT conrelid::regclass::text FROM pg_constraint WHERE conname = %s", (constraint_name,)) + result = cur.fetchone() + + if result: + table_name = result[0] + print(f"Found constraint '{constraint_name}' on table '{table_name}'.") + + # Drop the table (cascade to remove constraint) + print(f"Dropping table '{table_name}'...") + cur.execute(f"DROP TABLE IF EXISTS {table_name} CASCADE") + + # Verify removal + cur.execute("SELECT count(*) FROM pg_constraint WHERE conname = %s", (constraint_name,)) + if cur.fetchone()[0] == 0: + print("Table dropped and constraint removed.") + conn.commit() + print("Transaction committed.") + else: + print("ERROR: Constraint persists after drop! Rolling back.") + conn.rollback() + else: + print(f"Constraint '{constraint_name}' not found.") + # Optimization: Try dropping the likely table name anyway + print("Attempting to drop 'product_pricelist_res_config_settings_rel' just in case...") + cur.execute("DROP TABLE IF EXISTS product_pricelist_res_config_settings_rel CASCADE") + conn.commit() + print("Done.") + + cur.close() + conn.close() + + except Exception as e: + print(f"Database error: {e}") + sys.exit(1) + +if __name__ == "__main__": + main()