R/load_nba_stats.R

Defines functions update_nba_stats_db load_nba_stats_leaguedash load_nba_stats_team_season_stats load_nba_stats_team_boxscores load_nba_stats_standings load_nba_stats_shots load_nba_stats_schedule load_nba_stats_rosters load_nba_stats_possessions load_nba_stats_player_season_stats load_nba_stats_player_game_logs load_nba_stats_player_boxscores load_nba_stats_pbp load_nba_stats_officials load_nba_stats_lineups load_nba_stats_game_rosters load_nba_stats_game_lineups load_nba_stats_draft load_nba_stats_coaches

Documented in load_nba_stats_coaches load_nba_stats_draft load_nba_stats_game_lineups load_nba_stats_game_rosters load_nba_stats_leaguedash load_nba_stats_lineups load_nba_stats_officials load_nba_stats_pbp load_nba_stats_player_boxscores load_nba_stats_player_game_logs load_nba_stats_player_season_stats load_nba_stats_possessions load_nba_stats_rosters load_nba_stats_schedule load_nba_stats_shots load_nba_stats_standings load_nba_stats_team_boxscores load_nba_stats_team_season_stats update_nba_stats_db

# -----------------------------------------------------------------------------
# NBA Stats API release-dataset loaders
# -----------------------------------------------------------------------------
# Thin wrappers around rds_from_url() / parquet_from_url() that read the
# hoopR-nba-stats-data pipeline's published sportsdataverse-data release
# assets (the `nba_stats_*` tag family). Mirrors the shape of wehoop's
# R/load_wnba_stats.R (post-#78 dots-forwarding + update_*_db registration).
#
# Season convention: `seasons` here is the season's START year (e.g. `2024`
# for the 2024-25 season) -- matching sdv-py's `load_nba_stats_*` Python
# loaders -- NOT hoopR's ESPN-family `load_nba_*()` loaders, which key
# directly on the END year with no offset. The published `nba_stats_*`
# asset is keyed by the END year, so every URL below uses `seasons + 1`.
# [most_recent_nba_stats_season()] returns that START-year default; it is
# `most_recent_nba_season() - 1`, not a plain alias of it (unlike wehoop's
# `most_recent_wnba_stats_season()`, which *is* a plain alias -- the WNBA is
# a single-calendar-year league and needs no START/END offset).
#
# None of the `nba_stats_*` release tags currently ship an
# `*_in_data_repo.csv` manifest asset (several `wnba_stats_*` tags do), so
# there are no manifest loaders in this file.
# -----------------------------------------------------------------------------

#' **Load hoopR NBA Stats Coaches**
#' @name load_nba_stats_coaches
NULL

#' @title
#' **Load cleaned NBA Stats API season coaches from the data repo**
#' @rdname load_nba_stats_coaches
#' @description Loads season-level NBA coaching staff data scraped from the
#'   NBA Stats API. One row per coach-team-season triple. Backed by the
#'   `hoopR-nba-stats-data` pipeline that reads raw JSONs from
#'   `hoopR-nba-stats-raw` and publishes csv/parquet/rds artifacts to the
#'   `nba_stats_coaches` release tag.
#' @param seasons A vector of 4-digit years -- the season's **START** year
#'   (e.g. `2024` for the 2024-25 season), matching sdv-py's `load_nba_stats_*`
#'   convention (see the file-level note in `R/load_nba_stats.R`). Published
#'   coverage floors vary by loader (1996 for most; 2007 for
#'   [load_nba_stats_lineups()] -- see that function's own Description) and
#'   run through the most recent season with no gaps. Pass `seasons = TRUE`
#'   for every published season for that specific loader.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble with one row per coach-team-season.
#'
#'    \if{html}{\tabular{lll}{
#'       col_name \tab types \tab description \cr
#'       team_id \tab integer \tab Unique NBA Stats team identifier. \cr
#'       season \tab integer \tab Season identifier (4-digit year, END year of the season). \cr
#'       coach_id \tab integer \tab Unique coach identifier. \cr
#'       first_name \tab character \tab Coach's first name. \cr
#'       last_name \tab character \tab Coach's last name. \cr
#'       coach_name \tab character \tab Coach's full name. \cr
#'       is_assistant \tab integer \tab Whether the coach is an assistant coach (1) or head coach (0). \cr
#'       coach_type \tab character \tab Coaching role (e.g. 'Head Coach', 'Assistant Coach'). \cr
#'       sort_sequence \tab integer \tab Display sort order within the coaching staff. \cr
#'       sub_sort_sequence \tab integer \tab Secondary display sort order within the coaching staff. \cr
#'       season_type \tab character \tab Portion of the season (e.g. 'Regular Season', 'Playoffs'). \cr
#'    }}
#'    \if{latex}{See the HTML help or pkgdown reference for the column table.}
#'
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_coaches(seasons = most_recent_nba_stats_season()))
#' }
load_nba_stats_coaches <- function(seasons = most_recent_nba_stats_season(),
                                   ...,
                                   dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))
  dots <- rlang::dots_list(...)

  loader <- rds_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  if (isTRUE(seasons)) seasons <- 1996:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 1996),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_coaches/coaches_", seasons + 1, ".rds"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' @title
#' **Load cleaned NBA Stats API draft picks from the data repo**
#' @rdname load_nba_stats_coaches
#' @description Loads season-level NBA draft picks scraped from the NBA
#'   Stats API (`drafthistory`-style outputs). One row per pick. Backed by
#'   the `hoopR-nba-stats-data` pipeline that reads raw JSONs from
#'   `hoopR-nba-stats-raw` and publishes csv/parquet/rds artifacts to the
#'   `nba_stats_draft` release tag.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble of NBA draft picks.
#'
#'    \if{html}{\tabular{lll}{
#'       col_name \tab types \tab description \cr
#'       person_id \tab integer \tab Unique player identifier. \cr
#'       player_name \tab character \tab Player's name. \cr
#'       season \tab integer \tab Season identifier (4-digit year, END year of the season). \cr
#'       round_number \tab integer \tab Draft round number. \cr
#'       round_pick \tab integer \tab Pick number within the round. \cr
#'       overall_pick \tab integer \tab Overall pick number. \cr
#'       draft_type \tab character \tab Draft type ('Draft', 'Undrafted', etc.). \cr
#'       team_id \tab integer \tab Unique NBA Stats team identifier. \cr
#'       team_city \tab character \tab Team city or region. \cr
#'       team_name \tab character \tab Full team display name. \cr
#'       team_abbreviation \tab character \tab Three-letter team abbreviation. \cr
#'       organization \tab character \tab Player's college / organization prior to the draft. \cr
#'       organization_type \tab character \tab Type of organization (e.g. college, international). \cr
#'       player_profile_flag \tab integer \tab Player profile flag. \cr
#'    }}
#'    \if{latex}{See the HTML help or pkgdown reference for the column table.}
#'
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_draft(seasons = most_recent_nba_stats_season()))
#' }
load_nba_stats_draft <- function(seasons = most_recent_nba_stats_season(),
                                 ...,
                                 dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))
  dots <- rlang::dots_list(...)

  loader <- rds_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  if (isTRUE(seasons)) seasons <- 1996:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 1996),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_draft/draft_", seasons + 1, ".rds"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' @title
