Skip to content

Fix/236 unique index before self fk#705

Merged
zachdaniel merged 13 commits intoash-project:mainfrom
WillG2001:fix/236-unique-index-before-self-fk
Mar 11, 2026
Merged

Fix/236 unique index before self fk#705
zachdaniel merged 13 commits intoash-project:mainfrom
WillG2001:fix/236-unique-index-before-self-fk

Conversation

@WillG2001
Copy link
Contributor

Contributor checklist

Leave anything that you believe does not apply unchecked.

  • I accept the AI Policy, or AI was not used in the creation of this PR.
  • Bug fixes include regression tests
  • Chores
  • Documentation changes
  • Features include unit/acceptance tests
  • Refactoring
  • Update dependencies

@WillG2001
Copy link
Contributor Author

Summary

Background / Motivation

When a resource uses:

  • an identity on :id (creating a unique index on id), and
  • a self-referential belongs_to (e.g. follows pointing to id on the same table),

the migration generator previously produced migrations where the foreign key on follows could be emitted before (or otherwise out of order with respect to) the unique index on id. This could cause migration failures or brittle ordering.

Fixes issue: #236

Implementation

  • In AshPostgres.MigrationGenerator.Operation.AddUniqueIndex, add an insert_after_attribute_order field to track where the identity unique index should sit relative to added attributes.
  • In AshPostgres.MigrationGenerator:
    • Compute insert_after_attribute_order from the resource’s attributes when building %Operation.AddUniqueIndex{}.
    • In do_fetch_operations/4, split attribute operations into:
      • creates_and_adds (create/add operations), and
      • alter_and_rest (alter operations),
        and splice unique_indexes_to_add between these so identity unique indexes are generated after new columns but before alters that add FKs.
    • Extend after?/2 ordering rules so that:
      • CreateTable is always ordered before AddUniqueIndex for the same table.
      • AddUniqueIndex is placed after the last AddAttribute for the table but not after AlterAttribute that introduces a FK which depends on that index.
  • In AshPostgres.MigrationGeneratorTest:
    • Add "unique index is created before dependent foreign key (issue #236)" which defines Template, Phase, and TemplatePhase with:
      • uuid_primary_key :id,
      • an identity on :id,
      • a self-referential belongs_to :template_phase, __MODULE__, source_attribute: :follows.
    • Generate migrations into a tmp dir and assert that:
      • create unique_index(:template_phase, [:id], ...) is present, and
      • it appears before the references(:template_phase, ...) line used for the follows foreign key.

How to test

From the ash_postgres repo root:

Run the focused regression test

mix test test/migration_generator_test.exs --only issue_236

Run the full test suite

mix test

Static analysis (as per project conventions)

mix credo
mix dialyzer

Made-with: Cursor
identity: identity,
schema: snapshot.schema,
table: snapshot.table,
insert_after_attribute_order: max(0, length(snapshot.attributes) - 1),
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't this just end up placing them at the end if its using the max order?

Copy link
Contributor

Choose a reason for hiding this comment

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

If its being used to sort after a given attribute then it would make sense to track the name instead I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You’re right, the original code effectively used the last attribute order. I’ll update it so insert_after_attribute_order is computed from the identity keys’ attribute names, and so it tracks the highest attribute order among those keys instead of length(attributes) - 1.

@WillG2001
Copy link
Contributor Author

Updated insert_after_attribute_order to use the max attribute order among the identity’s keys (by source), instead of length(attributes) - 1, so it lines the index up relative to the identity columns themselves.

identity: identity,
schema: snapshot.schema,
table: snapshot.table,
insert_after_attribute_order: insert_after_attribute_order,
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we not just store the attribute name here as opposed to the order?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes we can. Submitting a fix right now!

@WillG2001
Copy link
Contributor Author

Updated to track attribute source (name) instead of numeric order.

@zachdaniel
Copy link
Contributor

Awesome, lets pull main and get all the checks passing 😄

@WillG2001
Copy link
Contributor Author

So, what's happening with the checks is a bit strange, because locally mix format --check-formatted, mix credo --strict, and mix test all pass.

The REUSE check is failing on two generated files:

  • priv/resource_snapshots/test_repo/posts/20260305045125.json
  • priv/test_repo/migrations/20260305045124_add_keyword_map.exs

These aren’t part of the repo and are created by the test/migration generator step in CI (the filenames are timestamped). I didn’t commit them or add headers since they change every run; happy to adjust if you’d prefer a change in how/where those are generated or in REUSE config.

@zachdaniel
Copy link
Contributor

20260305045124_add_keyword_map.exs

That file does indeed appear to be committed. But yeah the reuse errors appear to be in main

@zachdaniel zachdaniel merged commit db5d7b2 into ash-project:main Mar 11, 2026
54 of 66 checks passed
@zachdaniel
Copy link
Contributor

🚀 Thank you for your contribution! 🚀

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.

2 participants