Automate On-the-Fly Backup & Restore of Specific YCQL Tables via YBA API

Ever wrapped up a massive bulk load into your production YugabyteDB YCQL cluster and wished you could instantly refresh a secondary read-only reporting or test cluster with the same data, but only for a subset of tables?

This post shows how to automate that … triggering an on-demand backup of specific YCQL tables from one universe and restoring them into another, all through the YugabyteDB Anywhere (YBA) API.

We’ll walk through both a Bash script that call YBA’s /backups and /restore endpoints, parameterized so you can pass universe UUIDs, table lists, keyspaces, and more right from the CLI.

💼 Use Case

You have:

  • ● A production (read-write) universe that receives heavy data ingestion or batch loads.

  • ● A secondary (read-only or analytical) universe that should be periodically refreshed.

Instead of restoring an entire universe snapshot, you can:

  1. Back up only the YCQL tables of interest (e.g., orders, customers, order_items).

  2. Restore just those tables into your secondary universe.

  3. Schedule or trigger it post-load using a script or CI/CD job.

Prerequisites
  • ● YugabyteDB Anywhere (YBA) 2.16 or later (2.18+ recommended for table-level restore).

  • ● API token and customer UUID with permissions to create backups/restores.

  • ● Universes: one source, one target (compatible versions).

  • ● Configured storage config (S3, GCS, Azure, etc.).

  • jq, curl for bash or requests for python.

Find Your Universe UUIDs (CLI)

We’ll use the same YBA API pattern as in the xCluster post (auth header + customer-scoped paths) and the official List universes endpoint as was shown in the YugabyteDB Tip Creating and Inspecting xCluster Configs with the YBA API:

				
					# 1) Set your YBA base URL (usually https on 443)
export YBA_HOST="portal.dev.yugabyte.com"
export YBA_PROTO="https"
export YBA="$YBA_PROTO://$YBA_HOST"

# 2) Auth + Customer UUID
export YBA_TOKEN="<your_api_token_here>"
export CUUID="<your_customer_uuid_here>"

# 3) Convenience alias (same header used in the xCluster tips post)
alias ycurl='curl -sS -H "X-AUTH-YW-API-TOKEN: $YBA_TOKEN" -H "Content-Type: application/json"'

# 4) List universes (name + UUID). Filter to your naming pattern, then sort for readability.
ycurl "$YBA/api/v1/customers/$CUUID/universes" \
  | jq -r '.[] | "\(.name)\t\(.universeUUID)"' \
  | grep jimk | sort
				
			

Sample output:

				
					jimk-source-db	9bacfb49-39c2-4bc0-88e7-20365414e771
jimk-target-db	bc16a2ee-2d77-41ed-b97e-52edac452cd5
				
			

Now export them for the scripts:

				
					export SRC_UUUID=9bacfb49-39c2-4bc0-88e7-20365414e771
export TGT_UUUID=bc16a2ee-2d77-41ed-b97e-52edac452cd5
				
			

Tip: If you prefer to select by exact universe name (no grep), use jq only:

				
					# exact match
SRC_UUUID=$(ycurl "$YBA/api/v1/customers/$CUUID/universes" \
  | jq -r '.[] | select(.name=="jimk-source-db") | .universeUUID')
TGT_UUUID=$(ycurl "$YBA/api/v1/customers/$CUUID/universes" \
  | jq -r '.[] | select(.name=="jimk-target-db") | .universeUUID')
				
			
Find Your Storage Config UUID

Each backup uses a Storage Config defined in YBA.

Use the /configs endpoint to list them and identify the one you’ll use for your backups and restores (e.g. an S3 bucket config).

				
					# List all storage configs
ycurl "$YBA/api/v1/customers/$CUUID/configs" \
  | jq -r '.[] | "\(.configName)\t\(.configUUID)"' | sort

# Filter to your backup config name
ycurl "$YBA/api/v1/customers/$CUUID/configs" \
  | jq -r '.[] | "\(.configName)\t\(.configUUID)"' \
  | grep dev-dev-backup
				
			

Example output:

				
					dev-dev-backup	6304c87c-fa3b-4c57-b4c9-e685213f3517
				
			

Export for the scripts:

				
					export SCUUID=6304c87c-fa3b-4c57-b4c9-e685213f3517

				
			

Or …

				
					# Capture it automatically
export SCUUID=$(ycurl "$YBA/api/v1/customers/$CUUID/configs" \
  | jq -r '.[] | select(.configName=="dev-backup") | .configUUID')
				
			
🧩 The Parameterized Bash Script

Our bash script does the following:

  • ● Resolves YCQL table UUIDs (skips index tables automatically)
  • ● Creates one v2 backup for a subset of tables via keyspaceTableList

  • ● Discovers the backup location & UUID from /backups/page

  • ● Optionally drops the target tables (to avoid name collisions)

  • ● Restores only the requested tables to the target universe

  • ● Optionally deletes the backup when done (v2 POST /backups/delete)

  • ● Surfaces precise failure details using /tasks/{uuid}/failed

