safe-migrate 0.3.1

Lint PostgreSQL migrations against live database statistics to prevent blocking locks
Documentation
# Subscriptions AST Reference for safe-migrate

## Status

Inspection status: complete. Cross-checked directly against postgresql.ungram
and squawk.rs in a single pass.

---

## 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.

---

## Scope Note

PostgreSQL subscriptions are the receiving side of logical replication —
`CREATE SUBSCRIPTION` connects to a remote publisher and replicates its
publication(s) locally. This is the counterpart to publications.md.

---

# Core Nodes

## CreateSubscription

### Verified Accessors (line 5401)

```rust
pub fn literal(&self) -> Option<Literal>
pub fn name(&self) -> Option<Name>
pub fn name_ref(&self) -> Option<NameRef>
pub fn name_refs(&self) -> AstChildren<NameRef>
pub fn with_params(&self) -> Option<WithParams>
pub fn semicolon_token(&self) -> Option<SyntaxToken>
pub fn connection_token(&self) -> Option<SyntaxToken>
pub fn create_token(&self) -> Option<SyntaxToken>
pub fn publication_token(&self) -> Option<SyntaxToken>
pub fn server_token(&self) -> Option<SyntaxToken>
pub fn subscription_token(&self) -> Option<SyntaxToken>
```

### Grammar Confirmation

```
CreateSubscription =
  'create' 'subscription' Name
  ('connection' Literal | 'server' NameRef)
  'publication' (NameRef (',' NameRef)*)
  WithParams? ';'?
```

### PostgreSQL Semantics Caveat

This document's analysis of the `SERVER name` form is based purely on what
the grammar parses, not on independent confirmation that `CREATE
SUBSCRIPTION ... SERVER name ...` is valid real-world PostgreSQL syntax.
Standard PostgreSQL `CREATE SUBSCRIPTION` documentation describes only the
`CONNECTION 'conninfo'` form for specifying the publisher connection — a
`SERVER name` alternative (referencing a foreign server object, similar to
foreign data wrapper syntax) was not independently verified against
PostgreSQL's own documentation in this pass. It's possible this grammar
alternative exists for a non-standard extension, a different PostgreSQL
version, or was added speculatively/defensively by the parser author. This
document treats it as parseable per the grammar regardless of its
real-world applicability, but the disambiguation risk discussed below is
only practically relevant if this form is ever actually encountered in real
migration SQL — worth flagging as a question for the user's own PostgreSQL
version/knowledge rather than asserting as definitely-real syntax.

### Critical Finding — Ambiguous NameRef Disambiguation

The grammar shows **two separate `NameRef`-bearing positions**:
1. The `SERVER name` connection target (single `NameRef`, only present in
   the `SERVER` form, mutually exclusive with `CONNECTION 'literal'`)
2. The `PUBLICATION name, name, ...` list (one or more `NameRef`, always present)

The verified accessor surface exposes **both** `name_ref()` (singular,
`support::child()` — returns the *first* matching `NameRef` child) and
`name_refs()` (plural, `support::children()` — returns *all* matching
`NameRef` children).

**This creates a genuine disambiguation risk identical in pattern to the
`RenameValue` (enums.md) and `AsFuncOption` (functions.md) flat-accessor
findings:**

- If the statement uses `CONNECTION 'literal'` (not `SERVER`), there is only
  one group of `NameRef` children in the subtree — the publication list.
  In this case, `name_ref()` returns the *first publication name* (not a
  server name, since none exists), and `name_refs()` returns the full
  publication list correctly. No ambiguity in this case.

- If the statement uses `SERVER name` instead of `CONNECTION`, there are
  now **two distinct groups of `NameRef` children**: the server name (one)
  and the publication list (one or more). In this case:
  - `name_ref()` returns the *first* `NameRef` in document order, which
    is the **server name** (since `'server' NameRef` appears before
    `'publication' (NameRef...)` in the grammar sequence).
  - `name_refs()` returns **all** `NameRef` children, meaning it would
    include the server name **mixed in with** the publication list — there
    is no accessor that isolates just the publication list when the
    `SERVER` form is used, since both groups share the same underlying
    `NameRef` type and `support::children()` does not distinguish by
    grammar position, only by type.

**This is a confirmed, real extraction ambiguity specific to the `SERVER`
connection-target form.** The `CONNECTION 'literal'` form is unambiguous;
the `SERVER name` form is not, because the flat `name_refs()` accessor
cannot separate "the server name" from "the publication list" — both are
just `NameRef` children of the same node, and `support::children::<NameRef>()`
does not know about grammar-level positional semantics.

### Discrimination Strategy

