forgedb 0.3.0

ForgeDB — an application database generator. Compiles a declarative .forge schema into tailored Rust database code, a TypeScript SDK, and a REST API.
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
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
# ForgeDB `.forge` Schema Language Reference

The complete, parser-verified reference for the `.forge` schema language: every
type, modifier, relation kind, and directive the compiler accepts. New to
ForgeDB? Start with the [Getting Started](./GETTING_STARTED.md) guide, then use
this as the lookup reference. For 18 worked schemas across many domains, see
[`examples/`](../examples/README.md).

> **Verified against the parser.** Every rule below is grounded in
> `crates/parser/src/{ast.rs, lexer.rs, parser/core.rs}` and the validator. Where
> older docs disagree with this file, this file is correct — see
> [§10 Known Invalid Patterns](#10-known-invalid-patterns-parser-rejects).

---

## 1. Model/Entity Syntax

### Basic Form
```
ModelName {
  field: type
  field: type
}
```

**Rules:**
- Model name must be **PascalCase** (validated in `crates/parser/src/parser/core.rs:691-696` via `validate_model_name`)
- Models must contain at least one field (`crates/parser/src/parser/core.rs:768`)
- Field names must be **snake_case** (validated in `crates/parser/src/parser/core.rs:462-466` via `validate_field_name`)
- Model names must be unique within schema (`crates/parser/src/parser/core.rs:814-818`)

### Example
```
User {
  id: +uuid
  email: &string @email
  created_at: +timestamp
}
```

---

## 2. Field Syntax

### Form
```
name: [MODIFIER]type [@directive ...]
```

### Position of Modifiers
Modifiers (`+`, `&`, `^`) appear **between the colon and type name**:
```
field: +type      // auto-generate
field: &type      // unique
field: ^type      // indexed
field: +&^type    // multiple modifiers allowed (any combination)
```

**Nullable postfix** (`?`) can appear **after the type**:
```
field: string?              // nullable string
field: MyStruct?            // optional struct reference
field: ?string              // prefix nullable also works
field: +uuid?               // auto-gen + nullable
```

### Field Declaration Rules
- Field name is **required** and must be **snake_case** (`crates/validation/src/lib.rs:296-309`)
- Type is **required** immediately after `:`
- Modifiers (`+`, `&`, `^`) are **optional** and appear **before the type** (`crates/parser/src/parser/core.rs:471-492`)
- Constraints (`@...`) are **optional** and appear **after the type** (`crates/parser/src/parser/core.rs:534-595`)
- Field names must be **unique within a model** (`crates/parser/src/parser/core.rs:752-758`)

### Example
```
username: &^string @length(3, 50)       // unique, indexed string with a length constraint
age: ?i32 @min(0) @max(120)             // optional int with numeric range constraints
status: string? @default("pending")     // nullable string with a default (semantic-only marker)
```

---

## 3. Scalar Types (Complete List)

**Verified from `crates/parser/src/lexer.rs` (Token enum) and `crates/parser/src/ast.rs` (FieldType enum):**

| Type      | Form               | Rust Equivalent     | Notes                            |
|-----------|-------------------|---------------------|----------------------------------|
| `u32`     | `u32`             | `u32`               | Unsigned 32-bit integer          |
| `u64`     | `u64`             | `u64`               | Unsigned 64-bit integer          |
| `i32`     | `i32`             | `i32`               | Signed 32-bit integer            |
| `i64`     | `i64`             | `i64`               | Signed 64-bit integer            |
| `f64`     | `f64`             | `f64`               | Floating-point 64-bit            |
| `bool`    | `bool`            | `bool`              | Boolean (true/false)             |
| `string`  | `string`          | `String`            | Variable-length UTF-8 string     |
| `json`    | `json`            | `serde_json::Value` | Arbitrary JSON value (variable-length column, stored as serialized JSON) |
| `decimal` | `decimal`         | `rust_decimal::Decimal` | Exact fixed-point decimal (money/quantity); fixed 16-byte column, JSON string on the wire |
| `uuid`    | `uuid`            | `uuid::Uuid`        | Universal unique identifier      |
| `timestamp` | `timestamp`     | `i64`               | Unix timestamp (milliseconds)    |
| `char(N)` | `char(8)`         | `[u8; 8]`           | Fixed-size byte array |

**Key points:**
- No `text` type — use `string`
- `char(N)` is parsed as `FieldType::Char(usize)` and requires `(...)` syntax (`crates/parser/src/parser/core.rs:354-369`)
- `json` rides the same variable-length column path as `string` (its serialized JSON, always valid UTF-8, is stored via the string column) but is typed `serde_json::Value`. It is **not indexable, filterable, or sortable** (no `^`/`&` index, no REST `?field=` filter/sort, no `find_by_*`) — JSON has no total order the closed-set matcher can key on. `json?` uses the same 1-byte presence tag as `string?`, so `None` and `Some(Value::Null)` round-trip distinctly.
- `decimal` is an **exact** fixed-point number (`rust_decimal::Decimal`) for money/quantity where `f64` would drift. It rides the fixed **16-byte column** path (like `uuid`), encoded via `Decimal::serialize()`/`deserialize()`. It serializes to/from JSON as a **string** (precision-preserving; the TS SDK types it `string`, OpenAPI `{type:string,format:decimal}`). Because `Decimal` is `Ord`+`Hash` it **is filterable, sortable, and indexable** (`^`/`&`/composite `@index` + `find_by_*`) — the index key is normalized (`.normalize()`) so scale-only differences (`1.0` vs `1.00`) share one bucket. `decimal?` (`Option<Decimal>`) rides the same nullable fixed-byte path as `timestamp?`/`u64?`. Bare `decimal` only — `decimal(p, s)` precision/scale metadata is not yet parsed (deferred).

### Enum types

A closed set of named values is declared as a top-level `enum` (a sibling of `struct`/model) and referenced from a field by its bare PascalCase name (no sigil — sigils `*`/`?`/`[]` are relations):

```
enum OrderStatus { Pending, Paid, Shipped, Delivered, Cancelled }

Order {
  id: +uuid
  status: ^OrderStatus       // indexed enum field
  prev_status: OrderStatus?  // nullable enum -> Option
}
```

- **Declaration:** `enum Name { V1, V2, ... }` — name PascalCase (parser-enforced), variants PascalCase, unique, non-empty; trailing comma optional. Enums may be declared **before or after** the models that reference them.
- **Storage:** a fixed **1-byte `u8` discriminant** column (variants map to `0..N` in declaration order; a codegen error if an enum has more than 256 variants). Nullable enum is a 2-byte `[present, disc]` column, so `None` and `Some(variant-0)` round-trip distinctly.
- **Rust:** a fieldless `#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]` enum. **Serialized as the variant NAME string**, so REST / TS / JSON all agree on `"Active"`.
- **TypeScript:** a closed string union (`export type OrderStatus = "Pending" | ...`). **OpenAPI:** `{ "type": "string", "enum": [...] }`.
- **Filter / sort / index:** because the enum is `Ord`+`Hash` it **is filterable, sortable, and indexable** (`^`/`&`/composite `@index` + `find_by_*`) — sort orders by **declaration order** (the discriminant), and the index key is the variant **name** string. No runtime validation is needed on the Rust path (a closed enum cannot hold an invalid variant); an invalid variant string on the REST boundary fails serde `Deserialize` → 4xx automatically.
- A bare PascalCase identifier used as a field type must resolve to a declared enum (or an inline `struct`); otherwise it is an "unknown type" error.

---

## 4. Field Modifiers (Symbols)

### Complete List

| Symbol | Name          | Position | Meaning                                              | Valid On         | Example           |
|--------|---------------|----------|------------------------------------------------------|------------------|-------------------|
| `+`    | Auto-generate | Prefix   | Fill value on insert (`uuid`/`timestamp` only; integer is a marker — see below) | u32, u64, uuid, timestamp | `id: +uuid` |
| `&`    | Unique        | Prefix   | Field value must be unique (enforced)                | Any type         | `email: &string` |
| `^`    | Indexed       | Prefix   | Create index on this field for faster queries        | Any type         | `slug: ^string`   |
| `?`    | Nullable      | Postfix OR Prefix | Field is optional (NULL allowed)         | Any type         | `age: i32?` or `?i32` |

**Placement rules** (from `crates/parser/src/parser/core.rs:471-492` and `498-523`):
- `+`, `&`, `^` are **prefix modifiers** parsed **before type name**
- Multiple modifiers can be combined: `field: +&^type`
- `?` can appear **before type** (e.g., `?User` for optional reference) or **after type** (e.g., `string?` for nullable primitive)
- Postfix `?` converts struct types to `OptionalStructType` and primitives to `Nullable` wrapper (`crates/parser/src/parser/core.rs:499-523`)

**Validation:**
- `+` (auto-generate) only valid on auto-generatable types: u32, u64, uuid, timestamp (`crates/parser/src/parser/core.rs:526-531`)
  - **But only `uuid`/`timestamp` are actually synthesized on create today.** `+u32`/`+u64` parse
    and mark the field, but the generator does not fill an integer value — a create must supply
    the integer id. Correct integer auto-increment is RFC #187 (blocked on transaction/coordinator
    sequence-allocation design), deliberately unshipped rather than shipped subtly wrong.
