rok-sqlc 0.3.0

Generate SQLx Rust database modules from SQL schema files
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
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
# Changelog


All notable changes to rok-sqlc are recorded here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Changes to
the *generated* API count as breaking (see ADR 0012).

## [Unreleased]


## [0.3.0] - 2026-10-17


Validated bodies and HTTP extractors (ADR 0015). A run without the new
`validate` derive and `extractors` key generates byte-for-byte what 0.2.1
generates.

### Added


- `derive = ["serde", "validate"]` (`-d validate`, `Derive::Validate`) adds
  `validator::Validate` (validator 0.21) to every create input and update
  patch, with rules taken from the schema and English messages naming the
  column (`"name must be at most 80 characters"`):
  - `VARCHAR(n)`, `CHARACTER VARYING(n)`, `CHAR(n)` and `CHARACTER(n)` give
    `length(max = n)`, counted in characters;
  - a single-column `CHECK` comparing `char_length(col)`,
    `character_length(col)` or `length(col)` with an integer gives a
    `length` bound, combined with the declared length (the tighter bounds
    win, and `equal` when they meet);
  - a single-column `CHECK` comparing an integer or float column with a
    literal (`>=`, `>`, `<=`, `<`, either side), `BETWEEN`, or an `AND` of
    these gives `range`. A strict bound on an integer becomes inclusive
    (`> 0` is `min = 1`); on a float it stays strict (`exclusive_min`), and
    one that can't be represented exactly is dropped;
  - on a `NUMERIC` column the range is a `custom` rule calling a generated
    `<field>_in_range` function, with the code `range` and `range`'s
    `min`/`max` params, since validator's `range` can't compare `Decimal`;
  - a typed id over text implements `validator::ValidateLength<u64>`, and a
    typed id over an integer `validator::ValidateRange`, so a column's
    `length` or `range` rule applies through the id and through foreign keys
    using it. A typed id over `NUMERIC` gets no range rule.

  Patch fields are checked only when present. Other `CHECK`s, `NOT NULL`,
  enums and value lists are not rules. `db/mod.rs` names the `validator`
  dependency. `validate` without `serde` fails the run.
- `extractors = ["axum", "actix_web"]` (the JSON payload's `extractors`, the
  repeatable `-e`/`--extractor <axum|actix_web>` flag, and
  `Config::extractors` with the new `#[non_exhaustive]` `Extractor`, part of
  the stable library API) generates `db/http/`, which needs `validate`:
  - `db::http::axum::ValidatedJson<T>` (axum 0.8) and
    `db::http::actix_web::ValidatedJson<T>` (actix-web 4), extractors for
    any `T: DeserializeOwned + Validate`: a generated input or patch, or a
    hand-written request type. Each reads the body through its framework's
    own JSON extractor (`axum::Json<T>`, `web::Json<T>`), so axum's
    `DefaultBodyLimit` and actix-web's `JsonConfig` (limit and
    `Content-Type` predicate) apply; both limits default to 2 MiB;
  - `BodyRejection`, answered in the Rok error envelope
    `{"success": false, "error": {"message", "code", "details"?}}`: `400`
    `MALFORMED_BODY` for a missing or wrong `Content-Type`, bad JSON syntax,
    or a body that can't be read; `413` `PAYLOAD_TOO_LARGE` for a body over
    the framework's size limit, with the message
    `"the request body is over the size limit"` from both; `422`
    `VALIDATION_FAILED` with serde's message and no `details` for JSON that
    doesn't fit the type; `422` `VALIDATION_FAILED` with validator's errors
    as `details` for a broken rule. Each framework's rejection is classified
    in its module, with the message taken from serde's own error (never the
    framework's text), so both answer the same body with the same status,
    code and message;
  - `ErrorBody`, `ErrorPayload`, the three codes,
    `BodyRejection::from_serde`, and `validated(body)`, which checks a
    deserialized body against its rules.

  With actix-web, no `JsonConfig` error handler is needed. `db/mod.rs` names
  the `axum`, `serde_path_to_error` (whose error wraps serde's inside axum's
  rejection), `actix-web` and `serde_json` dependencies. `extractors` without
  `validate` fails the run, and an unknown framework lists the valid ones.
  With `extractors`, `http` is a reserved module name: a table whose module
  would be `http` fails with an error that says so (rename it with
  `[modules]`). Without `extractors`, such a table generates as before.
