> ## Documentation Index
> Fetch the complete documentation index at: https://docs.peliqan.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Sync data between parent & sub accounts

> Sync tables between parent and sub accounts in Peliqan using a Python data app with interactive selection, state persistence, and scheduled execution.

You can sync datasets from a parent account to a sub-account or vice versa. This can be used to share datasets with customers. This approach can also be used as a permission model to share data with teams, where each team uses their own sub-account.

## Sync app

This script supports syncing tables in both directions: from **subaccount → parent** and from **parent → subaccount**.

It can be used for one-time data migrations or recurring scheduled syncs, ensuring that records in subaccounts and parent accounts stay consistent and up to date.

### Steps

1. **Pick sync direction**
   * Choose whether data should flow from **subaccount → parent** or **parent → subaccount**. This determines the source and target of your sync.
2. **Select source account, schema, and tables**
   * Pick the account where the data currently resides.
   * Select the database schema and tables you want to sync.
3. **Select target account and schema**
   * Choose the account where the data should be copied to.
   * Select the target schema to ensure the data lands in the correct location.
4. **Run the sync immediately or save for scheduled sync**
   * Trigger an instant sync to move data now.
   * Or save the configuration to run automatically later (daily, weekly, etc.).
5. **Verify that records are synced and linked correctly**
   * Check that all selected tables are picked successfully.
   * Ensure that records in the target account are correctly linked to their parent records.