- `&` (unique) can be applied to any field (no type restriction in parser)

**NOT implemented:**
- `~` (auto-update) does not exist — the AST `Field` carries only `auto_generate: bool` (the `+`
  modifier). There is no auto-update-on-write modifier.
- **Integer auto-increment** — `+u32`/`+u64` are parsed and marked but *not synthesized*; only
  `+uuid`/`+timestamp` fill on create. See RFC #187.

---

## 5. Relations

### Relation Type Syntax

| Syntax      | Type               | Meaning                              | Example            |
|-------------|-------------------|--------------------------------------|--------------------|
| `[Model]`  | `OneToMany`       | Parent has many children             | `posts: [Post]`    |
| `*Model`   | `RequiredReference` | Must reference a record (FK, non-NULL) | `author: *User` |
| `?Model`   | `OptionalReference` | Can optionally reference (FK, NULL) | `editor: ?User`  |

**Bidirectional M2M detection** (from `crates/parser/src/ast.rs:233-342`):
- When both models have `[OtherModel]` fields pointing to each other **without a corresponding FK**, the parser auto-detects a `ManyToMany` relationship
- Example:
  ```
  Post {
    tags: [Tag]
  }
  Tag {
    posts: [Post]  // Detected as M2M if neither is a FK reference
  }
  ```