#' **Load cleaned NBA Stats API per-game 10-man on-court lineups from the data repo**
#' @rdname load_nba_stats_coaches
#' @description Loads the per-event 10-man on-court lineup (5 home + 5 away)
#'   scraped alongside the NBA Stats API play-by-play feed. One row per
#'   play-by-play action, with each player slot's NBA Stats person id. Backed
#'   by the `hoopR-nba-stats-data` pipeline that publishes csv.gz/parquet/rds
#'   artifacts to the `nba_stats_game_lineups` release tag.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble with one row per play-by-play action.
#'
#'    \if{html}{\tabular{lll}{
#'       col_name \tab types \tab description \cr
#'       game_id \tab character \tab Unique game identifier. \cr
#'       action_number \tab integer \tab Sequential play-by-play action number within the game. \cr
#'       period \tab integer \tab Period of the game (1-4 quarters; 5+ for OT). \cr
#'       home_player_1 \tab integer \tab Home on-court lineup slot 1 player identifier. \cr
#'       home_player_2 \tab integer \tab Home on-court lineup slot 2 player identifier. \cr
#'       home_player_3 \tab integer \tab Home on-court lineup slot 3 player identifier. \cr
#'       home_player_4 \tab integer \tab Home on-court lineup slot 4 player identifier. \cr
#'       home_player_5 \tab integer \tab Home on-court lineup slot 5 player identifier. \cr
#'       away_player_1 \tab integer \tab Away on-court lineup slot 1 player identifier. \cr
#'       away_player_2 \tab integer \tab Away on-court lineup slot 2 player identifier. \cr
#'       away_player_3 \tab integer \tab Away on-court lineup slot 3 player identifier. \cr
#'       away_player_4 \tab integer \tab Away on-court lineup slot 4 player identifier. \cr
#'       away_player_5 \tab integer \tab Away on-court lineup slot 5 player identifier. \cr
#'       season \tab integer \tab Season identifier (4-digit year, END year of the season). \cr
#'    }}
#'    \if{latex}{See the HTML help or pkgdown reference for the column table.}
#'
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_game_lineups(seasons = most_recent_nba_stats_season()))
#' }
load_nba_stats_game_lineups <- function(seasons = most_recent_nba_stats_season(),
                                        ...,
                                        dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))
  dots <- rlang::dots_list(...)

  loader <- rds_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  if (isTRUE(seasons)) seasons <- 1996:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 1996),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_game_lineups/nba_lineups_", seasons + 1, ".rds"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' @title
#' **Load cleaned NBA Stats API per-game inactive rosters from the data repo**
#' @rdname load_nba_stats_coaches
#' @description Loads the per-game **inactive player** list scraped from the
#'   NBA Stats API -- the `InactivePlayers` result set of
#'   `boxscoresummaryv2`. One row per inactive athlete-game pair, not a full
#'   per-game roster: use [load_nba_stats_player_game_logs()] for the
#'   athletes who did play. Backed by the `hoopR-nba-stats-data` pipeline
#'   that publishes csv/parquet/rds artifacts to the `nba_stats_game_rosters`
#'   release tag.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble with one row per inactive
#'   athlete-game pair.
#'
#'    \if{html}{\tabular{lll}{
#'       col_name \tab types \tab description \cr
#'       player_id \tab integer \tab Unique player identifier. \cr
#'       first_name \tab character \tab Player's first name. \cr
#'       last_name \tab character \tab Player's last name. \cr
#'       jersey_num \tab character \tab Jersey number worn by the player. \cr
#'       team_id \tab integer \tab Unique NBA Stats team identifier. \cr
#'       team_city \tab character \tab Team city or region. \cr
#'       team_name \tab character \tab Full team display name. \cr
#'       team_abbreviation \tab character \tab Three-letter team abbreviation. \cr
#'       season \tab integer \tab Season identifier (4-digit year, END year of the season). \cr
#'       game_id \tab character \tab Unique game identifier. \cr
#'    }}
#'    \if{latex}{See the HTML help or pkgdown reference for the column table.}
#'
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_game_rosters(seasons = most_recent_nba_stats_season()))
#' }
load_nba_stats_game_rosters <- function(seasons = most_recent_nba_stats_season(),
                                        ...,
                                        dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))
  dots <- rlang::dots_list(...)

  loader <- rds_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  if (isTRUE(seasons)) seasons <- 1996:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 1996),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_game_rosters/game_rosters_", seasons + 1, ".rds"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' @title
#' **Load cleaned NBA Stats API 5-man on-court lineup season stats from the data repo**
#' @rdname load_nba_stats_coaches
#' @description Loads season-level 5-man on-court lineup statistics
#'   (`leaguedashlineups`-style outputs, `Base` measure). Backed by the
#'   `hoopR-nba-stats-data` pipeline that publishes csv/parquet/rds artifacts
#'   to the `nba_stats_lineups` release tag. For `Advanced`/`Misc`/`Scoring`/
#'   `Opponent`/`Four Factors` measures and 2/3/4-man groupings, use
#'   [load_nba_stats_leaguedash()] with `table = "lineups_*"`.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble of 5-man lineup season stats (182
#'   columns: standard box-score rate/counting stats plus the 5 player-id
#'   slots and `group_id`/`group_name`). See `names(load_nba_stats_lineups())`
#'   for the full column set, or the `lineups_base` table of
#'   [load_nba_stats_leaguedash()] for the equivalent parameter-cube asset.
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_lineups(seasons = most_recent_nba_stats_season()))
#' }
load_nba_stats_lineups <- function(seasons = most_recent_nba_stats_season(),
                                   ...,
                                   dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))
  dots <- rlang::dots_list(...)

  loader <- rds_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  # Lineup publication starts at the 2007-08 season (file suffix 2008) --
  # earlier seasons don't exist upstream, so expanding `seasons = TRUE` from
  # 1996 would issue eleven guaranteed-404 downloads.
  if (isTRUE(seasons)) seasons <- 2007:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 2007),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_lineups/lineups_", seasons + 1, ".rds"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' @title
#' **Load cleaned NBA Stats API game officials from the data repo**
#' @rdname load_nba_stats_coaches
#' @description Loads game-level officials data scraped from the NBA Stats
#'   API (`boxscoresummaryv2`-style outputs). One row per official-game pair.
#'   Backed by the `hoopR-nba-stats-data` pipeline that publishes
#'   csv/parquet/rds artifacts to the `nba_stats_officials` release tag.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble with one row per official-game pair.
#'
#'    \if{html}{\tabular{lll}{
#'       col_name \tab types \tab description \cr
#'       official_id \tab integer \tab Unique official / referee identifier. \cr
#'       first_name \tab character \tab Official's first name. \cr
#'       last_name \tab character \tab Official's last name. \cr
#'       jersey_num \tab character \tab Jersey number worn by the official. \cr
#'       season \tab integer \tab Season identifier (4-digit year, END year of the season). \cr
#'       game_id \tab character \tab Unique game identifier. \cr
#'    }}
#'    \if{latex}{See the HTML help or pkgdown reference for the column table.}
#'
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_officials(seasons = most_recent_nba_stats_season()))
#' }
load_nba_stats_officials <- function(seasons = most_recent_nba_stats_season(),
                                     ...,
                                     dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))
  dots <- rlang::dots_list(...)

  loader <- rds_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  if (isTRUE(seasons)) seasons <- 1996:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 1996),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_officials/officials_", seasons + 1, ".rds"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' @title
