sqlcx-core 0.2.1

SQL-first cross-language type-safe code generator — core library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
<p align="center">
  <img src="banner.png" width="480" alt="sqlcx" />
</p>

<p align="center">
  <strong>SQL-first. Every language. Zero runtime.</strong>
</p>

<p align="center">
  Write SQL once, generate type-safe code for TypeScript, Python, Go, and Rust.<br/>
  No ORM. No runtime. No engine. Just your SQL and the types that follow.
</p>

<p align="center">
  <a href="https://www.npmjs.com/package/sqlcx-orm"><img src="https://img.shields.io/npm/v/sqlcx-orm?style=flat-square&color=c5d96e" alt="npm" /></a>
  <a href="https://github.com/JagritGumber/sqlcx/blob/main/LICENSE"><img src="https://img.shields.io/github/license/JagritGumber/sqlcx?style=flat-square" alt="MIT" /></a>
</p>

---

## What is sqlcx?

sqlcx reads your SQL schema and annotated queries, then generates fully typed client code — with no runtime library shipped to production.

```
sql/schema.sql        ──┐
sql/queries/users.sql ──┤── sqlcx generate ──┬── schema.ts + users.queries.ts  (TypeScript)
                        │                    ├── models.py                     (Python)
                        │                    ├── models.go + users.queries.go  (Go)
                        │                    └── models.rs + users_queries.rs  (Rust)
                        └────────────────────────────────────────────────────────────────
                                              0 KB runtime
```

## Why not Prisma / Drizzle / sqlc?

| | sqlcx | Prisma | Drizzle | sqlc |
|---|---|---|---|---|
| **Runtime bundle** | **0 KB** | 1.6 MB | 7.4 KB | **0 KB** |
| **TypeScript** |||| Community |
| **Python** | ✓ (Pydantic + psycopg/asyncpg) ||| Community |
| **Go** |||||
| **Rust** |||||
| **Drivers** | **10** (4 TS, 2 Py, 2 Go, 2 Rust) | 1 | 1 | 1 |
| **Validation** | TypeBox, Zod, Pydantic, Serde | Built-in | Built-in ||
| **Multi-language** | ✓ (one SQL, all targets) ||| Go only |

**sqlcx** = the sqlc model (SQL-first, zero runtime) but for every language.

---

## Quick Start

### Install

```bash
npm install sqlcx-orm
# or
cargo install sqlcx
```

### 1. Write your schema

```sql
-- sql/schema.sql
CREATE TYPE user_status AS ENUM ('active', 'inactive', 'banned');

CREATE TABLE users (
  id         SERIAL      PRIMARY KEY,
  name       TEXT        NOT NULL,
  email      TEXT        NOT NULL UNIQUE,
  bio        TEXT,
  status     user_status NOT NULL DEFAULT 'active',
  tags       TEXT[],
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```

### 2. Write annotated queries

```sql
-- sql/queries/users.sql

-- name: GetUser :one
SELECT * FROM users WHERE id = $1;

-- name: ListUsers :many
SELECT id, name, email, status FROM users ORDER BY created_at DESC;

-- name: CreateUser :exec
INSERT INTO users (name, email) VALUES ($1, $2);

-- name: DeleteUser :execresult
DELETE FROM users WHERE id = $1;
```

### 3. Configure

```toml
# sqlcx.toml
sql    = "./sql"
parser = "postgres"

[[targets]]
language = "typescript"
out      = "src/generated"
schema   = "typebox"
driver   = "bun-sql"

[[targets]]
language = "python"
out      = "py/generated"
schema   = "pydantic"
driver   = "psycopg"
```

### 4. Generate

```bash
npx sqlcx generate
```

### 5. Use

**TypeScript:**
```typescript
import { getUser, listUsers, createUser } from './generated/users.queries';

const user = await getUser(sql, { id: 42 });
console.log(user.name);    // string
console.log(user.status);  // "active" | "inactive" | "banned"
```

**Python (psycopg):**
```python
from generated.users_queries import get_user, list_users, create_user

user = get_user(conn, GetUserParams(id=42))
print(user.name)    # str
print(user.status)  # str
```

**Go:**
```go
user, err := queries.GetUser(ctx, 42)
fmt.Println(user.Name, user.Email)
```

**Rust:**
```rust
let user = get_user(&pool, 42).await?;
println!("{} <{}>", user.name, user.email);
```

Every version is fully typed from your SQL. No hand-written interfaces. No `any`.

---

## Migrations

sqlcx ships with an optional built-in migrator for PostgreSQL. Schema changes and typed clients stay in sync automatically: after every `migrate up`, sqlcx re-runs the codegen so your types reflect the new shape of the database.

### Configure

Add a `[migrate]` section to `sqlcx.toml` (or run `sqlcx init` which scaffolds this for you):