<Accordion title="Click to expand script">
  ```python theme={null}
  import json
  from datetime import datetime, timezone

  # --- Utility Functions ---
  def fetch_subaccounts(pq):
      current_time = datetime.now(timezone.utc)
      all_subaccounts = []
      page = 1
      while True:
          result = pq.list_subaccounts(page=page)
          if isinstance(result, str):
              result = json.loads(result)
          all_subaccounts.extend(result.get("data", []))
          if not result.get("next"):
              break
          page += 1
      return [
          {"id": item["id"], "name": item.get("name", "Unnamed Subaccount"), "owner_id": item.get("owner", {}).get("id")}
          for item in all_subaccounts
          if not item.get("expires_on") or datetime.fromisoformat(item["expires_on"].replace("Z", "+00:00")) >= current_time
      ]

  def fetch_schemas(instance):
      schemas = []
      databases = instance.list_databases()
      for db in databases:
          for schema in db.get("schemas", []):
              schemas.append({"id": schema.get("id"), "name": schema.get("name", "Unnamed Schema")})
      return schemas, databases

  def fetch_tables(databases, schema_id):
      tables = []
      for db in databases:
          for table in db.get("tables", []):
              if table.get("schema_id") == schema_id:
                  tables.append({"id": table.get("id"), "name": table.get("name", "Unnamed Table")})
      return tables

  def get_primary_key(table_id, databases):
      for db in databases:
          for table in db.get("tables", []):
              if table.get("id") == table_id:
                  return table.get("primary_field_name")
      return None

  def load_state(direction_key):
      full_state = pq.get_state() or {}
      return full_state.get(direction_key, {})

  def save_state(direction_key, data):
      full_state = pq.get_state() or {}
      full_state[direction_key] = data
      pq.set_state(full_state)

  def sync_sub2parent_from_state(subaccount_id, sub_schema_name, selected_table_names, target_schema_name, target_db_name):
      subaccount_instance = pq.get_subaccount_instance(account_id=subaccount_id)
      schemas, databases = fetch_schemas(subaccount_instance)
      selected_schema = next((s for s in schemas if s["name"] == sub_schema_name), None)
      if not selected_schema:
          return
      tables = fetch_tables(databases, selected_schema["id"])
      selected_tables = [t for t in tables if t["name"] in selected_table_names]
      pk_map = {t["name"]: get_primary_key(t["id"], databases) for t in selected_tables}
      dw_name = f'dw_{subaccount_id}'
      sub_account_dbconn = subaccount_instance.dbconnect(dw_name)
      LIMIT = 500
      for table in selected_tables:
          table_name = table["name"]
          pk = pk_map[table_name]
          offset = 0
          all_rows = []

          while True:
              query = f'SELECT * FROM "{sub_schema_name}"."{table_name}" LIMIT {LIMIT} OFFSET {offset}'
              rows = sub_account_dbconn.fetch(dw_name, query=query)
              if not rows:
                  break
              all_rows.extend(rows)
              offset += LIMIT
          if not all_rows:
              continue
          dbconn = pq.dbconnect(pq.DW_NAME)
          dbconn.write(target_schema_name, table_name, records=all_rows, pk=pk)

  def materialize_parent2sub_from_state(subaccount_id, subaccount_owner_id, subaccount_schema, source_db_name, source_schema_name, selected_table_names):
      parent_databases = pq.list_databases()
      selected_source_db = next((db for db in parent_databases if db["server"]["friendly_name"] == source_db_name), None)
      source_schemas = selected_source_db.get("schemas", []) if selected_source_db else []
      selected_source_schema = next((s for s in source_schemas if s["name"] == source_schema_name), None)
      parent_tables = []
      if selected_source_schema:
          for db in parent_databases:
              if db["server"]["friendly_name"] == source_db_name:
                  for table in db.get("tables", []):
                      if table.get("schema_id") == selected_source_schema["id"]:
                          parent_tables.append(table)
      selected_parent_tables = [t for t in parent_tables if t["name"] in selected_table_names]
      source_tables = [(t["id"], t["name"]) for t in selected_parent_tables]
      pq.subaccount_materialize(
          connection_name=f'dw_{subaccount_id}',
          database_name=f'dw_{subaccount_id}',
          subaccount_id=subaccount_owner_id,
          table_source_list=source_tables,
          target_schema=subaccount_schema
      )


  if RUN_CONTEXT == "interactive":
      # --- Interactive mode ---
      sync_direction = st.radio(
          "Choose sync direction",
          ("sub2parent", "parent2sub"),
          horizontal=True
      )

      stored_state = load_state("sub_to_parent" if sync_direction == "sub2parent" else "parent_to_sub")

      subaccounts = fetch_subaccounts(pq)

      if not subaccounts:
          st.warning("No subaccounts found.")
          st.stop()

      if sync_direction == "sub2parent":
          st.title("&#x1F501; Sync Subaccount Table(s) to Parent Account")
          col1, col2, col3 = st.columns(3)
          with col1:
              subaccount_names = [s["name"] for s in subaccounts]
              selected_sub_name = st.selectbox("Subaccount", subaccount_names, index=None)
              selected_sub = next((s for s in subaccounts if s["name"] == selected_sub_name), None)
          schemas, databases = [], []
          if selected_sub:
              subaccount_instance = pq.get_subaccount_instance(account_id=selected_sub["id"])
              schemas, databases = fetch_schemas(subaccount_instance)
          with col2:
              sub_schema_names = [s["name"] for s in schemas] if schemas else []
              selected_sub_schema_name = st.selectbox("Schema (Subaccount)", sub_schema_names, index=None)
              selected_schema = next((s for s in schemas if s["name"] == selected_sub_schema_name), None)
          with col3:
              tables = fetch_tables(databases, selected_schema["id"]) if selected_schema else []
              table_names = [t["name"] for t in tables]
              selected_table_names = st.multiselect("Table(s) (Subaccount)", table_names, default=None)

          st.subheader("&#x1F4E2; Select Target in Parent Account")

          col4, col5 = st.columns(2)
          parent_databases = pq.list_databases()
          with col4:
              target_db_options = [db["server"]["friendly_name"] for db in parent_databases if "server" in db]
              selected_target_db_name = st.selectbox("Target Database (Parent)", target_db_options, index=None)
              selected_target_db = next((db for db in parent_databases if db["server"]["friendly_name"] == selected_target_db_name), None)
          with col5:
              target_schema_list = selected_target_db.get("schemas", []) if selected_target_db else []
              target_schema_names = [s["name"] for s in target_schema_list]
              selected_target_schema_name = st.selectbox("Target Schema (Parent)", target_schema_names, index=None)

          if st.button("&#x1F4BE; Save selections for scheduled sync"):
              selection_state = {
                  "subaccount_id": selected_sub["id"] if selected_sub else None,
                  "subaccount_schema": selected_schema["name"] if selected_schema else None,
                  "selected_tables": selected_table_names,
                  "target_db_name": selected_target_db_name,
                  "target_schema_name": get.selected_target_schema_name,
              }
              set_state("sub_to_parent", selection_state)
              st.success("Selections saved for scheduled Subaccount&rarr;Parent sync!")

          if st.button("&#x1F680; Sync Table(s) to Parent"):
              if not (selected_sub and selected_schema and selected_table_names and selected_target_schema_name and selected_target_db_name):
                  st.error("Please select all fields.")
              else:
                  sync_sub2parent_from_state(
                      subaccount_id=selected_sub["id"],
                      sub_schema_name=selected_schema["name"],
                      selected_table_names=selected_table_names,
                      target_schema_name=selected_target_schema_name,
                      target_db_name=selected_target_db_name
                  )

      else:
          st.title("&#x1F501; Sync Parent Account Table(s) to Subaccount")
          parent_databases = pq.list_databases()
          col1, col2, col3 = st.columns(3)
          with col1:
              source_db_options = [db["server"]["friendly_name"] for db in parent_databases if "server" in db]
              selected_source_db_name = st.selectbox("Source Database (Parent)", source_db_options, index=None)
              selected_source_db = next((db for db in parent_databases if db["server"]["friendly_name"] == selected_source_db_name), None)
          with col2:
              source_schema_list = selected_source_db.get("schemas", []) if selected_source_db else []
              source_schema_names = [s["name"] for s in source_schema_list]
              selected_source_schema_name = st.selectbox("Source Schema (Parent)", source_schema_names, index=None)
              selected_source_schema = next((s for s in source_schema_list if s["name"] == selected_source_schema_name), None)
          with col3:
              parent_tables = []
              if selected_source_schema:
                  for db in parent_databases:
                      if db["server"]["friendly_name"] == selected_source_db_name:
                          for table in db.get("tables", []):
                              if table.get("schema_id") == selected_source_schema["id"]:
                                  parent_tables.append(table)
              parent_table_names = [t["name"] for t in parent_tables]
              selected_parent_table_names = st.multiselect("Table(s) to Materialize", parent_table_names, default=None)

          st.subheader("&#x1F4E6; Select Target Subaccount & Schema")
          col4, col5 = st.columns(2)
          with col4:
              subaccount_names = [s["name"] for s in subaccounts]
              selected_sub_name = st.selectbox("Target Subaccount", subaccount_names, index=None)
              selected_sub = next((s for s in subaccounts if s["name"] == selected_sub_name), None)
          with col5:
              if selected_sub:
                  subaccount_instance = pq.get_subaccount_instance(account_id=selected_sub["id"])
                  schemas, databases = fetch_schemas(subaccount_instance)
                  schema_names = [s["name"] for s in schemas]
                  selected_sub_schema_name = st.selectbox("Target Schema (Subaccount)", schema_names, index=None)
              else:
                  selected_sub_schema_name = st.selectbox("Target Schema (Subaccount)", [], index=None)

          if st.button("&#x1F4BE; Save selections for scheduled materialization"):
              selection_state = {
                  "source_db_name": selected_source_db_name,
                  "source_schema_name": selected_source_schema_name,
                  "selected_tables": selected_parent_table_names,
                  "subaccount_id": selected_sub["id"] if selected_sub else None,
                  "subaccount_schema": selected_sub_schema_name,
              }
              set_state("parent_to_sub", selection_state)
              st.success("Selections saved for scheduled Parent&rarr;Subaccount materialization!")

          if st.button("&#x1F680; Sync Table(s) to Subaccount"):
              if not (selected_sub and selected_sub_schema_name and selected_parent_table_names):
                  st.error("Please select all required fields.")
              else:
                  materialize_parent2sub_from_state(
                      subaccount_id=selected_sub["id"],
                      subaccount_owner_id=selected_sub["owner_id"],
                      subaccount_schema=selected_sub_schema_name,
                      source_db_name=selected_source_db_name,
                      source_schema_name=selected_source_schema_name,
                      selected_table_names=selected_parent_table_names
                  )


  elif RUN_CONTEXT == "scheduled":
      # --- Subaccount &rarr; Parent ---
      if load_state("sub_to_parent"):
          print(f"Starting Subaccount &rarr; Parent sync for {load_state('sub_to_parent').get('subaccount_id')}")
          state = load_state("sub_to_parent")
          print(state)
          sync_sub2parent_from_state(
              subaccount_id=state.get("subaccount_id"),
              sub_schema_name=state.get("subaccount_schema"),
              selected_table_names=state.get("selected_tables"),
              target_schema_name=state.get("target_schema_name"),
              target_db_name=state.get("target_db_name")
          )
          print("&#x2705; Subaccount &rarr; Parent sync complete.")

      # --- Parent &rarr; Subaccount ---
      if load_state("parent_to_sub"):
          print(f"Starting Parent &rarr; Subaccount materialization for {load_state('parent_to_sub').get('subaccount_id')}")
          state = load_state("parent_to_sub")
          # state = pq.get_state() or {}
          subaccounts = fetch_subaccounts(pq)
          selected_sub = next((s for s in subaccounts if s["id"] == state["subaccount_id"]), None)

          if selected_sub:
              materialize_parent2sub_from_state(
              subaccount_id=state.get("subaccount_id"),
              subaccount_owner_id=selected_sub.get("owner_id") if selected_sub else None,
              subaccount_schema=state.get("subaccount_schema"),
              source_db_name=state.get("source_db_name"),
              source_schema_name=state.get("source_schema_name"),
              selected_table_names=state.get("selected_tables")
          )
              print("&#x2705; Parent &rarr; Subaccount materialization complete.")
          else:
              print("&#x274C; Subaccount not found.")
  ```
</Accordion>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.