#' **Load cleaned NBA Stats API play-by-play from the data repo**
#' @rdname load_nba_stats_coaches
#' @description Loads season-level NBA play-by-play scraped from the NBA
#'   Stats API modern game-feed. One row per play-by-play action, with shot
#'   location, on/off-court lineup joins available via
#'   [load_nba_stats_game_lineups()]. Backed by the `hoopR-nba-stats-data`
#'   pipeline that publishes csv.gz/parquet/rds artifacts to the
#'   `nba_stats_pbp` release tag.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble of per-event play-by-play rows.
#'
#'    \if{html}{\tabular{lll}{
#'       col_name \tab types \tab description \cr
#'       order_index \tab integer \tab Play-by-play chronological order index within the game. \cr
#'       action_number \tab integer \tab Sequential play-by-play action number within the game. \cr
#'       clock \tab character \tab Game clock remaining in the period. \cr
#'       period \tab integer \tab Period of the game (1-4 quarters; 5+ for OT). \cr
#'       team_id \tab integer \tab Unique NBA Stats team identifier for the acting team. \cr
#'       team_tricode \tab character \tab Three-letter team code. \cr
#'       person_id \tab integer \tab Unique player identifier for the acting player. \cr
#'       player_name \tab character \tab Acting player's name. \cr
#'       player_name_i \tab character \tab Acting player's name with abbreviated first initial. \cr
#'       x_legacy \tab integer \tab Legacy X coordinate on the court (0 = basket center). \cr
#'       y_legacy \tab integer \tab Legacy Y coordinate on the court (baseline at 0). \cr
#'       shot_distance \tab integer \tab Shot distance from the basket, in feet (shot events only). \cr
#'       shot_result \tab character \tab Shot result, 'Made' or 'Missed' (shot events only). \cr
#'       is_field_goal \tab integer \tab Whether the action is a field goal attempt (1) or not (0). \cr
#'       score_home \tab character \tab Home team score after the play. \cr
#'       score_away \tab character \tab Away team score after the play. \cr
#'       points_total \tab integer \tab Points scored on the action, if any. \cr
#'       location \tab character \tab Court location code for the action. \cr
#'       description \tab character \tab Text description of the play. \cr
#'       action_type \tab character \tab Action type label (e.g. 'Made Shot', 'Missed Shot', 'Rebound'). \cr
#'       sub_type \tab character \tab Sub type of the action (e.g. 'Jump Shot', 'Layup'). \cr
#'       video_available \tab integer \tab Whether NBA Stats video is available for the action. \cr
#'       shot_value \tab integer \tab Points the shot was worth, 2 or 3 (shot events only). \cr
#'       action_id \tab integer \tab Unique play-by-play action identifier. \cr
#'       game_id \tab character \tab Unique game identifier. \cr
#'       seconds_remaining \tab numeric \tab Seconds remaining in the period at the action. \cr
#'       event_type \tab character \tab Normalized event type label. \cr
#'       is_made_shot \tab logical \tab Whether the action is a made shot. \cr
#'       is_missed_shot \tab logical \tab Whether the action is a missed shot. \cr
#'       is_free_throw \tab logical \tab Whether the action is a free throw. \cr
#'       is_rebound \tab logical \tab Whether the action is a rebound. \cr
#'       is_turnover \tab logical \tab Whether the action is a turnover. \cr
#'       is_foul \tab logical \tab Whether the action is a foul. \cr
#'       is_substitution \tab logical \tab Whether the action is a substitution. \cr
#'       is_jump_ball \tab logical \tab Whether the action is a jump ball. \cr
#'       is_timeout \tab logical \tab Whether the action is a timeout. \cr
#'       is_period \tab logical \tab Whether the action is a period-boundary marker. \cr
#'       possession_number \tab integer \tab Sequential possession number within the game. \cr
#'       off_player_1 \tab integer \tab Offensive on-court lineup slot 1 player identifier. \cr
#'       off_player_2 \tab integer \tab Offensive on-court lineup slot 2 player identifier. \cr
#'       off_player_3 \tab integer \tab Offensive on-court lineup slot 3 player identifier. \cr
#'       off_player_4 \tab integer \tab Offensive on-court lineup slot 4 player identifier. \cr
#'       off_player_5 \tab integer \tab Offensive on-court lineup slot 5 player identifier. \cr
#'       def_player_1 \tab integer \tab Defensive on-court lineup slot 1 player identifier. \cr
#'       def_player_2 \tab integer \tab Defensive on-court lineup slot 2 player identifier. \cr
#'       def_player_3 \tab integer \tab Defensive on-court lineup slot 3 player identifier. \cr
#'       def_player_4 \tab integer \tab Defensive on-court lineup slot 4 player identifier. \cr
#'       def_player_5 \tab integer \tab Defensive on-court lineup slot 5 player identifier. \cr
#'       season \tab integer \tab Season identifier (4-digit year, END year of the season). \cr
#'    }}
#'    \if{latex}{See the HTML help or pkgdown reference for the column table.}
#'
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_pbp(seasons = most_recent_nba_stats_season()))
#' }
load_nba_stats_pbp <- function(seasons = most_recent_nba_stats_season(),
                               ...,
                               dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))
  dots <- rlang::dots_list(...)

  loader <- rds_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  if (isTRUE(seasons)) seasons <- 1996:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 1996),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_pbp/nba_play_by_play_", seasons + 1, ".rds"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' @title
