# pg-upsert

A Rust library for PostgreSQL UPSERT operations using sqlx.

## Features

- Simple macro-based API for defining fields
- Support for `ON CONFLICT DO UPDATE` and `ON CONFLICT DO NOTHING`
- Optimistic locking with version field support
- Batch upsert for multiple rows
- Runtime-agnostic (you choose: tokio, async-std, etc.)

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
pg-upsert = "0.1"
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }
chrono = "0.4"  # Required for date/time field types
```

## Usage

### Basic Upsert

```rust
use pg_upsert::{upsert, fields, UpsertOptions};
use sqlx::PgPool;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let pool = PgPool::connect("postgres://localhost/mydb").await?;

    let rows_affected = upsert(
        &pool,
        "users",
        &["id"],  // conflict fields
        vec![
            fields!["id" => 1, "name" => "Alice", "email" => "alice@example.com"],
            fields!["id" => 2, "name" => "Bob", "email" => "bob@example.com"],
        ],
        UpsertOptions::default(),
    ).await?;

    println!("Affected rows: {}", rows_affected);
    Ok(())
}
```

### With Version Field (Optimistic Locking)

Only updates if incoming version > existing version:

```rust
use pg_upsert::FieldValue;

let rows_affected = upsert(
    &pool,
    "products",
    &["sku"],
    vec![
        fields![
            "sku" => "ABC123",
            "price" => FieldValue::Numeric("29.99".to_string()),
            "version" => 5
        ],
    ],
    UpsertOptions {
        version_field: Some("version".into()),
        ..Default::default()
    },
).await?;
```

### Do Nothing on Conflict

Insert only, skip if row exists:

```rust
let rows_affected = upsert(
    &pool,
    "events",
    &["event_id"],
    vec![
        fields!["event_id" => "evt_001", "data" => "payload"],
    ],
    UpsertOptions {
        do_nothing_on_conflict: true,
        ..Default::default()
    },
).await?;
```

### Multiple Conflict Fields

```rust
let rows_affected = upsert(
    &pool,
    "inventory",
    &["warehouse_id", "product_id"],  // composite key
    vec![
        fields!["warehouse_id" => 1, "product_id" => 100, "quantity" => 50],
    ],
    UpsertOptions::default(),
).await?;
```

### High-Precision Numeric Values

For financial data or any scenario requiring exact decimal precision:

```rust
use pg_upsert::FieldValue;

let rows_affected = upsert(
    &pool,
    "transactions",
    &["transaction_id"],
    vec![
        fields![
            "transaction_id" => "TXN-001",
            "amount" => FieldValue::Numeric("1234567.89".to_string()),
            "fee" => FieldValue::Numeric("0.0025".to_string()),
            "balance" => FieldValue::Numeric("99999999.999999".to_string())
        ],
    ],
    UpsertOptions::default(),
).await?;
```

### Supported Field Types

The `fields!` macro supports:
- Integers: `i32`, `i64`
- Floats: `f32`, `f64`
- Strings: `String`, `&str`
- Numeric: `FieldValue::Numeric(String)` for high-precision decimal values (PostgreSQL NUMERIC/DECIMAL types)
- Booleans: `bool`
- Bytes: `Vec<u8>`
- Date/Time: `chrono::NaiveDate`, `chrono::NaiveTime`, `chrono::NaiveDateTime`, `chrono::DateTime<Utc>`
- Optional: `Option<T>` (becomes NULL when None)

```rust
use chrono::{NaiveDate, Utc};
use pg_upsert::FieldValue;

fields![
    "id" => 1_i64,
    "name" => "test",
    "price" => 19.99_f64,
    "balance" => FieldValue::Numeric("99999.999999999999".to_string()),  // High-precision numeric
    "active" => true,
    "created_at" => Utc::now(),
    "birth_date" => NaiveDate::from_ymd_opt(1990, 1, 15).unwrap(),
    "description" => None::<String>,  // NULL value
]
```

## Builder Pattern for Options

```rust
let options = UpsertOptions::new()
    .with_version_field("version")
    .with_do_nothing_on_conflict(false);
```

## License

MIT
