@workerstack/database 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +197 -33
  2. package/docs/meta-contract.md +55 -1
  3. package/package.json +1 -6
  4. package/src/codegen/analyzer-types.ts +6 -1
  5. package/src/codegen/annotations.ts +66 -9
  6. package/src/codegen/format-constraints.ts +114 -0
  7. package/src/codegen/generators/dtos/entity-file.ts +2 -1
  8. package/src/codegen/generators/dtos/example-meta.ts +22 -2
  9. package/src/codegen/generators/dtos/field-schema.ts +31 -8
  10. package/src/codegen/generators/dtos/ip-patterns.ts +25 -11
  11. package/src/codegen/generators/dtos/number-range.ts +22 -3
  12. package/src/codegen/generators/dtos/schema-aliases.ts +5 -3
  13. package/src/codegen/generators/dtos/text-format.ts +63 -0
  14. package/src/codegen/generators/dtos/text-schema.ts +49 -2
  15. package/src/codegen/generators/manifest.ts +9 -1
  16. package/src/codegen/runtime/read-comb.ts +71 -3
  17. package/src/codegen/runtime/read-tables.ts +31 -19
  18. package/src/codegen/runtime/snapshot-types.ts +3 -1
  19. package/src/codegen/runtime/to-meta.ts +6 -3
  20. package/src/formats/catalog.ts +255 -0
  21. package/src/formats/iana-time-zone.ts +117 -0
  22. package/src/formats/iso3166-1.ts +78 -0
  23. package/src/formats/iso4217.ts +42 -0
  24. package/src/formats/iso639-1.ts +41 -0
  25. package/src/formats/uts35.ts +143 -0
  26. package/src/index.ts +5 -0
  27. package/src/meta/column.ts +22 -9
  28. package/src/pg/columns.ts +6 -4
  29. package/src/sqlite/columns.ts +6 -4
  30. package/src/sqlite/constraints.ts +120 -6
  31. package/types/codegen/analyzer-types.d.ts +6 -1
  32. package/types/codegen/analyzer-types.d.ts.map +1 -1
  33. package/types/codegen/annotations.d.ts +12 -7
  34. package/types/codegen/annotations.d.ts.map +1 -1
  35. package/types/codegen/format-constraints.d.ts +22 -0
  36. package/types/codegen/format-constraints.d.ts.map +1 -0
  37. package/types/codegen/generators/dtos/entity-file.d.ts.map +1 -1
  38. package/types/codegen/generators/dtos/example-meta.d.ts.map +1 -1
  39. package/types/codegen/generators/dtos/field-schema.d.ts.map +1 -1
  40. package/types/codegen/generators/dtos/ip-patterns.d.ts +7 -0
  41. package/types/codegen/generators/dtos/ip-patterns.d.ts.map +1 -1
  42. package/types/codegen/generators/dtos/number-range.d.ts +8 -0
  43. package/types/codegen/generators/dtos/number-range.d.ts.map +1 -1
  44. package/types/codegen/generators/dtos/schema-aliases.d.ts +1 -1
  45. package/types/codegen/generators/dtos/schema-aliases.d.ts.map +1 -1
  46. package/types/codegen/generators/dtos/text-format.d.ts +31 -0
  47. package/types/codegen/generators/dtos/text-format.d.ts.map +1 -0
  48. package/types/codegen/generators/dtos/text-schema.d.ts +8 -1
  49. package/types/codegen/generators/dtos/text-schema.d.ts.map +1 -1
  50. package/types/codegen/generators/manifest.d.ts +5 -0
  51. package/types/codegen/generators/manifest.d.ts.map +1 -1
  52. package/types/codegen/runtime/read-comb.d.ts +17 -3
  53. package/types/codegen/runtime/read-comb.d.ts.map +1 -1
  54. package/types/codegen/runtime/read-tables.d.ts.map +1 -1
  55. package/types/codegen/runtime/snapshot-types.d.ts +3 -1
  56. package/types/codegen/runtime/snapshot-types.d.ts.map +1 -1
  57. package/types/codegen/runtime/to-meta.d.ts.map +1 -1
  58. package/types/formats/catalog.d.ts +51 -0
  59. package/types/formats/catalog.d.ts.map +1 -0
  60. package/types/formats/iana-time-zone.d.ts +7 -0
  61. package/types/formats/iana-time-zone.d.ts.map +1 -0
  62. package/types/formats/iso3166-1.d.ts +12 -0
  63. package/types/formats/iso3166-1.d.ts.map +1 -0
  64. package/types/formats/iso4217.d.ts +7 -0
  65. package/types/formats/iso4217.d.ts.map +1 -0
  66. package/types/formats/iso639-1.d.ts +6 -0
  67. package/types/formats/iso639-1.d.ts.map +1 -0
  68. package/types/formats/uts35.d.ts +13 -0
  69. package/types/formats/uts35.d.ts.map +1 -0
  70. package/types/index.d.ts +5 -0
  71. package/types/index.d.ts.map +1 -1
  72. package/types/meta/column.d.ts +7 -1
  73. package/types/meta/column.d.ts.map +1 -1
  74. package/types/pg/columns.d.ts +3 -3
  75. package/types/pg/columns.d.ts.map +1 -1
  76. package/types/sqlite/columns.d.ts +3 -3
  77. package/types/sqlite/columns.d.ts.map +1 -1
  78. package/types/sqlite/constraints.d.ts +117 -5
  79. package/types/sqlite/constraints.d.ts.map +1 -1
  80. package/src/sqlite/patterns.ts +0 -19
  81. package/types/sqlite/patterns.d.ts +0 -19
  82. package/types/sqlite/patterns.d.ts.map +0 -1
package/README.md CHANGED
@@ -4,6 +4,8 @@ Database framework for Cloudflare D1, PostgreSQL, and SQLite. Dialect-agnostic c
4
4
 