#' **Load cleaned NBA Stats API player box scores from the data repo**
#' @rdname load_nba_stats_coaches
#' @description Loads per-player per-game box scores scraped from the NBA
#'   Stats API `boxscoretraditionalv3`-style output. One row per
#'   athlete-game pair. Backed by the `hoopR-nba-stats-data` pipeline that
#'   publishes csv/parquet/rds artifacts to the `nba_stats_player_boxscores`
#'   release tag.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble with one row per athlete-game pair.
#'
#'    \if{html}{\tabular{lll}{
#'       col_name \tab types \tab description \cr
#'       team_id \tab integer \tab Unique NBA Stats team identifier. \cr
#'       team_name \tab character \tab Full team display name. \cr
#'       team_tricode \tab character \tab Three-letter team code. \cr
#'       side \tab character \tab Whether the team was 'home' or 'away'. \cr
#'       person_id \tab integer \tab Unique player identifier. \cr
#'       first_name \tab character \tab Player's first name. \cr
#'       family_name \tab character \tab Player's family (last) name. \cr
#'       name_i \tab character \tab Player's name with abbreviated first initial. \cr
#'       player_slug \tab character \tab URL-safe player name slug. \cr
#'       position \tab character \tab Player's roster position. \cr
#'       comment \tab character \tab Reason the player did not play, if applicable (e.g. 'DND - Injury'). \cr
#'       jersey_num \tab character \tab Jersey number worn by the player. \cr
#'       minutes \tab character \tab Minutes played, as an \code{MM:SS} string. \cr
#'       field_goals_made \tab integer \tab Field goals made. \cr
#'       field_goals_attempted \tab integer \tab Field goal attempts. \cr
#'       field_goals_percentage \tab numeric \tab Field goal percentage (0-1). \cr
#'       three_pointers_made \tab integer \tab Three-point field goals made. \cr
#'       three_pointers_attempted \tab integer \tab Three-point field goal attempts. \cr
#'       three_pointers_percentage \tab numeric \tab Three-point field goal percentage (0-1). \cr
#'       free_throws_made \tab integer \tab Free throws made. \cr
#'       free_throws_attempted \tab integer \tab Free throw attempts. \cr
#'       free_throws_percentage \tab numeric \tab Free throw percentage (0-1). \cr
#'       rebounds_offensive \tab integer \tab Offensive rebounds. \cr
#'       rebounds_defensive \tab integer \tab Defensive rebounds. \cr
#'       rebounds_total \tab integer \tab Total rebounds. \cr
#'       assists \tab integer \tab Assists. \cr
#'       steals \tab integer \tab Steals. \cr
#'       blocks \tab integer \tab Blocks. \cr
#'       turnovers \tab integer \tab Turnovers. \cr
#'       fouls_personal \tab integer \tab Personal fouls. \cr
#'       points \tab integer \tab Points scored. \cr
#'       plus_minus_points \tab numeric \tab Plus/minus point differential while on court. \cr
#'       game_id \tab character \tab Unique game identifier. \cr
#'       season \tab integer \tab Season identifier (4-digit year, END year of the season). \cr
#'    }}
#'    \if{latex}{See the HTML help or pkgdown reference for the column table.}
#'
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_player_boxscores(seasons = most_recent_nba_stats_season()))
#' }
load_nba_stats_player_boxscores <- function(seasons = most_recent_nba_stats_season(),
                                            ...,
                                            dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))
  dots <- rlang::dots_list(...)

  loader <- rds_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  if (isTRUE(seasons)) seasons <- 1996:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 1996),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_player_boxscores/player_boxscores_", seasons + 1, ".rds"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' @title
#' **Load cleaned NBA Stats API per-player per-game logs from the data repo**
#' @rdname load_nba_stats_coaches
#' @description Loads per-player per-game logs scraped from
#'   `stats.nba.com/leaguegamelog?PlayerOrTeam=P` (one row per
#'   athlete-game pair: minutes, shooting splits, rebounds, steals, blocks,
#'   turnovers, personal fouls, plus/minus). Backed by the
#'   `hoopR-nba-stats-data` pipeline that publishes csv/parquet/rds
#'   artifacts to the `nba_stats_player_game_logs` release tag.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble of per-athlete per-game log rows.
#'
#'    \if{html}{\tabular{lll}{
#'       col_name \tab types \tab description \cr
#'       season_id \tab character \tab Unique season identifier string. \cr
#'       team_id \tab integer \tab Unique NBA Stats team identifier. \cr
#'       team_abbreviation \tab character \tab Three-letter team abbreviation. \cr
#'       team_name \tab character \tab Full team display name. \cr
#'       game_id \tab character \tab Unique game identifier. \cr
#'       game_date \tab character \tab Date the game was played, as an ISO 'YYYY-MM-DD' string. \cr
#'       matchup \tab character \tab Matchup string, e.g. 'BOS vs. NYK' or 'BOS @ NYK'. \cr
#'       wl \tab character \tab Game result, 'W' or 'L'. \cr
#'       min \tab integer \tab Minutes played. \cr
#'       fgm \tab integer \tab Field goals made. \cr
#'       fga \tab integer \tab Field goal attempts. \cr
#'       fg_pct \tab numeric \tab Field goal percentage (0-1). \cr
#'       fg3m \tab integer \tab Three-point field goals made. \cr
#'       fg3a \tab integer \tab Three-point field goal attempts. \cr
#'       fg3_pct \tab numeric \tab Three-point field goal percentage (0-1). \cr
#'       ftm \tab integer \tab Free throws made. \cr
#'       fta \tab integer \tab Free throw attempts. \cr
#'       ft_pct \tab numeric \tab Free throw percentage (0-1). \cr
#'       oreb \tab integer \tab Offensive rebounds. \cr
#'       dreb \tab integer \tab Defensive rebounds. \cr
#'       reb \tab integer \tab Total rebounds. \cr
#'       ast \tab integer \tab Assists. \cr
#'       stl \tab integer \tab Steals. \cr
#'       blk \tab integer \tab Blocks. \cr
#'       tov \tab integer \tab Turnovers. \cr
#'       pf \tab integer \tab Personal fouls. \cr
#'       pts \tab integer \tab Points scored. \cr
#'       plus_minus \tab integer \tab Plus/minus point differential while on court. \cr
#'       video_available \tab integer \tab Whether NBA Stats video is available for the game. \cr
#'       season \tab integer \tab Season identifier (4-digit year, END year of the season). \cr
#'       season_type \tab character \tab Portion of the season (e.g. 'Regular Season', 'Playoffs'). \cr
#'    }}
#'    \if{latex}{See the HTML help or pkgdown reference for the column table.}
#'
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_player_game_logs(seasons = most_recent_nba_stats_season()))
#' }
load_nba_stats_player_game_logs <- function(seasons = most_recent_nba_stats_season(),
                                            ...,
                                            dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))
  dots <- rlang::dots_list(...)

  loader <- rds_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  if (isTRUE(seasons)) seasons <- 1996:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 1996),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_player_game_logs/player_game_logs_", seasons + 1, ".rds"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' @title
#' **Load cleaned NBA Stats API player season stats from the data repo**
#' @rdname load_nba_stats_coaches
#' @description Loads season-level NBA player statistics
#'   (`leaguedashplayerstats`-style outputs, `Base` measure). Backed by the
#'   `hoopR-nba-stats-data` pipeline that publishes csv/parquet/rds
#'   artifacts to the `nba_stats_player_season_stats` release tag. For
#'   `Advanced`/`Misc`/`Scoring`/`Usage`/`Defense` measures and player
#'   tracking (drives, passing, touches, speed/distance, etc.), use
#'   [load_nba_stats_leaguedash()] with `table = "player_stats_*"` or
#'   `table = "player_tracking_*"`.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble of player season stats (210
#'   columns: rank fields, bio fields, and standard/rank box-score
#'   rate/counting stats for the season). See
#'   `names(load_nba_stats_player_season_stats())` for the full column set,
#'   or the `player_stats_base` table of [load_nba_stats_leaguedash()] for
#'   the equivalent parameter-cube asset.
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_player_season_stats(seasons = most_recent_nba_stats_season()))
#' }
load_nba_stats_player_season_stats <- function(seasons = most_recent_nba_stats_season(),
                                               ...,
                                               dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))
  dots <- rlang::dots_list(...)

  loader <- rds_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  if (isTRUE(seasons)) seasons <- 1996:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 1996),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_player_season_stats/player_season_stats_", seasons + 1, ".rds"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' @title