- The schema model keeps each text column's declared length and the simple
  `CHECK` bounds above, through `CREATE TABLE`, `ADD COLUMN`, `ALTER COLUMN …
  TYPE` (which replaces or clears them), `ADD CONSTRAINT`, and renames.
- The README has a "Validated bodies and HTTP extractors" section, from the
  new example crate `docs/examples/validated-api`.

## [0.2.1] - 2026-10-17


Additive: everything that generated with 0.2.0 generates the same way.

### Added


- `[conventions] primary_key_ids` (`Conventions::primary_key_ids`, the new
  `IdGeneration`): `"uuid_v4"`, `"uuid_v7"`, or `"ulid"` makes new primary
  keys in the generated code for tables keyed by one `UUID` column (or, with
  `"ulid"`, one text column). The typed id gets `generate()`, the create
  input takes the key as `Option<Id>`, and `None` inserts a new id, even over
  a database default. `db/mod.rs` names the `uuid` features or the `ulid`
  crate it needs. Off by default and not in the `rok` preset.
- `MONEY` (`PgMoney`), `INT4RANGE`, `INT8RANGE`, `NUMRANGE`, `DATERANGE`,
  `TSRANGE`, `TSTZRANGE` (`PgRange<T>`), `INET` and `CIDR`
  (`ipnetwork::IpNetwork`), and `MACADDR` (`mac_address::MacAddress`), in
  columns and query-file casts, with equality filters. The dependency hints
  name SQLx's `ipnetwork` and `mac_address` features. `PgMoney` and
  `PgRange` have no serde support, so like `INTERVAL` they fail a run with
  `derive = ["serde"]`.
- Partial unique indexes as `ON CONFLICT` targets in query files, when the
  target repeats the index's condition:
  `ON CONFLICT (email) WHERE deleted_at IS NULL DO …`. The soft-delete
  convention's live-row indexes can now be upsert targets. A target that
  matches only a partial index says which `WHERE` to add.
- `WITH RECURSIVE` in query files: a recursive CTE (`anchor UNION [ALL]
  term`) takes its columns from the anchor, and the recursive term must
  match them.
- `ALTER TABLE` forms that leave the model as it is no longer fail their
  table: `ALTER COLUMN … SET STATISTICS`, `SET STORAGE`, `SET COMPRESSION`,
  `SET`/`RESET (…)`, `SET GENERATED`, `RESTART` and other identity options;
  `RESET (…)`, `CLUSTER ON`, `SET WITHOUT CLUSTER`, `SET TABLESPACE`,
  `SET [UN]LOGGED`, `[NO] FORCE ROW LEVEL SECURITY`, `ALTER CONSTRAINT`,
  `SET (…)`, `REPLICA IDENTITY`, `OWNER TO`, `ENABLE`/`DISABLE
  TRIGGER`/`RULE`, and `VALIDATE CONSTRAINT`.
- `ALTER COLUMN … DROP IDENTITY` and `DROP EXPRESSION` act like `DROP
  DEFAULT`, and `TYPE … COLLATE` like `TYPE`.
- `ALTER TABLE … RENAME CONSTRAINT` renames a primary key or unique
  constraint, and `ALTER INDEX`, `ALTER SEQUENCE`, `ALTER VIEW` and `ALTER
  MATERIALIZED VIEW … RENAME TO` are followed for default constraint names.
  Renaming a key constraint's index renames the constraint.
- A migration with a `BEGIN ATOMIC` function or procedure body, a `DO`
  block, or another `ALTER` of an index, sequence or view that the SQL
  parser can't read is now read past instead of stopping the run.
- Prebuilt binaries. Pushing a `vX.Y.Z` tag runs the Release workflow, which
  builds rok-sqlc for Linux (x86_64 and aarch64, static musl), macOS (x86_64
  and Apple silicon), and Windows (x86_64 and ARM64) and publishes the
  archives, each with a `.sha256` file and a combined `SHA256SUMS`, as a
  GitHub release whose notes are this file's section for the version.
- `install.sh` (Linux, macOS) and `install.ps1` (Windows) install the binary
  for the current platform from a GitHub release and verify its checksum.
- Release builds strip debug symbols, which shrinks the binary.

## [0.2.0] - 2026-10-17


### Breaking changes to the generated API


- On a table with the tenant column, `{Table}Filter` has no field for the
  column (including its `*_is_null` field) and `{Table}SortKey` no variant
  for it. `search` is already scoped to the tenant parameter, so these could
  only repeat the scope or match nothing. Relationship filters are unchanged.
