Skip to content

Add support for PostgreSQL-style ALTER COLUMN syntax#276

Open
Niols wants to merge 2 commits into
ygrek:masterfrom
Niols:alter-column
Open

Add support for PostgreSQL-style ALTER COLUMN syntax#276
Niols wants to merge 2 commits into
ygrek:masterfrom
Niols:alter-column

Conversation

@Niols
Copy link
Copy Markdown

@Niols Niols commented May 1, 2026

PostgreSQL (and standard SQL) uses ALTER COLUMN instead of MySQL's CHANGE COLUMN / MODIFY COLUMN for modifying existing columns. This adds parsing and schema evaluation for the five main sub-commands:

  • ALTER COLUMN col TYPE newtype
  • ALTER COLUMN col SET NOT NULL
  • ALTER COLUMN col DROP NOT NULL
  • ALTER COLUMN col SET DEFAULT expr
  • ALTER COLUMN col DROP DEFAULT

These can be combined with other actions in a single ALTER TABLE statement, eg.:

ALTER TABLE t
  DROP COLUMN old_col,
  ALTER COLUMN col TYPE SMALLINT,
  ALTER COLUMN col SET NOT NULL;

Migration generation (inverse actions + SQL fragments) is also supported for all five variants.


I hope everything looks well; please let me know if you want me to change anything!

PostgreSQL (and standard SQL) uses `ALTER COLUMN` instead of MySQL's `CHANGE COLUMN` / `MODIFY COLUMN` for modifying existing columns. This adds parsing and schema evaluation for the five main sub-commands:

- `ALTER COLUMN col TYPE newtype`
- `ALTER COLUMN col SET NOT NULL`
- `ALTER COLUMN col DROP NOT NULL`
- `ALTER COLUMN col SET DEFAULT expr`
- `ALTER COLUMN col DROP DEFAULT`

These can be combined with other actions in a single `ALTER TABLE` statement, eg.:

```
ALTER TABLE t
  DROP COLUMN old_col,
  ALTER COLUMN col TYPE SMALLINT,
  ALTER COLUMN col SET NOT NULL;
```

Migration generation (inverse actions + SQL fragments) is also supported for all five variants.
@ygrek
Copy link
Copy Markdown
Owner

ygrek commented May 1, 2026

looks very repetitve, i believe all of these + the existing ones can be expressed as a single variant (e.g. can take old type, apply modifications, then use Change ctor)

@Niols
Copy link
Copy Markdown
Author

Niols commented May 1, 2026

Yes, I was wondering about that. It felt a bit wrong to encode granular instructions into something that just updates everything at once, but since CHANGE and MODIFY are already here, then why not. I was also worried about code generation, but in retrospect I think that it isn't really a problem.

Let me have a look and propose an updated version in the coming days!

@Niols
Copy link
Copy Markdown
Author

Niols commented May 3, 2026

I thought about this some more, and I don't really see how that would be doable, at least not just as taking the old type, applying modifications, then using the Change constructor, because taking the old type isn't really possible in the parser anyway. What I could imagine is to introduce extend the Change constructor such that it can encode some or all of the changes independently. MySQL's MODIFY would be everything, MySQL's CHANGE everything but the name, and PostgreSQL's ALTER COLUMN would only affect one of the coordinates. I'm not sure that would play well with everything else because I don't understand much, but does that sound like an approach worth pursuing?

@ygrek
Copy link
Copy Markdown
Owner

ygrek commented May 4, 2026

my thinking is as follows: in the current PR it is more like CST (variants directly correspond to syntax), which can be useful but currently all the other parsing outputs more abstract nodes, and that would help to have less changes in tables.ml. But then I realize in Gen_migrations.sql_to_fragment we actually seem to need to preserve exact sql syntax used... Probably best option would be to support dialect flag when generating migrations and convert from AST to the specific dialect, but this increases a scope of this PR quite a bit.
So I propose you keep one separate ctor for the pgsql alter but the actual alter change is described with one record ie smth like AlterPQ of { nullability: bool option; typ : typ option; etc }

@Niols
Copy link
Copy Markdown
Author

Niols commented May 4, 2026

That sounds like a fair compromise. Indeed, I also would see something like what you describe, with an AST that is fairly generic and then a dialect flag to know how to generate SQL back.

The PG-specific constructor that you propose could serve as a base for this future, generic mechanism, while keeping the current change small enough. I will make an update in those lines.

@jongleb
Copy link
Copy Markdown
Collaborator

jongleb commented May 4, 2026

Sorry, I didn't see this PR right away. And maybe I'm intruding, but
lib/dialect.ml already has a dialect identifier based on syntax. Just as it's used for the standard generator in subsequent steps, it can also be used for generating migrations

@Niols
Copy link
Copy Markdown
Author

Niols commented May 9, 2026

What do you think of f13bbfa?

A few notes:

  • I have merged the `AlterColumn* variants but not the alter_column_* helpers in lib/tables.ml. I think the helpers make more sense as small units, even if we are free.
  • I haven't tried generating migrations while using the dialect and I have kept `AlterColumnPG separate from the existing `Change constructor. I don't think it would be that much work to merge the two, but I wanted to keep the PR focused.
  • I am not really familiar with the code base, and there are some things that I did that look fishy to me. I will open a review and point at those things.

Let me know if you would like me to change anything. I could also attempt merging `Change and `AlterColumnPG (and `RenameColumn presumably!) into the same `AlterColumn constructor if you thought that that was useful in this PR.

Comment thread src/gen_migrations.ml
else sprintf "ALTER COLUMN %s DROP NOT NULL" (quote_id col_name)
) changes.not_null;
Option.map (function
| Some _ -> sprintf "ALTER COLUMN %s SET DEFAULT (* unknown *)" (quote_id col_name)
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this (* unknown *) since that produces invalid code. It was inspired by the (* unsupported: unknown previous charset *) above, but at least that one replaced a whole statement? Do you prefer that we replace a whole statement here too?

Comment thread src/gen_migrations.ml
Comment on lines +123 to +125
let inv_not_null = Option.map (fun _ ->
Sql.Type.is_strict entry.attr.domain
) changes.not_null in
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is here trying to respect that the inverse of SET NOT NULL is only DROP NOT NULL if the column was nullable before, otherwise it's a no-op, but I'm not sure if that's considerations that we should worry about?

Comment thread lib/tables.ml
alter name (fun cols ->
List.map (fun c ->
if c.attr.Sql.name = col_name then
{ c with attr = { c.attr with extra = Sql.Constraints.remove WithDefault c.attr.extra } }
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am tempted to introduce a helper to update attr in depth, since the pattern shows up everywhere. Let me know if you want this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants