Pgmigrate: Write migrations using plain SQL
github.com
github.com
Fun fact - you don't even need to maintain a special unicorn "_migrations" table or any other external state to keep track of things with SQLite migration. You can simply utilize the user_version pragma:
https://sqlite.org/pragma.html#pragma_user_version
We have a DatabaseVersion constant in the classes that own each type of SQLite database, and all they have to do at ctor time is query the database for current version and run a for loop over the difference to execute the required migration scripts.
Our migrators are one-way (we don't define a matching 'down.sql'). We would push new code to back something out of a SQL schema and then increment our counter just like if we added something new. Decrementing/skipping is disallowed since this would cause information loss on the migration path. Having a monotonic version number per type of database makes it super easy to keep everything on rails for us.
For database access, we also use raw SQL via Dapper.
Doing as you say requires two deployment for each change with a pause in between, and likely two separate merges (does it?). This reduces the deployment velocity. I might have to do as you describe when I start scaling out - there is no hope of updating many servers in sync so might as well embrace multi-version.
For example, this could mean that you don't delete any column that were still in use in the previous backend version.
This would have the advantage of letting you rollback your backend alone if anything wrong happens.
- migration queries for up and down should be persisted. Here migrations are always read from files. If one changes down migration it might not match the up migration.
- migration content should be tracked (crc, hashing, whatever) to prevent any changes to migrations once applied
- my opinion is that each migration should have strict order and not be relied upon file sorts to prevent adding migrations in the middle of existing ones. This is accomplished by enforcing each id/migration number in files to be incremental
- 1-to-1 mapping should be enforced and errors should be produced if there are migrations in database, but not present in files
- one nit pick: two tables for migrations seems overkill
This is mostly coming from Java background and having the above things was really life saver in some situations. I think the Playframwork's way of managing migrations was really good. Flyway is good too for SpringBoot (not sure if exists something besides it), but lacks down migrations.
I didn’t really care about crc checks, my idea is to not modify the files after you’ve submitted them.
The program uses natural sorting, not dependent on filesystem.
It is half-baked because my colleague posted it here while I was working on it for the lulz.
https://docs.liquibase.com/workflows/liquibase-community/mig...
Installing sqitch and pgTAP (a unit testing framework for PostgreSQL) has always been a pain, so I ended up making a docker image that we now use at work: https://github.com/kineticcafe/docker-sqitch-pgtap
But I don't know if they have pgTAP on them.
Edit: I just checked. It doesn't look like they do - https://github.com/sqitchers/docker-sqitch/blob/main/Dockerf...
I should write up examples of how we use it, but we use a variant of the `run` script in the docker image. I’ve also built my docker image with multiple architectures as I run on arm64 but need to run on amd64 servers. The Sqitch docker images appear to be _just_ amd64 images.
The other difference is that I’m using alpine instead of debian-slim.
In practice, we consciously avoid doing this, but we are perhaps making sacrifices for the tool's limitations. I've occasionally done hacks like creating a new table with the desired new shape, then creating a view that converts the old data to the new format and merges it with the new data. SSDT is capable of deploying that, but it's not the greatest situation.
1. There's no surprises in the SQL generated (or when it's generated) and when you are interacting with the DB. In the worst case for ORMs, simply changing a field on a ORM controlled model can result in IO with the DB. That can be pretty surprising
2. SQL, once learned, is fairly straight forward and easy to read. Once you are comfortable with it, even complex CTEs don't take too much effort to grok. I'd argue that there are minor readability gains from ORMs (in general). The biggest value add to ORMs is integration with things like intellisense. However, IDEs like Intellij are becoming context sensitive such that you can still get that intellisense even when writing SQL in something like Java.
Right tool and job and everything. However, writing SQL is often treated as if it were assembly or something. SQL is not that. It's a fairly high level DSL for set operations.
The classic problem with letting your ORM manage migration is if you are updating your database entity model as part of a wider change, in your ORM and you rename a column, does it know its a rename? or is it going to DROP COLUMN + ADD COLUMN? and lead to data-loss? To cover these cases and all the infinite other permutation you end up needing to give the ORM more and more state about the past, making it just a more complex migration. Making the migration explicit SQL and decoupled from the ORM forces thought, a human is unlikely to write DROP COLUMN..
However, ORMs can easily cut the other way. So, it's all about making sure you know what you are doing.
To answer you question though, using SQL is not just about raw performance, it is also about not having to copy large volumes of data from the database to the application server for processing. So in that regard it saves memory. The reason for this is that not all ORM operations can be cleanly mapped to SQL.
SQL is a simple language and it is sometimes easier to reason about what the developer is trying to do rather than reading application code. It is also sometimes easier to run stand-alone sql against the database so it's useful for troubleshooting. Of course there are many downsides but you asked for some good reasons to use it :)
I'll make a second (though I do believe "weaker" point): you are frankly likely to want to port your entire project at some point to some new framework / language, and if most of your system's behavior is just gluing SQL statements together with input models, that's going to be super easy. (The counter-argument, meanwhile, always feels really lame to me: "you might want to change your database at some point". Not only do people effectively never do that, but somehow even projects that use ORMs almost never achieve true database agnosticism either. Meanwhile, such a change is something that seems to come up a lot less often than "the awkward framework I'm using is now considered deprecated", in the grand scheme of things that are almost no database solutions that have ever mattered anyway, and the differences between them are sufficiently small vs. the differences between ORM framework concepts that porting has never felt like that big of a deal, unless you are literally going all-in on some database-specific component in which case there was likely a reason you were doing that and the ORM likely wasn't going to be useful anyway.)
I recognize and support the advantages of using an ORM in application code, but I find the migrations syntax (which, again, is different) a useless cognitive burden.
I have the feeling, if you don’t have a lot of different tables and migrations, writing them by hand probably saves you time compared to learning the intricacies of an ORM mapper (they are usually very leaky abstractions, so you have to read their source code to understand what’s going on). If you already know an ORM-mapper then it’s usually not worth it. The real trap seems to be to believe that ORM mappers will abstract the database away and that you don’t need to understand your database anymore.
- The built-in ORM mapper migrations facility might be an afterthought (I’m looking at you, Sequelize)
- You explicitly want to separate database migrations from code changes so that the code needs to be written in such a way that it can run (at reduced functionality if required) with or without the migration, and the presence of the migration should not affect the execution of the code.
It’s way too easy with in-built migrations to make a migration that results in downtime. Practicing direct migration with SQL drivers is going to be safer every time.
The missing magic in tools like liquibase is a low-maintenance parser framework. If we had composable grammars it wouldn't be that hard for a tool to track the SQL syntax of various databases and be able to apply some real intelligence to SQL definitions.
Trouble is nothing good ever happens in parsing frameworks because everybody things they have the same problem as Go and they worry too much about the speed of parsing.
That sounds interesting, could you elaborate on what that looks like? Are you thinking things like generating ORM code off just the SQL, or doing merges of schema changes in Git, or what?
Something that I would like to see would be an Excel-like app like pgadmin or phpmyadmin that lets you modify the schema, generate migrations as you perform these modifications, and save those migrations in a folder tracked in your source code repo.
https://github.com/mschaef/sql-file
Doesn’t get a lot of use aside from a few small things I use it for, but has been nice to have around.
(This was before I knew of Flyway…. These days I might just link to that for the migration part.)
Question: do you know if it will work with Azure DevOps? Where does it store the state of what scripts were executed so that it doesn't have to redo those the next time?
> Where does it store the state of what scripts were executed so that it doesn't have to redo those the next time? It stores this information in a table called "migrations".