- An enum primary key gets a typed id wrapping the enum
  (`PriorityNoteId(pub types::TicketPriority)`), which is `Copy`, `Eq` and
  `Hash` but not `Ord` or `Display`. The key column, key parameters, the
  `search_after` cursor, and foreign keys into the table take it instead of
  the bare enum.
- With `derive = ["serde"]`, a patch deserialized from JSON tells an absent
  field (leave the column alone) from `null` (set it to `NULL`); before, both
  read as `None`. Serializing a patch skips its `None` fields, so `{}` is an
  empty patch. A nullable input field with a default reads `null` as
  `Some(None)` (insert `NULL`) the same way. Both use the new generated
  `db/nullable.rs`, so a table can no longer be named `nullable`.
- With `derive = ["serde"]`, `{Table}Filter` (including its relationship
  fields), `{Table}SortKey`, and `Direction`, `Sort`, `Ordered`, `Equality`,
  and `Text` in `db::search` derive `Serialize` and `Deserialize`. A filter
  field absent from the JSON adds no condition. The enums among them use
  snake_case variants in JSON (`{"ge": 400}`, `"desc"`, `"price_cents"`).
- A `SERIAL`, `BIGSERIAL`, or `SMALLSERIAL` column off the primary key is
  `NOT NULL`, as in PostgreSQL: its row field is `i32`/`i64`/`i16` instead
  of `Option<…>`, and inserts still leave it to its sequence. After
  `ALTER COLUMN … DROP DEFAULT` it is a required `NOT NULL` input, and a
  managed one has no source of values, which fails its table.

### Added


- The `[modules]` config section (also in the JSON payload, and
  `Config::modules` in the library) renames a table's module, by
  schema-qualified or SQL name: `"audit.users" = "audit_users"`. The row
  struct, typed id, and key struct follow the new name, so same-named tables
  of two schemas both generate, each with its own typed id. An entry naming no
  table, or a name that isn't a snake_case Rust identifier, fails the run.
- Generation fails with a diagnostic when a resource would import two typed
  ids of one name (from tables like `user` and `users`), or an imported typed
  id named like one of the resource's own items, or any file would import
  two other items of one name (the row structs and filters of `category` and
  `categories`, say). Before, the generated code didn't compile.
- `INTERVAL` maps to `sqlx::postgres::types::PgInterval`, `BYTEA` to
  `Vec<u8>` (borrowed as `&[u8]` in key, lookup, and query parameters), and
  `TIME WITH TIME ZONE`/`TIMETZ` to `sqlx::postgres::types::PgTimeTz`, in
  columns, arrays, query-file casts (`$1::interval`), and result columns.
  All three get equality filters: SQLx implements no `PartialOrd` for
  `PgInterval` or `PgTimeTz`.
- With `derive = ["serde"]`, a table or query that would put `PgInterval` or
  `PgTimeTz` (which implement no serde traits) into a serde-deriving item
  fails the run with an error naming the column or parameter.
- In query files, a cast to an enum type infers the generated enum:
  `$1::product_status` and `$1::app.ticket_state` take `types::X`, and
  `$1::product_status[]` takes `&[types::X]`. Such casts were rejected.
- `ALTER TYPE x RENAME TO y` renames the enum: its Rust name follows the new
  name (schema-prefixed only on a clash, as for `CREATE TYPE`), its
  `sqlx(type_name)` becomes `y`, and the columns using it keep working. The
  clause used to fail every table using the type.
- `ALTER TABLE … ALTER COLUMN` is applied instead of failing the table:
  `[SET DATA] TYPE` (with or without `USING`), `SET`/`DROP NOT NULL`,
  `SET`/`DROP DEFAULT`, and `ADD GENERATED … AS IDENTITY`. The column ends
  up in its final shape, and its primary key, unique keys, and typed id
  follow a type change. A change rok-sqlc can't apply (a type with no
  mapping, a serial type, `DROP NOT NULL` on a primary key column, identity
  on a nullable column, a missing column) still fails the table with a
  `path:line` diagnostic. Dropping a managed column's default can now leave
  it without a source of values, which fails its table.

### Changed


