---
title: "Ingest Farallon Institute Bird & Mammal Census"
calcofi:
target_name: ingest_farallon_bird_mammal
workflow_type: ingest
dependency:
- ingest_swfsc_ichthyo
output: data/parquet/farallon_bird-mammal/manifest.json
provider: farallon
dataset: bird-mammal
workflow_url: https://calcofi.io/workflows/ingest_farallon_bird-mammal.html
questions_file: metadata/farallon/bird-mammal/questions.csv
dataset_meta:
dataset_name: CalCOFI Bird & Mammal Census
# display trio, read by the release `dataset` table and the
# consumer apps (calcofi4db >= 3.15.0) — see NEWS for why these
# left the apps' own hardcoded maps
dataset_name_short: "Seabirds & Marine Mammals"
category: "Seabirds & Marine Mammals"
color: "#ff8787"
description: >
Bird and mammal observations along CalCOFI (and NMFS, CPR) cruise transects,
1987-2021. Transect effort + counts + species/behavior lookups. Curated via
CCE-LTER DataZoo 255 (PI Bill Sydeman).
citation_main: ""
link_calcofi_org: ""
link_data_source: "https://oceaninformatics.ucsd.edu/datazoo/catalogs/ccelter/datasets/255"
link_others:
- http://dx.doi.org/10.6073/pasta/4ee1bd702acb11786277192a41626800
license: ""
pi_names: Bill Sydeman
# this ingest publishes the consolidated core, not per-dataset tables: the
# transect/observation/species/behavior source shape is wrangled in the
# notebook and projected into sample / obs / obs_attribute + the taxa refs.
tables_owned:
- {table: sample, shared: true, note: "core event dimension (transect grain)"}
- {table: obs, shared: true, note: "core occurrence headline (bio, per transect x species)"}
- {table: obs_attribute, shared: true, note: "behavior breakdown per occurrence"}
- {table: taxon, shared: true, note: "shared taxa reference (seabirds itis:, mammals worms:)"}
- {table: dataset_taxon, shared: true, note: "species_code -> taxon_key crosswalk"}
- {table: taxon_group, shared: true, note: "calcofi:seabirds / calcofi:marine_mammals"}
erd:
color: "#f0d2e8"
editor_options:
chunk_output_type: console
---
## Overview
**Source**: `whales-seabirds-turtles/bird-mammal-census/` (CCE-LTER DataZoo 255,
PI Bill Sydeman). Bird & mammal observations along CalCOFI/NMFS/CPR cruise
transects, 1987-2021.
- **Provider**: `calcofi` (tentative — curated via CCE-LTER; see questions)
- **Tables**: `bird_mammal_transect` (effort) ⨝ `bird_mammal_observation` (counts)
on `gis_key`, plus `bird_mammal_species` (ITIS taxonomy) and
`bird_mammal_behavior` lookups.
```{mermaid}
graph LR
T[transects] --> TR[bird_mammal_transect<br/>effort + position]
O[observations] --> OB[bird_mammal_observation<br/>counts]
OB -.gis_key.-> TR
OB -.species_code.-> SP[bird_mammal_species]
OB -.behavior_code.-> BH[bird_mammal_behavior]
```
## Setup
```{r}
#| label: setup
#| message: false
devtools::load_all(here::here("../calcofi4db"))
devtools::load_all(here::here("../calcofi4r"))
librarian::shelf(
CalCOFI/calcofi4db, CalCOFI/calcofi4r,
DBI, dplyr, DT, fs, glue, here, janitor, jsonlite, knitr,
lubridate, purrr, readr, sf, stringr, tibble, tidyr, units, quiet = T)
options(readr.show_col_types = F)
options(DT.options = list(scrollX = TRUE))
source(here("libs/ingest.R"))
cc <- read_calcofi_meta(here("ingest_farallon_bird-mammal.qmd"))
provider <- cc$provider
dataset <- cc$dataset
tables_owned <- cc$tables_owned
dir_label <- glue("{provider}_{dataset}")
dir_parquet <- here(glue("data/parquet/{dir_label}"))
dir_stage <- cc_stage_path("parquet", dir_label, create = TRUE)
db_path <- here(glue("data/wrangling/{dir_label}.duckdb"))
dir_src <- path_expand(glue("{dir_data}/whales-seabirds-turtles/bird-mammal-census"))
if (overwrite) {
if (file_exists(db_path)) file_delete(db_path)
if (file_exists(paste0(db_path, ".wal"))) file_delete(paste0(db_path, ".wal"))
}
dir_create(dirname(db_path))
con <- get_duckdb_con(db_path)
load_duckdb_extension(con, "spatial")
```
## Read Source Data
```{r}
#| label: read-source
stopifnot("source dir not found" = dir_exists(dir_src))
sync_to_gcs(local_dir = dir_src, gcs_prefix = glue("archive/{provider}/{dataset}"),
bucket = "calcofi-files-public", exclude = c(".DS_Store", "*.tmp", "*.gdoc"))
rd <- function(f) read_csv(file.path(dir_src, glue("CalCOFI_bird-mammal-census_{f}.csv")),
col_types = cols(.default = "c")) |> clean_names()
d_tr <- rd("transects"); d_ob <- rd("observations")
d_sp <- rd("allspecieslist"); d_bh <- rd("behaviorcodes")
cat(glue("transects {nrow(d_tr)}, observations {nrow(d_ob)}, species {nrow(d_sp)}, behaviors {nrow(d_bh)}"), "\n")
cat("transect clean names:", paste(names(d_tr), collapse=", "), "\n")
```
## Build Lookups (species, behavior)
```{r}
#| label: lookups
b01 <- function(x) !is.na(x) & x %in% c("1","TRUE","true","Y","y")
d_species <- d_sp |>
transmute(
species_code = species, common_name, scientific_name = latin_name,
itis_id = suppressWarnings(as.integer(itis)),
is_bird = b01(bird), is_mammal = b01(mammal), is_fish = b01(fish),
is_large_bird = b01(large_bird), is_unidentified = b01(unidentified),
include_flag = b01(include), nmfs_code = nmfs, comment)
dbWriteTable(con, "bird_mammal_species", d_species, overwrite = TRUE)
d_behavior <- d_bh |> transmute(behavior_code = id, behavior, description)
dbWriteTable(con, "bird_mammal_behavior", d_behavior, overwrite = TRUE)
cat(glue("species {nrow(d_species)}, behaviors {nrow(d_behavior)}"), "\n")
```
## Build Transect (effort) Table
```{r}
#| label: transect
d_transect <- d_tr |>
transmute(
gis_key,
cruise_label = cruise,
transect_number = suppressWarnings(as.integer(transect_number)),
bin_number = suppressWarnings(as.integer(bin_number)),
date = suppressWarnings(as.Date(date)),
time_sec = suppressWarnings(as.numeric(time_sec)),
latitude = suppressWarnings(as.numeric(latitude_mid_o)),
longitude = suppressWarnings(as.numeric(longitude_mid_o)),
latitude_start = suppressWarnings(as.numeric(latitude_start_o)),
longitude_start = suppressWarnings(as.numeric(longitude_start_o)),
latitude_stop = suppressWarnings(as.numeric(latitude_stop_o)),
longitude_stop = suppressWarnings(as.numeric(longitude_stop_o)),
length_m = suppressWarnings(as.numeric(length_m)),
width_m = suppressWarnings(as.numeric(width_m)),
area_m2 = suppressWarnings(as.numeric(area_m2)),
bottom_depth_m = suppressWarnings(as.numeric(depth_m)),
julian_date = suppressWarnings(as.numeric(julian_date)),
julian_day = suppressWarnings(as.integer(julian_day)),
svy, season,
# datetime from date + time_sec (Q01: tz unconfirmed, treated as given)
datetime_start_utc = as_datetime(date) + dseconds(coalesce(time_sec, 0)))
dbWriteTable(con, "bird_mammal_transect", d_transect, overwrite = TRUE)
cat(glue("bird_mammal_transect: {nrow(d_transect)} rows"), "\n")
```
## Build Observation (counts) Table
```{r}
#| label: observation
d_observation <- d_ob |>
transmute(gis_key, species_code = species, behavior_code = behavior,
count = suppressWarnings(as.integer(count))) |>
mutate(observation_id = row_number(), .before = 1)
dbWriteTable(con, "bird_mammal_observation", d_observation, overwrite = TRUE)
cat(glue("bird_mammal_observation: {nrow(d_observation)} rows"), "\n")
```
## Resolve cruise_key + Spatial
The source records a **survey label** (`CAC1987_05`), not a cruise, and carries no
ship column — so `cruise_key` has to be recovered from where the observers
actually were. Parsing year-month out of the label is not enough: it is ambiguous
whenever several ships sailed in one month (1998-10 had four), and it is wrong
outright for a survey straddling a month boundary (`CAC2014_01` ran 2014-01-29 →
02-04 and belongs to `2014-02-3322`). See Q02.
The observers ride a CalCOFI ship, so each transect sits on that day's station
track. `match_cruise_by_track()` matches every transect to the nearest station
occupied the same day, then takes the **majority vote per survey label** — one
label is one cruise — and applies the winner to all of that label's transects.
The reference track is the `swfsc_ichthyo` sample shard (already this notebook's
declared dependency). That choice matters for referential integrity: its
`cruise_key`s are exactly the `cruise` reference table's, so every key emitted
here resolves. Tracks assembled from other shards include cruises absent from
`cruise` (e.g. `2021-07-33P4`), which would ship a dangling FK.
```{r}
#| label: spatial-cruise
# geometry + grid from the transect midpoint
load_prior_tables(con, parquet_dir = cc_stage_path("parquet", "swfsc_ichthyo"),
tables = c("grid"), geom_tables = c("grid"), as_view = TRUE)
add_point_geom(con, "bird_mammal_transect", lon_col = "longitude", lat_col = "latitude")
assign_grid_key(con, "bird_mammal_transect") |> datatable(caption = "Grid assignment")
# cruise track: one row per (cruise, day, station position). matched on `date`,
# not datetime_start_utc, since the source timezone is unconfirmed (Q01) and the
# observer is aboard for the whole day regardless.
dbExecute(con, glue(
"CREATE OR REPLACE VIEW cruise_track AS
SELECT DISTINCT cruise_key, datetime, latitude, longitude
FROM read_parquet('{cc_stage_path('parquet','swfsc_ichthyo','sample.parquet')}')
WHERE cruise_key IS NOT NULL AND datetime IS NOT NULL"))
cruise_match <- match_cruise_by_track(
con, "bird_mammal_transect", "cruise_track",
datetime_col = "date", lon_col = "longitude", lat_col = "latitude",
ref_datetime_col = "datetime",
group_col = "cruise_label")
cat(glue("cruise_key resolved on {cruise_match$matched}/{cruise_match$total} ",
"transects ({cruise_match$pct}%)"), "\n")
```
```{r}
#| label: cruise-match-review
# surveys with no cruise sit at the top: they are the ones to eyeball, not the
# 100%-unanimous majority
cruise_match$groups |>
datatable(caption = "Survey label -> cruise_key (vote share; NULL = unresolved)",
rownames = FALSE, filter = "top") |>
formatPercentage("share", 1)
```
```{r}
#| label: cruise-match-validate
grp <- cruise_match$groups
n_unresolved <- sum(is.na(grp$cruise_key))
# a survey is one cruise on one ship, so two labels claiming the same cruise
# would mean the vote collapsed two distinct surveys together
dupe <- grp$cruise_key[!is.na(grp$cruise_key)]
dupe <- dupe[duplicated(dupe)]
stopifnot(
"cruise_key must resolve on >95% of transects" =
cruise_match$pct > 95,
"every survey label must resolve to a distinct cruise_key" =
length(dupe) == 0,
"resolved cruise_key must exist in the cruise reference (no dangling FK)" =
dbGetQuery(con, "
SELECT COUNT(*) FROM bird_mammal_transect t
WHERE t.cruise_key IS NOT NULL AND t.cruise_key NOT IN (
SELECT cruise_key FROM cruise_track)")[[1]] == 0)
cat(glue(
"{nrow(grp) - n_unresolved}/{nrow(grp)} survey labels resolved; ",
"{n_unresolved} left NULL: ",
"{paste(grp$cruise_label[is.na(grp$cruise_key)], collapse=', ')}"), "\n")
```
The unresolved labels are correct as NULL, not gaps to be filled:
- **`Fronts_0711`** (2011-06-21 → 07-16) is a CCE-LTER *Fronts* process cruise;
no CalCOFI cruise sailed in that window (2011-04 ended 04-27, the next began
07-27), so there is no CalCOFI cruise to point at.
- **`CAC2021_7`** (2021-07-19 → 08-02) *did* ride a CalCOFI cruise —
`2021-07-33P4`, an exact date-range match — but that cruise is missing from the
`cruise` reference table, which is built from the ichthyo source alone.
Assigning it would ship a dangling FK. It resolves for free once `cruise`
covers every cruise the other datasets reference (workflows#75 — 296 of 987
referenced keys are missing, so that gap is worth closing on its own merits
rather than for this one survey).
## Load Dataset Metadata + Schema
```{r}
#| label: meta-schema
d_dataset <- ingest_yaml_to_dataset_df(read_ingest_yaml(here()))
dbWriteTable(con, "dataset", d_dataset, overwrite = TRUE)
bmc_rels <- list(
primary_keys = list(
bird_mammal_transect = "gis_key", bird_mammal_observation = "observation_id",
bird_mammal_species = "species_code", bird_mammal_behavior = "behavior_code"),
foreign_keys = list(
list(table="bird_mammal_observation", column="gis_key", ref_table="bird_mammal_transect", ref_column="gis_key"),
list(table="bird_mammal_observation", column="species_code", ref_table="bird_mammal_species", ref_column="species_code"),
list(table="bird_mammal_observation", column="behavior_code", ref_table="bird_mammal_behavior", ref_column="behavior_code")))
# the SOURCE shape, documenting the wrangling below. The tables this ingest
# actually publishes are the consolidated core (see "Emit Core Tables"), so
# relationships.json is written there, from core_relationships().
cc_erd(con, tables = c("bird_mammal_transect","bird_mammal_observation","bird_mammal_species","bird_mammal_behavior","dataset"),
rels = bmc_rels,
colors = list(lightblue = c("bird_mammal_transect","bird_mammal_observation"),
lightyellow = c("bird_mammal_species","bird_mammal_behavior"), white = "dataset"))
```
## Validate + Preview
```{r}
#| label: validate
results <- validate_for_release(con, checks = "all", strict = FALSE)
cat("Validation:", ifelse(results$passed, "PASSED", "FAILED"), "\n")
# orphan check: observations whose gis_key is not in transects
n_orphan <- dbGetQuery(con, "SELECT COUNT(*) FROM bird_mammal_observation o
LEFT JOIN bird_mammal_transect t USING(gis_key) WHERE t.gis_key IS NULL")[[1]]
cat(glue("observation gis_key orphans (no transect): {n_orphan}"), "\n")
```
```{r}
#| label: preview
cols <- dbGetQuery(con, "SELECT column_name FROM information_schema.columns
WHERE table_name='bird_mammal_transect' AND data_type NOT LIKE 'GEOMETRY%'")$column_name
dbGetQuery(con, glue("SELECT {paste(cols, collapse=', ')} FROM bird_mammal_transect LIMIT 100")) |>
datatable(caption = "bird_mammal_transect — first 100", rownames = FALSE, filter = "top")
```
## Questions for Data Providers
```{r}
#| label: provider-questions
# one validated read + render for every ingest: the vocabulary and the column
# order live in calcofi4db, not in 16 hand-written factor() calls
questions_datatable(
here(cc$questions_file),
caption = "Questions for the Farallon bird/mammal data providers (ranked)")
```
## Emit Core Tables
Project this dataset into the shared consolidated core model
(`design_env-bio-consolidation.md`). These core tables **are** this ingest's
output: `release_database.qmd` concatenates the per-dataset shards rather than
re-deriving the core from per-dataset tables, so there is exactly one projection
to keep correct.
The seabird/mammal source records one row per (transect, species, **behavior**).
The `obs` headline is one row per (transect, species) with `count` summed across
behaviors — otherwise the same birds are counted once per behavior code — and the
behavior breakdown becomes `obs_attribute` rows carrying the behavior label.
```{r}
#| label: emit_core
ds_key <- "farallon_bird-mammal"
# registries that resolve taxon_key at ingest time (seabirds -> itis:, marine
# mammals -> worms: via the override table). Filter the crosswalk to THIS
# dataset -- an unfiltered read leaks other datasets' taxa into this shard (the
# retired emit_core_tables() wrapper filtered internally).
mt_taxon <- read_csv(here("metadata/measurement_taxon.csv"),
col_types = cols(worms_id = "i", itis_id = "i",
bin_value = "d", .default = "c")) |>
filter(dataset_key == ds_key)
tx_over <- read_csv(here("metadata/taxon_override.csv"), show_col_types = FALSE)
# This projection lives here, in the notebook that owns the dataset, not in a
# switch(dataset_key, ...) arm inside calcofi4db. The reusable SHAPE stays in the
# package (sample_arm_self), so this is a declaration.
# cross-reference: resolve each taxon against BOTH authorities (cached in
# metadata/taxon_xref.csv, so a re-run costs no API calls). This fills the
# `worms_id` COLUMN on itis:-keyed taxa without touching their key — a consumer
# joining on worms_id used to match ZERO rows for every seabird and marine
# mammal — backfills `itis_id` the other way, replaces an id its authority has
# deprecated so the key is always an accepted id, and fetches the real
# `taxonomic_status` with the date it was checked. Must precede the lineage
# fetch, which should ask about the accepted id, not the deprecated one.
ensure_taxon_xref(con, mt_taxon, tx_over,
cache_csv = here("metadata/taxon_xref.csv"))
# lineage: fetch each taxon's WoRMS/ITIS classification (cached in
# metadata/taxon_lineage.csv, so a re-run costs no API calls) and stage it as the
# `taxon` hierarchy build_taxon_reference() reads. Without it a crosswalk- or
# vocabulary-resolved taxon reaches the release with a key and a name and NOTHING
# else — no rank, no parent_taxon_key, no classification — so hierarchy rollups
# ("all Decapoda") silently match nothing and no error is raised anywhere.
ensure_taxon_lineage(con, mt_taxon, tx_over,
cache_csv = here("metadata/taxon_lineage.csv"))
n_taxon <- build_taxon_reference(con, mt_taxon, tx_over)
n_ds_taxon <- build_dataset_taxon(con, mt_taxon, tx_over)
n_tx_group <- build_taxon_group(con, mt_taxon, tx_over)
append_sample(con, sample_arm_self(
ds_key, "bird_mammal_transect", "gis_key", "transect"))
# obs — the occurrence headline: one row per (transect, SPECIES CODE), count
# SUMmed across behaviors.
#
# Grouping is on the source species_code, NOT on taxon_key alone: only 156 of the
# 207 observed codes resolve to a taxon (the rest are excluded by include_flag or
# are coarse unidentified categories), so grouping by taxon_key alone would sum
# every unresolved species into a single NULL-taxon row per transect, silently
# merging distinct species. taxon_key is functionally determined by species_code
# (dataset_taxon is unique on ds_taxa_code within a dataset), so carrying both
# does not split the grain.
#
# The behavior breakdown is sub-occurrence detail and goes to obs_attribute — it
# must NOT ride on the headline's life_stage, or the same bird is counted once
# per behavior code.
append_obs(con, glue("
SELECT 'bio', '{ds_key}', {ns_key(ds_key, 'transect', 'tr.gis_key')},
tr.grid_key, tr.cruise_key, tr.latitude, tr.longitude,
CAST(tr.datetime_start_utc AS TIMESTAMP), 0::DOUBLE, 0::DOUBLE,
dt.taxon_key, NULL::VARCHAR, 'count', CAST(SUM(o.count) AS DOUBLE),
NULL::VARCHAR, NULL::DOUBLE
FROM bird_mammal_observation o JOIN bird_mammal_transect tr USING (gis_key)
LEFT JOIN dataset_taxon dt ON dt.dataset_key = '{ds_key}'
AND dt.ds_taxa_code = CAST(o.species_code AS VARCHAR)
GROUP BY tr.gis_key, tr.grid_key, tr.cruise_key, tr.latitude, tr.longitude,
tr.datetime_start_utc, o.species_code, dt.taxon_key"))
# obs_attribute — the behavior breakdown, one row per source (transect, species,
# behavior): measurement_type = 'behavior', bin_label = the behavior description,
# bin_value NULL (behavior is categorical, not a numeric bin).
append_obs_attribute(con, glue("
SELECT '{ds_key}', {ns_key(ds_key, 'transect', 'tr.gis_key')},
dt.taxon_key, NULL::VARCHAR, 'behavior',
NULL::DOUBLE, bb.description, CAST(o.count AS INTEGER), NULL::VARCHAR
FROM bird_mammal_observation o JOIN bird_mammal_transect tr USING (gis_key)
LEFT JOIN dataset_taxon dt ON dt.dataset_key = '{ds_key}'
AND dt.ds_taxa_code = CAST(o.species_code AS VARCHAR)
LEFT JOIN bird_mammal_behavior bb ON bb.behavior_code = o.behavior_code"))
core <- list(
sample = dbGetQuery(con, "SELECT COUNT(*) FROM sample")[[1]],
obs = dbGetQuery(con, "SELECT COUNT(*) FROM obs")[[1]],
obs_attribute = dbGetQuery(con, "SELECT COUNT(*) FROM obs_attribute")[[1]],
taxon = n_taxon,
dataset_taxon = n_ds_taxon,
taxon_group = n_tx_group)
cat(glue(
"core projection — sample={core$sample %||% 0} obs={core$obs %||% 0} ",
"obs_attribute={core$obs_attribute %||% 0} ",
"taxon={core$taxon %||% 0} dataset_taxon={core$dataset_taxon %||% 0} ",
"taxon_group={core$taxon_group %||% 0}\n"))
# the headline must collapse behaviors, and the attribution must add back up
n_obs <- core$obs
# one headline row per (transect, species_code) — NOT per behavior, and NOT
# collapsed by taxon_key (only 156 of 207 codes resolve; grouping on taxon_key
# alone would merge every unresolved species into one row per transect)
n_exp <- dbGetQuery(con, "
SELECT COUNT(*) FROM (
SELECT tr.gis_key, o.species_code
FROM bird_mammal_observation o JOIN bird_mammal_transect tr USING (gis_key)
GROUP BY 1, 2)")[[1]]
n_mismatch <- dbGetQuery(con, "
WITH a AS (SELECT sample_key, taxon_key, SUM(count) s FROM obs_attribute
WHERE measurement_type = 'behavior' GROUP BY 1, 2),
o AS (SELECT sample_key, taxon_key, SUM(measurement_value) v FROM obs
GROUP BY 1, 2)
SELECT COUNT(*) FROM a JOIN o USING (sample_key, taxon_key) WHERE a.s <> o.v")[[1]]
# regression guard: grouping the headline on taxon_key alone (instead of
# species_code) merged every unresolved species on a transect into ONE NULL-taxon
# row. If that ever comes back, transects with several unresolved species collapse
# to a single row and this max drops to 1.
n_null_max <- dbGetQuery(con, "
SELECT COALESCE(MAX(n), 0) FROM (
SELECT sample_key, COUNT(*) n FROM obs WHERE taxon_key IS NULL GROUP BY 1)")[[1]]
n_null <- dbGetQuery(con, "SELECT COUNT(*) FROM obs WHERE taxon_key IS NULL")[[1]]
stopifnot(
"obs must be one row per (transect, species), not per behavior" = n_obs == n_exp,
"obs_attribute behavior counts must sum to the obs headline" = n_mismatch == 0,
"unresolved species must stay distinct, not merge into one NULL-taxon row per transect" =
n_null_max > 1,
"every obs.sample_key must resolve in sample" =
dbGetQuery(con, "SELECT COUNT(*) FROM obs o LEFT JOIN sample s USING (sample_key)
WHERE s.sample_key IS NULL")[[1]] == 0,
"every obs.taxon_key must RESOLVE in taxon (not merely be non-NULL)" =
dbGetQuery(con, "SELECT COUNT(*) FROM obs o LEFT JOIN taxon t USING (taxon_key)
WHERE o.taxon_key IS NOT NULL AND t.taxon_key IS NULL")[[1]] == 0,
"behavior is the only obs_attribute type here" =
dbGetQuery(con, "SELECT COUNT(*) FROM obs_attribute
WHERE measurement_type <> 'behavior'")[[1]] == 0)
cat(glue("obs parity: {format(n_obs, big.mark = ',')} headline rows ",
"({format(n_null, big.mark = ',')} without taxon_key, up to ",
"{n_null_max} distinct unresolved species per transect); ",
"behavior attribution reconciles"), "\n")
```
## Write Outputs + Upload
```{r}
#| label: outputs
dir_create(dir_parquet)
tbls_out <- core_output_tables(con, extra = "dataset")
write_parquet_outputs(
con = con, output_dir = dir_parquet, tables = tbls_out,
sort_by = list(obs = c("grid_key", "measurement_type")),
strip_provenance = FALSE)
build_relationships_json(
rels = core_relationships(tbls_out), output_dir = dir_parquet,
provider = provider, dataset = dataset)
d_tbls_rd <- read_csv(here("metadata/farallon/bird-mammal/tbls_redefine.csv"))
d_flds_rd <- read_csv(here("metadata/farallon/bird-mammal/flds_redefine.csv"))
build_metadata_json(
con = con, d_tbls_rd = d_tbls_rd, d_flds_rd = d_flds_rd,
# shared core descriptions first, this dataset's overrides second
metadata_derived_csv = c(
here("metadata/core_dictionary.csv"),
here("metadata/farallon/bird-mammal/metadata_derived.csv")),
output_dir = dir_parquet, tables = tbls_out,
set_comments = TRUE, provider = provider, dataset = dataset,
workflow_url = cc$workflow_url, tables_owned = tables_owned)
sync_to_gcs(local_dir = dir_stage, sidecar_dir = dir_parquet, gcs_prefix = glue("ingest/{dir_label}"), bucket = "calcofi-db")
```
```{r}
#| label: cleanup
close_duckdb(con)
cat(glue("Parquet outputs written to: {dir_parquet}"), "\n")
```