#' **Load cleaned NBA Stats API possessions from the data repo**
#' @rdname load_nba_stats_coaches
#' @description Loads possession-level data derived from the NBA Stats API
#'   play-by-play -- one row per possession, with the on-court 5-man
#'   lineups for both teams, shooting/rebounding/turnover splits, and the
#'   possession start type. Backed by the `hoopR-nba-stats-data` pipeline
#'   that publishes csv.gz/parquet/rds artifacts to the
#'   `nba_stats_possessions` release tag.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble with one row per possession.
#'
#'    \if{html}{\tabular{lll}{
#'       col_name \tab types \tab description \cr
#'       game_id \tab character \tab Unique game identifier. \cr
#'       period \tab integer \tab Period of the game (1-4 quarters; 5+ for OT). \cr
#'       possession_number \tab integer \tab Sequential possession number within the game. \cr
#'       offense_team_id \tab integer \tab Team identifier for the team on offense. \cr
#'       defense_team_id \tab integer \tab Team identifier for the team on defense. \cr
#'       start_order_index \tab integer \tab Play-by-play order index at the start of the possession. \cr
#'       end_order_index \tab integer \tab Play-by-play order index at the end of the possession. \cr
#'       start_seconds_remaining \tab numeric \tab Seconds remaining in the period at possession start. \cr
#'       end_seconds_remaining \tab numeric \tab Seconds remaining in the period at possession end. \cr
#'       points \tab integer \tab Points scored on the possession. \cr
#'       is_second_chance \tab logical \tab Whether the possession followed an offensive rebound. \cr
#'       number_in_period \tab integer \tab Sequential possession number within the period. \cr
#'       possession_start_type \tab character \tab How the possession started (e.g. 'OffDeadball', 'OffRebound'). \cr
#'       count_as_possession \tab logical \tab Whether the event counts as a scoreable possession. \cr
#'       fg2a \tab integer \tab 2-point field goal attempts by the lineup. \cr
#'       fg2m \tab integer \tab 2-point field goals made by the lineup. \cr
#'       fg3a \tab integer \tab 3-point field goal attempts by the lineup. \cr
#'       fg3m \tab integer \tab 3-point field goals made by the lineup. \cr
#'       fta \tab integer \tab Free throw attempts by the lineup. \cr
#'       ftm \tab integer \tab Free throws made by the lineup. \cr
#'       oreb \tab integer \tab Offensive rebounds. \cr
#'       dreb \tab integer \tab Defensive rebounds. \cr
#'       tov \tab integer \tab Turnovers. \cr
#'       off_player_1 \tab integer \tab Offensive lineup slot 1 player identifier. \cr
#'       off_player_2 \tab integer \tab Offensive lineup slot 2 player identifier. \cr
#'       off_player_3 \tab integer \tab Offensive lineup slot 3 player identifier. \cr
#'       off_player_4 \tab integer \tab Offensive lineup slot 4 player identifier. \cr
#'       off_player_5 \tab integer \tab Offensive lineup slot 5 player identifier. \cr
#'       def_player_1 \tab integer \tab Defensive lineup slot 1 player identifier. \cr
#'       def_player_2 \tab integer \tab Defensive lineup slot 2 player identifier. \cr
#'       def_player_3 \tab integer \tab Defensive lineup slot 3 player identifier. \cr
#'       def_player_4 \tab integer \tab Defensive lineup slot 4 player identifier. \cr
#'       def_player_5 \tab integer \tab Defensive lineup slot 5 player identifier. \cr
#'       lineup_source \tab character \tab Provenance of the lineup join (e.g. 'game_lineups', 'derived'). \cr
#'       season \tab integer \tab Season identifier (4-digit year, END year of the season). \cr
#'    }}
#'    \if{latex}{See the HTML help or pkgdown reference for the column table.}
#'
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_possessions(seasons = most_recent_nba_stats_season()))
#' }
load_nba_stats_possessions <- function(seasons = most_recent_nba_stats_season(),
                                       ...,
                                       dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))
  dots <- rlang::dots_list(...)

  loader <- rds_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  if (isTRUE(seasons)) seasons <- 1996:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 1996),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_possessions/nba_possessions_", seasons + 1, ".rds"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' @title
#' **Load cleaned NBA Stats API season rosters from the data repo**
#' @rdname load_nba_stats_coaches
#' @description Loads season-level NBA team rosters scraped from the NBA
#'   Stats API. One row per athlete-team-season triple. Backed by the
#'   `hoopR-nba-stats-data` pipeline that publishes csv/parquet/rds
#'   artifacts to the `nba_stats_rosters` release tag.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble with one row per athlete-team-season.
#'
#'    \if{html}{\tabular{lll}{
#'       col_name \tab types \tab description \cr
#'       team_id \tab integer \tab Unique NBA Stats team identifier. \cr
#'       season \tab integer \tab Season identifier (4-digit year, END year of the season). \cr
#'       league_id \tab character \tab League identifier ('00' for NBA). \cr
#'       player \tab character \tab Player's full name. \cr
#'       nickname \tab character \tab Player's nickname. \cr
#'       player_slug \tab character \tab URL-safe player name slug. \cr
#'       num \tab character \tab Jersey number worn by the player. \cr
#'       position \tab character \tab Player's roster position. \cr
#'       height \tab character \tab Player's listed height (feet-inches). \cr
#'       weight \tab character \tab Player's listed weight (lbs). \cr
#'       birth_date \tab character \tab Player's date of birth. \cr
#'       age \tab numeric \tab Player's age. \cr
#'       exp \tab character \tab Years of NBA experience ('R' for rookie). \cr
#'       school \tab character \tab Player's last college / school attended. \cr
#'       player_id \tab integer \tab Unique player identifier. \cr
#'       how_acquired \tab character \tab How the player joined the roster (e.g. 'Draft', 'Trade'). \cr
#'       supplemental_status \tab integer \tab Roster supplemental / two-way status flag. \cr
#'       season_type \tab character \tab Portion of the season (e.g. 'Regular Season', 'Playoffs'). \cr
#'    }}
#'    \if{latex}{See the HTML help or pkgdown reference for the column table.}
#'
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_rosters(seasons = most_recent_nba_stats_season()))
#' }
load_nba_stats_rosters <- function(seasons = most_recent_nba_stats_season(),
                                   ...,
                                   dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))
  dots <- rlang::dots_list(...)

  loader <- rds_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  if (isTRUE(seasons)) seasons <- 1996:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 1996),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_rosters/rosters_", seasons + 1, ".rds"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' @title
