# Flint ORM — API Reference

A type-safe, driver-agnostic SQLite ORM for JavaScript. One schema, any driver.

## Installation

```bash
bun add flint-orm
# or
npm install flint-orm
```

## Quick Start

```ts
import { flint } from 'flint-orm/bun-sqlite';
import { table, text, integer, date } from 'flint-orm/table';
import { eq } from 'flint-orm/expressions';

// Define schema
const users = table('users', {
  id: text().primaryKey(),
  name: text().notNull(),
  email: text().unique(),
  age: integer(),
  createdAt: date().defaultNow(),
});

// Connect
const db = flint({ url: './app.db' });

// Query
const user = await db.selectFrom(users).where(eq(users.id, 'u1')).single().execute();
// { id: "u1", name: "Alice", email: "alice@example.com", age: 30, createdAt: Date }
```

---

## Config

Create `flint.config.ts` in your project root:

```ts
import { defineConfig } from 'flint-orm/config';

export default defineConfig({
  driver: 'bun-sqlite', // 'bun-sqlite' | 'better-sqlite3' | 'libsql' | 'libsql-web' | 'turso' | 'turso-sync'
  database: {
    url: './app.db',
    authToken: '...', // for libsql/libsql-web/turso-sync only
  },
  schema: './src/schema',
  migrations: './flint',
});
```

---

## Schema

### `table(name, columns, indexFn?)`

Define a table. Columns live as direct properties. SQL metadata is under `._`.

```ts
import { table, text, integer, boolean, index } from 'flint-orm/table';

const users = table('users', {
  id: text().primaryKey(),
  name: text().notNull(),
  email: text().unique(),
  active: boolean().default(true),
  age: integer(),
});
```

### `snakeCase.table(name, columns, indexFn?)`

Auto-converts camelCase keys to snake_case SQL names.

```ts
import { snakeCase, text } from 'flint-orm/table';

const users = snakeCase.table('users', {
  id: text().primaryKey(), // SQL: id
  firstName: text().notNull(), // SQL: first_name
  createdAt: text(), // SQL: created_at
});
```

### Column Types

| Function    | TS Type   | SQLite Storage     | Notes                                            |
| ----------- | --------- | ------------------ | ------------------------------------------------ |
| `text()`    | `string`  | TEXT               |                                                  |
| `integer()` | `number`  | INTEGER            | Supports `.autoIncrement()`                      |
| `boolean()` | `boolean` | INTEGER (0/1)      | Encodes/decodes automatically                    |
| `json<T>()` | `T`       | TEXT (JSON)        | Generic, encodes/decodes automatically           |
| `real()`    | `number`  | REAL               |                                                  |
| `date()`    | `Date`    | INTEGER (epoch ms) | Supports `.defaultNow()`, `.onUpdateTimestamp()` |

### Column Modifiers

Every column supports chaining:

```ts
text()
  .primaryKey() // PRIMARY KEY
  .notNull() // NOT NULL
  .unique() // UNIQUE
  .default('hello') // DEFAULT 'hello'
  .defaultFn(() => new Date()) // DEFAULT (computed at insert)
  .references(otherColumn) // REFERENCES otherColumn
  .onDelete('cascade') // ON DELETE CASCADE
  .onUpdate('set null'); // ON UPDATE SET NULL
```

**Integer-only:**

```ts
integer().autoIncrement(); // AUTOINCREMENT
```

**Date-only:**

```ts
date()
  .defaultNow() // DEFAULT (current epoch ms)
  .onUpdateTimestamp(); // Always set to now on UPDATE
```

### `InferRow<T>`

Derives the row type from a table definition.

```ts
import type { InferRow } from 'flint-orm/table';

type UserRow = InferRow<typeof users>;
// { id: string; name: string; email: string | null; active: boolean; age: number | null; createdAt: Date }
```

### `InsertRow<T>`

Row type for INSERT. Columns with defaults or autoIncrement are optional.