✅ Tested on YBA 2.29.0.0-b81 (preview).

  • ● Works with the v2 backup/restore APIs introduced in the YBA 2.16.
  • ● Your exact JSON shapes may vary slightly across builds; the script handles common variants.

Save as yb-ycql-table-refresh.sh and chmod +x it:

				
					#!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<'EOS'
Usage:
  yb-ycql-table-refresh.sh \
    --yba-url https://portal.example.com \
    --api-token <TOKEN> \
    --customer-uuid <CUUID> \
    --source-universe <SRC_UUID> \
    --target-universe <TGT_UUID> \
    --keyspace <ks> \
    --tables t2,t3 \
    --storage-config <SCUUID> \
    [--poll-interval 20] \
    [--sse true|false] [--use-tablespaces true|false] \
    [--pre-drop] [--target-ycql-host <host[:9042]>] \
    [--delete-after] \
    [--target-ycql-user <user>] [--target-ycql-pass <pass>] [--target-ycql-pass-file <file>]

What it does (YBA ≥ 2.29, YCQL):
- Resolves table UUIDs for a subset of YCQL tables (skips index tables).
- Performs a single v2 backup (keyspaceTableList with tableUUIDList).
- Finds the backup via /backups/page.
- (If --pre-drop) Drops target tables AFTER backup succeeds, right before restore.
- Restores only the specified tables to the target universe.
- Displays exact YBA task errors via /tasks/<uuid>/failed (falls back to /subtasks).
- (If --delete-after) Deletes the backup via POST /backups/delete and waits on task.

Secure YCQL auth:
- Prefer env: YB_YCQL_USER / YB_YCQL_PASSWORD
- Or flags: --target-ycql-user / --target-ycql-pass (or --target-ycql-pass-file)
- Script writes a 0600 temp cqlshrc and uses --cqlshrc.

Requires: curl, jq. (ycqlsh required for --pre-drop or target diagnostics)
EOS
}

# ---------- Defaults
POLL_INTERVAL=20
DELETE_AFTER=false
PRE_DROP=false
TARGET_YCQL_HOST=""
SSE=true
USE_TABLESPACES=false

# ---------- YCQL auth (Option A)
TARGET_YCQL_USER="${YB_YCQL_USER:-}"
TARGET_YCQL_PASS=""
TARGET_YCQL_PASS_FILE=""

# ---------- Args
while [[ $# -gt 0 ]]; do
  case "$1" in
    --yba-url) YBA_URL="$2"; shift 2;;
    --api-token) API_TOKEN="$2"; shift 2;;
    --customer-uuid) CUUID="$2"; shift 2;;
    --source-universe) SRC_UUUID="$2"; shift 2;;
    --target-universe) TGT_UUUID="$2"; shift 2;;
    --keyspace) KEYSPACE="$2"; shift 2;;
    --tables) TABLES_CSV="$2"; shift 2;;
    --storage-config) SCUUID="$2"; shift 2;;
    --poll-interval) POLL_INTERVAL="$2"; shift 2;;
    --sse) SSE="$2"; shift 2;;
    --use-tablespaces) USE_TABLESPACES="$2"; shift 2;;
    --delete-after) DELETE_AFTER=true; shift 1;;
    --pre-drop) PRE_DROP=true; shift 1;;
    --target-ycql-host) TARGET_YCQL_HOST="$2"; shift 2;;
    --target-ycql-user) TARGET_YCQL_USER="$2"; shift 2;;
    --target-ycql-pass) TARGET_YCQL_PASS="$2"; shift 2;;
    --target-ycql-pass-file) TARGET_YCQL_PASS_FILE="$2"; shift 2;;
    -h|--help) usage; exit 0;;
    *) echo "Unknown arg: $1" >&2; usage; exit 1;;
  esac
done

# Resolve YCQL password (prefer file > flag > env)
if [[ -n "$TARGET_YCQL_PASS_FILE" ]]; then
  TARGET_YCQL_PASS="$(<"$TARGET_YCQL_PASS_FILE")"
elif [[ -z "$TARGET_YCQL_PASS" && -n "${YB_YCQL_PASSWORD:-}" ]]; then
  TARGET_YCQL_PASS="$YB_YCQL_PASSWORD"
fi

# ---------- Validate
for v in YBA_URL API_TOKEN CUUID SRC_UUUID TGT_UUUID KEYSPACE TABLES_CSV SCUUID; do
  [[ -n "${!v:-}" ]] || { echo "Missing --${v//_/-}" >&2; usage; exit 1; }