```rust
fn extract_create_subscription(node: &CreateSubscription) -> CreateSubscriptionFact {
    let uses_server = node.server_token().is_some();
    let uses_connection = node.connection_token().is_some();

    let all_name_refs: Vec<String> = node.name_refs().map(|n| n.text()).collect();

    let (server_name, publications) = if uses_server {
        // First NameRef is the server name; remaining are publications.
        // This relies on document order matching grammar declaration order,
        // which is true for support::children() but should be verified
        // empirically against real parsed output before relying on it,
        // since this is an inferred ordering assumption, not something
        // separately confirmed via a dedicated accessor.
        let mut iter = all_name_refs.into_iter();
        let server = iter.next();
        let pubs: Vec<String> = iter.collect();
        (server, pubs)
    } else {
        // CONNECTION form: no server NameRef exists, all NameRefs are publications.
        (None, all_name_refs)
    };

    CreateSubscriptionFact {
        name: node.name().map(|n| n.text()),
        connection: if uses_connection {
            ConnectionTarget::Literal(node.literal().map(|l| /* extract string */))
        } else {
            ConnectionTarget::Server(server_name)
        },
        publications,
        params: node.with_params().map(|p| /* extract */),
    }
}
```

**This positional-splitting approach (first `NameRef` = server, rest =
publications) is an inference based on grammar declaration order, not a
separately verified guarantee.** Unlike `ForeignKeyConstraint`'s
`from_columns()`/`to_columns()` (which are genuine handwritten accessors
verified directly in squawk.rs to do exactly this kind of positional split),
no equivalent handwritten extension exists for `CreateSubscription` per the
exhaustive `impl ast::*` inventory established in columns.md. This means the
positional-split approach above is the best available strategy but has NOT
been verified against actual parsed output in this pass — it should be
tested against a real `CREATE SUBSCRIPTION ... SERVER ... PUBLICATION ...`
statement before being trusted in production code.

### safe-migrate guidance

```rust
struct CreateSubscriptionFact {
    name: Option<String>,
    connection: ConnectionTarget,        // Literal(conn_string) | Server(name)
    publications: Vec<String>,
    params: Option<Vec<AttributeFact>>,  // includes e.g. enabled, slot_name, copy_data
}
```

A new subscription immediately begins replicating data from the publisher,
including an initial data copy (`copy_data = true` by default) unless
explicitly disabled via `with_params()`. This can be a substantial
operation against the source database depending on table sizes — relevant
context for safe-migrate if cross-database operational impact is ever part
of its risk model, though this is more of an operational/performance
concern than a schema-correctness one.

---

## DropSubscription

### Verified Accessors (line 8030)

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

### Grammar Confirmation

```
DropSubscription =
  'drop' 'subscription' IfExists? NameRef
  ('cascade' | 'restrict')? ';'?
```

Single subscription name only — unambiguous, no disambiguation risk (unlike
`CreateSubscription`, only one `NameRef`-bearing position exists here).

### safe-migrate guidance

```rust
struct DropSubscriptionFact {
    name: String,
    if_exists: bool,
}
```

`DROP SUBSCRIPTION` stops replication and (by default) drops the replication
slot on the publisher side too — an external-system side effect similar to
the one noted for `DropPublication`. Worth flagging as having impact beyond
the local database.

---

## AlterSubscription

### Verified Accessors (line 1822)

```rust
pub fn name_ref(&self) -> Option<NameRef>
pub fn semicolon_token(&self) -> Option<SyntaxToken>
pub fn alter_token(&self) -> Option<SyntaxToken>
pub fn subscription_token(&self) -> Option<SyntaxToken>
```

### Grammar Confirmation — CRITICAL FINDING

```
AlterSubscription =
  'alter' 'subscription' NameRef ';'?
```

**This is the complete grammar rule.** Identical severity finding to
`AlterPublication` (publications.md) and `AlterView` (views.md):
`AlterSubscription` carries genuinely nothing beyond the subscription's own
name. Confirmed by both grammar and squawk.rs accessor surface.

Real PostgreSQL `ALTER SUBSCRIPTION` syntax supports substantial
functionality not captured here at all:

```sql
ALTER SUBSCRIPTION name CONNECTION 'conninfo';
ALTER SUBSCRIPTION name SET PUBLICATION publication_name [, ...] [WITH (...)];
ALTER SUBSCRIPTION name ADD PUBLICATION publication_name [, ...] [WITH (...)];
ALTER SUBSCRIPTION name DROP PUBLICATION publication_name [, ...] [WITH (...)];
ALTER SUBSCRIPTION name REFRESH PUBLICATION [WITH (...)];
ALTER SUBSCRIPTION name ENABLE;
ALTER SUBSCRIPTION name DISABLE;
ALTER SUBSCRIPTION name SET (subscription_parameter [= value] [, ...]);
ALTER SUBSCRIPTION name SKIP (skip_option = value);
ALTER SUBSCRIPTION name OWNER TO new_owner;
ALTER SUBSCRIPTION name RENAME TO new_name;
```