```ts
import type { InsertRow } from 'flint-orm/table';

type UserInsert = InsertRow<typeof users>;
// { id: string; name: string; email?: string; active?: boolean; age?: number; createdAt?: Date }
```

---

## Indexes

### `index(name).on(columns).unique()`

Define indexes via the table callback. Chainable API.

```ts
const users = table(
  'users',
  {
    id: text().primaryKey(),
    email: text(),
    name: text(),
  },
  (t) => [index('idx_users_email').on(t.email).unique(), index('idx_users_name').on(t.name)],
);
```

---

## Query Builder

All `execute()` methods return `Promise<T>` — always `await` regardless of driver.

### `db.selectFrom(table)`

Start a SELECT query. Call `.columns()` to narrow, `.where()` to filter, `.execute()` to run.

```ts
await db.selectFrom(users).execute();
// SELECT * FROM users
```

### `.columns(keys)`

Narrow which columns appear in the result.

```ts
await db.selectFrom(users).columns(['id', 'name']).execute();
// SELECT id, name FROM users
// Returns: { id: string; name: string }[]
```

### `.where(condition)`

Filter rows.

```ts
await db.selectFrom(users).where(eq(users.active, true)).execute();
// SELECT * FROM users WHERE active = 1
```

### `.single()`

Return one row or null instead of an array. Adds `LIMIT 1`.

```ts
await db.selectFrom(users).where(eq(users.id, 'u1')).single().execute();
// SELECT * FROM users WHERE id = ? LIMIT 1
// Returns: UserRow | null
```

### `.orderBy(key, direction?)`

Sort results. Default direction is `"asc"`.

```ts
await db.selectFrom(users).orderBy('name', 'desc').execute();
// SELECT * FROM users ORDER BY name DESC
```

### `.limit(n)`

Limit the number of results.

```ts
await db.selectFrom(users).limit(10).execute();
// SELECT * FROM users LIMIT 10
```

### `.offset(n)`

Skip N rows.

```ts
await db.selectFrom(users).limit(10).offset(20).execute();
// SELECT * FROM users LIMIT 10 OFFSET 20
```

### `.distinct()`

Return unique rows.

```ts
await db.selectFrom(users).columns(['name']).distinct().execute();
// SELECT DISTINCT name FROM users
```

---

### `db.insert(table).values(row)`

Insert one or more rows. Two-phase: `.values()` is required before `.execute()`.

```ts
// Single row
await db.insert(users).values({ id: 'u1', name: 'Alice', email: 'alice@example.com' }).execute();

// Multiple rows
await db
  .insert(users)
  .values([
    { id: 'u1', name: 'Alice' },
    { id: 'u2', name: 'Bob' },
  ])
  .execute();
```

### `.returning()`

Return the inserted row(s) instead of void. Pass an array to narrow which columns are returned.

```ts
const user = await db.insert(users).values({ id: 'u1', name: 'Alice' }).returning().execute();
// Returns: { id: string; name: string; ... }[]

const user = await db.insert(users).values({ id: 'u1', name: 'Alice' }).returning(['id', 'name']).execute();
// Returns: { id: string; name: string }[]
```

### `.onConflictDoNothing()`

Skip the insert if a row with the same primary key already exists.

```ts
await db.insert(users).values({ id: 'u1', name: 'Alice' }).onConflictDoNothing().execute();
```

### `.onConflictDoUpdate()`

Update specific columns when a row with the same primary key already exists (upsert).

```ts
await db
  .insert(users)
  .values({ id: 'u1', name: 'Alice' })
  .onConflictDoUpdate({
    target: users.id,
    set: { name: 'Alice Updated' },
  })
  .execute();
```

---

### `db.update(table).set(partial).where(condition)`

Update rows. Two-phase: `.set()` is required before `.execute()`.

```ts
await db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).execute();
```

Multiple `.set()` calls merge:

```ts
await db.update(users).set({ name: 'Bob' }).set({ email: 'bob@example.com' }).where(eq(users.id, 'u1')).execute();
```

### `.returning()`