```toml
[migrate]
dir             = "./sql/migrations"
auto_regenerate = true
# database_url  = "postgres://user:pass@localhost:5432/mydb"
```

If `database_url` is not set in config, sqlcx reads `SQLCX_DATABASE_URL` from your environment. Keep secrets out of version control by preferring the env var.

### Workflow

```bash
# Create a new timestamped migration file in sql/migrations/
sqlcx migrate new create_users

# Edit the generated file, then apply all pending migrations
sqlcx migrate up

# See which migrations are pending, applied, or drifted
sqlcx migrate status
```

Each `migrate new` creates a file named `{YYYYMMDDHHMMSS}_{name}.sql`. You write plain SQL inside — no annotations, no framework-specific syntax. sqlcx tracks applied state in a `_sqlcx_migrations` table it creates on first run.

### Drift detection

sqlcx stores a SHA-256 checksum of every migration when it is applied. If a file is edited after it has been applied to the database, `migrate up` and `migrate status` will report **DRIFTED** on that version and refuse to apply new migrations until it is resolved. This catches the common "someone edited an old migration" footgun before it corrupts your schema history.

### Auto-regenerate

When `auto_regenerate = true` (the default), a successful `migrate up` automatically runs the codegen pipeline. Your typed clients stay in lockstep with the database without any manual re-run of `sqlcx generate`.

### Cargo feature

The migrator is gated behind the `migrate` Cargo feature (enabled by default for `sqlcx` the CLI binary). Library consumers of `sqlcx-core` who want pure codegen and zero database dependencies can opt out with `default-features = false`.

---

## Features

### Multi-language from one SQL source

Write SQL once. Generate TypeScript, Python, Go, and Rust from the same schema and queries. Perfect for polyglot backends, microservice architectures, or gradual language migrations.

### Zero runtime

sqlcx generates code at build time. The output imports only your database driver. There is nothing between your query and the wire — no engine, no proxy, no runtime library.

### Inline `@enum` annotations

No separate enum objects needed. Define values right where the column is:

```sql
-- @enum("draft", "published", "archived")
status TEXT NOT NULL DEFAULT 'draft'
```

Generates a proper union type (TypeScript), `str(Enum)` class (Python), or string constant (Go/Rust) — not a plain `string`.

### Inline `@json` annotations

No more `unknown` / `Any` for JSON columns:

```sql
-- @json({ theme: string, notifications: boolean, font_size: number })
preferences JSONB
```

Generates a fully typed schema. Supports nested objects, arrays (`string[]`), and nullable (`string?`).

### `@param` — named parameters

Give your query parameters descriptive names:

```sql
-- name: SearchUsers :many
-- @param $1 query
-- @param $2 limit
SELECT id, name, email FROM users
WHERE name ILIKE '%' || $1 || '%'
LIMIT $2;
```

### Query commands

| Annotation | Returns | Use for |
|-----------|---------|---------|
| `:one` | Single row or null | `SELECT ... WHERE id = $1` |
| `:many` | Array of rows | `SELECT ...` without unique filter |
| `:exec` | Nothing | `INSERT`, `UPDATE`, `DELETE` |
| `:execresult` | Affected row count | Mutations where count matters |

### Select / Insert type separation

Every table gets two types — one for reading, one for writing:

```python
# Python (Pydantic)
class SelectUsers(BaseModel):      # All columns present
    id: int
    name: str
    status: UserStatus
    created_at: datetime

class InsertUsers(BaseModel):      # Defaults are optional
    name: str
    email: str
    id: int | None = None
    status: UserStatus | None = None
    created_at: datetime | None = None
```

### Partial column selection

Only select the columns you need — the generated type matches exactly:

```sql
-- name: ListUserEmails :many
SELECT id, email FROM users;
```

Generates `ListUserEmailsRow` with only `{ id, email }` — not the full table type.

### Current query boundary

sqlcx currently supports single-table query shape inference for generated row types and parameter typing.

- `SELECT * FROM users`
- `SELECT id, email FROM users`
- `INSERT ... VALUES (...)`
- `UPDATE ... RETURNING id, name`

Qualified select expressions and join-shaped projections such as `SELECT users.id, orgs.slug ...` are rejected for now instead of generating invalid code. That keeps the generated output sound while the multi-table IR is still intentionally narrow.

### Caching

sqlcx hashes your SQL files together with the active parser. If nothing relevant changed, parsing is skipped entirely. Subsequent runs are near-instant.

```bash
# First run: parses SQL
$ npx sqlcx generate    # ~200ms

# Second run: cached
$ npx sqlcx generate    # ~20ms
```

---

## Supported Targets

### Languages & Schema Generators

