safe-migrate 0.3.2

Lint PostgreSQL migrations against live database statistics to prevent blocking locks
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
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
# Columns AST Reference for safe-migrate

## Status

Inspection status: complete for all core column nodes and AlterColumnOption variants.

This document is derived from direct inspection of squawk.rs and should be treated as the
current source of truth for safe-migrate column handling.

All claims are AST-verified via grep and line-range inspection.

---

## Documentation Contract

1. Only document AST behavior that has been directly verified.
2. Do not infer PostgreSQL semantics from missing AST accessors.
3. Distinguish verified facts from unresolved areas.
4. Assume additional nodes or helpers may exist outside the inspected surface.

---

## Handwritten Extension Policy

One handwritten extension exists for column nodes:

```
impl ast::RenameColumn  (line 38429)
```

No handwritten extensions exist for `Column`, `AddColumn`, `AlterColumn`, or `DropColumn`.

This was established by exhaustive grep of all `impl ast::*` blocks in squawk.rs:

```bash
grep -n "^impl ast::" squawk.rs
```

This grep covers lines 38145-39260 and is the authoritative source for all handwritten
extensions across the entire codebase. The complete inventory:

| Line  | Node                  | Methods                                      |
|-------|-----------------------|----------------------------------------------|
| 38145 | `ast::Literal`        | `kind()`                                     |
| 38168 | `ast::Constraint`     | `constraint_name()`                          |
| 38233 | `ast::BinExpr`        | `lhs()`, `rhs()`, `op()`                    |
| 38313 | `ast::PostfixExpr`    | `op()`                                       |
| 38376 | `ast::FieldExpr`      | `base()`, `field()`                          |
| 38390 | `ast::IndexExpr`      | `base()`, `index()`                          |
| 38401 | `ast::SliceExpr`      | `base()`, `start()`, `end()`                |
| 38429 | `ast::RenameColumn`   | `from()`, `to()`                             |
| 38440 | `ast::ForeignKeyConstraint` | `from_columns()`, `to_columns()`       |
| 38451 | `ast::BetweenExpr`    | `target()`, `start()`, `end()`              |
| 38466 | `ast::WhenClause`     | `condition()`, `then()`                      |
| 38477 | `ast::CompoundSelect` | `lhs()`, `rhs()`                             |
| 38488 | `ast::NameRef`        | `text()`, `is_quoted()`                      |
| 38500 | `ast::Name`           | `text()`, `is_quoted()`                      |
| 38565 | `ast::CharType`       | `text()`                                     |
| 38576 | `ast::Vacuum`         | `is_full()`                                  |
| 38599 | `ast::OpSig`          | `lhs()`, `rhs()`                             |
| 38611 | `ast::CastSig`        | `lhs()`, `rhs()`                             |
| 38638 | `ast::WithQuery`      | `with_clause()`                              |
| 38645 | `ast::SelectVariant`  | `target_list()`                              |
| 38663 | `ast::FunctionSig`    | `HasParamList` trait impl                    |
| 38664 | `ast::Aggregate`      | `HasParamList` trait impl                    |
| 38666 | `ast::Name`           | `NameLike` trait impl                        |
| 38672 | `ast::NameRef`        | `NameLike` trait impl                        |
| 38679 | `ast::Select`         | `HasWithClause` trait impl                   |
| 38680 | `ast::SelectInto`     | `HasWithClause` trait impl                   |
| 38681 | `ast::Insert`         | `HasWithClause` trait impl                   |
| 38682 | `ast::Update`         | `HasWithClause` trait impl                   |
| 38683 | `ast::Delete`         | `HasWithClause` trait impl                   |
| 38685 | `ast::CreateTable`    | `HasCreateTable` trait impl                  |
| 38686 | `ast::CreateForeignTable` | `HasCreateTable` trait impl              |
| 38687 | `ast::CreateTableLike`| `HasCreateTable` trait impl                  |
| 39252 | `ast::Whitespace`     | `spans_multiple_lines()`                     |
| 39260 | `ast::Comment`        | `kind()`                                     |

All other AST files reference this table rather than re-running the grep.

---

# High-Level Column Model

The verified AST surface exposes:

**Core column nodes:**
- `Column`
- `ColumnList`
- `AddColumn`
- `AlterColumn`
- `DropColumn`
- `RenameColumn`

**Alter column dispatch:**
- `AlterColumnOption` (21-member enum)