Return the updated row(s) instead of void.

```ts
const updated = await db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).returning().execute();
// Returns: { id: string; name: string; ... }[]
```

---

### `db.delete(table).where(condition)`

Delete rows.

```ts
await db.delete(users).where(eq(users.id, 'u1')).execute();
```

### `.returning()`

Return the deleted row(s) instead of void.

```ts
const deleted = await db.delete(users).where(eq(users.id, 'u1')).returning().execute();
// Returns: { id: string; name: string; ... }[]
```

---

## Joins

### `db.leftJoin(parent).on(child, condition?)`

LEFT JOIN. Returns all parent rows, with matching child data nested under the child table name.

```ts
const orders = table('orders', {
  id: text().primaryKey(),
  userId: text().notNull().references(users.id),
  total: integer().notNull(),
});

// With explicit condition
await db.leftJoin(users).on(orders, eq(orders.userId, users.id)).execute();

// Auto-join from foreign key (if .references() is defined)
await db.leftJoin(users).on(orders).execute();
```

### `db.innerJoin(parent).on(child, condition?)`

INNER JOIN. Returns only rows where both tables match.

```ts
await db.innerJoin(users).on(orders, eq(orders.userId, users.id)).execute();
```

### Join Result Shape

Joins return **nested** results, not flat-merged:

```ts
[
  {
    id: 'u1',
    name: 'Alice',
    orders: [
      { id: 'o1', userId: 'u1', total: 100 },
      { id: 'o2', userId: 'u1', total: 200 },
    ],
  },
];
```

### Join + Columns

Narrow parent columns with `.columns()`:

```ts
await db.leftJoin(users).on(orders).columns(['id', 'name']).execute();
// Returns: { id: string; name: string; orders: OrderRow[] }[]
```

### `.single()` on Joins

```ts
await db.leftJoin(users).on(orders).single().execute();
// Returns: { id: string; name: string; orders: OrderRow[] } | null
```

### Multi-Join

Chain multiple joins:

```ts
await db.leftJoin(users).on(orders).leftJoin(orders).on(orderItems).execute();
```

---

## Conditions

All conditions are imported from `flint-orm/expressions`.

### Comparison

```ts
import { eq, neq, gt, gte, lt, lte } from 'flint-orm/expressions';

eq(column, value); // column = value
eq(left, right); // left = right (column-to-column)
neq(column, value); // column != value
gt(column, value); // column > value
gte(column, value); // column >= value
lt(column, value); // column < value
lte(column, value); // column <= value
```

### Range

```ts
import { between } from 'flint-orm/expressions';

between(column, low, high); // column BETWEEN low AND high
```

### Null Checks

```ts
import { isNull, isNotNull } from 'flint-orm/expressions';

isNull(column); // column IS NULL
isNotNull(column); // column IS NOT NULL
```

### Array

```ts
import { isIn, isNotIn } from 'flint-orm/expressions';

isIn(column, values); // column IN (?, ?, ...)
isNotIn(column, values); // column NOT IN (?, ?, ...)
```

### Pattern Matching

```ts
import { like, glob } from 'flint-orm/expressions';

like(column, pattern); // column LIKE ?  (% and _ wildcards, case-insensitive)
glob(column, pattern); // column GLOB ?  (* and ? wildcards, case-sensitive)
```

### Logical

```ts
import { and, or } from 'flint-orm/expressions';

and(...conditions); // cond1 AND cond2 AND ...
or(...conditions); // (cond1 OR cond2 OR ...)
```

---

## Aggregates

Aggregate functions are methods on the `db` object. They return `Promise<T>`.

```ts
const total = await db.count(users);
const active = await db.count(users, eq(users.active, true));
const totalViews = await db.sum(posts, posts.views);
const avgAge = await db.avg(users, users.age);
const minAge = await db.min(users, users.age);
const maxAge = await db.max(users, users.age);
```

---

## Batch

Run multiple queries atomically in a single transaction.