5
5
  This repo is the source of the [`@workerstack/database`](https://www.npmjs.com/package/@workerstack/database) npm package. The CLI binary is `comb`.
6
6
 
7
+ Moved from `@lovrozagar/comb`: `@workerstack/database@0.21.0` is the same release as `@lovrozagar/comb@0.21.0` under the new name, and the old package is deprecated. To switch, follow step 3 of [Upgrading to 0.22](#upgrading-to-022).
8
+
7
9
  This README is the full usage manual. An agent that reads only this file should be able to define tables, generate artifacts, run migrations, and serve list queries without opening source.
8
10
 
9
11
  ## Table of contents
@@ -18,7 +20,8 @@ This README is the full usage manual. An agent that reads only this file should
18
20
  - [SQLite factories](#sqlite-factories)
19
21
  - [PostgreSQL factories](#postgresql-factories)
20
22
  - [Constraints](#constraints)
21
- - [Patterns](#patterns)
23
+ - [Formats](#formats)
24
+ - [Column schemas](#column-schemas)
22
25
  - [Errors](#errors)
23
26
  - [`CombError`](#comberror)
24
27
  - [Constraint maps](#constraint-maps)
@@ -49,6 +52,7 @@ This README is the full usage manual. An agent that reads only this file should
49
52
  - [Package exports](#package-exports)
50
53
  - [Repository layout](#repository-layout)
51
54
  - [Develop](#develop)
55
+ - [Upgrading to 0.22](#upgrading-to-022)
52
56
  - [Upgrading to 0.21](#upgrading-to-021)
53
57
  - [Upgrading to 0.20.1](#upgrading-to-0201)
54
58
  - [Upgrading to 0.20](#upgrading-to-020)
@@ -150,7 +154,6 @@ Define a table with Comb's column factories instead of Drizzle's raw ones.
150
154
  /* src/db/core.tables.ts */
151
155
  import { sqliteTable, index } from "drizzle-orm/sqlite-core"
152
156
  import { c } from "@workerstack/database/sqlite/columns"
153
- import { patterns } from "@workerstack/database/sqlite/patterns"
154
157
  import * as z from "zod"
155
158
 
156
159
  /* a c.json schema is exported, so the generated DTOs can import it, and is the stored document:
@@ -163,7 +166,7 @@ export const author = sqliteTable(
163
166
  id: c.id("aut"),
164
167
  email: c.text("email", { max: 254, email: true, lowercase: true, trim: true }),
165
168
  display_name: c.text("display_name", { min: 2, max: 64, trim: true }),
166
- locale: c.text("locale", { pattern: patterns.LOCALE }),
169
+ locale: c.text("locale", { format: "bcp47" }),
167
170
  settings: c.json("settings", authorSettings),
168
171
  created_at: c.createdAt("created_at"),
169
172
  updated_at: c.updatedAt("updated_at"),
@@ -183,7 +186,7 @@ export const post = sqliteTable("post", {
183
186
  })
184
187
  ```
185
188
 
186
- The second argument to each factory is a **constraint object**. At runtime most of it is inert — `c.text("title", { max: 200 })` just becomes `text("title", { length: 200 })` — and the helper records it on the column for codegen, which turns `{ min: 2, max: 64, trim: true }` into a Zod schema, a CHECK constraint, and an OpenAPI description. Constraints live next to the column, once. Codegen loads the tables module, so a constraint value is whatever the module computes: a constant (`pattern: patterns.E164_PHONE`, `max: NAME_MAX`), an enum member, an object spread over another, a constraint object passed by name, or an enum list built by a call. Each value must be a boolean, a number or a string; `pattern` is a `/source/flags` string.
189
+ The second argument to each factory is a **constraint object**. At runtime most of it is inert — `c.text("title", { max: 200 })` just becomes `text("title", { length: 200 })` — and the helper records it on the column for codegen, which turns `{ min: 2, max: 64, trim: true }` into a Zod schema, a CHECK constraint, and an OpenAPI description. Constraints live next to the column, once. Codegen loads the tables module, so a constraint value is whatever the module computes: a constant (`pattern: SKU_PATTERN`, `max: NAME_MAX`), an enum member, an object spread over another, a constraint object passed by name, or an enum list built by a call. Each value must be a boolean, a number or a string; `pattern` is a `/source/flags` string, and `schema`, the one value of another kind, an exported zod schema ([Column schemas](#column-schemas)).
187
190
 
188
191
  Point a config at the file:
189
192
 
@@ -322,6 +325,8 @@ The constraint object is shared by both dialects. `@workerstack/database/sqlite/
322
325
  | `min` / `max` | text, integer, real | length or numeric bounds |
323
326
  | `maxBytes` | json | UTF-8 byte bound on the serialized value, on writes |
324
327
  | `pattern` | text | regex, as a source-level string |
328
+ | `format` | text, enum, integer | a standard by its id: a text's pattern, case, length and registry, an enum's values checked at codegen, an integer's range ([Formats](#formats)) |
329
+ | `schema` | text, integer, real | an exported zod schema the write DTOs pipe the value into after comb's own rules ([Column schemas](#column-schemas)) |
325
330
  | `email` / `url` | text | an email checked by zod's email pattern, which publishes; a url by the WHATWG URL parser, https only, its https pattern published; neither publishes a `format` |
326
331
  | `trim`, `lowercase`, `uppercase` | text | transforms applied in generated schemas |
327
332
  | `password` | text | a stored password hash: kept as written, held to its declared bounds and pattern alone; `passwordText()` holds the plaintext |
@@ -350,19 +355,155 @@ const signIn = z.object({ email: z.email(), password: passwordText.typed() })
350
355
 
351
356
  `passwordText()` sets a password: any storable text of 15 to 128 characters, counted by code point, with no composition rule and nothing trimmed. A password used only beside a second factor may take `passwordText({ min: 8 })`, as rev 4 allows; `min` and `max` are yours to set. `passwordText.typed()` reads one typed to sign in and holds only the max, so a password set under an older or shorter minimum still signs in. Both normalize the value to NFC before they count it, and give the NFC string, so a password typed in another normalization hashes to the same value; an app that adopts it over hashes made from input it never normalized rehashes each one on its next successful sign-in. Under `{ hash: "bcrypt" }` both refuse past 72 UTF-8 bytes, which bcrypt reads no further than, and publish `x-comb-maxUtf8Bytes: 72`, the string's own bytes (`x-comb-maxBytes` is a JSON text's); `passwordText({ hash: "bcrypt", min })` with a `min` past 72 throws, as no password would pass it. The JSON Schema states the rewrite as `x-comb-transform: ["nfc"]` and the bounds as `minLength` and `maxLength`, which count the NFC form's code points, and a password out of bounds is zod's own `too_small` or `too_big` issue with its `minimum` or `maximum`, bcrypt's bytes included.
352
357
 
353
- ### Patterns
358
+ ### Formats
359
+
360
+ `format` holds a column to a published standard, named by its id: a `c.text` value's syntax, case and length, and its membership where the standard is a registry; a `c.enum`'s listed values, at codegen; a `c.integer`'s range. Like every constraint it shapes the generated DTOs alone, with no CHECK and no change to the column's type, the same on SQLite, D1 and Postgres, and a column stays nullable unless `.notNull()`: a null passes, a value is checked.
361
+
362
+ ```ts
363
+ import { sqliteTable } from "drizzle-orm/sqlite-core"
364
+ import { c } from "@workerstack/database/sqlite/columns"
365
+
366
+ export const users = sqliteTable("users", {
367
+ id: c.id("usr"),
368
+ currency: c.text("currency", { format: "iso4217" }),
369
+ time_zone: c.text("time_zone", { format: "iana-time-zone" }),
370
+ locale: c.text("locale", { format: "bcp47" }),
371
+ country: c.text("country", { format: "iso3166-1-alpha-2" }),
372
+ pay_currency: c.enum("pay_currency", ["USD", "EUR", "GBP"], { format: "iso4217" }),
373
+ hour_cycle: c.enum("hour_cycle", ["h12", "h23"], { format: "uts35-hc" }),
374
+ week_start: c.integer("week_start", { format: "iso8601-weekday" }), // 1 Monday … 7 Sunday
375
+ })
376
+ ```
377
+
378
+ A text format trims the value, then writes it in its standard's case, before it checks it. The table names each text format's standard and release, the case a written value is put in, and the registry it must also belong to; an editor shows the same list on the `format` key.
379
+
380
+ | Format | Standard | Case | Registry |
381
+ | --------------------- | ---------------------------------------------------------------------- | ---------- | ----------------- |
382
+ | `base64url` | RFC 4648 §5 base64url, unpadded | kept | |
383
+ | `bcp47` | BCP 47 language tag, RFC 5646 §2.1 well-formed | kept | |
384
+ | `e164` | ITU-T E.164 phone number, + and up to 15 digits | kept | |
385
+ | `iana-time-zone` | IANA time zone name, tzdata 2026f | kept | `isIanaTimeZone` |
386
+ | `ip` | IPv4 dotted quad or IPv6 address, RFC 4291 §2.2 | lowercased | |
387
+ | `ipv4` | IPv4 address, dotted quad without leading zeros | kept | |
388
+ | `ipv6` | IPv6 address, RFC 4291 §2.2 | lowercased | |
389
+ | `iso3166-1-alpha-2` | ISO 3166-1 alpha-2 country code, Debian iso-codes v4.20.1 | uppercased | `isIso3166Alpha2` |
390
+ | `iso3166-1-alpha-3` | ISO 3166-1 alpha-3 country code, Debian iso-codes v4.20.1 | uppercased | `isIso3166Alpha3` |
391
+ | `iso4217` | ISO 4217 alphabetic currency code, SIX list 2026-09-17 | uppercased | `isIso4217` |
392
+ | `iso639-1` | ISO 639-1 language code, Debian iso-codes v4.20.1 | lowercased | `isIso639Alpha2` |
393
+ | `kebab-slug` | kebab-case slug, lowercase letters and digits joined by single hyphens | kept | |
394
+ | `pkce-s256-challenge` | RFC 7636 §4.2 PKCE SHA-256 code challenge | kept | |
395
+ | `rfc1123-hostname` | RFC 1123 §2.1 host name | lowercased | |
396
+ | `semver` | Semantic Versioning 2.0.0 version | kept | |
397
+ | `sha256-hex` | SHA-256 digest, FIPS 180-4, lowercase hex | lowercased | |
398
+ | `sha512-hex` | SHA-512 digest, FIPS 180-4, lowercase hex | lowercased | |
399
+ | `srgb-hex` | sRGB color as #rrggbb, CSS Color 4 | lowercased | |
400
+ | `srgb-hex-alpha` | sRGB color with alpha as #rrggbbaa, CSS Color 4 | lowercased | |
401
+ | `ulid` | ULID, Crockford base32 in lowercase | lowercased | |
402
+ | `uts35-ca` | UTS #35 calendar (ca) key value, CLDR 49 | lowercased | |
403
+ | `uts35-fw` | UTS #35 first day of the week (fw) key value, CLDR 49 | lowercased | |
404
+ | `uts35-hc` | UTS #35 hour cycle (hc) key value, CLDR 49 | lowercased | |
405
+ | `uts35-ms` | UTS #35 measurement system (ms) key value, CLDR 49 | lowercased | |
406
+ | `uts35-mu` | UTS #35 measurement unit (mu) key value, CLDR 49 | lowercased | |
407
+ | `uts35-nu` | UTS #35 numbering system (nu) key value, CLDR 49 | lowercased | |
408
+ | `uuid` | RFC 9562 UUID, lowercase hex | lowercased | |
409
+ | `uuid-v4` | RFC 9562 UUID version 4, lowercase hex | lowercased | |
410
+ | `uuid-v7` | RFC 9562 UUID version 7, lowercase hex | lowercased | |
411
+
412
+ A text format's rule is its pattern, which Go's RE2, Rust's regex and JavaScript's `u` mode read alike, and its length bounds, published as `pattern`, `minLength` and `maxLength` beside `x-comb-format` and the `x-comb-transform` it runs (`["trim", "uppercase"]` on `iso4217`), and its OpenAPI example is a value of the standard as written (`EUR` on `iso4217`), none where the column's own bounds leave that value out. It takes no `x-comb-text`, as no format's pattern admits U+0000 or a surrogate. The format sets the column's trim, case and length, so codegen refuses a `trim`, `lowercase` or `uppercase` beside it (`users.currency: an iso4217 text takes no lowercase: the format sets its case and trim`), a `min` or `max` outside its range, which the format already holds or which leaves no value (`users.currency: max 4 is past iso4217's length 3: the format holds it`), and an `email`, `url`, `password`, `imageUri` or `name` beside it, each a rule of its own (`users.currency: format and email contradict; declare one`). A bound inside the range, or a `pattern`, beside a format narrows it. `bcp47` sets no maximum, as RFC 5646's 35 characters are the least an implementation must hold, not a cap on a tag, so a column declares its own `max`. An id comb does not know is refused with the list it does (`users.currency: comb knows no format "iso-4217"; text formats: base64url, bcp47, …`).
413
+
414
+ Where the table names a predicate, a value must also be a member of the pinned registry it reads, which no pattern states: `HRK` matches `^[A-Z]{3}$` and is no `iso4217` value, withdrawn in 2023, and `Europe/Atlantis` matches the zone pattern and names no zone. Each registry is a snapshot of the release the table names, committed in comb's source: nothing fetches at build, test or run time, and a comb release moves to a newer one. The five predicates are root exports, each taking any string, exactly as written, and never throwing; a bundle that calls none of them carries none of their lists.
415
+
416
+ ```ts
417
+ import { isIanaTimeZone, isIso3166Alpha2, isIso3166Alpha3, isIso4217, isIso639Alpha2 } from "@workerstack/database"
418
+
419
+ isIso4217("EUR") // true
420
+ isIso4217("HRK") // false: withdrawn in 2023
421
+ isIso4217("eur") // false: the DTO uppercases a value first, the predicate does not
422
+ isIso3166Alpha2("UK") // false: reserved, not assigned; GB is the code
423
+ ```
424
+
425
+ A reader that validates from the JSON Schema alone checks such a value with the predicate, or with the release meta-contract §2.1 names beside the id. The `uts35-*` formats are the values of UTS #35's Unicode locale extension keys in CLDR 49, deprecated ones left out (`uts35-hc` is `h11`, `h12`, `h23`, `h24`, `c12` and `c24`); their pattern lists every value, so it is the whole rule, as on every format the table names no registry for.
426
+
427
+ On a `c.enum` the listed values stay the rule, and `format` says what they mean: codegen refuses a value the format does not take as written (`users.pay_currency: "usd" is no iso4217 value; write the standard's form, "USD"`, `users.pay_currency: "HRK" is no iso4217 value`), and every DTO of the column publishes its `enum` with `x-comb-format`.
428
+
429
+ `c.integer` takes the two integer formats: `iso8601-month`, 1 January to 12 December, and `iso8601-weekday`, 1 Monday to 7 Sunday (ISO 8601 §3.2.2), the numbering `Intl.Locale`'s `getWeekInfo().firstDay` returns. The write DTOs hold the value to the standard's range, or to a declared `min` or `max` inside it, published as `minimum` and `maximum` beside `x-comb-format`, and a value outside it gets zod's own `too_small` or `too_big`; codegen refuses a bound past the standard's (`users.week_start: max 8 is past iso8601-weekday's 7: the format holds it`) and a text format's id (`users.week_start: c.integer takes no format "iso4217"; integer formats: iso8601-month, iso8601-weekday`). `c.real`, `c.ref`, `c.json` and the timestamps take no format: TypeScript refuses one, and codegen refuses it from a tables module in JavaScript (`users.owner_id: c.ref takes no format`).
430
+
431
+ A text its format refuses gets exactly one issue, zod's `invalid_format` with the id as its `format` and `origin: "string"`, and no message of comb's, so zod words it in the locale you configure, and an error map can key a message of your own on the id:
432
+
433
+ ```ts
434
+ import * as z from "zod"
435
+
436
+ // " eu " or "HRK" on an iso4217 field:
437
+ // { code: "invalid_format", format: "iso4217", origin: "string", path: ["currency"], message: "Invalid iso4217" }
438
+
439
+ z.config(z.locales.de()) // message: "Ungültig: iso4217"
440
+
441
+ z.config({
442
+ customError: (issue) =>
443
+ issue.code === "invalid_format" && issue.format === "iso4217" ? "Unknown currency" : undefined,
444
+ })
445
+ ```
446
+
447
+ An error map that returns `undefined` leaves the issue to the locale, as that one does for every other issue.
448
+
449
+ ### Column schemas
450
+
451
+ `schema` takes a rule comb has no key for: a zod schema the create and update DTOs pipe a `c.text`, `c.integer` or `c.real` value into after every rule of comb's, so it reads the value trimmed, cased and checked. Codegen imports it as it imports a `c.json` schema, so it must be exported, and it is best in a module of its own:
452
+
453
+ ```ts
454
+ /* src/db/schemas.ts: a module of its own, so a DTO that imports a schema loads zod and this module alone */
455
+ import * as z from "zod"
456
+
457
+ export const dateOrder = z.enum(["iso", "dmy", "mdy"])
458
+ export const evenMinutes = z.int().refine((value) => value % 2 === 0, { error: "Must be even" })
459
+ ```
460
+
461
+ The tables module imports both, `import { dateOrder, evenMinutes } from "./schemas.ts"`, and names each on its column:
354
462
 
355
- `@workerstack/database/sqlite/patterns` ships regexes as source-level strings so the codegen parser can copy them verbatim into `.regex()` calls:
463
+ <!-- ts-context
464
+ declare const dateOrder: import("zod").ZodType<string, string>
465
+ declare const evenMinutes: import("zod").ZodType<number, number>
466
+ -->
356
467
 
357
468
  ```ts
358
- import { patterns } from "@workerstack/database/sqlite/patterns"
469
+ import { sqliteTable } from "drizzle-orm/sqlite-core"
470
+ import { c } from "@workerstack/database/sqlite/columns"
471
+
472
+ export const users = sqliteTable("users", {
473
+ id: c.id("usr"),
474
+ date_order: c.text("date_order", { schema: dateOrder, trim: true }),
475
+ slot_minutes: c.integer("slot_minutes", { min: 0, schema: evenMinutes }),
476
+ })
477
+ ```
359
478
 
360
- patterns.COUNTRY_ALPHA2 // "/^[A-Z]{2}$/"
361
- patterns.E164_PHONE // "/^\\+[1-9][0-9]{7,14}$/"
362
- patterns.IANA_TIMEZONE // "/^[A-Za-z_/]+$/"
363
- patterns.LOCALE // "/^[a-z]{2}(-[A-Z]{2})?$/"
479
+ A schema exported from the tables module works too, but then every DTO that imports it loads the tables module and drizzle with it, a frontend form's bundle among them, and as a `const` it must sit above the table that names it: one below throws a `ReferenceError` as the module loads, which codegen reports in the module's own words. An inline schema type-checks, yet codegen refuses it, as it can only import an export: `users.date_order: no module of the project exports the schema its schema key is given; export it, above the table that names it, so the generated DTOs can import it`, with the note of packages that would not load on their own that a `c.json` schema's error carries.
480
+
481
+ The text key takes a `z.ZodType<string, string>` and the number keys a `z.ZodType<number, number>`, so a `z.int()` on a text, or a transform to another type, fails to compile. The schema must come from the `zod` the generated DTOs import, one `zod` in the project's tree, as a `c.json` schema must. Codegen refuses a value that is no zod schema (`users.date_order: the constraint schema is no zod schema; comb's DTOs are zod 4, so the schema must be one`) and one with no JSON Schema form, such as a `z.custom()`, keeping zod's reason (`… has no JSON Schema form (zod: Custom types cannot be represented in JSON Schema); comb publishes every DTO rule`).
482
+
483
+ Its issues reach the caller exactly as zod yields them, and comb adds no code or message: `"x"` on `date_order` is the enum's own `invalid_value` with its values, and `3` on `slot_minutes` is the refine's `custom` issue, `"Must be even"`. A value comb's own rules refuse never reaches the schema, so `-3` there gets the `too_small` of `min: 0` alone.
484
+
485
+ The JSON Schema publishes the schema's export name as `x-comb-schema` and the schema's own input JSON Schema in an `allOf` beside it, which codegen computes with `z.toJSONSchema(schema, { io: "input" })` and writes as a literal; on a nullable field the `allOf` sits on the non-null member of its `anyOf`, as the schema's `type` would refuse the null the field takes. A nullable `date_order` publishes:
486
+
487
+ ```text
488
+ {
489
+ "anyOf": [
490
+ {
491
+ "type": "string",
492
+ "allOf": [{ "type": "string", "enum": ["iso", "dmy", "mdy"] }],
493
+ "x-comb-schema": "dateOrder",
494
+ "x-comb-text": "no U+0000 or unpaired surrogate",
495
+ "x-comb-transform": ["trim"]
496
+ },
497
+ { "type": "null" }
498
+ ],
499
+ "x-comb-schema": "dateOrder",
500
+ "x-comb-text": "no U+0000 or unpaired surrogate",
501
+ "x-comb-transform": ["trim"]
502
+ }
364
503
  ```
365
504
 
505
+ A refine publishes nothing: `evenMinutes` publishes `{ "type": "integer" }` with a safe integer's range and no word of evenness. `x-comb-schema` is how a reader that runs no JavaScript learns the producer runs a rule it cannot see, and such a reader may take a value the server then refuses. A column with a `schema` carries no generated example, as a refine its JSON Schema does not state may refuse any value comb would pick. The read DTO runs no schema, as a read never refuses a stored row, and the manifest names the export as the column's `schema`. `c.enum`, `c.json`, `c.ref` and the timestamps take no `schema` (`users.status: c.enum takes no schema`): an enum's values and a `c.json` schema are rules of their own, and a reference's value is another table's key.
506
+
366
507
  ## Errors
367
508
 
368
509
  ### `CombError`
@@ -1057,28 +1198,27 @@ ULIDs use Crockford base32 with a 10-character timestamp and 16 characters of ra
1057
1198
 
1058
1199
  ## Package exports
1059
1200
 
1060
- | Specifier | Contents |
1061
- | -------------------------------------------- | -------------------------------------------------------------------------------------------------- |
1062
- | `@workerstack/database` | `CombError`, `createUlidFactory`, `generateId`, `generateUlid`, `LruCache`, error keys and types |
1063
- | `@workerstack/database/types` | `StatusKey`, `ConstraintMap`, `DatabaseErrorHandler`, … |
1064
- | `@workerstack/database/cli` | `runCli(argv, io)`: run the CLI in process and get its exit code |
1065
- | `@workerstack/database/codegen` | `analyze`, `validateTables`, `fieldConstraintsOf`, analysis types |
1066
- | `@workerstack/database/codegen/generators` | every `generate*` function |
1067
- | `@workerstack/database/meta` | `combMeta`, `readCombMeta`, `readCombEntityMeta`, `readCombQueryMeta`, `carryCombMeta`, meta types |
1068
- | `@workerstack/database/migrate` | config, providers, apply, parse, Atlas helpers |
1069
- | `@workerstack/database/query` | filter, order, select, cursors, Zod builders |
1070
- | `@workerstack/database/query/sqlite` | SQL generation, FTS5, JSON helpers |
1071
- | `@workerstack/database/sqlite/columns` | `c` for SQLite |
1072
- | `@workerstack/database/sqlite/constraints` | constraint types |
1073
- | `@workerstack/database/sqlite/patterns` | shared regex patterns |
1074
- | `@workerstack/database/sqlite/constraint-handler` | `sqliteErrorHandler`, `classifySqliteError`, `handleConstraintViolation` |
1075
- | `@workerstack/database/sqlite/d1/session` | `openD1Session` |
1076
- | `@workerstack/database/sqlite/d1/bookmark` | `BookmarkTracker` |
1077
- | `@workerstack/database/sqlite/d1/error-handler` | `d1ErrorHandler`, `classifyD1Error` |
1078
- | `@workerstack/database/sqlite/d1/sharding` | `ShardRouter`, `shardMap` |
1079
- | `@workerstack/database/states` | `defineStateMachine`, `canTransition`, `assertTransition`, `nextStates`, `isState`, `isTerminal` |
1080
- | `@workerstack/database/pg/columns` | `c` for PostgreSQL |
1081
- | `@workerstack/database/pg/error-handler` | `pgErrorHandler`, `classifyPgError` |
1201
+ | Specifier | Contents |
1202
+ | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
1203
+ | `@workerstack/database` | `CombError`, `createUlidFactory`, `generateId`, `generateUlid`, `LruCache`, the format registry predicates, error keys and types |
1204
+ | `@workerstack/database/types` | `StatusKey`, `ConstraintMap`, `DatabaseErrorHandler`, … |
1205
+ | `@workerstack/database/cli` | `runCli(argv, io)`: run the CLI in process and get its exit code |
1206
+ | `@workerstack/database/codegen` | `analyze`, `validateTables`, `fieldConstraintsOf`, analysis types |
1207
+ | `@workerstack/database/codegen/generators` | every `generate*` function |
1208
+ | `@workerstack/database/meta` | `combMeta`, `readCombMeta`, `readCombEntityMeta`, `readCombQueryMeta`, `carryCombMeta`, meta types |
1209
+ | `@workerstack/database/migrate` | config, providers, apply, parse, Atlas helpers |
1210
+ | `@workerstack/database/query` | filter, order, select, cursors, Zod builders |
1211
+ | `@workerstack/database/query/sqlite` | SQL generation, FTS5, JSON helpers |
1212
+ | `@workerstack/database/sqlite/columns` | `c` for SQLite |
1213
+ | `@workerstack/database/sqlite/constraints` | constraint types |
1214
+ | `@workerstack/database/sqlite/constraint-handler` | `sqliteErrorHandler`, `classifySqliteError`, `handleConstraintViolation` |
1215
+ | `@workerstack/database/sqlite/d1/session` | `openD1Session` |
1216
+ | `@workerstack/database/sqlite/d1/bookmark` | `BookmarkTracker` |
1217
+ | `@workerstack/database/sqlite/d1/error-handler` | `d1ErrorHandler`, `classifyD1Error` |
1218
+ | `@workerstack/database/sqlite/d1/sharding` | `ShardRouter`, `shardMap` |
1219
+ | `@workerstack/database/states` | `defineStateMachine`, `canTransition`, `assertTransition`, `nextStates`, `isState`, `isTerminal` |
1220
+ | `@workerstack/database/pg/columns` | `c` for PostgreSQL |
1221
+ | `@workerstack/database/pg/error-handler` | `pgErrorHandler`, `classifyPgError` |
1082
1222
 
1083
1223
  ## Repository layout
1084
1224
 
@@ -1126,6 +1266,30 @@ bun run test:bun # bun test — the bun:sqlite replay suite
1126
1266
 
1127
1267
  Commits follow [Conventional Commits](https://www.conventionalcommits.org). `bun install` points `core.hooksPath` at `.githooks`, so the `commit-msg` hook enforces it locally; CI re-checks every commit on a pull request.
1128
1268
 
1269
+ ## Upgrading to 0.22
1270
+
1271
+ What changes for a project on 0.21. Before deploying it:
1272
+
1273
+ 1. **Know what changed in the API.** A test holds these three lists to the exports themselves.
1274
+
1275
+ New exports: `IntegerFormat`, `RealConstraints`, `RefConstraints`, `TextFormat`, `isIanaTimeZone`, `isIso3166Alpha2`, `isIso3166Alpha3`, `isIso4217`, `isIso639Alpha2`.
1276
+
1277
+ Removed: `patterns`, with its entry point, @workerstack/database/sqlite/patterns. **Fix:** declare the format that holds the standard a pattern of it stood for, as item 2 maps each one.
1278
+
1279
+ Changed: `AnalysisResult`, `CodegenConfig`, `DatabaseCodegen`, `EnumOptions`, `FieldConstraints`, `FieldConstraintsMeta`, `FieldMeta`, `GeneratorContext`, `IntegerConstraints`, `Manifest`, `ManifestColumn`, `ManifestEntity`, `OrmConfig`, `PgColumns`, `ShardMapColumns`, `ShardMapTable`, `SqliteColumns`, `TableMeta`, `TextConstraints`, `analyze`, `buildManifest`, `c`, `defineCodegenConfig`, `deriveEntityMeta`, `fieldConstraintsOf`, `generateColumns`, `generateConstraints`, `generateDiagrams`, `generateDtos`, `generateEntities`, `generateEnumChecks`, `generateFieldNames`, `generateFts`, `generateFtsSql`, `generateManifest`, `generateOrm`, `generateRelations`, `generateRows`, `generateTranslations`, `isExcludedFromUpdate`, `shardMap`, `validateTables`.
1280
+
1281
+ `TextConstraints` gains `format` and `schema`, `IntegerConstraints` gains `format` and `schema`, and `EnumOptions` gains `format`, each optional ([Formats](#formats), [Column schemas](#column-schemas)), a `format` standing where a regex of the removed `patterns` did (step 2). `c.real` takes `RealConstraints`, a real's bounds and `schema` with no `format`, where 0.21 typed it with `IntegerConstraints`, and `c.ref` takes `RefConstraints`, a text's keys but `format` and `schema`. The `c` of @workerstack/database/sqlite/columns, @workerstack/database/pg/columns and @workerstack/database/sqlite/d1/sharding changes with them, as do `SqliteColumns`, `PgColumns`, `ShardMapColumns`, `ShardMapTable` and `shardMap`. **Fix:** none: a call that compiled on 0.21, one passing a value typed `IntegerConstraints` to `c.real` or `TextConstraints` to `c.ref` among them, compiles on 0.22.
1282
+
1283
+ `ManifestColumn` gains `format`, the id of the standard a write checks the value against, and `schema`, the export name of the zod schema a text or number column declares, each null when the column declares none and `schema` null on a `c.json` column, whose schema the manifest already describes. `FieldConstraintsMeta`, which the package root, @workerstack/database/codegen, @workerstack/database/meta and @workerstack/database/query reach, gains `format`, and `FieldMeta` gains `schemaJson`, the schema's input JSON Schema, which the DTOs publish. Each is required, as every field of these types is (`ManifestColumn.format`, `ManifestColumn.schema`, `FieldConstraintsMeta.format`, `FieldMeta.schemaJson`), so a `ManifestColumn` or `FieldConstraintsMeta` you build fails to compile (TS2741). The manifest stays `v` 3, and a manifest reader meets the two fields as nulls on every column that declares neither. **Fix:** a `ManifestColumn` you build sets `format: null` and `schema: null`, a `FieldConstraintsMeta` sets `format: null`, and a `FieldMeta` sets `schemaJson: null`.
1284
+
1285
+ `fieldConstraintsOf` takes `values`, the column's enum values, each of which a `c.enum`'s format must take, or `null` for a column with none. **Fix:** a direct caller of `fieldConstraintsOf` passes `values`.
1286
+
1287
+ The rest of the list changes only through those types: `FieldConstraints` is `FieldConstraintsMeta`; `AnalysisResult`, `TableMeta`, `Manifest` and `ManifestEntity` hold a `FieldMeta` or a `ManifestColumn`; and `analyze`, `buildManifest`, `validateTables`, `deriveEntityMeta`, `isExcludedFromUpdate`, `CodegenConfig`, `DatabaseCodegen`, `GeneratorContext`, `OrmConfig`, `defineCodegenConfig` and each `generate*` function take or return one. **Fix:** none.
1288
+
1289
+ 2. **Declare a `format` where a `patterns` regex stood.** Each of the four stood for a standard it held loosely: `COUNTRY_ALPHA2` took any two capitals, `UK` and `XX` among them; `E164_PHONE` refused a number of fewer than 8 digits; `IANA_TIMEZONE` took `a/b/c` and refused `Etc/GMT+5` and `America/Port-au-Prince`; `LOCALE` refused `zh-Hant-TW` and `es-419`. A format holds the standard itself, and a registry's format its pinned list: `iso3166-1-alpha-2` refuses `UK` and `XX`, `e164` takes 2 to 15 digits after the `+`, `iana-time-zone` takes the names of tzdata 2026f alone, and `bcp47` takes every well-formed tag. A format also trims the value and writes it in its standard's case before its check, so `" hr"` is stored as `HR` where the pattern refused it. A value it refuses gets zod's `invalid_format` with the id as its `format` and the locale's message, where a pattern's issue was `format: "regex"` with the pattern and an English message. **Fix:** replace `pattern: patterns.COUNTRY_ALPHA2` with `format: "iso3166-1-alpha-2"`, `pattern: patterns.E164_PHONE` with `format: "e164"`, `pattern: patterns.IANA_TIMEZONE` with `format: "iana-time-zone"` and `pattern: patterns.LOCALE` with `format: "bcp47"`; drop a `trim`, `lowercase` or `uppercase` beside it, and a `min` or `max` outside its range, which codegen refuses; a client that read a pattern's `format: "regex"` reads the id.
1290
+
1291
+ 3. **Install `@workerstack/database` in place of `@lovrozagar/comb`.** The package moved to the workerstack org and the old name gets no new releases; `@workerstack/database@0.21.0` is `@lovrozagar/comb@0.21.0` under the new name, so you can switch before taking 0.22. The `comb` CLI, `comb.config.ts`, `CombError` and the migration tables keep their names. Generated DTOs and entities import from the package by name, so files generated on the old name still import `@lovrozagar/comb`. **Fix:** run `bun remove @lovrozagar/comb && bun add @workerstack/database`, replace every `@lovrozagar/comb` import prefix with `@workerstack/database`, and rerun `comb codegen`.
1292
+
1129
1293
  ## Upgrading to 0.21
1130
1294
 
1131
1295
  What changes for a project on 0.20. Before deploying it:
@@ -166,6 +166,55 @@ no `anyOf` walk. A nullable trimmed text with a `max` of 9, as codegen publishes
166
166
  - `x-comb-rectangular`: `true` on a multidimensional array: every sub-array of one level, across the
167
167
  whole level, has the same length, as Postgres stores an array: `[[1, 2], [3, 4]]` is one,
168
168
  `[[1, 2], [3]]` is not.
169
+ - `x-comb-format`: the standard a written value is held to, by its id, on a text, an enum or an
170
+ integer. On a text, the value after its `x-comb-transform` matches the published `pattern` and
171
+ length bounds; where the id below names a registry, it is also a member of that pinned registry,
172
+ which no pattern states, so a reader must check it, with the predicate named, which
173
+ `@workerstack/database` exports, or with the release named (`HRK` matches `^[A-Z]{3}$` and is no
174
+ `iso4217` value: withdrawn in 2023). A text failing it gets zod's `invalid_format` issue with the
175
+ id as its `format` and no message of comb's, so a zod locale or error map words it. On an enum the
176
+ listed values are the rule, each one the format takes, and the id is what they mean; every DTO of
177
+ an enum publishes it, as each holds the values. On an integer the published `minimum` and
178
+ `maximum` are the standard's range, or a declared one inside it. A read of a text or an integer
179
+ publishes none. The ids, each with its standard and release:
180
+ - `base64url`: RFC 4648 §5 base64url, unpadded.
181
+ - `bcp47`: BCP 47 language tag, RFC 5646 §2.1 well-formed.
182
+ - `e164`: ITU-T E.164 phone number, + and up to 15 digits.
183
+ - `iana-time-zone`: IANA time zone name, tzdata 2026f. Registry: `isIanaTimeZone`.
184
+ - `ip`: IPv4 dotted quad or IPv6 address, RFC 4291 §2.2.
185
+ - `ipv4`: IPv4 address, dotted quad without leading zeros.
186
+ - `ipv6`: IPv6 address, RFC 4291 §2.2.
187
+ - `iso3166-1-alpha-2`: ISO 3166-1 alpha-2 country code, Debian iso-codes v4.20.1. Registry: `isIso3166Alpha2`.
188
+ - `iso3166-1-alpha-3`: ISO 3166-1 alpha-3 country code, Debian iso-codes v4.20.1. Registry: `isIso3166Alpha3`.
189
+ - `iso4217`: ISO 4217 alphabetic currency code, SIX list 2026-09-17. Registry: `isIso4217`.
190
+ - `iso639-1`: ISO 639-1 language code, Debian iso-codes v4.20.1. Registry: `isIso639Alpha2`.
191
+ - `kebab-slug`: kebab-case slug, lowercase letters and digits joined by single hyphens.
192
+ - `pkce-s256-challenge`: RFC 7636 §4.2 PKCE SHA-256 code challenge.
193
+ - `rfc1123-hostname`: RFC 1123 §2.1 host name.
194
+ - `semver`: Semantic Versioning 2.0.0 version.
195
+ - `sha256-hex`: SHA-256 digest, FIPS 180-4, lowercase hex.
196
+ - `sha512-hex`: SHA-512 digest, FIPS 180-4, lowercase hex.
197
+ - `srgb-hex`: sRGB color as #rrggbb, CSS Color 4.
198
+ - `srgb-hex-alpha`: sRGB color with alpha as #rrggbbaa, CSS Color 4.
199
+ - `ulid`: ULID, Crockford base32 in lowercase.
200
+ - `uts35-ca`: UTS #35 calendar (ca) key value, CLDR 49.
201
+ - `uts35-fw`: UTS #35 first day of the week (fw) key value, CLDR 49.
202
+ - `uts35-hc`: UTS #35 hour cycle (hc) key value, CLDR 49.
203
+ - `uts35-ms`: UTS #35 measurement system (ms) key value, CLDR 49.
204
+ - `uts35-mu`: UTS #35 measurement unit (mu) key value, CLDR 49.
205
+ - `uts35-nu`: UTS #35 numbering system (nu) key value, CLDR 49.
206
+ - `uuid`: RFC 9562 UUID, lowercase hex.
207
+ - `uuid-v4`: RFC 9562 UUID version 4, lowercase hex.
208
+ - `uuid-v7`: RFC 9562 UUID version 7, lowercase hex.
209
+ - `iso8601-month`: ISO 8601 month number, 1 January to 12 December.
210
+ - `iso8601-weekday`: ISO 8601 §3.2.2 weekday number, 1 Monday to 7 Sunday.
211
+ - `x-comb-schema`: the export name of the zod schema a written text or number is piped into after
212
+ every other rule, on the value as rewritten; its issues reach the caller as the schema yields
213
+ them. The `allOf` beside it is the schema's published part: its input JSON Schema, last, after
214
+ the patterns of the field's own chain when that has two or more. A refine in the schema publishes
215
+ nothing, so a rule it states only as a refine is one the producer alone runs: a reader may take a
216
+ value the producer then refuses. On a nullable field the `allOf` sits on the non-null member
217
+ alone, as its `type` would refuse the null the field takes. A read publishes none.
169
218
  - `x-comb-minLength`, `x-comb-maxLength`, `x-comb-minimum`, `x-comb-maximum`: on a read, the
170
219
  declared bounds it states without enforcing them, so a stored row past one still reads.
171
220
 
@@ -652,7 +701,12 @@ reader joins through the manifest alone. A composite foreign key is one relation
652
701
  per column, each carrying the whole key as `key: { columns, refColumns }`, paired by position, so
653
702
  a reader joins on every column together rather than on independent pairs. Per column it carries the helper that
654
703
  declared it, its `sqlType`, the column's SQL type with its modifier as drizzle writes it (`timestamp(3) with time zone`, `interval day`, `numeric(3, -2)`), an array's that of its element, beside its `dimensions` (0 for a column of one value, 2 for `integer[][]`) and `nullElements`, whether a written element may be null, as the column's drizzle type declares it (false for a column of one value), the bounds a write takes and enum values, `textKind` (`"email"`, `"url"`, `"imageUri"`,
655
- `"password"`, `"name"`, or null), `transform`, the list the DTO publishes as `x-comb-transform`
704
+ `"password"`, `"name"`, or null), `format`, the id of the standard format a write checks the
705
+ value against (`"iso4217"`, `"iso8601-weekday"`), as `x-comb-format` publishes it (§2.1), or null
706
+ when the column declares none, `schema`, the export name of the zod schema a text or number column
707
+ declares, which a write runs after the column's own rules and whose published part is
708
+ `x-comb-schema`'s (§2.1), or null when none and on a `c.json` column, whose schema is its value's
709
+ shape rather than a rule beside it, `transform`, the list the DTO publishes as `x-comb-transform`
656
710
  (§2.1), and `generated`, `immutable` and `private` flags. `min`, `max`, `patterns` and `maxBytes`
657
711
  are what the create DTO enforces, not what the column declares, from `writeRules`, which reads
658
712
  the rules the DTO's builders read: a text's declared bounds with comb's own (a required text's 1, a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workerstack/database",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Database framework for Cloudflare D1, PostgreSQL, and SQLite. Schema builders, error handling, D1 sessions, sharding, codegen, migrations.",
5
5
  "keywords": [
6
6
  "cloudflare-d1",
@@ -124,11 +124,6 @@
124
124
  "types": "./types/typescript-floor-types.d.ts",
125
125
  "default": "./src/sqlite/d1/sharding/index.ts"
126
126
  },
127
- "./sqlite/patterns": {
128
- "types@>=5.7": "./types/sqlite/patterns.d.ts",
129
- "types": "./types/typescript-floor-types.d.ts",
130
- "default": "./src/sqlite/patterns.ts"
131
- },
132
127
  "./states": {
133
128
  "types@>=5.7": "./types/states.d.ts",
134
129
  "types": "./types/typescript-floor-types.d.ts",
@@ -1,3 +1,4 @@
1
+ import type { IntegerFormat, TextFormat } from "../formats/catalog.ts"
1
2
  import type { CombColumnKind } from "../meta/column.ts"
2
3
  /**
3
4
  * TableMeta — serializable intermediate format produced by TableAnalyzer.
@@ -10,6 +11,8 @@ type FieldConstraintsMeta = {
10
11
  autogenerate: boolean
11
12
  /** the generated DTOs take only an email address */
12
13
  email: boolean
14
+ /** the standard the value is held to: a text's or an enum's text format, an integer's integer format; null when none is declared */
15
+ format: TextFormat | IntegerFormat | null
13
16
  /** an https URL or a data:image/ URI */
14
17
  imageUri: boolean
15
18
  /** the generated DTOs lowercase the text before they check it */
@@ -127,8 +130,10 @@ type FieldMeta = {
127
130
  isPrimaryKey: boolean
128
131
  /** the name the generated DTO imports a c.json column's schema under, null for any other column */
129
132
  jsonSchemaName: string | null
130
- /** the names the field's generated schema uses and must import (a c.json schema) */
133
+ /** the names the field's generated schema uses and must import: a c.json schema, or the zod schema a text or number column's `schema` key names */
131
134
  schemaImports: ImportRef[]
135
+ /** the input JSON Schema of the zod schema a text or number column's `schema` key names, self-contained and without `$schema`, which the generated DTOs publish beside it; null for any other column. The schema's import is the field's one `schemaImports` entry */
136
+ schemaJson: Readonly<Record<string, unknown>> | null
132
137
  /** a Postgres array's declared dimensions; 0 or absent for a column of one value */
133
138
  dimensions?: number | undefined
134
139
  /** whether a Postgres array's elements may be null, as its drizzle type declares; false or absent otherwise */
@@ -5,12 +5,14 @@
5
5
  */
6
6
  import type { CombColumnKind } from "../meta/column.ts"
7
7
  import type { ConstantValue, FieldConstraintsMeta } from "./analyzer-types.ts"
8
+ import { formatOf } from "./format-constraints.ts"
8
9
  import { patternLiteral } from "./utils/pattern.ts"
9
10
 
10
11
  /** Field constraints extracted from type arguments */
11
12
  export type FieldConstraints = FieldConstraintsMeta
12
13
 
13
14
  /* the keys of each constraint type in src/sqlite/constraints.ts, which tests/types/constraint-keys.typetest.ts holds them to */
15
+ /* a timestamp's keys: its bounds and the flags every helper takes */
14
16
  export const NUMBER_KEYS = [
15
17
  "autogenerate",
16
18
  "max",
@@ -21,11 +23,57 @@ export const NUMBER_KEYS = [
21
23
  "relation",
22
24
  "tenant",
23
25
  ] as const
26
+ /* a real's keys: a timestamp's, and the zod schema its value is piped into */
27
+ export const REAL_KEYS = [
28
+ "autogenerate",
29
+ "max",
30
+ "min",
31
+ "mutable",
32
+ "nomutate",
33
+ "private",
34
+ "relation",
35
+ "schema",
36
+ "tenant",
37
+ ] as const
38
+ /* an integer's keys: a real's, and the standard format that numbers its values */
39
+ export const INTEGER_KEYS = [
40
+ "autogenerate",
41
+ "format",
42
+ "max",
43
+ "min",
44
+ "mutable",
45
+ "nomutate",
46
+ "private",
47
+ "relation",
48
+ "schema",
49
+ "tenant",
50
+ ] as const
24
51
  export const JSON_KEYS = ["autogenerate", "maxBytes", "mutable", "nomutate", "private", "relation", "tenant"] as const
25
- export const ENUM_KEYS = ["nomutate", "private", "tenant"] as const
52
+ export const ENUM_KEYS = ["format", "nomutate", "private", "tenant"] as const
53
+ /* a foreign key's keys: a text's but format and schema, as its value is another table's key */
54
+ export const REF_KEYS = [
55
+ "autogenerate",
56
+ "email",
57
+ "imageUri",
58
+ "lowercase",
59
+ "max",
60
+ "min",
61
+ "mutable",
62
+ "name",
63
+ "nomutate",
64
+ "password",
65
+ "pattern",
66
+ "private",
67
+ "relation",
68
+ "tenant",
69
+ "trim",
70
+ "uppercase",
71
+ "url",
72
+ ] as const
26
73
  export const TEXT_KEYS = [
27
74
  "autogenerate",
28
75
  "email",
76
+ "format",
29
77
  "imageUri",
30
78
  "lowercase",
31
79
  "max",
@@ -37,6 +85,7 @@ export const TEXT_KEYS = [
37
85
  "pattern",
38
86
  "private",
39
87
  "relation",
88
+ "schema",
40
89
  "tenant",
41
90
  "trim",
42
91
  "uppercase",
@@ -53,10 +102,10 @@ export type ConstraintKeys = {
53
102
  readonly deletedAt: Keys
54
103
  readonly enum: typeof ENUM_KEYS
55
104
  readonly id: None
56
- readonly integer: Keys
105
+ readonly integer: typeof INTEGER_KEYS
57
106
  readonly json: typeof JSON_KEYS
58
- readonly real: Keys
59
- readonly ref: typeof TEXT_KEYS
107
+ readonly real: typeof REAL_KEYS
108
+ readonly ref: typeof REF_KEYS
60
109
  readonly serialId: None
61
110
  readonly text: typeof TEXT_KEYS
62
111
  readonly timestamp: Keys
@@ -70,10 +119,10 @@ export const CONSTRAINT_KEYS: ConstraintKeys = {
70
119
  deletedAt: NUMBER_KEYS,
71
120
  enum: ENUM_KEYS,
72
121
  id: [],
73
- integer: NUMBER_KEYS,
122
+ integer: INTEGER_KEYS,
74
123
  json: JSON_KEYS,
75
- real: NUMBER_KEYS,
76
- ref: TEXT_KEYS,
124
+ real: REAL_KEYS,
125
+ ref: REF_KEYS,
77
126
  serialId: [],
78
127
  text: TEXT_KEYS,
79
128
  timestamp: NUMBER_KEYS,
@@ -115,10 +164,13 @@ const DROPPED: Readonly<Record<string, { keys: readonly string[]; said: string;
115
164
  url: { keys: ["lowercase", "uppercase"], said: "it is kept as written, but for its trim", what: "a url" },
116
165
  }
117
166
 
118
- /** Refuses a second kind, or a key the one declared would drop, each of which a reader of the tables file believes holds */
167
+ /** Refuses a second kind, a format beside a kind, or a key the one declared would drop, each of which a reader of the tables file believes holds */
119
168
  function refuseKinds(constraints: Readonly<Record<string, ConstantValue>>, place: string): void {
120
169
  const [kind, second] = KINDS.filter((each) => constraints[each] === true)
121
170
  if (second !== undefined) throw new Error(`${place}: ${kind} and ${second} contradict; declare one`)
171
+ /* a format is a text's whole rule, as a kind is */
172
+ if (kind !== undefined && constraints["format"] !== undefined)
173
+ throw new Error(`${place}: format and ${kind} contradict; declare one`)
122
174
  const dropped = DROPPED[kind as string]
123
175
  const key = dropped?.keys.find((each) => constraints[each] === true)
124
176
  if (key !== undefined) throw new Error(`${place}: ${dropped?.what} takes no ${key}: ${dropped?.said}`)
@@ -180,7 +232,7 @@ function refuseMalformed(
180
232
  /**
181
233
  * A column's declared constraints, checked and with comb's defaults applied. Throws on a key the
182
234
  * helper does not take, a value of the wrong type, two keys that contradict, or bounds no value
183
- * meets, each message prefixed with `place`
235
+ * meets, or a format the helper cannot hold, each message prefixed with `place`
184
236
  */
185
237
  export function fieldConstraintsOf(
186
238
  constraints: Readonly<Record<string, ConstantValue>>,
@@ -190,6 +242,7 @@ export function fieldConstraintsOf(
190
242
  helper,
191
243
  measure,
192
244
  place,
245
+ values,
193
246
  }: {
194
247
  /** the column's property name; one starting with `_` is private */
195
248
  fieldName: string
@@ -201,9 +254,12 @@ export function fieldConstraintsOf(
201
254
  measure: "length" | "value"
202
255
  /** where the column is declared, as each error message names it */
203
256
  place: string
257
+ /** the column's enum values, each of which a c.enum's format must take; null when it has none */
258
+ values: readonly string[] | null
204
259
  },
205
260
  ): FieldConstraints {
206
261
  refuseMalformed(constraints, { helper, measure, place })
262
+ const format = formatOf(constraints, { helper, place, values })
207
263
  const relation = constraints["relation"]
208
264
 
209
265
  const autogenerate = filled ? constraints["autogenerate"] !== false : constraints["autogenerate"] === true
@@ -211,6 +267,7 @@ export function fieldConstraintsOf(
211
267
  return {
212
268
  autogenerate,
213
269
  email: constraints["email"] === true,
270
+ format,
214
271
  imageUri: constraints["imageUri"] === true,
215
272
  lowercase: constraints["lowercase"] === true,
216
273
  max: typeof constraints["max"] === "number" ? constraints["max"] : null,