forked from anomalyco/opencode
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmigrate-dbs.sh
More file actions
executable file
·296 lines (248 loc) · 6.45 KB
/
migrate-dbs.sh
File metadata and controls
executable file
·296 lines (248 loc) · 6.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
#!/usr/bin/env bash
set -euo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DATA_DIR="${HOME}/.local/share/opencode"
TARGET=""
DRY_RUN=0
FORCE=0
declare -a SOURCES=()
TABLES=(
"project"
"workspace"
"session"
"message"
"part"
"todo"
"permission"
"session_share"
)
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Merge legacy opencode-*.db files into the canonical opencode.db offline.
Options:
--data-dir DIR Override the data directory (default: ~/.local/share/opencode)
--target PATH Override the canonical target DB path
--source PATH Merge a specific source DB (repeatable)
--dry-run Print the plan without modifying anything
--force Skip open-file safety checks
--help Show this help message
Examples:
$(basename "$0")
$(basename "$0") --dry-run
$(basename "$0") --source "$HOME/.local/share/opencode/opencode-custom-v1.2.24.db"
EOF
}
die() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
need() {
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
}
sqlq() {
local value="$1"
value=${value//\'/\'\'}
printf "'%s'" "$value"
}
db_files() {
local db="$1"
printf '%s\n' "$db"
[[ -f "${db}-wal" ]] && printf '%s\n' "${db}-wal"
[[ -f "${db}-shm" ]] && printf '%s\n' "${db}-shm"
}
checkpoint() {
local db="$1"
[[ -f "$db" ]] || return
sqlite3 "$db" "PRAGMA wal_checkpoint(TRUNCATE);" >/dev/null
}
table_exists() {
local db="$1"
local name="$2"
local hit
hit="$(sqlite3 "$db" "SELECT 1 FROM sqlite_master WHERE type='table' AND name=$(sqlq "$name");")"
[[ "$hit" == "1" ]]
}
count() {
local db="$1"
local name="$2"
if ! table_exists "$db" "$name"; then
printf '0\n'
return
fi
sqlite3 "$db" "SELECT COUNT(*) FROM $name;"
}
check_db() {
local db="$1"
local label="$2"
local quick
local full
local fk
quick="$(sqlite3 "$db" "PRAGMA quick_check;")"
[[ "$quick" == "ok" ]] || die "$label quick_check failed: $quick"
full="$(sqlite3 "$db" "PRAGMA integrity_check;")"
[[ "$full" == "ok" ]] || die "$label integrity_check failed: $full"
fk="$(sqlite3 "$db" "PRAGMA foreign_key_check;")"
[[ -z "$fk" ]] || die "$label foreign_key_check failed: $fk"
}
report_counts() {
local db="$1"
local label="$2"
printf '%s\n' "$label"
local table
for table in "${TABLES[@]}"; do
printf ' %s: %s\n' "$table" "$(count "$db" "$table")"
done
}
discover() {
if (( ${#SOURCES[@]} > 0 )); then
return
fi
shopt -s nullglob
local glob=("$DATA_DIR"/opencode-*.db)
shopt -u nullglob
local file
if (( ${#glob[@]} == 0 )); then
return
fi
while IFS= read -r file; do
SOURCES+=("$file")
done < <(printf '%s\n' "${glob[@]}" | LC_ALL=C sort)
}
check_open() {
(( FORCE == 1 )) && return
local files=()
local db
for db in "$TARGET" "${SOURCES[@]}"; do
while IFS= read -r file; do
files+=("$file")
done < <(db_files "$db")
done
(( ${#files[@]} == 0 )) && return
local out
out="$(lsof "${files[@]}" 2>/dev/null || true)"
[[ -z "$out" ]] || die "Some opencode DB files are open. Stop opencode completely or rerun with --force if you know they are offline.\n$out"
}
copy_set() {
local db="$1"
local dst="$2"
while IFS= read -r file; do
cp -p "$file" "$dst/"
done < <(db_files "$db")
}
merge() {
local work="$1"
local src="$2"
local sql table
sql="PRAGMA foreign_keys = ON; ATTACH DATABASE $(sqlq "$src") AS src; BEGIN IMMEDIATE;"
for table in "${TABLES[@]}"; do
if table_exists "$src" "$table"; then
sql+="INSERT OR IGNORE INTO $table SELECT * FROM src.$table;"
fi
done
sql+="COMMIT; DETACH DATABASE src;"
sqlite3 -bail "$work" "$sql"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--data-dir)
[[ -n "${2:-}" ]] || die "--data-dir requires a value"
DATA_DIR="$2"
shift 2
;;
--target)
[[ -n "${2:-}" ]] || die "--target requires a value"
TARGET="$2"
shift 2
;;
--source)
[[ -n "${2:-}" ]] || die "--source requires a value"
SOURCES+=("$2")
shift 2
;;
--dry-run)
DRY_RUN=1
shift
;;
--force)
FORCE=1
shift
;;
--help|-h)
usage
exit 0
;;
*)
die "Unknown option: $1"
;;
esac
done
need sqlite3
need lsof
TARGET="${TARGET:-$DATA_DIR/opencode.db}"
[[ -d "$DATA_DIR" ]] || die "Data directory not found: $DATA_DIR"
[[ -f "$TARGET" ]] || die "Target DB not found: $TARGET"
discover
if (( ${#SOURCES[@]} == 0 )); then
printf 'No legacy source DBs found in %s\n' "$DATA_DIR"
exit 0
fi
local_target="$(cd "$(dirname "$TARGET")" && pwd)/$(basename "$TARGET")"
TARGET="$local_target"
declare -a resolved=()
for src in "${SOURCES[@]}"; do
[[ -f "$src" ]] || die "Source DB not found: $src"
src="$(cd "$(dirname "$src")" && pwd)/$(basename "$src")"
[[ "$src" != "$TARGET" ]] || die "Source DB must not be the target DB: $src"
resolved+=("$src")
done
SOURCES=("${resolved[@]}")
check_open
for db in "$TARGET" "${SOURCES[@]}"; do
checkpoint "$db"
done
check_db "$TARGET" "target"
for src in "${SOURCES[@]}"; do
check_db "$src" "source $src"
done
STAMP="$(date +%Y%m%d-%H%M%S)"
BACKUP_DIR="$(mktemp -d "$DATA_DIR/db-merge-backup-$STAMP.XXXXXX")"
WORK_DIR="$(mktemp -d "$DATA_DIR/db-merge-work-$STAMP.XXXXXX")"
WORK_DB="$WORK_DIR/opencode.db"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
printf '=== opencode offline DB merge ===\n'
printf 'Repo: %s\n' "$REPO_DIR"
printf 'Data dir: %s\n' "$DATA_DIR"
printf 'Target: %s\n' "$TARGET"
printf 'Backup: %s\n' "$BACKUP_DIR"
printf 'Sources (%s):\n' "${#SOURCES[@]}"
for src in "${SOURCES[@]}"; do
printf ' - %s\n' "$src"
done
if (( DRY_RUN == 1 )); then
printf 'Dry run only. No files were modified.\n'
exit 0
fi
copy_set "$TARGET" "$BACKUP_DIR"
for src in "${SOURCES[@]}"; do
copy_set "$src" "$BACKUP_DIR"
done
cp -p "$TARGET" "$WORK_DB"
report_counts "$WORK_DB" "--- Pre-merge counts ---"
for src in "${SOURCES[@]}"; do
printf -- '--- Merging: %s ---\n' "$src"
merge "$WORK_DB" "$src"
check_db "$WORK_DB" "work DB after merging $src"
done
report_counts "$WORK_DB" "--- Post-merge counts ---"
rm -f "${TARGET}-wal" "${TARGET}-shm"
mv "$WORK_DB" "$TARGET"
sqlite3 "$TARGET" "PRAGMA wal_checkpoint(TRUNCATE);" >/dev/null || true
printf '=== Merge complete ===\n'
printf 'Canonical DB: %s\n' "$TARGET"
printf 'Backup dir: %s\n' "$BACKUP_DIR"
printf 'Verify: sqlite3 %q "PRAGMA integrity_check;"\n' "$TARGET"
printf 'Verify: sqlite3 %q "PRAGMA foreign_key_check;"\n' "$TARGET"