/**
 * BigQueryFactStore — provisions fact tables and MERGEs fact rows into
 * BigQuery (`data-platform-302218.ailf_facts`), the store side of the D0091
 * registry-driven fact loader.
 *
 * The fact-loader engine talks only to the `BigQueryFactStore` port. This
 * adapter owns two responsibilities:
 *
 *   1. **Schema reconciliation** (`ensureTable`). Create the table from the
 *      descriptor's declared `schema` if it does not exist (DAY-partitioned on
 *      `completed_at`, clustered on the declared fields), or additively
 *      `ADD COLUMN` any declared columns the live table lacks. Reconciliation
 *      is additive only: a declared column whose live type differs is a
 *      migration, surfaced as a loud error rather than silently retyped or
 *      coerced (D0091).
 *
 *   2. **Idempotent loading** (`mergeRows`). Batch-load the rows to a throwaway
 *      staging table, then `MERGE` into the partitioned target on the
 *      descriptor's natural key, partition-pruning on `completed_at`. Re-running
 *      the loader re-derives the same rows — matched keys update in place, so
 *      row counts are stable (safe retries, safe backfill).
 *
 * The pure builders (`toBigQueryField`, `dedupeByMergeKey`, `buildMergeSql`,
 * …) are exported so the SQL and schema mapping are unit-testable without a
 * BigQuery client; the class wires them to the SDK.
 *
 * @see docs/decisions/D0091-bigquery-analytics-fact-loader.md
 * @see packages/core/src/ports/bigquery-fact-store.ts
 */
import { BigQuery } from "@google-cloud/bigquery";
import type { BigQueryColumn, BigQueryFactStore, FactRow, FactTableSpec, MergeResult } from "../_vendor/ailf-core/index.d.ts";
/**
 * Audit column stamped by the store at load time. The extractor is pure and
 * cannot read the clock, so if a descriptor declares `_loaded_at` the store
 * sets it to `CURRENT_TIMESTAMP()` on both insert and update rather than from
 * the source row. It is excluded from the staging load and never a merge key,
 * so stamping it does not change row counts (idempotency holds).
 */
export declare const LOADED_AT_COLUMN = "_loaded_at";
/** Options for constructing a {@link BigQueryFactStoreAdapter}. */
export interface BigQueryFactStoreOptions {
    /** BigQuery project the fact dataset lives in (e.g. "data-platform-302218"). */
    project: string;
    /**
     * Location of the fact dataset (e.g. "EU"). Required for query jobs (MERGE,
     * match-count) to run in the dataset's region; table DDL calls infer it from
     * the dataset. Omit only when the dataset is in the client default (US).
     */
    location?: string;
    /** Path to a service-account key file. Omit to use ADC. */
    credentials?: string;
    /**
     * Pre-constructed client. When omitted the adapter constructs one lazily
     * from `project` + ADC. Tests supply a fake here.
     */
    bigquery?: BigQuery;
}
/**
 * BigQuery-backed {@link BigQueryFactStore}. Provisions tables from declared
 * schemas and loads rows idempotently via staging + MERGE.
 */
export declare class BigQueryFactStoreAdapter implements BigQueryFactStore {
    private client;
    private readonly options;
    /**
     * Specs captured from `ensureTable`, so `mergeRows` can resolve a table's
     * dataset, partition field, and column list without re-reading metadata.
     * The engine always `ensureTable`s before `mergeRows`; a `mergeRows` for an
     * un-ensured table throws (mirroring the in-memory double's contract).
     */
    private readonly specs;
    constructor(options: BigQueryFactStoreOptions);
    ensureTable(spec: FactTableSpec): Promise<void>;
    mergeRows(table: string, rows: readonly FactRow[], mergeKey: readonly string[]): Promise<MergeResult>;
    private getClient;
}
/** A BigQuery schema field the adapter writes. */
export interface BigQuerySchemaField {
    name: string;
    type: string;
    mode: string;
    description?: string;
}
/**
 * Reject an identifier that would break the raw SQL the store builds. Table and
 * column names come from code-authored `FactTableDescriptor`s (not user input),
 * so this is a fail-fast against a descriptor typo, not an injection defense —
 * a name with a space or backtick becomes a clear error, not broken SQL.
 */
export declare function assertSafeIdentifier(kind: string, name: string): void;
/**
 * Map a declared column to a BigQuery schema field. The `BigQueryFieldType`
 * union is already BigQuery-native, so `type` passes through; mode defaults to
 * NULLABLE.
 */
export declare function toBigQueryField(col: BigQueryColumn): BigQuerySchemaField;
/**
 * Canonicalize a BigQuery field type for drift comparison. Live metadata may
 * report legacy names (INTEGER/FLOAT/BOOLEAN) even for columns created with the
 * standard-SQL names (INT64/FLOAT64/BOOL), so both sides are normalized before
 * comparison.
 */
export declare function normalizeFieldType(type: string): string;
/** Stable string key from a row's merge-key columns. */
export declare function mergeKeyString(row: FactRow, mergeKey: readonly string[]): string;
/** Collapse rows to one per merge-key tuple (last wins), preserving order. */
export declare function dedupeByMergeKey(rows: readonly FactRow[], mergeKey: readonly string[]): FactRow[];
/**
 * Min/max of the partition column across rows, as ISO timestamps, or null when
 * no row carries a usable value (in which case the MERGE runs unpruned).
 *
 * IDEMPOTENCY INVARIANT: the bounds come from the incoming batch and prune the
 * target's partitions in both the match-count and the MERGE `ON` clause. This
 * is correct only while the partition value is functionally determined by the
 * merge key — i.e. re-deriving the same logical row always yields the same
 * `completed_at`. In v1 that holds because every merge key is run-scoped and
 * `completed_at` is the run's fixed completion time. A future descriptor whose
 * partition column can drift for a stable merge key would let the prune miss
 * the pre-existing target row and double-insert it in another partition — such
 * a descriptor must include the partition column in its merge key.
 */
export declare function partitionBounds(rows: readonly FactRow[], partitionField: string): {
    min: string;
    max: string;
} | null;
/**
 * `SELECT COUNT(*)` of target rows whose key is present in staging (partition-
 * pruned when bounds are known). This is the pre-MERGE matched count → the
 * number of rows the MERGE will UPDATE rather than INSERT.
 */
export declare function buildMatchCountSql(args: {
    target: string;
    staging: string;
    mergeKey: readonly string[];
    partitionField: string;
    hasBounds: boolean;
}): string;
/**
 * Build the idempotent upsert `MERGE`. Matched keys update their non-key data
 * columns (and re-stamp `_loaded_at` when present); unmatched keys insert every
 * column. The WHEN MATCHED clause is omitted when there is nothing to update
 * (an all-key table with no audit column), degenerating to insert-if-absent.
 */
export declare function buildMergeSql(args: {
    target: string;
    staging: string;
    columns: readonly string[];
    mergeKey: readonly string[];
    partitionField: string;
    hasBounds: boolean;
    stampLoadedAt: boolean;
}): string;