**FK Scalar Generation** (from `crates/parser/src/ast.rs:382-388`):
- `RequiredReference(Model)` fields generate scalar FK columns: `model_id: uuid` (example: `author: *User` → `author_id`)
- `OptionalReference(Model)` fields generate nullable FK columns: `editor_id: Option<uuid>`
- OneToMany and ManyToMany fields are **virtual** (not persisted; stored as empty `()`) (`crates/parser/src/ast.rs:389-391`)

---

## 6. Every `@` Directive (Complete List)

### Directives Parsed by Parser Core

**Field-level directives** (attached to individual fields; parsed as `Constraint` structs):

| Directive              | Arguments       | Field Types         | Meaning                                  | Example                      |
|------------------------|-----------------|---------------------|------------------------------------------|------------------------------|
| `@min`                 | `(number)`      | Numeric (u32/u64/i32/i64/f64) | Minimum value — **ENFORCED** (violation → 422)     | `age: u32 @min(13)` |
| `@max`                 | `(number)`      | Numeric (u32/u64/i32/i64/f64) | Maximum value — **ENFORCED** (violation → 422). *Not* a string-length check — use `@length` for strings. | `age: u32 @max(150)` |
| `@length`              | `(min, max)` or `(count)` | `string`  | String length — **ENFORCED** (violation → 422)     | `name: string @length(1, 100)` |
| `@email`               | (none)          | `string`            | Email format — **ENFORCED** (violation → 422)      | `email: string @email` |
| `@url`                 | (none)          | `string`            | URL format — **ENFORCED** (violation → 422)        | `website: string @url` |
| `@pattern`             | `(regex_string)` | `string`           | Regex match — **ENFORCED** via `LazyLock<Regex>` (non-match → 422) | `phone: string @pattern("^[0-9]+$")` |
| `@regex`               | `(pattern)`     | `string`            | Regex match — **ENFORCED** (non-match → 422)       | `handle: string @regex("[a-z]+")` |
| `@default`             | `(value)`       | Any                 | Default value on insert — **semantic-only marker** (not applied at write) | `status: string @default("pending")` |
| `@index`               | (none)          | Any                 | Field-level index marker — **semantic-only**; use the `^` modifier to actually index | `slug: string @index` |
| `@computed`            | (none)          | Any                 | Field is computed (read-only) — **semantic-only marker** | `full_name: string @computed` |
| `@fulltext`            | (none)          | `string`            | Full-text search — **semantic-only marker** (no index generated) | `content: string @fulltext` |
| `@materialized`        | (none)          | Any                 | Field is materialized — **semantic-only marker**   | `count: u32 @materialized` |
| `@relations`           | `(*)` or `(field_list)` | Component refs | Component relation inclusion | `card: tsx://path @relations(*)` |
| `@on_delete`           | `(restrict\|cascade\|set_null)` | FK field (`*Target`/`?Target`) | On-delete referential policy (ENFORCED, delete semantics) | `author: *User @on_delete(cascade)` |