| Language | Schema | Output |
|----------|--------|--------|
| TypeScript | `typebox` | TypeBox validators + static types |
| TypeScript | `zod` | Zod v4 schemas |
| TypeScript | `zod/v3` | Zod v3 schemas |
| Python | `pydantic` | Pydantic v2 BaseModel classes |
| Go | `structs` | Go structs with `db`/`json` tags |
| Rust | `serde` | Serde + sqlx::FromRow structs |

### Database Drivers

| Language | Driver | Description |
|----------|--------|-------------|
| TypeScript | `bun-sql` | Typed functions for Bun's built-in SQL |
| TypeScript | `pg` | Typed functions for node-postgres |
| TypeScript | `mysql2` | Typed functions for mysql2 (MySQL) |
| TypeScript | `better-sqlite3` | Typed synchronous functions for better-sqlite3 (SQLite) |
| Python | `psycopg` | Typed functions for psycopg3 (sync Postgres) |
| Python | `asyncpg` | Typed async functions for asyncpg (async Postgres) |
| Go | `database-sql` | Typed functions for `database/sql` |
| Go | `pgx` | Typed functions for jackc/pgx v5 (modern Postgres) |
| Rust | `sqlx` | Typed async functions for sqlx |
| Rust | `tokio-postgres` | Typed async functions for tokio-postgres |

### Database Parsers

| Parser | Features |
|--------|----------|
| `postgres` | ENUMs, arrays, JSONB, UUID, `$1` params |
| `mysql` | Inline ENUMs, `TINYINT(1)` booleans, `AUTO_INCREMENT`, `?` params |
| `sqlite` | Type affinity mapping, `AUTOINCREMENT`, `?` params |

---

## CLI

```bash
npx sqlcx generate    # Parse SQL → generate typed code
npx sqlcx check       # Validate SQL without generating (CI-friendly)
npx sqlcx init        # Scaffold sql/ directory + sqlcx.toml
npx sqlcx schema      # Emit JSON Schema for config validation
```

---

## Configuration

**`sqlcx.toml`** — minimal:

```toml
sql    = "./sql"
parser = "postgres"

[[targets]]
language = "typescript"
out      = "src/generated"
schema   = "typebox"
driver   = "bun-sql"
```

**Multi-target** — generate all languages at once:

```toml
sql    = "./sql"
parser = "postgres"

[[targets]]
language = "typescript"
out      = "src/generated"
schema   = "typebox"
driver   = "bun-sql"

[[targets]]
language = "python"
out      = "py/generated"
schema   = "pydantic"
driver   = "asyncpg"

[[targets]]
language = "go"
out      = "internal/db"
schema   = "structs"
driver   = "pgx"

[[targets]]
language = "rust"
out      = "src/db"
schema   = "serde"
driver   = "sqlx"
```

**Type overrides:**

```toml
[overrides]
uuid = "string"    # Map UUID to string in all targets
```

---

## Architecture

```
SQL files ──▶ Parser (postgres/mysql/sqlite) ──▶ IR (tables, queries, enums)
                                                    ├──▶ TypeScript Plugin ──▶ schema.ts + queries.ts
                                                    │    (bun-sql, pg, mysql2, better-sqlite3)
                                                    ├──▶ Python Plugin    ──▶ models.py + queries.py
                                                    │    (psycopg, asyncpg)
                                                    ├──▶ Go Plugin        ──▶ models.go + queries.go
                                                    │    (database-sql, pgx)
                                                    └──▶ Rust Plugin      ──▶ models.rs + queries.rs
                                                         (sqlx, tokio-postgres)
```

The IR (Intermediate Representation) is language-agnostic and cacheable. Each language plugin consumes the same IR and produces idiomatic output for its ecosystem.

Adding a new language = implementing `SchemaGenerator` + `DriverGenerator` traits against the IR.

---

## Project Structure

```
sqlcx/
├── crates/
│   ├── sqlcx-core/          # Core library
│   │   └── src/
│   │       ├── parser/      # SQL parsers (postgres, mysql, sqlite)
│   │       ├── generator/   # Language plugins
│   │       │   ├── typescript/  # TypeBox, Zod, Bun SQL, pg, mysql2, better-sqlite3
│   │       │   ├── python/      # Pydantic, psycopg, asyncpg
│   │       │   ├── go/          # Structs, database/sql, pgx
│   │       │   └── rust_lang/   # Serde, sqlx, tokio-postgres
│   │       ├── ir.rs        # Intermediate representation
│   │       └── config.rs    # Config parsing
│   └── sqlcx/               # CLI binary
├── packages/
│   └── js/                  # npm binary distribution
├── docs/                    # Documentation site (Astro + Starlight)
└── tests/                   # Integration tests + fixtures
```

---

## Contributing

Contributions welcome. The best way to get started:

1. Look at an existing generator (e.g., `generator/typescript/typebox.rs`)
2. The pattern is the same for every language: implement `SchemaGenerator` and optionally `DriverGenerator`
3. Run `cargo test` to verify

---

## License

MIT