**Column context nodes:**
- `TableArg` (enum containing `Column`)
- `SetSingleColumn`
- `SetMultipleColumns`
- `SetColumn` (enum)

---

# Core Column Nodes

## Column

### Verified Accessors (line 3279)

```rust
pub fn collate(&self) -> Option<Collate>
pub fn compression_method(&self) -> Option<CompressionMethod>
pub fn constraints(&self) -> AstChildren<ColumnConstraint>
pub fn deferrable_constraint_option(&self) -> Option<DeferrableConstraintOption>
pub fn enforced(&self) -> Option<Enforced>
pub fn field_expr(&self) -> Option<FieldExpr>
pub fn index_expr(&self) -> Option<IndexExpr>
pub fn initially_deferred_constraint_option(&self) -> Option<InitiallyDeferredConstraintOption>
pub fn initially_immediate_constraint_option(&self) -> Option<InitiallyImmediateConstraintOption>
pub fn name(&self) -> Option<Name>
pub fn name_ref(&self) -> Option<NameRef>
pub fn not_deferrable_constraint_option(&self) -> Option<NotDeferrableConstraintOption>
pub fn not_enforced(&self) -> Option<NotEnforced>
pub fn storage(&self) -> Option<Storage>
pub fn ty(&self) -> Option<Type>
pub fn with_options(&self) -> Option<WithOptions>
pub fn period_token(&self) -> Option<SyntaxToken>
```

### Membership

`Column` is a member of the `TableArg` enum (line 19931, 37505), alongside
`LikeClause` and `TableConstraint`. See the TableArg section below — this is a
3-member enum, not 2-member as an earlier draft of this document stated.
Columns appear as `TableArg::Column` inside `TableArgList`.

### Notes

- Both `name()` and `name_ref()` exist. Context determines which is populated.
- `deferrable_constraint_option`, `enforced`, `not_enforced`, `not_deferrable_constraint_option`,
  `initially_deferred_constraint_option`, `initially_immediate_constraint_option` are present
  directly on `Column` — these are column-level deferrability and enforcement modifiers,
  not constraint-level.
- `constraints()` returns `AstChildren<ColumnConstraint>`, not `AstChildren<Constraint>`.
  See constraints.md for the `ColumnConstraint` enum.

### safe-migrate guidance

```rust
ColumnFact {
    name: String,                           // from name() or name_ref()
    type_ref: TypeIr,                       // from ty()
    constraints: Vec<ColumnConstraintFact>, // from constraints()
    collation: Option<String>,              // from collate()
    storage: Option<StorageKind>,           // from storage()
    compression: Option<String>,            // from compression_method()
    deferrable: Option<DeferrableState>,    // from deferrable/not_deferrable accessors
    enforced: Option<EnforcedState>,        // from enforced/not_enforced
}
```

Column identity must be preserved when extracting inline constraints.

---

## ColumnList

### Verified Accessors (line 3358)

```rust
pub fn columns(&self) -> AstChildren<Column>
pub fn l_paren_token(&self) -> Option<SyntaxToken>
pub fn r_paren_token(&self) -> Option<SyntaxToken>
```

### Meaning

Ordered list of columns. Used in:
- `PrimaryKeyConstraint`
- `UniqueConstraint`
- `ForeignKeyConstraint` (via `from_columns()` / `to_columns()`)
- `INSERT ... (col1, col2)`
- `COPY`
- `CreateView`, `CreateMaterializedView`
- Many other contexts

---

## AddColumn

### Verified Accessors (line 175)

```rust
pub fn collate(&self) -> Option<Collate>
pub fn constraints(&self) -> AstChildren<Constraint>
pub fn if_not_exists(&self) -> Option<IfNotExists>
pub fn name(&self) -> Option<Name>
pub fn ty(&self) -> Option<Type>
pub fn add_token(&self) -> Option<SyntaxToken>
pub fn column_token(&self) -> Option<SyntaxToken>
```

### Membership

Member of `AlterTableAction` (line 19416, 33166).

### Important Distinction

`AddColumn.constraints()` returns `AstChildren<Constraint>` — the general enum.
`Column.constraints()` returns `AstChildren<ColumnConstraint>` — the column-specific enum.

This is a verified difference. See constraints.md for enum membership details.

### safe-migrate guidance

```rust
Mutation::AddColumn {
    name: String,
    type_ref: TypeIr,
    constraints: Vec<ConstraintFact>,
    collation: Option<String>,
    if_not_exists: bool,
}
```

