# Check DSL

Build type-safe, composable CHECK constraint expressions with IDbCheckExpression and the DbCheck builder.

## Overview

The `[Check]` attribute accepts either a raw SQL string or a class that implements `IDbCheckExpression`. The class-based approach is strongly preferred: it is type-safe, reusable across multiple models, composable with boolean operators, and refactoring-friendly.

## IDbCheckExpression

```csharp
public interface IDbCheckExpression
{
    DbCheckExpr Build(string? columnName);
}
```

`DbCheckExpr` is a lightweight struct that wraps the raw SQL fragment. Implement `Build` and return the result of composing `DbCheck` methods. The `columnName` parameter receives the snake_case column name when the attribute is on a property, or `null` when it is on the class.

Apply it with:

```csharp
[Check(typeof(MyCheckExpression))]
public string Email { get; set; }
```

> **TIP** Share expression classes across multiple model classes. A `ValidEmailExpression` class defined once can be applied to `UserLogin.Email`, `InviteCode.Email`, and `ContactForm.Email` with a single attribute each.

## Why Use the DSL Instead of Raw SQL

| Raw SQL string | DbCheck DSL |
|----------------|-------------|
| `[Check("LENGTH(\"email\") < 200")]` | `[Check(typeof(EmailLengthExpr))]` |
| No refactoring support | Rename property → update one class |
| Duplicated across models | Defined once, referenced everywhere |
| Easy to misquote column names | `DbCheck.Value("Email")` converts automatically |

## DbCheck Builder: All Methods

All methods return a `DbCheckExpr` value that can be further composed.

### Column Reference Methods

| Method | SQL output | Notes |
|--------|------------|-------|
| `DbCheck.Value("PropertyName")` | `"snake_case_name"` | Converts the C# property name to snake_case |
| `DbCheck.Column("raw_col")` | `"raw_col"` | Uses the name as-is |

> **WARNING** `DbCheck.Value("PropertyName")` converts the argument to snake_case automatically. If the column is already in snake_case (or uses a custom `[Column("name")]`), use `DbCheck.Column("col_name")` to avoid double-conversion.

### String Methods

| Method | SQL output |
|--------|------------|
| `DbCheck.StartsWith(col, "prefix")` | `"col" LIKE 'prefix%'` |
| `DbCheck.EndsWith(col, "suffix")` | `"col" LIKE '%suffix'` |
| `DbCheck.Contains(col, "sub")` | `"col" LIKE '%sub%'` |
| `DbCheck.Regex(col, "pattern")` | `"col" ~ 'pattern'` |

### Length Method

```csharp
DbCheck.Len(col, DbCheck.Operators.LessThan, 100)
// → length("col") < 100
```

The second argument is one of the `DbCheck.Operators` constants (see below).

### Literal Values

```csharp
DbCheck.Literal("value")  // → 'value'
DbCheck.Literal(42)       // → 42
```

### Boolean Composition

| Method | SQL output |
|--------|------------|
| `DbCheck.And(expr1, expr2, ...)` | `(expr1 AND expr2 ...)` |
| `DbCheck.Or(expr1, expr2, ...)` | `(expr1 OR expr2 ...)` |
| `DbCheck.Not(expr)` | `NOT (expr)` |

### DbCheck.Operators Constants

| Constant | SQL symbol |
|----------|------------|
| `DbCheck.Operators.LessThan` | `<` |
| `DbCheck.Operators.GreaterThan` | `>` |
| `DbCheck.Operators.Equal` | `=` |
| `DbCheck.Operators.NotEqual` | `<>` |
| `DbCheck.Operators.LessOrEqual` | `<=` |
| `DbCheck.Operators.GreaterOrEqual` | `>=` |

## Implementing an Expression Class

```csharp
public class ValidUsernameExpression : IDbCheckExpression
{
    public DbCheckExpr Build(string? columnName)
    {
        var col = DbCheck.Column("username");

        return DbCheck.And(
            DbCheck.Len(col, DbCheck.Operators.GreaterOrEqual, 3),
            DbCheck.Len(col, DbCheck.Operators.LessOrEqual, 50),
            DbCheck.Regex(col, "^[a-zA-Z0-9_]+$")
        );
    }
}
```

Generated DDL:

```sql
CHECK ((length("username") >= 3 AND length("username") <= 50 AND "username" ~ '^[a-zA-Z0-9_]+$'))
```

Applied to a model:

```csharp
[Table("user_logins")]
public partial class UserLogin
{
    [PrimaryKey, Default(DbDefaults.Guid.Random)]
    public Guid Id { get; set; }

    [Check(typeof(ValidUsernameExpression))]
    public string Username { get; set; }
}
```

## Composing Expressions with And / Or / Not

Expression classes can call `.Build()` on each other to layer constraints:

```csharp
public class ValidEmailExpression : IDbCheckExpression
{
    public DbCheckExpr Build(string? columnName)
    {
        var col = DbCheck.Column("email");

        return DbCheck.And(
            DbCheck.Len(col, DbCheck.Operators.LessThan, 256),
            DbCheck.Contains(col, "@")
        );
    }
}

public class NotDisposableEmailExpression : IDbCheckExpression
{
    public DbCheckExpr Build(string? columnName)
    {
        var col = DbCheck.Column("email");

        return DbCheck.And(
            new ValidEmailExpression().Build(columnName),
            DbCheck.Not(DbCheck.EndsWith(col, "@mailinator.com"))
        );
    }
}
```

```csharp
[Check(typeof(NotDisposableEmailExpression))]
public string Email { get; set; }
```

> **NOTE** Expression classes have no dependency injection. They are instantiated with `new()`. Keep them as pure SQL-building helpers with no external dependencies.