- A table that a `--lenient` run skips keeps the resource directory an
  earlier run generated for it, byte for byte, and `db/mod.rs` keeps
  declaring it (and `UpdateOutcome`, when its `update` uses it). Those files
  are no longer stale: a successful run does not remove them, and a lenient
  `--check` reports the table's warning but no drift. A table the schema no
  longer defines (dropped, or never created) is still stale (ADR 0001).
  The rest of the output is generated against the kept table's last good
  shape (its model before the statement that broke it), so the crate still
  compiles: `types.rs` keeps its enums, foreign keys into it keep its typed
  id, and relationships and filters still reach it. The kept directory is
  the table's module, `[modules]` renames included. A table that fails at
  its own `CREATE TABLE`, or whose value lists can't be generated, has no
  such shape, so its earlier files are stale.
- A managed column with no source of values fails its table with a
  `path:line` diagnostic: one that is `NOT NULL`, has no `DEFAULT`, is not
  serial, identity, or `GENERATED`, on a table with no row-level
  `BEFORE INSERT` trigger. Inserts leave managed columns out, so such a
  table could never be inserted into. `CREATE TRIGGER` and `DROP TRIGGER`
  are now read to find those triggers (per table; function bodies are not
  inspected). Nullable managed columns are unaffected. When a later
  migration takes the source away (`ALTER COLUMN … DROP DEFAULT`, or
  `DROP TRIGGER` of the last `BEFORE INSERT` trigger), the diagnostic
  points at that statement.
- `--resource` (and the `resource` key) is described as what it does: it
  generates the named table and regenerates every resource already
  generated in the output directory; tables not generated yet stay
  ungenerated.