#' **Load cleaned NBA Stats API season schedules from the data repo**
#' @rdname load_nba_stats_coaches
#' @description Loads season-level NBA schedules scraped from
#'   `stats.nba.com/leaguegamefinder` (regular season + playoffs combined,
#'   pre-rejoined home/away). Backed by the `hoopR-nba-stats-data` pipeline
#'   that publishes csv/parquet/rds artifacts to the `nba_stats_schedules`
#'   release tag.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble of per-season schedules, one row per
#'   game with the home/away sides pre-joined into `home_*` / `away_*` columns.
#'
#'    \if{html}{\tabular{lll}{
#'       col_name \tab types \tab description \cr
#'       game_id \tab character \tab Unique game identifier. \cr
#'       season \tab integer \tab Season identifier (4-digit year, END year of the season). \cr
#'       season_type \tab character \tab Portion of the season (e.g. 'Regular Season', 'Playoffs'). \cr
#'       game_date \tab character \tab Date the game was played, as an ISO 'YYYY-MM-DD' string. \cr
#'       matchup \tab character \tab Matchup string, home side first (e.g. 'BOS vs. NYK'). \cr
#'       home_team_id \tab integer \tab Unique NBA Stats team identifier for the home team. \cr
#'       home_team_abbreviation \tab character \tab Home team abbreviation. \cr
#'       home_team_name \tab character \tab Home team full name. \cr
#'       home_pts \tab integer \tab Points scored by the home team. \cr
#'       home_wl \tab character \tab Home team result, 'W' or 'L'. \cr
#'       away_team_id \tab integer \tab Unique NBA Stats team identifier for the away team. \cr
#'       away_team_abbreviation \tab character \tab Away team abbreviation. \cr
#'       away_team_name \tab character \tab Away team full name. \cr
#'       away_pts \tab integer \tab Points scored by the away team. \cr
#'       away_wl \tab character \tab Away team result, 'W' or 'L'. \cr
#'    }}
#'    \if{latex}{See the HTML help or pkgdown reference for the column table.}
#'
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_schedule(seasons = most_recent_nba_stats_season()))
#' }
load_nba_stats_schedule <- function(seasons = most_recent_nba_stats_season(),
                                    ...,
                                    dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))
  dots <- rlang::dots_list(...)

  loader <- rds_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  if (isTRUE(seasons)) seasons <- 1996:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 1996),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_schedules/nba_schedule_", seasons + 1, ".rds"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' @title
#' **Load cleaned NBA Stats API shot events from the data repo**
#' @rdname load_nba_stats_coaches
#' @description Loads shot events scraped from the NBA Stats API. One row
#'   per shot attempt with legacy court coordinates, action/sub type,
#'   distance, and made/missed result, carried through from the play-by-play
#'   feed (not `shotchartdetail`). Backed by the `hoopR-nba-stats-data`
#'   pipeline that publishes csv/parquet/rds artifacts to the
#'   `nba_stats_shots` release tag.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble with one row per shot attempt.
#'
#'    \if{html}{\tabular{lll}{
#'       col_name \tab types \tab description \cr
#'       game_id \tab character \tab Unique game identifier. \cr
#'       season \tab integer \tab Season identifier (4-digit year, END year of the season). \cr
#'       period \tab integer \tab Period of the game (1-4 quarters; 5+ for OT). \cr
#'       clock \tab character \tab Game clock remaining in the period. \cr
#'       team_id \tab integer \tab Unique NBA Stats team identifier for the shooting team. \cr
#'       team_tricode \tab character \tab Three-letter team code (e.g. 'BOS' / 'NYK'). \cr
#'       person_id \tab integer \tab Unique player identifier for the shooter. \cr
#'       player_name \tab character \tab Shooter's name. \cr
#'       action_type \tab character \tab Action type label ('Made Shot' or 'Missed Shot'). \cr
#'       sub_type \tab character \tab Shot sub type (e.g. 'Jump Shot', 'Layup', 'Dunk'). \cr
#'       shot_result \tab character \tab Shot result, 'Made' or 'Missed'. \cr
#'       shot_value \tab integer \tab Points the shot was worth (2 or 3). \cr
#'       shot_distance \tab integer \tab Shot distance from the basket, in feet. \cr
#'       x_legacy \tab integer \tab Legacy X coordinate on the court (0 = basket center). \cr
#'       y_legacy \tab integer \tab Legacy Y coordinate on the court (baseline at 0). \cr
#'       description \tab character \tab Text description of the play. \cr
#'       score_home \tab character \tab Home team score after the play. \cr
#'       score_away \tab character \tab Away team score after the play. \cr
#'    }}
#'    \if{latex}{See the HTML help or pkgdown reference for the column table.}
#'
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_shots(seasons = most_recent_nba_stats_season()))
#' }
load_nba_stats_shots <- function(seasons = most_recent_nba_stats_season(),
                                 ...,
                                 dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))
  dots <- rlang::dots_list(...)

  loader <- rds_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  if (isTRUE(seasons)) seasons <- 1996:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 1996),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_shots/shots_", seasons + 1, ".rds"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' @title
#' **Load cleaned NBA Stats API season standings from the data repo**
#' @rdname load_nba_stats_coaches
#' @description Loads season-level NBA standings (`leaguestandingsv3`-style
#'   outputs). One row per team-season. Backed by the `hoopR-nba-stats-data`
#'   pipeline that publishes csv/parquet/rds artifacts to the
#'   `nba_stats_standings` release tag.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble of team standings (94 columns: win/
#'   loss splits by home/road/conference/division/last-10, streak fields,
#'   and points-per-game aggregates). See
#'   `names(load_nba_stats_standings())` for the full column set. The
#'   `standings` table of [load_nba_stats_leaguedash()] is the equivalent
#'   parameter-cube asset (same underlying `leaguestandingsv3` endpoint).
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_standings(seasons = most_recent_nba_stats_season()))
#' }
load_nba_stats_standings <- function(seasons = most_recent_nba_stats_season(),
                                     ...,
                                     dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))
  dots <- rlang::dots_list(...)

  loader <- rds_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  if (isTRUE(seasons)) seasons <- 1996:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 1996),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_standings/standings_", seasons + 1, ".rds"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' @title