**Model-level directives:**

| Directive              | Arguments       | Meaning                                          | Example                      |
|------------------------|-----------------|--------------------------------------------------|------------------------------|
| `@soft_delete`         | (none)          | Enable soft delete                   | `@soft_delete` in model block |
| `@index`               | `(field1, field2, ...)` | Composite index on multiple fields | `@index(user_id, created_at)` |

> **Quoted string literals.** Directive arguments accept **quoted string literals** in addition
> to numbers and bare identifiers. `@pattern("^[0-9]+$")`, `@regex("...")`, and `@default("text")`
> parse — the lexer tokenizes `"..."` (escapes `\" \\ \n \t \r`; unterminated/multiline strings are
> a lex error). Values are stored as `ConstraintParam::String`, so `@default(pending)` and
> `@default("pending")` are equivalent. `@pattern`/`@regex` are **enforced** (non-match → 422);
> `@default` remains a **semantic-only marker** (parsed, not enforced).
> Superseded the earlier limitation note that said `"` was an unexpected character.

**Parser source:** `crates/parser/src/parser/core.rs:113-184` (constraint parsing, incl. the `Token::Str` arm), `crates/parser/src/parser/core.rs:381-447` (directive parsing); string-literal lexing in `crates/parser/src/lexer.rs` (`read_string`)