---

## AlterColumn

### Verified Accessors (line 595)

```rust
pub fn name_ref(&self) -> Option<NameRef>
pub fn option(&self) -> Option<AlterColumnOption>
pub fn alter_token(&self) -> Option<SyntaxToken>
pub fn column_token(&self) -> Option<SyntaxToken>
```

### Membership

Member of `AlterTableAction` (line 19418, 33168).

### Meaning

Represents:

```sql
ALTER TABLE t ALTER COLUMN col <option>
```

The column being altered is identified by `name_ref()`.
The operation is dispatched through `option()` → `AlterColumnOption`.

### safe-migrate guidance

```rust
Mutation::AlterColumn {
    column_name: String,           // from name_ref()
    operation: AlterColumnOp,      // from option() -> AlterColumnOption
}
```

See AlterColumnOption section below for the full dispatch surface.

---

## DropColumn

### Verified Accessors (line 6819)

```rust
pub fn if_exists(&self) -> Option<IfExists>
pub fn name_ref(&self) -> Option<NameRef>
pub fn cascade_token(&self) -> Option<SyntaxToken>
pub fn column_token(&self) -> Option<SyntaxToken>
pub fn drop_token(&self) -> Option<SyntaxToken>
pub fn restrict_token(&self) -> Option<SyntaxToken>
```

### Membership

Member of `AlterTableAction` (line 19426, 33184).

### safe-migrate guidance

```rust
Mutation::DropColumn {
    name: String,
    if_exists: bool,
    cascade: bool,
}
```

CASCADE must propagate through the dependency graph to all constraints,
indexes, and views referencing the dropped column.

---

## RenameColumn

### Verified Accessors — Generated (line 15034)

```rust
pub fn column_token(&self) -> Option<SyntaxToken>
pub fn rename_token(&self) -> Option<SyntaxToken>
pub fn to_token(&self) -> Option<SyntaxToken>
```

### Verified Accessors — Handwritten (line 38429)

```rust
pub fn from(&self) -> Option<ast::NameRef>   // old name, nth(0)
pub fn to(&self) -> Option<ast::NameRef>     // new name, nth(1)
```

### Grammar vs Implementation Discrepancy

postgresql.ungram states:

```
RenameColumn =
  'rename' 'column'? from:NameRef 'to' to:Name
```

This labels `to` as type `Name`, not `NameRef`. However, the actual Rust
implementation at line 38429-38436 uses `support::children::<NameRef>(&self.syntax)`
for both `nth(0)` and `nth(1)` — meaning the implementation treats both as
`NameRef` regardless of what the grammar label states. Since
`support::children::<T>()` filters the child list by a single concrete type `T`,
if the actual syntax node for the new name were genuinely a different type
(`Name` rather than `NameRef`), `.nth(1)` would not find it this way.

This document follows the verified Rust implementation (both `NameRef`) as the
source of truth here, since it is the literal compiled behavior. The grammar
label is noted as a discrepancy that should be flagged to the squawk maintainers
or re-verified against a newer grammar version, but does not change what
safe-migrate's code will actually receive at runtime.

### Membership

Member of `AlterTableAction` (line 19444, 33218).
Also member of `AlterMaterializedViewAction` (line 19394, 32928).

### Important Finding

The generated accessors expose **only syntax tokens**.
The old and new column names are **only accessible through the handwritten extension**.
Any code reading `RenameColumn` without using `from()` and `to()` cannot extract the names.

### safe-migrate guidance

```rust
Mutation::RenameColumn {
    from: String,    // from handwritten from()
    to: String,      // from handwritten to()
}
```

Treat as identity preservation. Do not model as drop + create.

---

# AlterColumnOption

## Enum Definition (line 19339)

```rust
pub enum AlterColumnOption {
    AddGenerated(AddGenerated),
    DropDefault(DropDefault),
    DropExpression(DropExpression),
    DropIdentity(DropIdentity),
    DropNotNull(DropNotNull),
    Inherit(Inherit),
    NoInherit(NoInherit),
    ResetOptions(ResetOptions),
    Restart(Restart),
    SetCompression(SetCompression),
    SetDefault(SetDefault),
    SetExpression(SetExpression),
    SetGenerated(SetGenerated),
    SetGeneratedOptions(SetGeneratedOptions),
    SetNotNull(SetNotNull),
    SetOptions(SetOptions),
    SetOptionsList(SetOptionsList),
    SetSequenceOption(SetSequenceOption),
    SetStatistics(SetStatistics),
    SetStorage(SetStorage),
    SetType(SetType),
}
```

