Add script to clone inventory between tenant DBs
This commit is contained in:
142
scripts/clone_inventory.py
Normal file
142
scripts/clone_inventory.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Clone inventory tables from one tenant DB to another.
|
||||||
|
|
||||||
|
Uses COPY with the columns common to both source and target, so minor schema
|
||||||
|
mismatches (e.g. missing latitude/longitude in branches) are handled.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
python scripts/clone_inventory.py \
|
||||||
|
--source tenant_autopartes_estrada \
|
||||||
|
--target tenant_originales_autopartes
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
from contextlib import closing
|
||||||
|
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
|
||||||
|
# (table, source_where_clause)
|
||||||
|
TABLES = [
|
||||||
|
("branches", "id <> 1"),
|
||||||
|
("inventory", None),
|
||||||
|
("inventory_stock", None),
|
||||||
|
("inventory_sku_aliases", None),
|
||||||
|
("inventory_stock_summary", None),
|
||||||
|
("inventory_vehicle_compat", None),
|
||||||
|
]
|
||||||
|
|
||||||
|
SEQUENCES = [
|
||||||
|
("branches_id_seq", "branches"),
|
||||||
|
("inventory_id_seq", "inventory"),
|
||||||
|
("inventory_stock_id_seq", "inventory_stock"),
|
||||||
|
("inventory_sku_aliases_id_seq", "inventory_sku_aliases"),
|
||||||
|
("inventory_vehicle_compat_id_seq", "inventory_vehicle_compat"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def connect(db_name: str):
|
||||||
|
return psycopg2.connect(host="localhost", user="postgres", dbname=db_name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_columns(cur, table: str):
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = %s
|
||||||
|
AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""",
|
||||||
|
(table,),
|
||||||
|
)
|
||||||
|
return [r[0] for r in cur.fetchall()]
|
||||||
|
|
||||||
|
|
||||||
|
def copy_table(src_conn, dst_conn, table: str, where: str | None):
|
||||||
|
with src_conn.cursor() as src_cur, dst_conn.cursor() as dst_cur:
|
||||||
|
src_cols = get_columns(src_cur, table)
|
||||||
|
dst_cols = set(get_columns(dst_cur, table))
|
||||||
|
common = [c for c in src_cols if c in dst_cols]
|
||||||
|
if not common:
|
||||||
|
print(f"Skipping {table}: no common columns")
|
||||||
|
return
|
||||||
|
|
||||||
|
col_sql = ", ".join(f'"{c}"' for c in common)
|
||||||
|
copy_to = f'COPY (SELECT {col_sql} FROM "{table}"'
|
||||||
|
if where:
|
||||||
|
copy_to += f" WHERE {where}"
|
||||||
|
copy_to += ") TO STDOUT"
|
||||||
|
copy_from = f'COPY "{table}" ({col_sql}) FROM STDIN'
|
||||||
|
|
||||||
|
print(f"Copying {table} ({len(common)} columns)...", end=" ", flush=True)
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
|
with tempfile.SpooledTemporaryFile(max_size=50 * 1024 * 1024, mode="w+b") as tmp:
|
||||||
|
src_cur.copy_expert(copy_to, tmp)
|
||||||
|
tmp.seek(0)
|
||||||
|
dst_cur.copy_expert(copy_from, tmp)
|
||||||
|
|
||||||
|
dst_conn.commit()
|
||||||
|
elapsed = time.time() - start
|
||||||
|
print(f"done in {elapsed:.1f}s")
|
||||||
|
|
||||||
|
|
||||||
|
def reset_sequences(dst_conn):
|
||||||
|
with dst_conn.cursor() as cur:
|
||||||
|
for seq, table in SEQUENCES:
|
||||||
|
cur.execute(
|
||||||
|
f"SELECT setval('{seq}', COALESCE((SELECT MAX(id) FROM \"{table}\"), 1), true)"
|
||||||
|
)
|
||||||
|
dst_conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Clone inventory between tenant DBs")
|
||||||
|
parser.add_argument("--source", required=True)
|
||||||
|
parser.add_argument("--target", required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
src = connect(args.source)
|
||||||
|
dst = connect(args.target)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Prepare target: remove extra branches, truncate inventory tables.
|
||||||
|
with dst.cursor() as cur:
|
||||||
|
print("Preparing target tables...", end=" ", flush=True)
|
||||||
|
cur.execute("DELETE FROM branches WHERE id <> 1")
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
TRUNCATE TABLE inventory,
|
||||||
|
inventory_stock,
|
||||||
|
inventory_sku_aliases,
|
||||||
|
inventory_stock_summary,
|
||||||
|
inventory_vehicle_compat
|
||||||
|
CASCADE
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
dst.commit()
|
||||||
|
print("done")
|
||||||
|
|
||||||
|
for table, where in TABLES:
|
||||||
|
copy_table(src, dst, table, where)
|
||||||
|
|
||||||
|
print("Resetting sequences...", end=" ", flush=True)
|
||||||
|
reset_sequences(dst)
|
||||||
|
print("done")
|
||||||
|
|
||||||
|
print("Inventory clone completed.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: {e}", file=sys.stderr)
|
||||||
|
dst.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
src.close()
|
||||||
|
dst.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user