**None of these eleven real PostgreSQL `ALTER SUBSCRIPTION` forms can be
distinguished or extracted from this AST.** This is a parser-level
limitation, not an accessor gap.

### safe-migrate guidance

```rust
struct AlterSubscriptionFact {
    name: String,    // from name_ref() — only extractable field
    // operation type and parameters: NOT EXTRACTABLE
}
```

This gap is particularly significant because `ENABLE`/`DISABLE` and
`DROP PUBLICATION` operations directly control whether replication is
actively running and what data flows — exactly the kind of operationally
critical state change safe-migrate would want to flag, and exactly what
cannot be distinguished here. As with `AlterPublication`, recommend treating
all `AlterSubscription` statements as `Confidence::Tainted` and/or flagged
for manual review by default, since the simulator cannot determine whether
a given statement disables replication, changes the connection target,
adds/removes published tables, or merely renames the subscription.

---

# Verified Findings Summary

## Confirmed Complete

- `CreateSubscription`: accessor surface fully resolved, though see the
  critical disambiguation finding below for the `SERVER` form
- `DropSubscription`: fully resolved, unambiguous

## Confirmed Partial — Genuine Extraction Ambiguity

- `CreateSubscription` using the `SERVER name` connection form: the server
  name and the publication list cannot be cleanly separated using only the
  generated accessors (`name_ref()` returns the first `NameRef`, which would
  be the server name in this form; `name_refs()` returns all `NameRef`
  children, mixing server name and publication list together). A
  positional-splitting strategy is proposed in this document but has not
  been empirically verified against real parsed output, since no handwritten
  accessor extension exists to do this disambiguation reliably (unlike the
  analogous `ForeignKeyConstraint.from_columns()`/`to_columns()` case). The
  `CONNECTION 'literal'` form does not have this ambiguity.

## Grammar-Confirmed Limitations

- `AlterSubscription`: confirmed by both grammar and squawk.rs to carry
  nothing beyond the subscription name. None of the eleven real PostgreSQL
  `ALTER SUBSCRIPTION` operation forms can be distinguished or extracted.
  Same severity as the `AlterPublication` finding in publications.md —
  together these represent the two most significant grammar gaps found
  across the entire Tier 3 documentation pass, both involving operationally
  critical replication state changes that cannot be analyzed.

## Key Architectural Findings

1. **`CreateSubscription`'s `SERVER` form has a confirmed, real
   disambiguation ambiguity** that has no clean resolution via existing
   accessors — this should be flagged for empirical testing against actual
   parsed output before any safe-migrate code relies on the proposed
   positional-splitting workaround.
2. **`AlterSubscription`, like `AlterPublication`, is functionally a
   black box** beyond the target object's name — both should be treated
   conservatively (tainted confidence / manual review) given the
   operational criticality of what they can represent (enabling/disabling
   replication, changing data flow) versus what can actually be detected
   (nothing beyond "an alter happened to this object").

## Grammar Cross-Check

This document was written with postgresql.ungram available from the start.
All nodes cross-checked in this single pass; the `AlterSubscription` finding
was independently confirmed against both the grammar and squawk.rs accessor
bodies, matching the same pattern already established for `AlterPublication`.

---

# Remaining Open Questions

1. Whether the positional-splitting strategy for `CreateSubscription`'s
   `SERVER` form (first `NameRef` = server name, remainder = publication
   list) is empirically reliable against real parsed output.

   **Current status: reasonably well-supported but not empirically verified.**

   Supporting evidence: `support::children()` in rowan-based ASTs iterates
   in source-text document order, since the underlying CST preserves the
   complete source text with all tokens. This is confirmed as a reliable
   property by the `ForeignKeyConstraint.from_columns()`/`to_columns()`
   handwritten extension (squawk.rs line 38440), which uses exactly this
   positional ordering guarantee (`nth(0)` = first `ColumnList` = FROM
   columns, `nth(1)` = second `ColumnList` = TO columns). An equivalent
   positional split for `CreateSubscription`'s `NameRef` children follows
   the identical logic.

   The caveat about the `SERVER` form's real-world PostgreSQL validity
   (noted in the `CreateSubscription` section above — standard PostgreSQL
   `CREATE SUBSCRIPTION` may only support `CONNECTION`, not `SERVER`) means
   this disambiguation may never be exercised in practice regardless. It is
   retained as a documented open question because:
   (a) the grammar explicitly supports it, and
   (b) it would be a subtle, silent correctness bug if the `SERVER` form
       ever is encountered and the positional split is wrong.

   Resolution path: run `SourceFile::parse("CREATE SUBSCRIPTION s SERVER
   srv PUBLICATION pub1, pub2")` and inspect the resulting syntax tree's
   `NameRef` children order. This requires a live squawk.rs test environment,
   not static analysis of source text.