dbkit-core 0.3.1

Core types and SQL building blocks for the dbkit ORM.
Documentation
use dbkit_core::{expr::Value, func, Column, Expr, Select, Table};

#[derive(Debug)]
struct Job;

fn jobs_table() -> Table {
    Table::new("jobs")
}

fn job_id() -> Column<Job, i64> {
    Column::new(jobs_table(), "id")
}

fn job_retry_count() -> Column<Job, i64> {
    Column::new(jobs_table(), "retry_count")
}

fn job_max_retries() -> Column<Job, i64> {
    Column::new(jobs_table(), "max_retries")
}

fn job_content_hash() -> Column<Job, String> {
    Column::new(jobs_table(), "content_hash")
}

fn job_last_content_hash() -> Column<Job, String> {
    Column::new(jobs_table(), "last_content_hash")
}

fn job_embedding() -> Column<Job, String> {
    Column::new(jobs_table(), "embedding")
}

fn job_embedding_hash() -> Column<Job, String> {
    Column::new(jobs_table(), "embedding_hash")
}

#[test]
fn compiles_ne_col_between_non_nullable_columns() {
    let expr = job_content_hash().ne_col(job_last_content_hash());
    let sql = expr_sql(expr);
    assert_eq!(
        sql.sql,
        "SELECT jobs.* FROM jobs WHERE (jobs.content_hash <> jobs.last_content_hash)"
    );
    assert!(sql.binds.is_empty());
}

#[test]
fn compiles_lt_col_between_numeric_columns() {
    let expr = job_retry_count().lt_col(job_max_retries());
    let sql = expr_sql(expr);
    assert_eq!(sql.sql, "SELECT jobs.* FROM jobs WHERE (jobs.retry_count < jobs.max_retries)");
    assert!(sql.binds.is_empty());
}

#[test]
fn compiles_le_col_between_numeric_columns() {
    let expr = job_retry_count().le_col(job_max_retries());
    let sql = expr_sql(expr);
    assert_eq!(sql.sql, "SELECT jobs.* FROM jobs WHERE (jobs.retry_count <= jobs.max_retries)");
    assert!(sql.binds.is_empty());
}

#[test]
fn compiles_gt_col_between_numeric_columns() {
    let expr = job_retry_count().gt_col(job_max_retries());
    let sql = expr_sql(expr);
    assert_eq!(sql.sql, "SELECT jobs.* FROM jobs WHERE (jobs.retry_count > jobs.max_retries)");
    assert!(sql.binds.is_empty());
}

#[test]
fn compiles_ge_col_between_numeric_columns() {
    let expr = job_retry_count().ge_col(job_max_retries());
    let sql = expr_sql(expr);
    assert_eq!(sql.sql, "SELECT jobs.* FROM jobs WHERE (jobs.retry_count >= jobs.max_retries)");
    assert!(sql.binds.is_empty());
}

#[test]
fn compiles_ne_col_between_potentially_nullable_columns() {
    let expr = job_embedding_hash().ne_col(job_embedding());
    let sql = expr_sql(expr);
    assert_eq!(sql.sql, "SELECT jobs.* FROM jobs WHERE (jobs.embedding_hash <> jobs.embedding)");
    assert!(sql.binds.is_empty());
}

#[test]
fn compiles_is_distinct_from_col_between_potentially_nullable_columns() {
    let expr = job_embedding_hash().is_distinct_from_col(job_embedding());
    let sql = expr_sql(expr);
    assert_eq!(
        sql.sql,
        "SELECT jobs.* FROM jobs WHERE (jobs.embedding_hash IS DISTINCT FROM jobs.embedding)"
    );
    assert!(sql.binds.is_empty());
}

#[test]
fn compiles_is_not_distinct_from_col_between_potentially_nullable_columns() {
    let expr = job_embedding_hash().is_not_distinct_from_col(job_embedding());
    let sql = expr_sql(expr);
    assert_eq!(
        sql.sql,
        "SELECT jobs.* FROM jobs WHERE (jobs.embedding_hash IS NOT DISTINCT FROM jobs.embedding)"
    );
    assert!(sql.binds.is_empty());
}

#[test]
fn compiles_expr_ne_col_with_coalesce_for_nullable_vs_nonnullable() {
    let expr = func::coalesce(job_embedding_hash(), "").ne_col(job_content_hash());
    let sql = expr_sql(expr);
    assert_eq!(
        sql.sql,
        "SELECT jobs.* FROM jobs WHERE (COALESCE(jobs.embedding_hash, $1) <> jobs.content_hash)"
    );
    assert_eq!(sql.binds, vec![Value::String("".to_string())]);
}

#[test]
fn compiles_stale_embedding_predicate_with_null_checks_and_hash_mismatch() {
    let expr = job_embedding()
        .is_null()
        .or(job_embedding_hash().is_distinct_from_col(job_content_hash()));
    let sql = expr_sql(expr);
    assert_eq!(
        sql.sql,
        "SELECT jobs.* FROM jobs WHERE ((jobs.embedding IS NULL) OR (jobs.embedding_hash IS DISTINCT FROM jobs.content_hash))"
    );
    assert!(sql.binds.is_empty());
}

#[test]
fn compiles_stale_embedding_predicate_with_coalesce_and_hash_mismatch() {
    let expr = job_embedding()
        .is_null()
        .or(job_embedding_hash().is_null())
        .or(func::coalesce(job_embedding_hash(), "").ne_col(job_content_hash()));
    let sql = expr_sql(expr);
    assert_eq!(
        sql.sql,
        "SELECT jobs.* FROM jobs WHERE (((jobs.embedding IS NULL) OR (jobs.embedding_hash IS NULL)) OR (COALESCE(jobs.embedding_hash, $1) <> jobs.content_hash))"
    );
    assert_eq!(sql.binds, vec![Value::String("".to_string())]);
}

#[test]
fn compiles_mixed_boolean_condition_using_col_comparison_helpers() {
    let expr = job_retry_count()
        .lt_col(job_max_retries())
        .and(job_content_hash().ne_col(job_last_content_hash()))
        .and(job_id().gt(0_i64));
    let sql = expr_sql(expr);
    assert_eq!(
        sql.sql,
        "SELECT jobs.* FROM jobs WHERE (((jobs.retry_count < jobs.max_retries) AND (jobs.content_hash <> jobs.last_content_hash)) AND (jobs.id > $1))"
    );
    assert_eq!(sql.binds, vec![Value::I64(0)]);
}

fn expr_sql(expr: Expr<bool>) -> dbkit_core::CompiledSql {
    let query: Select<Job> = Select::new(jobs_table()).filter(expr);
    query.compile()
}