21 members. Fully verified at line 19339 and cross-checked against `From<X> for AlterColumnOption`
impls at lines 32549-32672.

---

## Key AlterColumnOption Variants

### DropNotNull (line 7409)

```rust
pub fn drop_token(&self) -> Option<SyntaxToken>
pub fn not_token(&self) -> Option<SyntaxToken>
pub fn null_token(&self) -> Option<SyntaxToken>
```

Presence detection only — no payload beyond keyword tokens.

Represents: `ALTER COLUMN col DROP NOT NULL`

---

### SetDefault (line 16625)

```rust
pub fn expr(&self) -> Option<Expr>
pub fn default_token(&self) -> Option<SyntaxToken>
pub fn set_token(&self) -> Option<SyntaxToken>
```

Expression payload accessible via `expr()`.

Represents: `ALTER COLUMN col SET DEFAULT <expr>`

---

### SetCompression (line 16560)

```rust
pub fn compression_token(&self) -> Option<SyntaxToken>
pub fn set_token(&self) -> Option<SyntaxToken>
```

### Grammar Confirmation — RESOLVED

postgresql.ungram confirms: `SetCompression = 'set' 'compression'` — the
complete rule. The compression method name (e.g. `pglz`, `lz4`) is genuinely
absent from the grammar, not an accessor gap.

### Status
```
Grammar verified — FULLY RESOLVED
Compression method name confirmed absent from grammar entirely.
```

---

### SetStatistics (line 16994)

```rust
pub fn set_token(&self) -> Option<SyntaxToken>
pub fn statistics_token(&self) -> Option<SyntaxToken>
```

### Grammar Confirmation — RESOLVED

postgresql.ungram confirms: `SetStatistics = 'set' 'statistics'` — the
complete rule. The statistics target value is genuinely absent from the
grammar, not an accessor gap.

### Status
```
Grammar verified — FULLY RESOLVED
Statistics target value confirmed absent from grammar entirely.
```

---

### SetStorage (line 17009)

```rust
pub fn set_token(&self) -> Option<SyntaxToken>
pub fn storage_token(&self) -> Option<SyntaxToken>
```

### Grammar Confirmation — RESOLVED

postgresql.ungram confirms: `SetStorage = 'set' 'storage'` — the complete
rule. The storage mode (`PLAIN`/`EXTERNAL`/`EXTENDED`/`MAIN`) is genuinely
absent from the grammar, not an accessor gap.

### Status
```
Grammar verified — FULLY RESOLVED
Storage mode confirmed absent from grammar entirely.
```

---

### SetType (line 17086)

```rust
pub fn collate(&self) -> Option<Collate>
pub fn ty(&self) -> Option<Type>
pub fn set_token(&self) -> Option<SyntaxToken>
pub fn type_token(&self) -> Option<SyntaxToken>
```

New type fully accessible via `ty()`. Optional collation via `collate()`.

Represents: `ALTER COLUMN col SET DATA TYPE <type> [COLLATE <collation>]`

---

### SetNotNull

Token-only presence node. Represents: `ALTER COLUMN col SET NOT NULL`

---

### DropDefault

Token-only presence node. Represents: `ALTER COLUMN col DROP DEFAULT`

---

### Remaining Variants — Now Grammar-Resolved

All 13 previously uninspected variants have been cross-checked against
postgresql.ungram:

**Variants with real payloads:**

```
Inherit =
  'inherit' Path

NoInherit =
  'no' 'inherit' Path

SetExpression =
  'set' 'expression' Expr

SetOptions =
  'set' AttributeList

SetOptionsList =
  'set' 'options' AlterOptionList
```

- `Inherit` / `NoInherit`: carry the parent table `Path` for inheritance
  manipulation on a column's generated/identity behavior context.
- `SetExpression`: carries a full `Expr` — used for `SET EXPRESSION AS (expr)`
  on generated columns. Should flow into ExprIr like other expression-bearing nodes.
- `SetOptions`: carries an `AttributeList` of key-value option pairs.
- `SetOptionsList`: carries an `AlterOptionList` — a different option
  representation than `SetOptions`. Both exist as distinct grammar paths;
  the resolver must handle both shapes.