#' **Load cleaned NBA Stats API team box scores from the data repo**
#' @rdname load_nba_stats_coaches
#' @description Loads per-team per-game box scores scraped from the NBA
#'   Stats API `boxscoretraditionalv3`-style output. One row per team-game
#'   pair. Backed by the `hoopR-nba-stats-data` pipeline that publishes
#'   csv/parquet/rds artifacts to the `nba_stats_team_boxscores` release tag.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble with one row per team-game pair.
#'
#'    \if{html}{\tabular{lll}{
#'       col_name \tab types \tab description \cr
#'       team_id \tab integer \tab Unique NBA Stats team identifier. \cr
#'       team_name \tab character \tab Full team display name. \cr
#'       team_tricode \tab character \tab Three-letter team code. \cr
#'       side \tab character \tab Whether the team was 'home' or 'away'. \cr
#'       minutes \tab character \tab Total minutes played, as an \code{MM:SS} string. \cr
#'       field_goals_made \tab integer \tab Field goals made. \cr
#'       field_goals_attempted \tab integer \tab Field goal attempts. \cr
#'       field_goals_percentage \tab numeric \tab Field goal percentage (0-1). \cr
#'       three_pointers_made \tab integer \tab Three-point field goals made. \cr
#'       three_pointers_attempted \tab integer \tab Three-point field goal attempts. \cr
#'       three_pointers_percentage \tab numeric \tab Three-point field goal percentage (0-1). \cr
#'       free_throws_made \tab integer \tab Free throws made. \cr
#'       free_throws_attempted \tab integer \tab Free throw attempts. \cr
#'       free_throws_percentage \tab numeric \tab Free throw percentage (0-1). \cr
#'       rebounds_offensive \tab integer \tab Offensive rebounds. \cr
#'       rebounds_defensive \tab integer \tab Defensive rebounds. \cr
#'       rebounds_total \tab integer \tab Total rebounds. \cr
#'       assists \tab integer \tab Assists. \cr
#'       steals \tab integer \tab Steals. \cr
#'       blocks \tab integer \tab Blocks. \cr
#'       turnovers \tab integer \tab Turnovers. \cr
#'       fouls_personal \tab integer \tab Personal fouls. \cr
#'       points \tab integer \tab Points scored. \cr
#'       plus_minus_points \tab numeric \tab Plus/minus point differential. \cr
#'       game_id \tab character \tab Unique game identifier. \cr
#'       season \tab integer \tab Season identifier (4-digit year, END year of the season). \cr
#'    }}
#'    \if{latex}{See the HTML help or pkgdown reference for the column table.}
#'
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_team_boxscores(seasons = most_recent_nba_stats_season()))
#' }
load_nba_stats_team_boxscores <- function(seasons = most_recent_nba_stats_season(),
                                          ...,
                                          dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))
  dots <- rlang::dots_list(...)

  loader <- rds_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  if (isTRUE(seasons)) seasons <- 1996:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 1996),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_team_boxscores/team_boxscores_", seasons + 1, ".rds"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' @title
#' **Load cleaned NBA Stats API team season stats from the data repo**
#' @rdname load_nba_stats_coaches
#' @description Loads season-level team statistics
#'   (`leaguedashteamstats`-style outputs, `Base` measure). Backed by the
#'   `hoopR-nba-stats-data` pipeline that publishes csv/parquet/rds
#'   artifacts to the `nba_stats_team_season_stats` release tag. For
#'   `Advanced`/`Misc`/`Scoring`/`Defense`/`Opponent`/`Four Factors`
#'   measures, use [load_nba_stats_leaguedash()] with `table = "team_stats_*"`.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble of team season stats (178 columns:
#'   rank fields plus standard/rank box-score rate/counting stats for the
#'   season). See `names(load_nba_stats_team_season_stats())` for the full
#'   column set, or the `team_stats_base` table of
#'   [load_nba_stats_leaguedash()] for the equivalent parameter-cube asset.
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_team_season_stats(seasons = most_recent_nba_stats_season()))
#' }
load_nba_stats_team_season_stats <- function(seasons = most_recent_nba_stats_season(),
                                             ...,
                                             dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))
  dots <- rlang::dots_list(...)

  loader <- rds_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  if (isTRUE(seasons)) seasons <- 1996:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 1996),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_team_season_stats/team_season_stats_", seasons + 1, ".rds"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' **Load hoopR NBA Stats League Dashboard cube**
#' @name load_nba_stats_leaguedash
NULL

#' Valid `table` values for [load_nba_stats_leaguedash()].
#' @keywords Internal
#' @noRd
nba_stats_leaguedash_tables <- c(
  "player_bio", "player_master",
  "player_stats_base", "player_stats_advanced", "player_stats_misc",
  "player_stats_scoring", "player_stats_usage", "player_stats_defense",
  "player_tracking_catchshoot", "player_tracking_defense",
  "player_tracking_drives", "player_tracking_efficiency",
  "player_tracking_elbowtouch", "player_tracking_painttouch",
  "player_tracking_passing", "player_tracking_possessions",
  "player_tracking_posttouch", "player_tracking_pullupshot",
  "player_tracking_rebounding", "player_tracking_speeddistance",
  "team_master",
  "team_stats_base", "team_stats_advanced", "team_stats_misc",
  "team_stats_scoring", "team_stats_defense", "team_stats_opponent",
  "team_stats_fourfactors",
  "lineups_master",
  "lineups_base", "lineups_advanced", "lineups_misc",
  "lineups_scoring", "lineups_opponent", "lineups_fourfactors",
  "standings"
)

#' @title
#' **Load a single table of the NBA Stats league dashboard cube**
#' @rdname load_nba_stats_leaguedash
#' @description Loads one asset of the `nba_stats_leaguedash` release tag --
#'   the parameter cube that supersedes the narrower `Base`-measure tags
#'   ([load_nba_stats_player_season_stats()], [load_nba_stats_team_season_stats()],
#'   [load_nba_stats_lineups()], [load_nba_stats_standings()]). The cube
#'   publishes 36 tables per season and is the only route to
#'   `Advanced`/`Misc`/`Scoring`/`Usage`/`Defense`/`Opponent`/`Four Factors`
#'   measures, 2/3/4-man lineup groupings, the `*_bio`/`*_master` wide joins,
#'   and player tracking (drives, passing, touches, catch-and-shoot,
#'   pull-up shooting, rebounding, speed/distance).
#'
#'   Coverage floors differ by table and are **not** individually enforced
#'   here (only the global 1996 floor is): the `lineups_*` tables start at
#'   the 2007-08 season and the `player_tracking_*` tables (other than
#'   `catchshoot`/`pullupshot`, which go back to 1996) start at the 2013-14
#'   season. Requesting an out-of-range season for those tables 404s
#'   gracefully with a warning and contributes no rows, same as any other
#'   `nba_stats_*` loader.
#' @param seasons A vector of 4-digit years -- the season's **START** year
#'   (e.g. `2024` for the 2024-25 season). Published coverage runs 1996
#'   through the most recent season for most tables (see Description for
#'   per-table floors). Pass `seasons = TRUE` for every published season.
#'   (Min: 1996)
#' @param table Name of the cube table to load. One of `player_bio`,
#'   `player_master`, `player_stats_base`, `player_stats_advanced`,
#'   `player_stats_misc`, `player_stats_scoring`, `player_stats_usage`,
#'   `player_stats_defense`, `player_tracking_catchshoot`,
#'   `player_tracking_defense`, `player_tracking_drives`,
#'   `player_tracking_efficiency`, `player_tracking_elbowtouch`,
#'   `player_tracking_painttouch`, `player_tracking_passing`,
#'   `player_tracking_possessions`, `player_tracking_posttouch`,
#'   `player_tracking_pullupshot`, `player_tracking_rebounding`,
#'   `player_tracking_speeddistance`, `team_master`, `team_stats_base`,
#'   `team_stats_advanced`, `team_stats_misc`, `team_stats_scoring`,
#'   `team_stats_defense`, `team_stats_opponent`, `team_stats_fourfactors`,
#'   `lineups_master`, `lineups_base`, `lineups_advanced`, `lineups_misc`,
#'   `lineups_scoring`, `lineups_opponent`, `lineups_fourfactors`,
#'   `standings`.
#' @param ... Additional arguments passed to an underlying function that writes
#'   the season data into a database.
#' @param dbConnection A `DBIConnection` object, as returned by [DBI::dbConnect()]
#' @param tablename The name of the data table within the database
#' @return Returns a `hoopR_data` tibble of the requested cube table, one
#'   row per player-season, team-season or lineup-season depending on
#'   `table`. Column sets differ per table (11 to 625 columns); the
#'   `*_master` tables are wide joins of every measure type for that entity.
#' @export
#' @family NBA Stats loader functions
#' @examples
#' \donttest{
#'   try(load_nba_stats_leaguedash(seasons = most_recent_nba_stats_season(),
#'                                 table = "player_bio"))
#' }
load_nba_stats_leaguedash <- function(seasons = most_recent_nba_stats_season(),
                                      table = NULL,
                                      ...,
                                      dbConnection = NULL, tablename = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))

  if (is.null(table) || length(table) != 1 ||
      !table %in% nba_stats_leaguedash_tables) {
    cli::cli_abort(c(
      "x" = "{.arg table} must be exactly one of the published cube tables.",
      "i" = "Valid choices: {.val {nba_stats_leaguedash_tables}}."
    ))
  }

  loader <- parquet_from_url
  if (!is.null(dbConnection) && !is.null(tablename)) in_db <- TRUE else in_db <- FALSE

  if (isTRUE(seasons)) seasons <- 1996:most_recent_nba_stats_season()

  stopifnot(is.numeric(seasons),
            all(seasons >= 1996),
            all(seasons <= most_recent_nba_stats_season()))

  urls <- paste0(
    "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/",
    "nba_stats_leaguedash/", table, "_", seasons + 1, ".parquet"
  )

  p <- NULL
  if (is_installed("progressr")) p <- progressr::progressor(along = seasons)

  out <- lapply(urls, progressively(loader, p))
  out <- data.table::rbindlist(out, use.names = TRUE, fill = TRUE)
  if (in_db) {
    DBI::dbWriteTable(dbConnection, tablename, out, append = TRUE, ...)
    out <- NULL
  } else {
    class(out) <- c("hoopR_data","tbl_df","tbl","data.table","data.frame")
  }
  out
}