done
command -v curl >/dev/null || { echo "curl required" >&2; exit 1; }
command -v jq   >/dev/null || { echo "jq required" >&2; exit 1; }
if $PRE_DROP; then command -v ycqlsh >/dev/null || { echo "ycqlsh required for --pre-drop" >&2; exit 1; }; fi

AUTH="X-AUTH-YW-API-TOKEN: $API_TOKEN"
CT="Content-Type: application/json"

# ---------- Secure ycqlsh wrapper (Option A)
YCQL_RCFILE=""
make_ycqlsh_cmd() {
  local host="$1" port="$2"
  if [[ -z "$TARGET_YCQL_USER" || -z "$TARGET_YCQL_PASS" ]]; then
    echo "ycqlsh $host $port"; return
  fi
  if [[ -z "$YCQL_RCFILE" ]]; then
    YCQL_RCFILE="$(mktemp)"
    chmod 600 "$YCQL_RCFILE"
    cat >"$YCQL_RCFILE" <<EOF
[authentication]
username = $TARGET_YCQL_USER
password = $TARGET_YCQL_PASS
EOF
  fi
  echo "ycqlsh --cqlshrc \"$YCQL_RCFILE\" $host $port"
}
cleanup_ycqlrc() { [[ -n "$YCQL_RCFILE" && -f "$YCQL_RCFILE" ]] && rm -f "$YCQL_RCFILE"; }
trap cleanup_ycqlrc EXIT