**Variants confirmed grammar-empty (no payload beyond keywords):**

```
AddGenerated =
  'add'

DropExpression =
  'drop' 'expression' IfExists?

DropIdentity =
  'drop' 'identity' IfExists?

ResetOptions =
  'reset' '(' ')'

Restart =
  'restart' 'with'?

SetGenerated =
  'set'

SetGeneratedOptions =
  'set' 'generated'
```

- `AddGenerated`: presence-only — detects `ADD GENERATED ...` but the
  generated clause details are not captured here (likely require
  cross-referencing a `GeneratedConstraint` node, see constraints.md).
- `DropExpression` / `DropIdentity`: presence + `IfExists?` only — no value
  needed since these are removal operations.
- `ResetOptions`: confirmed empty parens — the specific option names being
  reset are not captured, mirroring the `ConstraintIncludeClause` and
  `MergePartitions` grammar-gap pattern seen elsewhere in this documentation set.
- `Restart`: presence + optional `WITH` token only — the restart value
  itself is not captured in this variant's own grammar (distinct from the
  `Restart` value accessible via `literal()` documented in sequences.md's
  `SequenceOption` — this is a different node despite the same name pattern;
  verify which `Restart` struct is meant before assuming a value is present).
- `SetGenerated` / `SetGeneratedOptions`: both confirmed token-only.

### safe-migrate guidance

```rust
enum AlterColumnOpFact {
    AddGenerated,                              // presence-only
    DropDefault,
    DropExpression { if_exists: bool },
    DropIdentity { if_exists: bool },
    DropNotNull,
    Inherit { parent: QualifiedName },
    NoInherit { parent: QualifiedName },
    ResetOptions,                              // option names not extractable
    Restart,                                   // value not extractable on this variant
    SetCompression,                            // method name not extractable
    SetDefault { expr: ExprIr },
    SetExpression { expr: ExprIr },
    SetGenerated,                              // presence-only
    SetGeneratedOptions,                       // presence-only
    SetNotNull,
    SetOptions { attributes: Vec<AttributeFact> },
    SetOptionsList { options: Vec<AlterOptionFact> },
    SetSequenceOption,                         // payload not extractable, see sequences.md
    SetStatistics,                             // value not extractable
    SetStorage,                                // mode not extractable
    SetType { ty: TypeIr, collation: Option<String> },
}
```

Several variants are presence-only detectors — the AST confirms *that* an
operation occurred but not its specific parameters. Rules built on these
variants can only flag "an X happened," not evaluate the safety of the
specific value being set.

---

# Context Nodes

## TableArg

### CORRECTION (found during grammar cross-check)

An earlier version of this document incorrectly listed `TableArg` as a 2-member
enum. It is a **3-member enum**:

```rust
pub enum TableArg {
    Column(Column),
    LikeClause(LikeClause),
    TableConstraint(TableConstraint),
}
```

### Grammar Confirmation

postgresql.ungram confirms:

```
TableArg =
  Column
| LikeClause
| TableConstraint
```

### Verified Implementation Detail (line 37477)

The `can_cast` implementation only explicitly matches `COLUMN | LIKE_CLAUSE`:

```rust
impl AstNode for TableArg {
    fn can_cast(kind: SyntaxKind) -> bool {
        matches!(kind, SyntaxKind::COLUMN | SyntaxKind::LIKE_CLAUSE)
    }
    fn cast(syntax: SyntaxNode) -> Option<Self> {
        let res = match syntax.kind() {
            SyntaxKind::COLUMN => TableArg::Column(Column { syntax }),
            SyntaxKind::LIKE_CLAUSE => TableArg::LikeClause(LikeClause { syntax }),
            _ => {
                if let Some(result) = TableConstraint::cast(syntax) {
                    return Some(TableArg::TableConstraint(result));
                }
                return None;
            }
        };
        Some(res)
    }
}
```

**Important implementation note:** `can_cast()` returns `false` for table-constraint
syntax kinds, but `cast()` still succeeds for them via fallthrough to
`TableConstraint::cast()`. Any safe-migrate code that checks `TableArg::can_cast()`
before casting (rather than calling `cast()` directly) will silently skip
table-level constraints. This is a real risk for the AST Visitor layer — it must
call `cast()` directly, not gate on `can_cast()`.

`TableArgList.args()` returns `AstChildren<TableArg>`.
This is the primary extraction point for **column definitions AND table-level
constraints** in `CREATE TABLE` and `CREATE FOREIGN TABLE` — not columns alone.

