fraiseql-db 2.16.0

Database abstraction layer for FraiseQL v2
Documentation
//! Unit tests for the change-log duration computation (no database required).

use super::{
    CDC_MEDIATED_ON, CDC_MEDIATED_VAR, CHANGELOG_PORTABLE_INSERT_COLUMNS,
    CLOCK_TIMESTAMP_DIRECTIVE, DURATION_CALC_VERSION, STARTED_AT_VAR, build_changelog_insert_sql,
    duration_ms_sql,
};
use crate::types::DatabaseType;

#[test]
fn duration_expr_uses_epoch_not_the_milliseconds_trap() {
    let sql = duration_ms_sql(STARTED_AT_VAR);
    assert!(
        sql.contains("EXTRACT(EPOCH"),
        "must use EXTRACT(EPOCH FROM interval) for full wall-clock seconds"
    );
    assert!(
        !sql.to_uppercase().contains("MILLISECONDS"),
        "must NOT use EXTRACT(MILLISECONDS) — it truncates intervals >= 1 minute"
    );
    assert!(sql.contains("* 1000"), "seconds are scaled to milliseconds");
    assert!(sql.contains("::INTEGER"), "result is an INTEGER duration_ms");
}

#[test]
fn duration_expr_reads_back_the_started_at_var_on_the_db_clock() {
    let sql = duration_ms_sql(STARTED_AT_VAR);
    assert!(
        sql.contains("current_setting('fraiseql.started_at')"),
        "reads the start timestamp back from the session var"
    );
    assert!(
        sql.contains("clock_timestamp()"),
        "closes the interval against the DB clock (same clock that set started_at)"
    );
}

#[test]
fn clock_directive_is_not_a_plausible_real_value() {
    // The sentinel must never collide with a real timestamp/string a caller
    // could legitimately set, hence the control-character framing.
    assert!(CLOCK_TIMESTAMP_DIRECTIVE.starts_with('\u{1}'));
    assert!(!CLOCK_TIMESTAMP_DIRECTIVE.contains(' '));
}

#[test]
fn duration_calc_version_is_the_post_fix_marker() {
    assert_eq!(DURATION_CALC_VERSION, 2);
}

// ── #366 CDC-mediated marker ─────────────────────────────────────────────────

#[test]
fn cdc_mediated_marker_is_a_dotted_custom_guc() {
    // Same dotted-custom shape as STARTED_AT_VAR, so `set_config`/`current_setting`
    // work with no postgresql.conf declaration. A dotted prefix is what PostgreSQL
    // requires for a user-defined GUC.
    assert_eq!(CDC_MEDIATED_VAR, "fraiseql.cdc_mediated");
    assert!(CDC_MEDIATED_VAR.contains('.'), "a custom GUC needs a dotted prefix");
    assert!(STARTED_AT_VAR.contains('.'), "sibling GUC is also dotted");
}

#[test]
fn cdc_mediated_on_value_is_an_exact_match_token() {
    // The trigger suppresses only on an exact equality to this token, so the unset
    // state (NULL from `current_setting(..., true)`) never suppresses capture.
    assert_eq!(CDC_MEDIATED_ON, "on");
    assert!(!CDC_MEDIATED_ON.is_empty(), "must be a non-empty truthy token");
}

// ── Portable outbox INSERT builder (multi-DB parity smoke construct) ─────────

#[test]
fn portable_insert_uses_dialect_specific_placeholders() {
    let table = "core.tb_entity_change_log";
    let n = CHANGELOG_PORTABLE_INSERT_COLUMNS.len();

    // PostgreSQL: $1..$N positional.
    let pg = build_changelog_insert_sql(table, DatabaseType::PostgreSQL);
    assert!(
        pg.contains("VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)"),
        "PG placeholders: {pg}"
    );

    let _ = n;
}

#[test]
fn portable_insert_writes_the_identity_and_envelope_subset() {
    let sql = build_changelog_insert_sql("t", DatabaseType::PostgreSQL);
    // Target table + the contract column list (double-quoted for PostgreSQL),
    // in lockstep with the constant.
    assert!(sql.starts_with("INSERT INTO t ("), "names the target table: {sql}");
    let cols = CHANGELOG_PORTABLE_INSERT_COLUMNS
        .iter()
        .map(|c| format!("\"{c}\""))
        .collect::<Vec<_>>()
        .join(", ");
    assert!(sql.contains(&cols), "writes the portable contract columns `{cols}`: {sql}");
    // started_at/duration_ms are PG-request-scoped → omitted on the portable path.
    assert!(!sql.contains("started_at"), "started_at is omitted (PG-only): {sql}");
    assert!(!sql.contains("duration_ms"), "duration_ms is omitted (PG-only): {sql}");
    // seq comes from the table default, never the INSERT.
    assert!(!CHANGELOG_PORTABLE_INSERT_COLUMNS.contains(&"seq"), "seq is not in the INSERT");
    // trace_id is a plain text column → portable across every dialect (#375).
    assert!(
        CHANGELOG_PORTABLE_INSERT_COLUMNS.contains(&"trace_id"),
        "trace_id is written: {sql}"
    );
    // schema_version is a plain text column → portable across every dialect (#377).
    assert!(
        CHANGELOG_PORTABLE_INSERT_COLUMNS.contains(&"schema_version"),
        "schema_version is written: {sql}"
    );
    // trace_context is written as JSON text (JSON/JSONB/NVARCHAR(MAX) per dialect) (#375).
    assert!(
        CHANGELOG_PORTABLE_INSERT_COLUMNS.contains(&"trace_context"),
        "trace_context is written: {sql}"
    );
}

#[test]
fn portable_insert_quotes_identifiers_per_dialect() {
    // `cascade` is a reserved keyword in several engines → must be quoted.
    let pg = build_changelog_insert_sql("t", DatabaseType::PostgreSQL);
    assert!(pg.contains("\"cascade\""), "PostgreSQL double-quotes columns: {pg}");
}