```ts
await db.batch([
  db.insert(orders).values({ id: 'o1', userId: 'u1', total: 100 }),
  db.update(users).set({ totalOrders: 1 }).where(eq(users.id, 'u1')),
]);
```

---

## Raw SQL

### `db.$run(sql, ...params)`

Execute raw SQL directly against the database.

```ts
await db.$run('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
await db.$run('INSERT INTO test VALUES (?, ?)', 1, 'Alice');
```

### Tagged Template SQL

Build parameterized SQL expressions with automatic placeholder handling.

```ts
import { sql } from 'flint-orm';

const expr = sql`name = ${'Alice'} AND age > ${18}`;
// { sql: "name = ? AND age > ?", params: ["Alice", 18] }

const result = await db.selectFrom(users).where(expr).execute();
```

---

## Migration System

### CLI

```bash
flint generate --name init_schema   # Generate migration
flint generate --preview            # Preview SQL without writing
flint migrate                       # Apply pending migrations
flint migrate --status              # Show applied vs pending
flint migrate --dry-run             # Preview without executing
```

### Programmatic API

```ts
import { generate, migrate, getMigrationStatus, serializeSchema, diffSchemas, generateSQL } from 'flint-orm/migration';

// Serialize table definitions to JSON
const state = serializeSchema([users, orders]);

// Diff two schema states
const operations = diffSchemas(previousState, currentState);

// Generate SQL from operations
const sql = generateSQL(operations);

// Generate a migration folder
const result = await generate([users, orders], './flint', { name: 'init_schema', interactive: true });

// Apply pending migrations
const result = await migrate(executor, { migrationsDir: './flint' });

// Check migration status
const status = await getMigrationStatus(executor, './flint');
```

### Migration Operations

```ts
import { addTable, dropTable, renameTable, addColumn, dropColumn, renameColumn, createIndex, dropIndex } from 'flint-orm/migration';
```

| Operation      | SQL Generated                          |
| -------------- | -------------------------------------- |
| `addTable`     | `CREATE TABLE ...`                     |
| `dropTable`    | `DROP TABLE ...`                       |
| `renameTable`  | `ALTER TABLE ... RENAME TO ...`        |
| `addColumn`    | `ALTER TABLE ... ADD COLUMN ...`       |
| `dropColumn`   | `ALTER TABLE ... DROP COLUMN ...`      |
| `renameColumn` | `ALTER TABLE ... RENAME COLUMN ... TO` |
| `createIndex`  | `CREATE [UNIQUE] INDEX ...`            |
| `dropIndex`    | `DROP INDEX ...`                       |
| `modifyColumn` | `ALTER TABLE ... ALTER COLUMN ...`     |
| `modifyIndex`  | `DROP INDEX IF EXISTS ...; CREATE ...` |
| `rebuildTable` | Temp table → copy → drop → rename      |

---

## Types

| Type              | Description                                                                               |
| ----------------- | ----------------------------------------------------------------------------------------- |
| `TableDef<T>`     | Table definition with hidden `._` metadata                                                |
| `ColumnDef<T, S>` | Column definition with phantom types                                                      |
| `InferRow<T>`     | Derives row type from table definition                                                    |
| `InsertRow<T>`    | Derives insert type (defaults are optional)                                               |
| `Executor`        | Database executor interface (all, get, run, transaction)                                  |
| `SQLExpression`   | `{ sql: string; params: unknown[] }`                                                      |
| `Executable`      | Anything with a `.toSQL()` method (for `batch()`)                                         |
| `Driver`          | `'bun-sqlite' \| 'better-sqlite3' \| 'libsql' \| 'libsql-web' \| 'turso' \| 'turso-sync'` |
| `RebuildTableOp`  | Migration op that recreates a table with a new schema                                     |

---

## Error Classes

| Class                  | When                                                              |
| ---------------------- | ----------------------------------------------------------------- |
| `FlintValidationError` | Invalid query construction (e.g., no primary key for `.single()`) |
| `FlintQueryError`      | Runtime SQL execution failure                                     |