#' **Build/update hoopR NBA Stats database**
#' @name update_nba_stats_db
NULL

#' @title
#' **Update or create a hoopR NBA Stats database**
#' @rdname update_nba_stats_db
#' @description `update_nba_stats_db()` writes one or more NBA Stats API
#'   datasets into a database. Mirrors the NBA / MBB `update_*_db()` shape
#'   but points at the `load_nba_stats_*` family. Each dataset is written
#'   to its own table, named `nba_stats_<dataset>`.
#' @details
#' Unlike `update_nba_db()`, there is no historical "play-by-play table"
#' default to preserve, so `datasets` is required.
#'
#' Valid `datasets` values:
#' `"coaches"`, `"draft"`, `"game_lineups"`, `"game_rosters"`, `"lineups"`,
#' `"officials"`, `"pbp"`, `"player_boxscores"`, `"player_game_logs"`,
#' `"player_season_stats"`, `"possessions"`, `"rosters"`, `"schedule"`,
#' `"shots"`, `"standings"`, `"team_boxscores"`, `"team_season_stats"`.
#'
#' @param dbdir Directory in which the database is or shall be located.
#' @param dbname File name of an existing or desired SQLite database within
#'   `dbdir`.
#' @param datasets Character vector of dataset shortnames (see Details).
#' @param seasons Optional integer vector of seasons to load. Defaults to all
#'   available seasons (`seasons = TRUE`).
#' @param db_connection A `DBIConnection` object. When supplied, `dbdir` and
#'   `dbname` are ignored.
#' @return Invisibly returns `TRUE` on success.
#' @export
update_nba_stats_db <- function(dbdir = ".",
                                dbname = "hoopR_db",
                                datasets = NULL,
                                seasons = TRUE,
                                db_connection = NULL) {
  old <- options(list(stringsAsFactors = FALSE, scipen = 999))
  on.exit(options(old))

  if (!is_installed("DBI") |
      (!is_installed("RSQLite") & is.null(db_connection))) {
    cli::cli_abort(c(
      "x" = "Packages {.pkg DBI} and {.pkg RSQLite} required for database communication.",
      "i" = "Install them and retry."
    ))
  }

  valid <- c("coaches", "draft", "game_lineups", "game_rosters", "lineups",
             "officials", "pbp", "player_boxscores", "player_game_logs",
             "player_season_stats", "possessions", "rosters", "schedule",
             "shots", "standings", "team_boxscores", "team_season_stats")
  if (is.null(datasets) || length(datasets) == 0) {
    cli::cli_abort(c(
      "x" = "{.arg datasets} is required for {.fn update_nba_stats_db}.",
      "i" = "Valid choices: {.val {valid}}."
    ))
  }
  bad <- setdiff(datasets, valid)
  if (length(bad) > 0) {
    cli::cli_abort(c(
      "x" = "Unknown {.arg datasets} value{?s}: {.val {bad}}.",
      "i" = "Valid choices: {.val {valid}}."
    ))
  }

  if (!dir.exists(dbdir) & is.null(db_connection)) {
    user_message(paste0("Directory '", dbdir, "' doesn't exist yet. Try creating..."), "oops")
    dir.create(dbdir)
  }

  if (is.null(db_connection)) {
    connection <- DBI::dbConnect(RSQLite::SQLite(), file.path(dbdir, dbname))
  } else {
    connection <- db_connection
  }

  loader_map <- list(
    coaches              = load_nba_stats_coaches,
    draft                = load_nba_stats_draft,
    game_lineups         = load_nba_stats_game_lineups,
    game_rosters         = load_nba_stats_game_rosters,
    lineups              = load_nba_stats_lineups,
    officials            = load_nba_stats_officials,
    pbp                  = load_nba_stats_pbp,
    player_boxscores     = load_nba_stats_player_boxscores,
    player_game_logs     = load_nba_stats_player_game_logs,
    player_season_stats  = load_nba_stats_player_season_stats,
    possessions          = load_nba_stats_possessions,
    rosters              = load_nba_stats_rosters,
    schedule             = load_nba_stats_schedule,
    shots                = load_nba_stats_shots,
    standings            = load_nba_stats_standings,
    team_boxscores       = load_nba_stats_team_boxscores,
    team_season_stats    = load_nba_stats_team_season_stats
  )

  for (ds in datasets) {
    ds_table <- paste0("nba_stats_", ds)
    user_message(paste0("Writing nba_stats dataset '", ds, "' to table '", ds_table, "'..."), "todo")
    tryCatch(
      loader_map[[ds]](seasons = seasons,
                       dbConnection = connection,
                       tablename = ds_table),
      error = function(e) {
        cli::cli_alert_danger("{Sys.time()}: dataset {.val {ds}} failed: {e$message}")
      }
    )
  }

  message_completed("Database update completed", in_builder = TRUE)
  user_message(paste0("Path to your db: ", DBI::dbGetInfo(connection)$dbname), "info")
  if (is.null(db_connection)) DBI::dbDisconnect(connection)
  invisible(TRUE)
}

Try the hoopR package in your browser

Any scripts or data that you put into this service are public.

hoopR documentation built on Aug. 25, 2026, 9:07 a.m.