Skip to content

refactor(workspace): one adapter per migration tool - #993

Open
psteinroe wants to merge 6 commits into
mainfrom
refactor/migration-tool-adapters
Open

psteinroe wants to merge 6 commits into
mainfrom
refactor/migration-tool-adapters

Conversation

@psteinroe

@psteinroe psteinroe commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

The migration tool support had grown into one file per concern rather than one place per tool: the dbmate and goose markers shared a single function with a match on SectionMarkers in every loop, the file name parsing lived next to them in workspace/server/migration.rs, and the Supabase detection lived in the init command. The server rebuilt the sections from the settings on every request (document_sections), up to three times per diagnostics pull. This PR is the structural cleanup asked for on #949, without behaviour changes, so that ENVSUB and the next tools have a place to go.

What changed

  • New pgls_workspace::migrations module:
    • MigrationTool trait with sections(text) and marker_diagnostics(text, sections, statements), and migration_tool(SectionMarkers) to pick the adapter from the settings (None for files that are one migration).
    • dbmate.rs and goose.rs, one adapter each, with their marker parsing and tests. Each describes its markers (MarkerSyntax: tool name, first marker, what a look-alike line means), and markers.rs holds what they share: the line scan, building the sections, and SectionMarkerDiagnostic with the "statements before the first marker" and look-alike line checks.
    • files.rs: which files are migrations and their sequence numbers (get_migration, is_in_migrations_dir), used for migrations.after and the migration-only rules. Moved unchanged.
    • supabase.rs: the Supabase project detection from init (folder lookup, config.toml port), moved unchanged, plus SupabaseProject::configure, which fills the db and migrations settings that init used to fill inline. init keeps the console output. toml moves from pgls_cli to pgls_workspace with it.
  • Document gets the migration tool from the settings of its file when it's opened (Document::open), keeps the sections of its content, and recomputes them on update_content. When the root or nested settings of a project change, update_settings and update_nested_settings apply the new tool to the project's open documents. For that, the document remembers its project key. The analyser, walk_statements and the marker diagnostics read doc.migration_sections() instead of recomputing them. fix_file builds its intermediate documents with the tool of the open one.
  • The settings and the documents are never locked at the same time while applying the settings, since other methods take the two locks in either order.

The configuration (sectionMarkers, lintDown, migrationsDir, after) and MigrationSection in pgls_analyser are unchanged.

Cost

The sections are computed once per document version instead of once or more per request: a line scan, O(file). On a 20,000-statement file (1.3 MB, release build) that's 0.6 ms with goose and 1.7 ms with dbmate per edit, and a request no longer pays it. A settings change rescans the open documents of the project.

Tests

The tests of the old module moved with the code into dbmate.rs, goose.rs, files.rs and supabase.rs, with the same inputs and assertions; the ones that covered both tools are split per tool. New: section_markers_follow_settings_changes opens a file, then switches sectionMarkers to dbmate and back to none, and checks that the marker diagnostics follow without reopening the file. cargo test -p pgls_workspace -p pgls_cli -p pgls_lsp passes, and no snapshots changed.

Not covered

Update (review follow-up): Every migration tool, Supabase included, is now one MigrationTool (pgls_workspace::migrations), which describes everything the language server knows about it:

  • its name and the migrations.sectionMarkers value that selects it,
  • its default migrations folder (Supabase supabase/migrations, dbmate db/migrations, none for goose),
  • how init finds its projects (detect, and is_tool_directory for running inside the supabase folder),
  • which files are migrations and their sequence numbers (is_migration, migration, used for migrationsDir and after),
  • how a file splits into sections and which marker diagnostics apply (sections, marker_diagnostics).

The defaults fit a tool that runs every file as one migration, so Plain (no tool) and Supabase only override what's theirs, and dbmate and goose add their markers. migration_tool(sectionMarkers) always returns a tool (Plain for "none"), and a document always has one, so the workspace doesn't special-case files without markers. migrations::project holds what init needs from any tool: find_projects asks every tool in the folder and, at the root of a monorepo, one or two levels below it; a Project carries the tool, its folder, its migrations folder, its section markers, its local database and a warning, and Project::configure fills db and migrations. init only prints the result. The Supabase helpers (SupabaseProject, find_supabase_directories, …) are gone.

Behaviour changes, all in init:

  • dbmate projects are detected: a first <version>_<name>.sql in db/migrations with a -- migrate:up line, which dbmate requires in every migration. init then sets migrationsDir to db/migrations and sectionMarkers to "dbmate", and leaves the connection alone, since dbmate reads it from DATABASE_URL. Also found one or two levels below the root, like Supabase projects.
  • goose is not detected: it has no configuration file and no default folder (-dir/GOOSE_MIGRATION_DIR, . by default), so there's no reliable sign of a goose project.
  • With a Supabase and a dbmate project, init treats them like several Supabase projects: it sets nothing and lists them ("Found several migration projects (supabase: Supabase, db: dbmate)"). The message for several Supabase projects is unchanged.

