Skip to content

Commit 74534f3

Browse files
authored
Merge pull request #324 from ropensci/data-release-2026
Ship 2026 data release (fb v26.07, slb v26.04) and script the import workflow
2 parents 03671a5 + 6228164 commit 74534f3

13 files changed

Lines changed: 319 additions & 203 deletions

.Rbuildignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ circle.yml
2929
^CRAN-SUBMISSION$
3030
^\.claude$
3131
^issues-triage\.md$
32+
^imports/$

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ parquet
1111
fbapp
1212
slbapp
1313
CRAN-SUBMISSION
14+
__pycache__

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Description: A programmatic interface to 'FishBase', re-written
66
supports experimental access to 'SeaLifeBase' data, which contains
77
nearly 200,000 species records for all types of aquatic life not covered by
88
'FishBase.'
9-
Version: 5.0.3
9+
Version: 5.0.4
1010
Encoding: UTF-8
1111
License: CC0
1212
Authors@R: c(person("Carl", "Boettiger",

NEWS.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ And constructed with the following guidelines:
2323

2424
For more information on SemVer, please visit http://semver.org/.
2525

26+
v 5.0.4
27+
-------
28+
29+
Data updated to the 2026 releases: FishBase `v26.07` and SeaLifeBase `v26.04`.
30+
These are now what `version = "latest"` resolves to. Earlier releases remain
31+
available via `available_releases()`. The new snapshots add 6 tables to
32+
FishBase (222 total) and 3 to SeaLifeBase (202 total); no previously published
33+
table was dropped.
34+
35+
The workflow for importing a new database dump and publishing it to Source
36+
Cooperative is now scripted and documented in `data-raw/README.md`.
37+
2638
v 5.0.3
2739
-------
2840

data-raw/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Preparing a new data release
2+
3+
FishBase and SeaLifeBase send `fbapp.7z` / `slbapp.7z` (7z-compressed
4+
`mysqldump` output) roughly annually. Publishing them is two commands:
5+
6+
```bash
7+
# 1. drop the dumps in imports/ (git-ignored, excluded from the build), then
8+
bash data-raw/import_dumps.sh /tmp/fishbase-import
9+
# 2. read the verification summary, then publish (args are fb and slb versions)
10+
bash data-raw/upload.sh /tmp/fishbase-import 26.07 26.04
11+
```
12+
13+
`import_dumps.sh` unpacks the dumps into a throwaway local MariaDB instance and
14+
calls `export_parquet.py`, which writes one zstd-compressed parquet file per
15+
table via DuckDB's `mysql` extension, then `verify_export.py`, which checks the
16+
result and exits non-zero if anything looks wrong. `upload.sh` publishes to
17+
Source Cooperative under `cboettig/fishbase/{fb,slb}/v<version>/parquet/`,
18+
which is what `fb_urls()` lists.
19+
20+
Nothing in the R package needs to change to ship a release:
21+
`available_releases()` reads the bucket and `version = "latest"` picks the
22+
highest version present.
23+
24+
Version tags encode the *snapshot date* of the dump (`v26.07` = July 2026).
25+
The two servers can carry different tags when their dumps arrive at different
26+
times, since releases are listed per server.
27+
28+
Requires `p7zip-full`, `mariadb-server` (10.11+), `python3` with `duckdb`, and
29+
an `rclone` remote named `source` holding Source Cooperative credentials.
30+
Budget roughly 10 GB of scratch disk and an hour of wall clock.
31+
32+
## What the verification covers
33+
34+
`verify_export.py` compares every table's parquet row count against the source
35+
database, diffs the table list against the most recent release already on
36+
Source Cooperative, and checks that the two servers did not get crossed. Run
37+
it *before* uploading: it compares against whatever release is currently
38+
newest, so once you publish, the comparison baseline becomes the new release.
39+
40+
## Notes and gotchas
41+
42+
- The two dumps come from different MySQL versions (5.6 for `fbapp`, 8.0 for
43+
`slbapp`). MariaDB rejects MySQL 8's `utf8mb4_0900_ai_ci` collation, so the
44+
import rewrites it; only the data matters here, not collation semantics.
45+
- DuckDB's `mysql` ATTACH exposes *every* schema on the server, not just the
46+
one named in the connection string. `export_parquet.py` therefore filters on
47+
`schema_name` and fully qualifies each table — an unqualified `src."species"`
48+
silently resolves across databases and will mix FishBase and SeaLifeBase.
49+
`verify_export.py` guards against a regression here.
50+
- The publishing credentials can write the prefix but are not granted
51+
`s3:CreateBucket`, which `rclone` probes for unless given
52+
`--s3-no-check-bucket`.
53+
- Recent dumps have included working tables from the FishBase team (`*_copy`,
54+
`mp_withphotos`, `country.orig`). They are published as-is, since these are
55+
raw snapshots of the backend, but they show up in the verification diff as
56+
new tables — worth a glance before publishing.

data-raw/export_parquet.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/usr/bin/env python3
2+
"""Export one MySQL schema of a fishbase snapshot to per-table parquet files.
3+
4+
Usage: export_parquet.py <mysql_schema> <outdir>
5+
"""
6+
import json
7+
import sys
8+
9+
import duckdb
10+
11+
db, outdir = sys.argv[1], sys.argv[2]
12+
13+
con = duckdb.connect()
14+
con.execute("INSTALL mysql; LOAD mysql;")
15+
con.execute(
16+
f"ATTACH 'host=127.0.0.1 port=3307 user=root database={db}' AS src (TYPE mysql, READ_ONLY)"
17+
)
18+
19+
# NB: the mysql attach exposes every schema on the server, so scope explicitly
20+
tables = [
21+
r[0]
22+
for r in con.execute(
23+
"SELECT table_name FROM duckdb_tables() "
24+
"WHERE database_name='src' AND schema_name=? ORDER BY table_name",
25+
[db],
26+
).fetchall()
27+
]
28+
print(f"{db}: {len(tables)} tables", flush=True)
29+
30+
ok, failed = [], {}
31+
for i, tbl in enumerate(tables, 1):
32+
dest = f"{outdir}/{tbl}.parquet"
33+
try:
34+
con.execute(
35+
f'COPY (SELECT * FROM src."{db}"."{tbl}") TO \'{dest}\' '
36+
"(FORMAT parquet, COMPRESSION zstd)"
37+
)
38+
n = con.execute(f"SELECT count(*) FROM read_parquet('{dest}')").fetchone()[0]
39+
ok.append((tbl, n))
40+
print(f"[{i}/{len(tables)}] {tbl}: {n}", flush=True)
41+
except Exception as e:
42+
failed[tbl] = str(e).split("\n")[0]
43+
print(f"[{i}/{len(tables)}] {tbl}: FAILED {failed[tbl]}", flush=True)
44+
45+
with open(f"{outdir}/../{db}_export_report.json", "w") as f:
46+
json.dump({"ok": ok, "failed": failed}, f, indent=2)
47+
48+
print(f"done: {len(ok)} ok, {len(failed)} failed", flush=True)

data-raw/huggingface.R

Lines changed: 0 additions & 86 deletions
This file was deleted.

data-raw/import_db.R

Lines changed: 0 additions & 40 deletions
This file was deleted.

data-raw/import_dumps.sh

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env bash
2+
# Import FishBase / SeaLifeBase MySQL dumps and export them as parquet.
3+
#
4+
# The FishBase team sends `fbapp.7z` / `slbapp.7z` (7z-compressed mysqldump)
5+
# roughly annually. Drop them in `imports/` (git- and build-ignored), run this
6+
# script, sanity-check the result, then publish with `upload.sh`.
7+
#
8+
# Requires: p7zip-full, mariadb-server (10.11+), python3 with duckdb.
9+
# Budget ~10 GB of scratch disk and about an hour of wall clock.
10+
#
11+
# Usage: bash data-raw/import_dumps.sh [workdir]
12+
set -euo pipefail
13+
14+
W="${1:-/tmp/fishbase-import}"
15+
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
16+
PORT=3307
17+
18+
for f in fbapp slbapp; do
19+
[ -f "$REPO/imports/$f.7z" ] || { echo "missing $REPO/imports/$f.7z" >&2; exit 1; }
20+
done
21+
22+
mkdir -p "$W"/{sql,mysql-data,tmp,out/fb/parquet,out/slb/parquet}
23+
24+
7z x -o"$W/sql" -y "$REPO/imports/fbapp.7z"
25+
7z x -o"$W/sql" -y "$REPO/imports/slbapp.7z"
26+
27+
mariadb-install-db --datadir="$W/mysql-data" --user="$(whoami)" \
28+
--auth-root-authentication-method=normal --skip-test-db
29+
30+
cat > "$W/my.cnf" <<CNF
31+
[mysqld]
32+
user = $(whoami)
33+
bind-address = 127.0.0.1
34+
port = $PORT
35+
socket = $W/mysqld.sock
36+
pid-file = $W/mysqld.pid
37+
datadir = $W/mysql-data
38+
tmpdir = $W/tmp
39+
key_buffer_size = 8G
40+
myisam_sort_buffer_size = 4G
41+
innodb_buffer_pool_size = 16G
42+
innodb_flush_log_at_trx_commit = 0
43+
innodb_doublewrite = 0
44+
max_allowed_packet = 1G
45+
skip-log-bin
46+
sql_mode = NO_ENGINE_SUBSTITUTION
47+
CNF
48+
49+
mariadbd --defaults-file="$W/my.cnf" > "$W/mysqld.log" 2>&1 &
50+
for _ in $(seq 1 60); do
51+
mariadb-admin --socket="$W/mysqld.sock" -u root ping >/dev/null 2>&1 && break
52+
sleep 2
53+
done
54+
mariadb-admin --socket="$W/mysqld.sock" -u root ping >/dev/null \
55+
|| { echo "server failed to start, see $W/mysqld.log" >&2; exit 1; }
56+
57+
# The fbapp dump comes from MySQL 5.6, slbapp from MySQL 8.0. MariaDB does not
58+
# know MySQL 8's default collation, so rewrite it on the way in; only the data
59+
# matters here, not the collation semantics.
60+
mariadb --socket="$W/mysqld.sock" -u root < "$W/sql/fbapp.sql"
61+
sed 's/utf8mb4_0900_ai_ci/utf8mb4_general_ci/g' "$W/sql/slbapp.sql" \
62+
| mariadb --socket="$W/mysqld.sock" -u root
63+
64+
python3 "$REPO/data-raw/export_parquet.py" fbapp "$W/out/fb/parquet"
65+
python3 "$REPO/data-raw/export_parquet.py" slbapp "$W/out/slb/parquet"
66+
67+
python3 "$REPO/data-raw/verify_export.py" "$W" "$PORT"
68+
69+
mariadb-admin --socket="$W/mysqld.sock" -u root shutdown
70+
echo "Export complete: $W/out. Publish with data-raw/upload.sh"

data-raw/upload-and-register.R

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)