- An `ALTER TABLE` the SQL parser can't read (`ALTER COLUMN … DROP
  IDENTITY`, `SET STATISTICS`, `SET STORAGE`, `DROP EXPRESSION`,
  `SET GENERATED`, `TYPE … COLLATE`) fails only the table it alters, at its
  `path:line`, so `--lenient` skips that table and reads on. Any other
  statement the parser can't read still stops the run, now at its
  `path:line`. Before, either stopped the run with a syntax error for the
  whole file.

### Fixed


- A joined read (`get_with_<relationship>`) of a table whose column names
  come within two bytes of PostgreSQL's 63-byte identifier limit works:
  such a column is selected under a shortened alias that names its
  position (`t3_<name>`), where the `t_`/`r_` prefix used to make
  PostgreSQL cut two aliases to the same name. The `is_empty` of a patch and
  the `column` of a sort key are also laid out as rustfmt lays them out for
  such long names.
- `ON CONFLICT ON CONSTRAINT` accepts the names PostgreSQL gives unnamed
  primary key and unique constraints when a default name is already taken
  by another relation in the same schema (`<table>_pkey1`): a table, another
  table's constraint, an index (named, or PostgreSQL's default
  `<table>_<columns>_idx`), a sequence (`CREATE SEQUENCE`, or a serial
  column's `<table>_<column>_seq`), or a view or materialized view, until it
  is dropped. Before, only the table's own constraints counted.
- A table whose search names would collide only through the tenant column
  generates: the tenant column has no `*_is_null` filter field and no sort
  key variant, so it collides with nothing. The check now runs after the
  conventions are applied.

## [0.1.0] - 2026-10-17


First release on crates.io. The entries below include generated-API changes
that break code written against earlier development snapshots.

### Breaking changes to the generated API


- Generated code targets SQLx 0.9 instead of 0.8: the consuming crate needs
  `sqlx = "0.9"` (same feature names) and Rust 1.94 or newer, SQLx 0.9's
  minimum. Search code takes `&mut sqlx::QueryBuilder<sqlx::Postgres>`, which
  has no lifetime parameter in 0.9.
- Struct names are singular: `Product`, `NewProduct`, and `UpdateProduct`
  for a `products` table, instead of names built from the plural table name.
- Generated CRUD functions take a generic `executor: E` (any
  `sqlx::PgExecutor`, such as a pool, a connection, or a transaction) as
  their first argument, instead of a concrete `&PgPool`.
- `get_by_id`, `exists_by_id`, `update`, and `delete` take one parameter per
  primary-key column, so tables with composite keys get the same shape as
  single-key tables.
- `read::search_after` takes a typed `Limit` (clamped to
  `1..=MAX_PAGE_SIZE`, 500) instead of a bare integer. `Page` is built on
  `Limit`, and `Page::next` saturates instead of overflowing.
- `read::search` is generated only when the table has at least one sort key,
  and `filter.rs` only when `search` or `search_after` exists.
- `list` orders rows by primary key.
- Generated SQL keeps schema qualifiers (`app.products`) and lists columns
  explicitly; partial updates and inserts with defaulted columns are built
  with `sqlx::QueryBuilder`.
- `NUMERIC`/`DECIMAL` columns map to `rust_decimal::Decimal`.
- With more than one query input (or a query directory), queries are
  generated as `db/queries/<stem>.rs`, one module per file, instead of a
  single `db/queries.rs`. A lone query file still generates `db/queries.rs`.
- Text columns restricted by a value-list `CHECK` use a generated enum
  (`types::OrderStatus`) instead of `String`.
- Text keys are borrowed: `get_by_id`, `exists_by_id`, `update`, `delete`,
  and unique-key lookups take a text key as `&` its typed id
  (`&CurrencyId`), a key struct with a text column as `&OrderItemKey`, and
  `search_after` takes `Option<&…>` for such keys. `Copy` keys stay by value.
- Query-file functions take text parameters as `&str` and array parameters
  as slices (`&[T]`), also inside `Option`. Parameter structs (more than
  three parameters) stay owned.
- Query-file functions use typed ids: a parameter typed by a primary key or
  foreign key column takes the table's typed id (`category_id: CategoryId`,
  `&CurrencyId` for a text key, `&[CategoryId]` for `= ANY($1)`), and a
  result column taken straight from such a column decodes as it. A
  parameter compared with two different ids, and computed expressions, keep
  the plain type.

### Breaking changes to the library


- `rok_sqlc::generate(&Config) -> Result<Report>` is the stable entry point.
  The `generator`, `output`, and `schema` modules are now private, and
  `cli`, `parser`, and `queries` are hidden from the documentation and not
  covered by semver.

### Added


- Models, reads (`get_by_id`, `exists_by_id`, `list`, `list_page`, `count`),
  and writes (`create`, `update`, `delete`) for every table in PostgreSQL
  DDL, with `@generated` overwrite protection and a schema snapshot.
- Composite primary keys, array columns, `TIME`, and `NUMERIC`.
- PostgreSQL enum types (`CREATE TYPE ... AS ENUM`, `ALTER TYPE ... ADD/RENAME
  VALUE`) become Rust enums in a generated `db::types` module, used by
  rows, inputs, filters, sort keys, and query files. `types` is a reserved
  module name.
- Value-list columns (text columns restricted by `CHECK (col IN ('a', ...))`,
  inline, as a table constraint, or added by `ALTER TABLE`) become Rust enums
  in `db::types`, named after table and column (`OrderStatus`), that encode
  as text and keep each exact value.
- Typed search: per-table `Filter` and `SortKey` types, `search`, and keyset
  pagination with `search_after`.
- Unique-key lookups: every unique constraint (inline, table-level, or added
  by `ALTER TABLE`) and unique index on plain columns gets
  `read::get_by_<col>[_and_<col>...]`, returning `Option<Row>`. Text key
  columns are borrowed (`&str`, or `&` a text typed id), other columns by
  value.
  Expression indexes and partial indexes get no lookup.
- Annotated query files (`-- name: X :one|:maybe_one|:many|:exec|:execrows`)
  generate typed query functions, including `WITH` (CTE) queries.
- A query file's `ON CONFLICT (cols)` target must be a unique key of the
  table (the primary key, a unique constraint, or a full unique index, in
  any column order); otherwise generation fails with a `path:line`
  diagnostic listing the table's unique keys. `ON CONFLICT ON CONSTRAINT
  name` must name the table's primary key or a unique constraint, by its
  declared name or PostgreSQL's default (`<table>_pkey`,
  `<table>_<columns>_key`); otherwise the diagnostic lists the valid names.
- `--schema` accepts files or migration directories and can be repeated;
  `ALTER TABLE` statements in later migrations change earlier tables.
  Diagnostics print `path:line:`. `ALTER TABLE … ADD CONSTRAINT … CHECK`
  that is not a value list is ignored, like such a `CHECK` in `CREATE
  TABLE`, instead of failing the table.
- `DROP TABLE` (also `IF EXISTS`, schema-qualified, or several names) in a
  later migration removes the table, so its generated files are stale and
  get removed. Foreign keys other tables hold to it are dropped as with
  `CASCADE`: the column keeps its raw type and makes no relationship.
  `DROP TYPE` removes an enum. With `CASCADE`, its columns are dropped too.
  Without it, tables still using the type fail.
- `--queries` accepts files or directories and can be repeated.
- `rok-sqlc.toml` discovery (or `-c/--config`) and a `-j/--json`
  configuration payload, layered as flags > payload > file > defaults.
- A `[conventions]` section in `rok-sqlc.toml` and the JSON payload
  (`rok_sqlc::Conventions` in the library), off by default, with a `rok`
  preset for the Rok ERP. Managed columns (`managed_columns`, such as
  `updated_at` and `row_version`) are left out of create inputs and update
  patches and kept in rows. See ADR 0010.
- Tenant scoping (`tenant_column`, `organization_id` in the `rok` preset). On
  a table with that column, every generated function takes the tenant id
  right after the executor and adds `<column> = <tenant>` to every statement:
  reads, unique-key lookups, `search`, `search_after`, `create`, `update`
  (versioned or not, empty patch included), and every delete. The column is
  left out of create inputs (it is set from the parameter) and update patches
  (it never changes). When it is part of the primary key, the tenant
  parameter replaces that key column, and unique-key lookups leave it out of
  their name (`UNIQUE (organization_id, code)` gives `get_by_code`).
- `db::session` (generated when `tenant_setting` or `user_setting` is set):
  `set_tenant(&mut tx, id)` and `set_user(&mut tx, id)` run a
  transaction-local `set_config`, for row-level security policies and audit
  triggers. `session` is a reserved module name.
- A version column (`version_column`, `row_version` in the `rok` preset) for
  optimistic concurrency. On a table that has it, `update` takes
  `expected_version` and returns `UpdateOutcome<Row>` (`Updated(row)`,
  `NotFound`, or `VersionConflict`), defined once in `db/mod.rs`. The check
  is part of the `UPDATE`'s condition, and one statement tells a stale
  version from a missing row. The version column is always managed. A
  soft-deleted row is `NotFound`. Tables
  without the column keep the plain `update`. See ADRs 0006 and 0010.
- Soft delete (`soft_delete_column`, `deleted_at` in the `rok` preset). On a
  table with that column, which must be a nullable `TIMESTAMP` or
  `TIMESTAMPTZ` (otherwise generation fails), `delete` sets it to `now()` on a
  live row and reports whether a row changed. `restore` clears it, and
  `delete_permanently` removes the row. Every generated read skips
  soft-deleted rows. The search filter gains `include_deleted: bool`, and
  `update` on a deleted row returns `None`. The column is left out of create
  inputs and update patches. A partial unique index whose condition is
  exactly `<column> IS NULL` now gets a `get_by_<cols>` lookup. Query files
  are not rewritten.
- Relationships inferred from foreign keys between the tables generated
  together (ADR 0014): a belongs-to per foreign key (`category_id` →
  `category`), its inverse has-many named after the child module
  (`products`, or `invoices_by_created_by_user` when the child has several
  foreign keys to the parent), a has-one when the foreign key column is a
  unique key, and `parent`/`children` for a `parent_id` self-reference. The
  tenant column's foreign key is not a relationship. Each resource with
  relationships gets `relationships.rs` with a `load_<name>` loader per
  relationship: it takes already-fetched rows and loads the relationship for
  all of them in one `= ANY($n)` query, returning a `HashMap` keyed by the
  joining key (typed id where there is one): one row per key for to-one,
  a list per key (empty when nothing relates) for to-many. Loaders on a
  tenant table take the tenant and skip soft-deleted rows. Two relationships
  of a table with the same name fail generation, naming both.
- Many-to-many relationships through join tables: a table whose two
  non-tenant `NOT NULL` foreign keys, to two other different tables, form a
  unique key (payload columns allowed) relates those tables many-to-many,
  named after the far table (`permissions`, or
  `permissions_through_role_permissions` when the name is taken or several
  join tables lead there). Its `load_<name>` loader goes through the join
  table in one query and respects both tables' tenant and soft-delete
  columns. The join table's `write.rs` gains `attach` (idempotent, returns
  whether the pair is new), `detach`, and `sync(near, &[far])`, which
  replaces the set in one statement, so it is atomic in the caller's
  transaction. On a soft-delete join table `detach` soft-deletes and
  `attach`/`sync` restore the soft-deleted row instead of inserting another.
  `attach` and `sync` are left out when another column needs a value. A
  table with two foreign keys but no unique pair (like `user_roles`) is not a
  join table. Opting out one side of a many-to-many removes the other;
  opting out a join table's foreign key removes the pair through it and
  its join writes. A many-to-many relationship filter goes through a live
  join row in the same tenant.
- Joined reads for to-one relationships (ADR 0014): a table with a primary
  key gets `get_with_<name>` in `relationships.rs` for each belongs-to and
  has-one, reading the row and its related row in one query into a
  `{Row}With{Relationship}` struct (`ProductWithCategory`). The related row
  is an `Option` unless a NOT NULL foreign key guarantees it (a has-one, a
  nullable foreign key, or a related table with a tenant or soft-delete
  column makes it optional). Columns are selected under per-side aliases, so
  shared column names decode unambiguously; the related table's tenant and
  soft-delete conditions sit in the join's `ON` clause, so a deleted or
  other-tenant parent reads as `None` instead of hiding the child.
- Relationship filters (ADR 0014): a resource's `{Struct}Filter` gains an
  `Option<Box<{Related}Filter>>` field per relationship
  (`ProductFilter { category: Some(Box::new(CategoryFilter { name, .. })), .. }`),
  rendered as a correlated `EXISTS` subquery so rows never repeat; for a
  to-many relationship it means "at least one related row matches". An
  empty related filter requires a related row (a `NULL` foreign key has
  none). Soft-deleted related rows never match, and a related tenant
  table's rows must share the searched row's tenant; an untenanted
  resource gets no filter field into a tenant table. Filters nest up to
  `db::search::MAX_RELATIONSHIP_DEPTH` (4) deep, and `search` fails with
  `sqlx::Error::InvalidArgument` beyond it. A relationship named like one of
  the filter's column fields fails generation, naming both; renaming it
  in `[relationships]` resolves the clash, and filters use renamed names
  and skip opted-out relationships.
- A `[relationships]` section (config file, JSON payload, or
  `Config::relationships` with `rok_sqlc::RelationshipChange`) renames or
  opts out inferred relationships, by table and inferred name:
  `[relationships.invoices]` with `created_by_user = "author"` generates
  `load_author`, and `approved_by_user = false` generates nothing for that
  foreign key on either side. Entries are checked against the whole schema,
  so `--resource` runs accept the same configuration; an entry naming an
  unknown table or relationship, an invalid name, or a rename that collides
  fails, naming the entry (`relationships.invoices.created_by_user`).
- `-k/--check` reports drift without writing and exits 1 when generated
  files are missing, changed, or stale. Successful runs remove stale
  generated files.
- Generation and `--check` ignore line endings: migrations and query files
  are read with CRLF normalized to LF (so the schema snapshot is the same on
  every platform), and generated files that differ only in CRLF/LF count as
  unchanged.
- Every generated public item is documented, and generated code passes
  `missing_docs` and `cargo clippy -- -D warnings` with a selected set of
  pedantic lints; both fixture crates enforce the bar (ADR 0013).
- `derive = ["serde"]` (config file, JSON payload, `-d/--derive serde`, or
  `Config::derive` with `rok_sqlc::Derive::Serde`) adds
  `serde::Serialize`/`Deserialize` to rows, typed ids (transparent), key
  structs, enums (by database label), inputs, patches, joined-read structs
  (`ProductWithCategory`), and query rows and parameters, and adds `serde` to the dependency hints. Without it,
  generated code does not mention `serde`.
- Short aliases for every command and flag; `--help` and `--version` exit 0.
- Crate metadata, license files, this changelog, and CI on Linux (Rust 1.88,
  1.94, and stable, with PostgreSQL), macOS, and Windows.

### Changed


- The minimum supported Rust version is 1.88 (`rust-version = "1.88"`).
  Generated code needs Rust 1.94 (SQLx 0.9), so on toolchains below 1.94
  the test suites that compile generated code print a skip notice instead
  of running.
- Usage errors print clap's message once and exit 1.
- Diagnostics and file lists print paths with `/` on every platform, and
  paths under the current directory relative to it (also when they come
  from `rok-sqlc.toml`).

[Unreleased]: https://github.com/ateeq1999/rok-sqlc/compare/v0.3.0...HEAD
[0.3.0]: https://github.com/ateeq1999/rok-sqlc/compare/v0.2.1...v0.3.0
[0.2.1]: https://github.com/ateeq1999/rok-sqlc/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/ateeq1999/rok-sqlc/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/ateeq1999/rok-sqlc/releases/tag/v0.1.0