### safe-migrate guidance

```rust
match table_arg {
    TableArg::Column(col) => { /* extract ColumnFact */ }
    TableArg::LikeClause(like) => { /* extract LikeClauseFact */ }
    TableArg::TableConstraint(constraint) => { /* extract ConstraintFact, see constraints.md */ }
}
```

A `CREATE TABLE` visitor that only handles `TableArg::Column` will silently miss
every table-level `PRIMARY KEY`, `UNIQUE`, `CHECK`, `FOREIGN KEY`, and `EXCLUDE`
constraint declared inline in the table definition.

---

## SetColumn

```rust
pub enum SetColumn {
    SetMultipleColumns(SetMultipleColumns),
    SetSingleColumn(SetSingleColumn),
}
```

Used in UPDATE SET clauses. Not a column definition node.

### SetSingleColumn (line 16975)

```rust
pub fn column(&self) -> Option<Column>
pub fn set_expr(&self) -> Option<SetExpr>
pub fn eq_token(&self) -> Option<SyntaxToken>
```

### SetMultipleColumns (line 16772)

```rust
pub fn column_list(&self) -> Option<ColumnList>
pub fn paren_select(&self) -> Option<ParenSelect>
pub fn set_expr_list(&self) -> Option<SetExprList>
pub fn eq_token(&self) -> Option<SyntaxToken>
```

---

# Verified Findings Summary

## Confirmed Complete

- `Column`: fully resolved
- `ColumnList`: fully resolved
- `AddColumn`: fully resolved
- `AlterColumn`: fully resolved
- `DropColumn`: fully resolved
- `RenameColumn`: fully resolved including handwritten `from()` / `to()`
  (grammar/implementation discrepancy noted and documented)
- `AlterColumnOption` enum: all 21 members verified and grammar cross-checked
- `TableArg` enum: corrected to 3 members (`Column`, `LikeClause`,
  `TableConstraint`) after grammar cross-check found a documentation error

## Grammar-Confirmed Limitations

- `SetCompression`: compression method name confirmed absent from grammar
- `SetStatistics`: statistics value confirmed absent from grammar
- `SetStorage`: storage mode confirmed absent from grammar
- `AddGenerated`, `SetGenerated`, `SetGeneratedOptions`: confirmed presence-only
- `ResetOptions`: confirmed empty parens, option names not captured
- `Restart` (AlterColumnOption variant): restart value not captured on this
  variant — distinct from the `Restart`-equivalent value documented in
  sequences.md's `SequenceOption`

## Grammar Cross-Check

This document has been fully cross-checked against postgresql.ungram,
including all 21 `AlterColumnOption` variants. One real documentation error
was found and corrected (`TableArg` was incorrectly documented as 2-member,
actually 3-member with `TableConstraint`) — this is a meaningful finding since
any `CREATE TABLE` visitor built on the original documentation would have
silently skipped all table-level constraints. One grammar/implementation
discrepancy was flagged for `RenameColumn`'s `to` field type.

---

# Remaining Open Questions

None remaining. Both previously open questions have been resolved or
appropriately reclassified:

1. **`TableArg::can_cast()` returning `false` for table-constraint kinds**:
   This is a real implementation quirk, confirmed in squawk.rs at line 37477
   and documented extensively in the `TableArg` section above. It does NOT
   affect safe-migrate directly, since safe-migrate must call
   `TableArgList.args()` (which returns `AstChildren<TableArg>` via
   `support::children::<TableArg>()`) — and `AstChildren` iteration uses
   `cast()` directly, not `can_cast()` as a gate. The `can_cast()` issue
   only bites code that explicitly calls `TableArg::can_cast(node.kind())`
   before deciding whether to cast, which is not the idiomatic visitor
   pattern for this AST library. The correct pattern — `args()` then
   `match` on the returned `TableArg` variants — is not affected.
   No action required in safe-migrate's visitor code beyond using the
   standard `AstChildren` iteration pattern.

2. **Handwritten extensions beyond line 38429**: The exhaustive `impl ast::*`
   grep established in this document already covers the complete handwritten
   extension surface as of the squawk.rs version inspected. This is properly
   a maintenance caveat (re-run the grep if squawk.rs is upgraded), not an
   open question requiring further investigation now. Reclassified as a
   standing maintenance note rather than an active open question.