# ---------- Error detail helpers
print_failure_details() {
  local task_uuid="$1"
  echo "---- YBA failure details for task $task_uuid ----"
  local FAILED
  FAILED=$(curl -sS -H "$AUTH" "$YBA_URL/api/v1/customers/$CUUID/tasks/$task_uuid/failed" || true)
  if echo "$FAILED" | jq -e . >/dev/null 2>&1; then
    echo "$FAILED" | jq -r '
      .failedSubTasks // []
      | if length==0 then "No failedSubTasks reported."
        else .[] |
          "subTaskType: " + (.subTaskType // "unknown") + "\n" +
          "state      : " + (.subTaskState // "") + "\n" +
          "error      : " + ((.errorString // "") | gsub("\\n"; "\n              ")) + "\n"
        end
    '
  else
    echo "failed-subtasks endpoint not available (or non-JSON)."
  fi
  local SUBS
  SUBS=$(curl -sS -H "$AUTH" "$YBA_URL/api/v1/customers/$CUUID/tasks/$task_uuid/subtasks" || true)
  if echo "$SUBS" | jq -e . >/dev/null 2>&1; then
    echo "$SUBS" | jq -r '
      (if type=="array" then . else (.entities // .data // []) end)
      | map({
          title: (.title // .name // .subTaskGroupType // "subtask"),
          status: (.status // .state // ""),
          err: (.errorString // .details?.errorString // .details?.error?.message // "")
        })
      | map(select(.err != "" or (.status|ascii_downcase)=="failure"))
      | if length==0 then "No errorString fields found in subtasks."
        else .[] | "\(.title): \(.err)\n"
        end
    '
  fi
  local TOP
  TOP=$(curl -sS -H "$AUTH" "$YBA_URL/api/v1/customers/$CUUID/tasks/$task_uuid" || true)
  if echo "$TOP" | jq -e . >/dev/null 2>&1; then
    echo "---- Task summary ----"
    echo "$TOP" | jq '{title,status,percent,details}'
  fi
  echo "-----------------------------------------------"
}

diagnose_target_existing_tables() {
  [[ -n "$TARGET_YCQL_HOST" ]] || return 0
  command -v ycqlsh >/dev/null || return 0
  local ks="$1" tables_csv="$2"
  local host="${TARGET_YCQL_HOST%%:*}"
  local port="${TARGET_YCQL_HOST##*:}"; [[ "$host" == "$port" ]] && port="9042"
  local YCQLSH_CMD; YCQLSH_CMD=$(make_ycqlsh_cmd "$host" "$port")
  echo "---- Target YCQL tables already existing in ${ks} ----"
  eval "$YCQLSH_CMD -e \"SELECT table_name FROM system_schema.tables WHERE keyspace_name='${ks}' AND table_name IN ('${tables_csv//,/','}');\"" 2>/dev/null || true
  echo "------------------------------------------------------"
}

wait_task() {
  local label="$1" task_uuid="$2"
  while true; do
    local st
    st=$(curl -sS -H "$AUTH" "$YBA_URL/api/v1/customers/$CUUID/tasks/$task_uuid" | jq -r '.status // .state // empty')
    echo "   $label: ${st:-unknown}"
    if [[ "$st" == "Success" || "$st" == "Completed" ]]; then
      return 0
    elif [[ "$st" == "Failure" || "$st" == "Failed" ]]; then
      print_failure_details "$task_uuid"
      if [[ "$label" == "restore" ]]; then
        diagnose_target_existing_tables "$KEYSPACE" "$TABLES_CSV"
        echo "Hint: YCQL restore will not overwrite existing tables. Use --pre-drop --target-ycql-host <host[:9042]> to drop before restore."
      fi
      return 1
    fi
    sleep "$POLL_INTERVAL"
  done
}

# ---------- Universe table listing
fetch_tables_json() {
  local url r
  url="$YBA_URL/api/v1/customers/$CUUID/universes/$SRC_UUUID/tables?tableType=YQL_TABLE_TYPE&includeSystemTables=false&excludeIndexTables=true"
  r=$(curl -sS -H "$AUTH" "$url" 2>/dev/null || true)
  if echo "$r" | jq -e . >/dev/null 2>&1; then echo "$r"; return; fi
  url="$YBA_URL/api/v1/customers/$CUUID/universes/$SRC_UUUID/tables?tableType=YQL_TABLE_TYPE&includeSystemTables=false&excludeIndexTables=false"
  r=$(curl -sS -H "$AUTH" "$url" 2>/dev/null || true)
  if echo "$r" | jq -e . >/dev/null 2>&1; then echo "$r"; return; fi
  url="$YBA_URL/api/v1/customers/$CUUID/universes/$SRC_UUUID/tables"
  r=$(curl -sS -H "$AUTH" "$url" 2>/dev/null || true)
  if echo "$r" | jq -e . >/dev/null 2>&1; then echo "$r"; return; fi
  echo "[]"
}

# ---------- Inputs
IFS=',' read -r -a TABLES_ARR <<< "$TABLES_CSV"
TABLES_JSON=$(printf '%s\n' "${TABLES_ARR[@]}" | jq -R . | jq -s .)

# ---------- Resolve table UUIDs (skip indexes)
echo "==> Resolving YCQL table UUIDs for keyspace '$KEYSPACE' and tables: $TABLES_CSV"
ALL_TABLES_JSON=$(fetch_tables_json)
NORM=$(echo "$ALL_TABLES_JSON" | jq '
  (if type=="array" then . else (.data // .entities // []) end)
  | map({
      tableName: (.tableName // .name // ""),
      tableUUID: (.tableUUID // .uuid // .id // .tableID // ""),
      tableType: (.tableType // .type // ""),
      isIndex: (.isIndex // .isIndexTable // false)
    })
  | map(select(.tableType == "YQL_TABLE_TYPE"))
  | map(select((.isIndex | not) and (.tableName | test("_idx$") | not)))
')
TBL_UUIDS_JSON=$(jq -n --argjson want "$TABLES_JSON" --argjson all "$NORM" '
  $all | map(select(.tableName as $n | ($want | index($n)))) | map(.tableUUID)
')
echo "==> Resolved table UUIDs:"
jq -n --argjson want "$TABLES_JSON" --argjson all "$NORM" '
  $all | map(select(.tableName as $n | ($want | index($n)))) | map({tableName, tableUUID})
' | jq .
if [[ -z "$TBL_UUIDS_JSON" || "$TBL_UUIDS_JSON" == "[]" ]]; then
  echo "Could not resolve YCQL table UUIDs for: $TABLES_CSV" >&2
  echo "$NORM" | jq . >&2
  exit 50
fi
COUNT_WANT=$(echo "$TABLES_JSON" | jq 'length')
COUNT_UUID=$(echo "$TBL_UUIDS_JSON" | jq 'length')
if [[ "$COUNT_WANT" -ne "$COUNT_UUID" ]]; then
  echo "Resolved $COUNT_UUID UUID(s) for $COUNT_WANT requested table(s) — mismatch. Aborting." >&2
  echo "Names:";  echo "$TABLES_JSON"    | jq .
  echo "UUIDs:";  echo "$TBL_UUIDS_JSON" | jq .
  exit 51
fi

# ---------- 1) Create ONE subset backup (names + UUIDs)
echo "==> Creating FULL YCQL backup for keyspace '$KEYSPACE' tables: $TABLES_CSV"
BACKUP_PAYLOAD=$(jq -n \
  --arg u "$SRC_UUUID" --arg sc "$SCUUID" --arg ks "$KEYSPACE" \
  --argjson tl "$TABLES_JSON" --argjson tul "$TBL_UUIDS_JSON" \
  --arg cu "$CUUID" --argjson sse "$SSE" --argjson ut "$USE_TABLESPACES" '
  {
    backupType:"YQL_TABLE_TYPE", customerUUID:$cu, sse:$sse,
    storageConfigUUID:$sc, universeUUID:$u,
    tableByTableBackup:false, useTablespaces:$ut,
    keyspaceTableList:[{keyspace:$ks, tableNameList:$tl, tableUUIDList:$tul}],
    timeBeforeDelete:0
  }')
CREATE=$(curl -sS -X POST "$YBA_URL/api/v1/customers/$CUUID/backups" -H "$AUTH" -H "$CT" -d "$BACKUP_PAYLOAD")
TASK_ID=$(echo "$CREATE" | jq -r '.taskUUID // .taskId // empty')
[[ -n "$TASK_ID" ]] || { echo "Backup create did not return taskUUID"; echo "$CREATE" | jq . || echo "$CREATE"; exit 1; }

echo "==> Waiting for backup task $TASK_ID"
wait_task "backup" "$TASK_ID" || { echo "Backup failed"; exit 2; }

# ---------- 2) Find this backup by taskUUID; extract defaultLocation
echo "==> Finding backup by taskUUID via /backups/page"
RAW=$(curl -sS -X POST "$YBA_URL/api/v1/customers/$CUUID/backups/page" \
  -H "$AUTH" -H "$CT" \
  -d "$(jq -n --arg u "$SRC_UUUID" \
        '{sortBy:"createTime", direction:"DESC",
          filter:{universeUUIDList:[$u], showHidden:true},
          limit:50, offset:0, needTotalCount:true}')")
ENTITY=$(echo "$RAW" | jq -r --arg tid "$TASK_ID" '.entities | map(select(.commonBackupInfo.taskUUID==$tid)) | first')
[[ "$ENTITY" != "null" && -n "$ENTITY" ]] || { echo "Could not locate backup entity for task $TASK_ID"; echo "$RAW" | jq .; exit 4; }
RESP_OBJ=$(echo "$ENTITY" | jq '(.commonBackupInfo.responseList | if type=="array" then .[0] else . end)')
LOC=$(echo "$RESP_OBJ" | jq -r '.defaultLocation // empty')
BID=$(echo "$ENTITY" | jq -r '.commonBackupInfo.backupUUID // empty')
[[ -n "$LOC" ]] || { echo "Could not extract defaultLocation"; echo "$ENTITY" | jq .; exit 5; }
echo "   location:   $LOC"
[[ -n "$BID" ]] && echo "   backupUUID: $BID"

# ---------- 2.5) Optional pre-drop ON TARGET (after backup success)
if $PRE_DROP; then
  [[ -n "$TARGET_YCQL_HOST" ]] || { echo "Need --target-ycql-host for --pre-drop"; exit 1; }
  command -v ycqlsh >/dev/null || { echo "ycqlsh required for --pre-drop" >&2; exit 1; }
  host="${TARGET_YCQL_HOST%%:*}"
  port="${TARGET_YCQL_HOST##*:}"; [[ "$host" == "$port" ]] && port="9042"
  YCQLSH_CMD=$(make_ycqlsh_cmd "$host" "$port")
  echo "==> Dropping target tables on $host:$port"
  for t in "${TABLES_ARR[@]}"; do
    echo "   DROP TABLE IF EXISTS ${KEYSPACE}.${t};"
    eval "$YCQLSH_CMD -e \"DROP TABLE IF EXISTS ${KEYSPACE}.${t};\"" || true
  done
fi

# ---------- 3) Restore only the requested tables (names + UUIDs)
RESTORE_PAYLOAD=$(jq -n \
  --arg action "RESTORE" --arg sc "$SCUUID" --arg tgt "$TGT_UUUID" --arg cu "$CUUID" \
  --arg ks "$KEYSPACE" --arg loc "$LOC" --argjson tl "$TABLES_JSON" --argjson tul "$TBL_UUIDS_JSON" \
  --argjson sse "$SSE" --argjson ut "$USE_TABLESPACES" '
  {actionType:$action, storageConfigUUID:$sc, universeUUID:$tgt,
   backupStorageInfoList:[{backupType:"YQL_TABLE_TYPE",keyspace:$ks,sse:$sse,storageLocation:$loc,
                           useTablespaces:$ut,tableNameList:$tl,tableUUIDList:$tul}],
   customerUUID:$cu}')
echo "==> Restoring selected tables via POST /api/v1/customers/$CUUID/restore"
RSP=$(curl -sS -w '\n%{http_code}' -X POST "$YBA_URL/api/v1/customers/$CUUID/restore" -H "$AUTH" -H "$CT" -d "$RESTORE_PAYLOAD")
BODY=$(echo "$RSP" | sed '$d'); CODE=$(echo "$RSP" | tail -n1)
[[ "$CODE" == "200" || "$CODE" == "202" ]] || { echo "Restore create failed (HTTP $CODE). Body:"; echo "$BODY" | jq . || echo "$BODY"; exit 6; }
RTASK=$(echo "$BODY" | jq -r '.taskUUID // .taskId // empty')
[[ -n "$RTASK" ]] || { echo "Restore response had no taskUUID"; echo "$BODY" | jq . || echo "$BODY"; exit 7; }
echo "==> Waiting for restore task $RTASK"
wait_task "restore" "$RTASK" || { echo "Restore failed"; exit 8; }

# ---------- 4) Optional delete (via POST /backups/delete)
if $DELETE_AFTER && [[ -n "$BID" ]]; then
  echo "==> Deleting backup $BID"
  DELETE_PAYLOAD=$(jq -n --arg bid "$BID" --arg sc "$SCUUID" \
    '{deleteBackupInfos:[{backupUUID:$bid, storageConfigUUID:$sc}]}')
  DEL=$(curl -sS -w '\n%{http_code}' -X POST "$YBA_URL/api/v1/customers/$CUUID/backups/delete" -H "$AUTH" -H "$CT" -d "$DELETE_PAYLOAD")
  DEL_BODY=$(echo "$DEL" | sed '$d'); DEL_CODE=$(echo "$DEL" | tail -n1)
  if [[ "$DEL_CODE" != "200" && "$DEL_CODE" != "202" ]]; then
    echo "Delete request failed (HTTP $DEL_CODE). Body:"; echo "$DEL_BODY" | jq . || echo "$DEL_BODY"
  else
    for dt in $(echo "$DEL_BODY" | jq -r '.taskUUID[]? // empty'); do
      echo "   Waiting for delete task $dt"
      wait_task "delete" "$dt" || { echo "Delete task $dt failed"; exit 9; }
    done
    if [[ -z "$(echo "$DEL_BODY" | jq -r '.taskUUID[]? // empty')" ]]; then
      maybe=$(echo "$DEL_BODY" | jq -r '.taskUUID // empty')
      if [[ -n "$maybe" && "$maybe" != "null" ]]; then
        echo "   Waiting for delete task $maybe"
        wait_task "delete" "$maybe" || { echo "Delete task $maybe failed"; exit 9; }
      fi
    fi
  fi
fi

echo "🎉 YCQL selective table refresh complete (subset backup + subset restore via UUIDs)."
				
			

💡 Example:

				
					./yb-ycql-table-refresh.sh \
  --yba-url "$YBA" \
  --api-token "$YBA_TOKEN" \
  --customer-uuid "$CUUID" \
  --source-universe "$SRC_UUUID" \
  --target-universe "$TGT_UUUID" \
  --keyspace app_ks \
  --tables orders,order_items,customers \
  --storage-config "$SCUUID"
				
			

Let’s try it out!

On the Source Universe, let’s create a keyspace with four tables.

				
					 -- On the Source Universe
CREATE KEYSPACE k1;
CREATE TABLE k1.t1 (c1 INT PRIMARY KEY, c2 INT) WITH transactions = {'enabled': 'true'};
CREATE INDEX t1_c2_idx ON k1.t1(c2);
CREATE TABLE k1.t2 (c1 INT PRIMARY KEY, c2 INT) WITH transactions = {'enabled': 'true'};
CREATE TABLE k1.t3 (c1 INT PRIMARY KEY, c2 INT) WITH transactions = {'enabled': 'true'};
CREATE INDEX t3_c2_idx ON k1.t3(c2);
CREATE TABLE k1.t4 (c1 INT PRIMARY KEY, c2 INT) WITH transactions = {'enabled': 'true'};
CREATE INDEX t4_c2_idx ON k1.t4(c2);
INSERT INTO k1.t1 (c1, c2) VALUES (1, 1);
INSERT INTO k1.t2 (c1, c2) VALUES (1, 1);
INSERT INTO k1.t3 (c1, c2) VALUES (1, 1);
INSERT INTO k1.t4 (c1, c2) VALUES (1, 1);
				
			

🏃‍♂️ Example run:

				
					[ec2-user@ip-10-9-3-89 jimk]$ ./yb-ycql-table-refresh.sh --yba-url "$YBA" --api-token "$YBA_TOKEN" --customer-uuid "$CUUID" --source-universe "$SRC_UUUID" --target-universe "$TGT_UUUID" --keyspace k1 --tables t2,t3 --storage-config "$SCUUID"
==> Resolving YCQL table UUIDs for keyspace 'k1' and tables: t2,t3
==> Resolved table UUIDs:
[
  {
    "tableName": "t3",
    "tableUUID": "6777c82c-27b8-40c8-b1e8-b341bfb36a69"
  },
  {
    "tableName": "t2",
    "tableUUID": "9f61ba02-dd53-4c6c-9224-c83f63f3d329"
  }
]
==> Creating FULL YCQL backup for keyspace 'k1' tables: t2,t3
==> Waiting for backup task 807edde6-6133-45c7-be6e-1ee46756a731
   backup: Running
   backup: Success
==> Finding backup by taskUUID via /backups/page
   location:   s3://dev-portal-backup/yb-adoption/univ-jimk-source-db-9bacfb49-39c2-4bc0-88e7-20365414e771/k1/ybc_backup-d08006f050ab499bb629ca0bfb6e7263/full/2025-11-05T03:03:36/multi-table-k1_09a2e8560f3b44dda817888630896d02
   backupUUID: d08006f0-50ab-499b-b629-ca0bfb6e7263
==> Restoring selected tables via POST /api/v1/customers/11d78d93-1381-4d1d-8393-ba76f47ba7a6/restore
==> Waiting for restore task 8e7593e3-29e4-484b-8078-a810388c536e
   restore: Running
   restore: Running
   restore: Success
🎉 YCQL selective table refresh complete (subset backup + subset restore via UUIDs).
				
			

Once the restore completes, we can describe the keyspace and query the tables in the target universe.

				
					cassandra@ycqlsh> -- On the Target Universe

cassandra@ycqlsh> DESCRIBE KEYSPACE k1;

CREATE KEYSPACE k1 WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}  AND durable_writes = true;

CREATE TABLE k1.t2 (
    c1 int PRIMARY KEY,
    c2 int
) WITH default_time_to_live = 0
    AND transactions = {'enabled': 'true'};

CREATE TABLE k1.t3 (
    c1 int PRIMARY KEY,
    c2 int
) WITH default_time_to_live = 0
    AND transactions = {'enabled': 'true'};
CREATE INDEX t3_c2_idx ON k1.t3 (c2, c1)
    WITH CLUSTERING ORDER BY (c1 ASC)
    AND transactions = {'enabled': 'true'};

cassandra@ycqlsh> SELECT * FROM k1.t2;

 c1 | c2
----+----
  1 |  1

(1 rows)
cassandra@ycqlsh> SELECT * FROM k1.t3;

 c1 | c2
----+----
  1 |  1

(1 rows)
				
			

Let’s add some data on the source universe tables and make another backup and restore…

				
					cassandra@ycqlsh> -- On the Source Universe
cassandra@ycqlsh> INSERT INTO k1.t2 (c1, c2) VALUES (2, 2);
cassandra@ycqlsh> INSERT INTO k1.t2 (c1, c2) VALUES (3, 3);
cassandra@ycqlsh> INSERT INTO k1.t2 (c1, c2) VALUES (4, 4);
cassandra@ycqlsh> INSERT INTO k1.t3 (c1, c2) VALUES (2, 2);
				
			
				
					[ec2-user@ip-10-9-3-89 jimk]$ ./yb-ycql-table-refresh.sh --yba-url "$YBA" --api-token "$YBA_TOKEN" --customer-uuid "$CUUID" --source-universe "$SRC_UUUID" --target-universe "$TGT_UUUID" --keyspace k1 --tables t2,t3 --storage-config "$SCUUID"
==> Resolving YCQL table UUIDs for keyspace 'k1' and tables: t2,t3
==> Resolved table UUIDs:
[
  {
    "tableName": "t3",
    "tableUUID": "6777c82c-27b8-40c8-b1e8-b341bfb36a69"
  },
  {
    "tableName": "t2",
    "tableUUID": "9f61ba02-dd53-4c6c-9224-c83f63f3d329"
  }
]
==> Creating FULL YCQL backup for keyspace 'k1' tables: t2,t3
==> Waiting for backup task f5c996b2-6ca4-4a0f-a546-128d98891f0f
   backup: Running
   backup: Success
   location:   s3://dev-portal-backup/yb-adoption/univ-jimk-source-db-9bacfb49-39c2-4bc0-88e7-20365414e771/k1/ybc_backup-8bbd170c905148c1b2e060df30b24adf/full/2025-11-04T20:48:33/multi-table-k1_f82e4c2f51f1474cab924ab98657b252
   backupUUID: 8bbd170c-9051-48c1-b2e0-60df30b24adf
==> Restoring selected tables
==> Waiting for restore task c05919b7-c99f-41aa-8fb7-230cd0834c72
   restore: Running
   restore: Failure
---- YBA failure details for task c05919b7-c99f-41aa-8fb7-230cd0834c72 ----
subTaskType: RestoreBackup
state      : Failure
error      : Failed to execute task {"platformVersion":"2.29.0.0-b81","sleepAfterMasterRestartMillis":180000,"sleepAfterTServerRestartMillis":180000,"nodeExporterUser":"prometheus","universeUUID":"bc16a2ee-2d77-41ed-b97e-52edac452cd5","enableYbc":false,"installYbc":false,"ybcInstalled":false,"encryptionAtRestConfig":{"encryptionAtRestEnabled":false,"opType":"UNDEFINED","type":"DATA_KEY"},"communicationPorts":{"masterHttpPort":7000,"masterRpcPort":7100,"tserverHttpPort":9000,"tserverRpcPort":9100,"ybControllerHttpPort":14000,"yb..., hit error:

               Keyspace k1 contains tables with same names, overwriting data is not allowed.

---- Task summary ----
{
  "title": "Restored Universe : jimk-target-db",
  "status": "Failure",
  "percent": 100.0,
  "details": {
    "taskDetails": [
      {
        "title": "Configuring the universe",
        "description": "Creating and populating the universe config, waiting for the various machines to discover one another.",
        "state": "Success",
        "extraDetails": []
      }
    ]
  }
}
-----------------------------------------------
Hint: YCQL restore will not overwrite existing tables. Use --pre-drop --target-ycql-host <host> to drop before restore.
				
			

This time, the restore failed. The error message on line 29 explains why: “Keyspace k1 contains tables with the same names; overwriting data is not allowed.

The script also provides a helpful hint (see line 48 above) suggesting a possible solution: “Use --pre-drop --target-ycql-host <host> to drop before restore.

Let’s give it a try! 🚀 And this time, we’ll include the --delete-after option to automatically remove the backup once the restore completes:

				
					[ec2-user@ip-10-9-3-89 jimk]$ ./yb-ycql-table-refresh.sh --yba-url "$YBA" --api-token "$YBA_TOKEN" --customer-uuid "$CUUID" --source-universe "$SRC_UUUID" --target-universe "$TGT_UUUID" --keyspace k1 --tables t2,t3 --storage-config "$SCUUID" --delete-after --pre-drop --target-ycql-host 172.150.25.244 --target-ycql-user cassandra --target-ycql-pass Yugabyte123!
==> Resolving YCQL table UUIDs for keyspace 'k1' and tables: t2,t3
==> Resolved table UUIDs:
[
  {
    "tableName": "t3",
    "tableUUID": "6777c82c-27b8-40c8-b1e8-b341bfb36a69"
  },
  {
    "tableName": "t2",
    "tableUUID": "9f61ba02-dd53-4c6c-9224-c83f63f3d329"
  }
]
==> Creating FULL YCQL backup for keyspace 'k1' tables: t2,t3
==> Waiting for backup task db507966-510b-4cff-88ef-730e29c35a38
   backup: Running
   backup: Success
==> Finding backup by taskUUID via /backups/page
   location:   s3://dev-backup/yb-adoption/univ-jimk-source-db-9bacfb49-39c2-4bc0-88e7-20365414e771/k1/ybc_backup-e0f3704196d14a9a8b65586a07a9b67f/full/2025-11-05T03:14:49/multi-table-k1_6df407cb79f540a88871c05abd1ee442
   backupUUID: e0f37041-96d1-4a9a-8b65-586a07a9b67f
==> Dropping target tables on 172.150.25.244:9042
   DROP TABLE IF EXISTS k1.t2;
   DROP TABLE IF EXISTS k1.t3;
==> Restoring selected tables via POST /api/v1/customers/11d78d93-1381-4d1d-8393-ba76f47ba7a6/restore
==> Waiting for restore task 7aa7b3f8-21a7-4fd9-a309-fe32cad0768b
   restore: Running
   restore: Running
   restore: Success
==> Deleting backup e0f37041-96d1-4a9a-8b65-586a07a9b67f
   Waiting for delete task fd5ae5b9-33ac-40d3-b211-669d0343b67f
   delete: Running
   delete: Success
🎉 YCQL selective table refresh complete (subset backup + subset restore via UUIDs).
				
			

Notice the DROP TABLE commands on lines 22 and 23. Once the tables are dropped, the restore can complete successfully.

Verifying that the new rows added in the source universe now exists in the target universe:

				
					cassandra@ycqlsh> -- On the Target Universe

cassandra@ycqlsh> SELECT * FROM k1.t2;

 c1 | c2
----+----
  1 |  1
  4 |  4
  2 |  2
  3 |  3

(4 rows)

cassandra@ycqlsh> SELECT * FROM k1.t3;

 c1 | c2
----+----
  1 |  1
  2 |  2

(2 rows)
				
			
📝 Summary

Automate a clean YCQL “table refresh” from one universe to another using YBA’s v2 APIs and a single Bash script.

The script resolves table UUIDs, creates one subset backup (only the tables you choose), discovers the backup location automatically, and restores those tables to your target universe. It can optionally drop target tables right before restore to avoid name collisions and optionally delete the backup afterward. Credentials are handled securely (temp cqlshrc), and failures surface clear YBA error details so you can fix issues fast.

Perfect for post–bulk-load repopulation of staging/UAT clusters without touching the rest of the keyspace.

📘 API Reference docs:

Have Fun!

Voted today! ✅ If you’re in the U.S., hope you had a chance to do the same. 🇺🇸