% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/fd_upsert.R
\name{fd_upsert}
\alias{fd_upsert}
\title{Upsert computed features into a database features table}
\usage{
fd_upsert(
  con,
  features_df,
  feat_table_name,
  key,
  create_table = "auto",
  alter_table = TRUE,
  update_table = TRUE,
  chunk_size = NULL,
  verbose = TRUE,
  return_report = TRUE,
  dialect = NULL
)
}
\arguments{
\item{con}{A \code{DBIConnection}. Must be a live connection to the database where
\code{feat_table_name} exists (or will be created).}

\item{features_df}{A \code{data.frame} containing at minimum the primary key column
specified by \code{key}, plus one or more feature columns to be stored.
Requirements:
\itemize{
\item \code{key} must exist in \code{names(features_df)}.
\item \code{features_df[[key]]} must contain no \code{NA} and no duplicate values.
\item Each feature column should be an atomic vector (numeric, integer, logical,
character, Date/POSIXct) that can be written by DBI for the chosen dialect.
}}

\item{feat_table_name}{A single string. Name of the database table where features
are stored (the "target" table). For dialects supporting schemas (e.g. Postgres),
this may be schema-qualified (e.g. \code{"public.my_features"}), subject to dialect rules.}

\item{key}{A single string. Name of the primary key column in both \code{features_df}
and \code{feat_table_name}. This column uniquely identifies each entity/row.}

\item{create_table}{Logical, or character value \code{auto}. If \code{TRUE}, create \code{feat_table_name} when it does not exist.
The created table includes all columns present in \code{features_df} and defines
\verb{PRIMARY KEY(key)}. If \code{FALSE} and \code{feat_table_name} is missing, the function errors.If \code{auto},
create the table it hasn't been already created.}

\item{alter_table}{Logical. If \code{TRUE}, add missing feature columns found in
\code{features_df} but not present in \code{feat_table_name} using \verb{ALTER TABLE ... ADD COLUMN ...}.
Only additive schema changes are performed. If \code{FALSE}, the function errors
when \code{features_df} contains columns not present in \code{feat_table_name}.}

\item{update_table}{Logical. Controls incremental behavior for keys that already exist
in \code{feat_table_name}:
\itemize{
\item If \code{TRUE} (default), existing keys are updated (upsert).
\item If \code{FALSE}, only new keys are inserted; any conflict with existing keys
results in an error (insert-only mode).
}}

\item{chunk_size}{Optional integer batch size. If provided, \code{features_df} is written
in batches of up to \code{chunk_size} rows, each merged via a staging table.
Use this to limit memory/packet sizes for large writes. If \code{NULL}, write in one batch.}

\item{verbose}{Logical. If \code{TRUE}, emit progress messages (e.g., chunk progress,
table creation/alteration actions). Does not affect returned results.}

\item{return_report}{Logical. If \code{TRUE}, return a structured
\code{fd_upsert_report}; if \code{FALSE}, return \code{TRUE} invisibly after successful
side effects.}

\item{dialect}{Optional dialect override. Supported values are \code{"postgres"},
\code{"sqlite"}, and \code{"mysql"}.}
}
\value{
An \code{fd_upsert_report} object (S3) with a structured summary of actions performed:
\itemize{
\item \code{table_created} (logical)
\item \code{columns_added} (character vector)
\item \code{extra_columns} (character vector): target-table columns not present
in the incoming features data, excluding \code{key}
\item \code{counts$would_insert} (integer)
\item \code{counts$would_update} (integer; \code{0} when \code{update_table = FALSE})
\item optional per-chunk breakdown (if chunking is used)
}
}
\description{
Takes a feature \code{data.frame} produced by \code{fd_compute()} (one row per entity id)
and writes it into a database table, using explicit incremental semantics:
new keys are inserted; existing keys are optionally updated.
}
\details{
The function is set-based (staging + merge) and avoids "read all ids into R"
patterns. Writes are performed in chunks (optional) inside a transaction.

Schema evolution is supported in a restricted, safe form: when \code{alter_table = TRUE},
missing feature columns are added to the target table via \verb{ALTER TABLE ... ADD COLUMN ...}.
No column drops/renames/type changes are performed. Columns that exist in
the target table but are not present in \code{features_df} are left untouched and
reported as \code{extra_columns}.

\strong{Incremental semantics}
\itemize{
\item Insert new keys (keys in \code{features_df} not present in \code{feat_table_name}).
\item Update existing keys only when \code{update_table = TRUE}.
\item When \code{update_table = FALSE}, any overlap between staged keys and existing keys
is treated as a conflict and aborts the write.
}

\strong{Counts}
The returned report contains \code{would_insert} / \code{would_update} counts, computed as
existence-based counts \emph{prior to the merge} (within the transaction). These are
not "rows whose values changed", only "rows targeted as insert/update".

\strong{Transactions}
The operation runs in a transaction. If any step fails (schema change, staging write,
merge), the transaction is rolled back and the target table is unchanged.
Existing target tables used with \code{update_table = TRUE} must have a primary
key or unique constraint/index on \code{key}. Tables created by \code{fd_upsert()} get
this primary key automatically.

\strong{Concurrency}
\code{fd_upsert()} does not currently take an explicit table-level lock. Avoid
running concurrent writes to the same feature table. Concurrent writes to
different feature tables are independent.
}
\examples{
if (requireNamespace("RSQLite", quietly = TRUE)) {
con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:")
on.exit(DBI::dbDisconnect(con), add = TRUE)

feats <- data.frame(
  id = c(1, 2, 3),
  f_age = c(10, 20, 30),
  f_flag = c(TRUE, FALSE, TRUE)
)

# Create table and insert initial rows
r1 <- fd_upsert(
  con = con,
  features_df = feats,
  feat_table_name = "features_tbl",
  key = "id",
  create_table = TRUE,
  alter_table = FALSE,
  update_table = TRUE,
  verbose = FALSE
)

# Upsert: update ids 2-3, insert id 4
feats2 <- data.frame(
  id = c(2, 3, 4),
  f_age = c(21, 31, 40),
  f_flag = c(FALSE, TRUE, FALSE)
)

r2 <- fd_upsert(
  con = con,
  features_df = feats2,
  feat_table_name = "features_tbl",
  key = "id",
  update_table = TRUE,
  verbose = FALSE
)

# Schema evolution: add a new feature column
feats3 <- data.frame(
  id = c(4, 5),
  f_age = c(41, 50),
  f_flag = c(TRUE, TRUE),
  f_new = c("A", "B")
)

r3 <- fd_upsert(
  con = con,
  features_df = feats3,
  feat_table_name = "features_tbl",
  key = "id",
  alter_table = TRUE,
  update_table = TRUE,
  verbose = FALSE
)
}

}