The Supabase output and settings are unchanged, and so is the workspace: Supabase has no markers, so its files run with Plain, as before.

Tests: the existing ones moved with the code. New: selects_the_tool_of_the_section_markers, supabase_files_are_one_migration, migration_file_names (dbmate), and in migrations::project detection of Supabase and dbmate, db/migrations without dbmate markers, projects below the root of a monorepo, projects at the root taking precedence, and the folder above supabase/. CLI: init_with_dbmate_project_sets_the_migrations_and_section_markers, init_with_supabase_and_dbmate_projects_leaves_the_configuration_alone.

Update (redesign): Updated with main, and the migration tools are now one trait without defaults, in a crate of their own (pgls_migrations):

  • MigrationTool has id, name, detect, is_tool_directory, default_dir, file, version and read, and every tool (plain SQL, Supabase, dbmate, goose, in tools/) implements all of them. Nothing is inherited: what tools share are helpers they call, naming (<number>_<name>.sql and its folder form), directives (the line scan and the look-alike marker lines), units and detect (the monorepo search).
  • read(text) returns a Script: the units of the file (Unit, which replaces pgls_analyser::MigrationSection) and the marker problems. The document keeps it per version, as before. file(path, migrationsDir) returns the role (migration or not) and the version, which the migration-only rules and migrations.after use.
  • init gets Detected projects from detect::find_projects and writes the configuration itself; Project::configure is gone from the workspace.
  • pgls_analyser and pgls_workspace depend on pgls_migrations; it depends on neither, nor on pgls_configuration. sectionMarkers still selects the tool, so Supabase isn't selectable yet (that's feat(configuration): rename migrations.sectionMarkers to migrations.tool #1036).

No behaviour change: the tests moved with the code, and the marker messages are unchanged.

Update: Two more pieces of the design:

  • is_tool_directory is gone. Supabase's detect also finds the project when init runs inside its supabase folder, and returns it with Detected::configuration_directory set to ..; init writes the configuration there. One difference: inside supabase/, only the Supabase project is found now. Before, every tool was detected in the folder above, so a dbmate db/migrations next to supabase/ made it ambiguous and nothing was configured. configures_the_project_above_the_supabase_folder covers it.
  • MigrationTool::database says where the tool records the migrations it ran: History::Schema("supabase_migrations"), History::Table("schema_migrations") for dbmate, History::Table("goose_db_version") for goose, none for plain SQL. Completions rank the history of every tool low, instead of the hard-coded schema; _sqlx_migrations stays as an extra until sqlx is a tool. New: dbmate's and goose's tables are ranked low too (ranks_the_history_tables_of_migration_tools_low).

The dbmate and goose markers, the migration file names and the Supabase
project detection of `init` move into a `migrations` module of
pgls_workspace, with one adapter per tool behind a `MigrationTool` trait.
A document gets the adapter from the settings of its file when it is
opened, and again when the settings change, and keeps the sections of
its content instead of computing them on every request.

@psteinroe psteinroe left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Supabase and the others should be handled by a single model. This is still too loose.

…luded

`MigrationTool` now describes everything the language server knows about a
tool: how `init` finds its projects and what it sets for them, its default
migrations folder, which files are migrations and their sequence numbers,
and how a file splits into sections with which marker diagnostics. Plain
files and Supabase run every file as one migration, dbmate and goose split
it at their markers.

`init` goes through `find_projects`, which asks every tool, instead of the
Supabase helpers. It also detects dbmate projects (a first migration in
`db/migrations` with `-- migrate:up`). goose has no default folder or
configuration file, so it isn't detected.
# Conflicts:
#	crates/pgls_workspace/src/workspace/server.rs
…s own crate

Move the migration tools out of pgls_workspace into the new leaf crate
pgls_migrations. Every tool (plain SQL, Supabase, dbmate, goose) implements
every method of the trait: id, name, detect, is_tool_directory, default_dir,
file, version and read. What tools share are helpers they call: naming,
directives, units and detect.

read() returns a Script with the units of the file (replacing
pgls_analyser::MigrationSection) and the marker problems; the document keeps
it per version. file() returns the role and version of a path, used for the
migration-only rules and migrations.after. init uses detect::find_projects and
writes the configuration itself.

No behaviour change.
… history tables in database

- `is_tool_directory` is gone: Supabase's `detect` also finds the project when
  `init` runs inside its `supabase` folder, and says so with
  `Detected::configuration_directory` (`..`). `init` takes the folder of the
  configuration from the project it found.
- `MigrationTool::database` tells where the tool records the migrations it ran
  (`History::Table` or `History::Schema`). Completions rank the history of every
  tool low, instead of the hard-coded `supabase_migrations` schema; sqlx's
  `_sqlx_migrations` stays as an extra until sqlx is a tool.

This branch has not been deployed

No deployments
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.

1 participant