**`@on_delete` (ENFORCED — delete semantics):**
- `@on_delete(restrict | cascade | set_null)` on a relation FK field (`*Target` required / `?Target` optional) declares what happens to children when the parent is deleted. It parses as a generic directive (bare-identifier arg — no special lexer rule) and is **enforced by codegen** in the generated `Database::delete_<parent>` wrapper:
  - **`restrict`** (also the DEFAULT when `@on_delete` is absent): refuse to delete a parent that still has any live child referencing it (→ `ValidationError::ReferencedByChildren`, HTTP 409).
  - **`cascade`**: recursively delete every referencing child (each child's own `@on_delete` rules fire, so multi-level chains work; a pathological FK cycle is bounded by `MAX_CASCADE_DEPTH`).
  - **`set_null`**: null each referencing child's FK — **only valid on an OPTIONAL FK (`?Target`)**; `@on_delete(set_null)` on a required `*Target` is a hard codegen error.
- The REST `DELETE /{id}` route goes through this wrapper (Rust API + REST both get integrity). The direct `db.<model>.delete` storage path skips these checks. M2M links to a cascade-deleted model are also unlinked.

**Semantic vs. Enforcement:**
- Directives marked "(semantic)" are parsed but **not enforced by the parser**; enforcement is left to validators/codegen
- Example: `@email` is parsed but the parser doesn't validate email format; that's done elsewhere

---

## 7. Composite & Collection Constructs

### Fixed-Size Arrays

**Syntax:**
```
field: [type; count]
```

**Rules:**
- Inner type can be primitive (`u32`, `string`, etc.) or struct name
- Count must be numeric literal
- Parsed as `FieldType::FixedArray(Box<FieldType>, usize)` (`crates/parser/src/ast.rs:55`)
- **Must be fixed-size types** (can't use in variable-length types like `string` inside arrays in structs) (`crates/parser/src/ast.rs:169-176`)

**Example:**
```
Product {
  image_urls: [char(255); 5]    // array of 5 strings (max 255 chars each)
  scores: [u32; 10]              // array of 10 unsigned ints
}
```

### Inline Structs

**Definition syntax:**
```
struct StructName {
  field: type
  field: type
}
```

**Usage in models:**
```
field: StructName          // required struct field
field: StructName?         // optional struct field
```

**Rules:**
- Struct names must be **PascalCase** (same as models)
- Structs can **only contain fixed-size types** (`crates/parser/src/ast.rs:169-176`)
- Struct references in fields are stored as `FieldType::StructType(name)` or `FieldType::OptionalStructType(name)` (`crates/parser/src/ast.rs:56-57`)
- Cannot contain variable-length types (string, relations, components) (`crates/parser/src/ast.rs:169-176`)

**Example:**
```
struct Address {
  street: char(100)
  city: char(50)
  zip: char(10)
}

User {
  id: +uuid
  address: Address?         // optional embedded Address
}
```

### Composite Indexes

**Syntax (model-level):**
```
ModelName {
  field1: type
  field2: type
  @index(field1, field2)
}
```

**Rules:**
- Must include **at least 2 fields** (`crates/parser/src/parser/core.rs:438-440`)
- Fields must exist in the model (`crates/parser/src/parser/core.rs:773-782`)
- Parsed as `CompositeIndex { fields: Vec<String> }` and stored in `Model.composite_indexes` (`crates/parser/src/ast.rs:127`)

**Example:**
```
Order {
  user_id: uuid
  created_at: timestamp
  @index(user_id, created_at)
}
```

### Soft Delete

**Syntax (model-level):**
```
ModelName {
  field: type
  @soft_delete
}
```

**Rules:**
- Model-level directive (not field-level)
- Sets `Model.soft_delete: bool` to `true` (`crates/parser/src/ast.rs:128`)
- Parsed in `crates/parser/src/parser/core.rs:735-738`

**Example:**
```
User {
  id: +uuid
  email: string
  @soft_delete
}
```

---

## 8. Component References

### Syntax

```
field: protocol://path [@relations(...)]
```

**Protocols:**
- `tsx://` — TSX (TypeScript React) component
- `jsx://` — JSX component
- `api://` — API route handler

**Path syntax:**
- Path is a series of identifiers separated by `/`
- Examples: `components/user/Card`, `pages/user/profile`, `routes/user/update`

**@relations modifier:**
```
@relations(*)                          // Include all relation fields
@relations(field1, field2, ...)        // Include specific relations
```

**Rules:**
- `ComponentProtocol` enum: `Tsx`, `Jsx`, `Api` (`crates/parser/src/ast.rs:67-72`)
- `ComponentReference` struct stores protocol, path, and relation inclusion (`crates/parser/src/ast.rs:84-88`)
- `@relations` is **only valid on component fields** (`crates/parser/src/parser/core.rs:576-580`)
- Parsed in `crates/parser/src/parser/core.rs:299-330`

**Examples:**
```
User {
  id: +uuid
  posts: [Post]
  comments: [Comment]
  
  profileCard: tsx://components/user/ProfileCard @relations(*)
  avatar: jsx://components/user/Avatar @relations(posts)
  updateEndpoint: api://routes/user/update
}
```

---

## 9. Comments and Whitespace

### Supported Comments

**Line comments:**
```
// This is a comment
field: string  // inline comment
```

**Parsed as:** `Token::Slash` followed by another `Token::Slash`, then skips to end of line (`crates/parser/src/lexer.rs:160-172`)

**NOT supported:**
- Block comments (`/* ... */`) — **NOT parsed by lexer** (`crates/parser/src/lexer.rs` has no `/*` handling)
  - Example files use them (e.g., `apps/vscode-forgedb/examples/example.forge:42-43`), but they will **fail to parse** in the actual CLI
  - This is a **drift issue** — example.forge uses `/* */` but parser doesn't support it

### Whitespace Rules

- **Newlines are significant** — parsed as `Token::Newline` and used to delimit logical tokens
- **Horizontal whitespace** (space, tab) is skipped via `skip_whitespace()` (`crates/parser/src/lexer.rs:93-101`)
- **Carriage returns** (`\r`) are also skipped (`crates/parser/src/lexer.rs:94-95`)
- Model/struct definitions can span multiple lines (newlines are skipped between major tokens)

**Terminators:**
- No semicolons required for fields or models (only `{` and `}` block delimiters)
- `@` directives must appear after field type (before newline or next constraint)

---

## 10. Known Invalid Patterns (Parser Rejects)

### Cannot Parse

1. **Block comments**
   ```
   /* This will fail */
   User { id: +uuid }
   ```
   Parser error: unexpected `/` and `*` tokens

2. **@on_delete(set_null) on a REQUIRED FK** (codegen error)
   ```
   author: *User @on_delete(set_null)   // CODEGEN ERROR: a required FK can't be nulled
   ```
   `@on_delete` itself parses and is enforced (see §6); only `set_null` on a required
   `*Target` is rejected (use `?Target`, or `cascade`/`restrict`).

3. **Duplicate field names**
   ```
   User {
     id: +uuid
     id: string    // ERROR: duplicate field
   }
   ```

4. **Duplicate model/struct names**
   ```
   User { id: +uuid }
   User { email: string }   // ERROR: duplicate model
   ```

5. **Model/struct without fields**
   ```
   User { }   // ERROR: model has no fields
   ```

6. **Wrong auto-generate type**
   ```
   count: string +   // ERROR: only u32, u64, uuid, timestamp support +
   ```

7. **Nullable primitive inline without wrapping in parent**
   ```
   age: ?u32       // OK
   age: u32?       // OK
   age: ??u32      // Double nullable — probably invalid, untested
   ```

8. **Non-PascalCase model/struct names**
   ```
   user { id: +uuid }   // ERROR: must be 'User' (PascalCase)
   ```

9. **Non-snake_case field names**
   ```
   User {
     userId: +uuid   // ERROR: must be 'user_id' (snake_case)
   }
   ```

10. **Struct containing variable-length types**
    ```
    struct Address {
      street: string    // ERROR: string is variable-length
    }
    ```

11. **Composite index with < 2 fields**
    ```
    Order {
      id: +uuid
      @index(id)    // ERROR: need at least 2 fields
    }
    ```

12. **Composite index referencing non-existent field**
    ```
    Order {
      id: +uuid
      @index(id, missing_field)   // ERROR: field not found
    }
    ```

13. **Component field without protocol**
    ```
    card: path/to/component     // ERROR: need tsx://, jsx://, or api://
    ```

14. **@relations on non-component field**
    ```
    id: +uuid @relations(posts)   // ERROR: only component fields
    ```

15. **Relation to undefined model**
    ```
    author: *Undefined    // ERROR: model 'Undefined' doesn't exist
    ```

---

## 11. Example Valid Schemas

### Minimal Valid Schema
```
User {
  id: +uuid
  email: &string
}
```

### With Modifiers and Constraints
```
Post {
  id: +uuid
  title: &string @length(1, 200)
  slug: ^&string @length(1, 100)
  content: string
  view_count: u32 @default(0)
  published: bool @default(false)
  published_at: timestamp?
  created_at: +timestamp
  author: *User
  comments: [Comment]
}

Comment {
  id: +uuid
  text: &string @length(1, 1000)
  author: *User
  post: *Post
  created_at: +timestamp
}

User {
  id: +uuid
  email: ^&string @email
  posts: [Post]
  comments: [Comment]
}
```

### With Structs
```
struct GeoLocation {
  latitude: f64
  longitude: f64
}

Venue {
  id: +uuid
  name: &string
  location: GeoLocation
  created_at: +timestamp
}
```

### With Components
```
User {
  id: +uuid
  email: string
  posts: [Post]
  
  profileCard: tsx://components/user/ProfileCard @relations(posts)
  updateEndpoint: api://routes/user/update
}
```

### With Composite Indexes
```
Order {
  id: +uuid
  user_id: uuid
  status: string
  created_at: timestamp
  
  @index(user_id, created_at)
  @index(status, created_at)
}
```

---

## 12. Where the parser lives

The rules in this reference are grounded in the parser and validator source:

- **AST:** `crates/parser/src/ast.rs`
- **Lexer (tokens):** `crates/parser/src/lexer.rs`
- **Parser logic:** `crates/parser/src/parser/core.rs`
- **Validation:** `crates/validation/src/lib.rs`
- **Example schemas:** [`examples/`](../examples/README.md) and `apps/vscode-forgedb/examples/example.forge`

---

## Summary for New Schema Authors

**Write schemas using this recipe:**

1. **Define models** (PascalCase names) with **snake_case fields**
2. **Use type modifiers** (`+`, `&`, `^`) **before type**, nullable `?` **after type**
3. **Valid scalar types:** u32, u64, i32, i64, f64, bool, string, json, decimal, uuid, timestamp, char(N)
4. **Relations:** `[Model]` (one-to-many), `*Model` (required FK), `?Model` (optional FK)
5. **Constraints are ENFORCED at write (violation → 422):** `@min`/`@max` (numeric only), `@length` (string length), `@email`, `@url`, `@pattern`/`@regex`. Still semantic-only markers (parsed, not applied): `@default`, `@computed`, `@fulltext`, `@materialized`, field-level `@index`
6. **Composite indexes:** `@index(field1, field2, ...)` at model level (≥2 fields)
7. **Structs:** Define with `struct Name { ... }` and use in models (fixed-size only)
7b. **Enums:** Define with `enum Name { V1, V2, ... }` (PascalCase variants) and reference by bare name; stored as a 1-byte discriminant, serialized as the variant name string, filterable/sortable/indexable
8. **Components:** `field: tsx://path @relations(*)`
9. **Comments:** Only `//` line comments work; `/* */` blocks will **fail to parse**
10. **DO NOT use:** `~` (auto-update), `text` (use `string`), block comments `/* */`, duplicate names, non-PascalCase models, non-snake_case fields

**Verify with:**
```bash
cargo run -- validate --config forgedb.toml
```