-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Imperative Migrations with a TypeScript DSL #4688
Comments
cc @peterp you'll probably be interested in this for Redwood too? |
@flybayer +1 really like this idea |
100% - This is the only thing stopping me from being able to fully utilise migrate. I even think migrate({
async up(prisma) {
// Prisma migration steps for intial setup
// a lot of the time we need to migrate the data before we can drop columns etc
// or setup defaults for non-nullable fields
await prisma.db.addColumn(...)
// Data migration
await prisma.someModel.update(/*..*/)
// Prisma migration for db cleanup
// only once we know the data is safe in another location and defaults setup
// can we drop columns and make columns non-nullable
await prisma.db.alterColumn(...)
await prisma.db.dropColumn(...)
// Raw when needed
await prisma.executeRaw(`some SQL stuff`)
},
async down(prisma) {
await prisma.db.addColumn(...)
//...
await prisma.db.dropColumn(...)
}
}) You would need to verify that the resulting db will match the schema but it would allow for much more sophisticated migrations. |
@ryanking1809 yes!! I definitely thought about that — that would be super cool. That also opens up the possibility to have a single migrate({
// Automatically generates both `up` and `down` migrations
async change(prisma) {
await prisma.db.addColumn(...)
await prisma.db.addIndex(...)
}
}) |
Thanks for the suggestion! We're currently working on Migrate so any feedback is welcome 😄 @flybayer So what I see is the main driver for imperative migrations is to provide an escape hatch for unsupported features and data migrations. We really care about these two cases and want them covered. If this was covered not by imperative migrations but before / after hooks will this work for you? Let me know what you think |
I don't think that would be sufficient. Let's say I want to create an SQL function to use for default values. This is an imperative migration that's not tied to anything else. There's no existing migration for which I can add a before or after hook since this is a standalone thing. Also for complex migrations requiring data migrations, only before after hooks would be very cumbersome. Example:
|
Yes, to extend what @flybayer is saying with some examples, say I had a model Project {
id Int @id
tasks Json
} After a bit of use, my customers want more features like task model Project {
id Int @id
tasks Task[]
}
model Task {
id Int @id
project Project
completed Boolean
dueDate DateTime
} And in the migration file I would be able to:
A before and after hook would force this to be executed in 2 migrations. And there's a little confusing on how to define the relationships in the Migration Schema 1 model Project {
id Int @id
// this has to stay intact so we can migrate the data
tasks Json
}
model Task {
id Int @id
// project relation not possible due to the definition being on one side
// but the foreign key is required in the data so we just use the projectId Column for now
projectId Int
completed Boolean
dueDate DateTime
} We can then migrate the data with and model Project {
id Int @id
tasks Task[]
}
model Task {
id Int @id
project Project
completed Boolean
dueDate DateTime
} It works but is a lot more cumbersome. I've also had situations in the past where I've just been required to migrate data and make no changes to the database. An example - maybe we previously decided to store the Whilst the data can migrated with a script. It's very useful to have it integrated into your deploy process. Especially if you're dealing with multiple application instances. So being able to have a migration that isn't directly tied the the |
I'm definitely hyped about this feature -- lack of data migrations is the main thing holding me back from using prisma in all my projects -- but I disagree with the suggested solution in terms of API. For example, the suggested solution would produce code like this: // migration.ts
import { migrate } from '@prisma/migrate`
migrate({
async up(prisma) {
// update some fields
await prisma.updateField({ tag: "UpdateField", model: "Following", field: "id", type: "Int" })
// create some new directives, arguments, feilds, or models
await prisma.createDirective({ tag: "CreateDirective", location: { path: { tag: "Field", model: "Following", field: "id" }, directive: "id" } })
await prisma.createDirective({ tag: "CreateDirective", location: { path: { tag: "Field", model: "Following", field: "id" }, directive: "default" } })
await prisma.createArgument({ tag: "CreateArgument", location: { tag: "Directive", path: { tag: "Field", model: "Following", field: "id" }, directive: "default" }, argument: "", value: "autoincrement()" })
// Data migration
await prisma.someModel.rows.forEach(r => {
// move data around with typescript
});
await prisma.executeRaw(`some SQL stuff`) // ...or just use SQL
// delete the old directives
await prisma.deleteDirective({ tag: "DeleteDirective", location: { path: { tag: "Field", model: "Following", field: "id" }, directive: "unique" } })
},
async down(prisma) {
// repeat EVERYTHING from above, but in the reverse order
}
}) This seems like way too much generated/tedious code in my opinion, especially once you scale up. I think it would be a good idea to leverage the existing
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "UpdateField",
"model": "Following",
"field": "id",
"type": "Int"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Following",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Following",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Following",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "ExternalMigration", <--------- there it is!!!
"path": "./data-migration.ts"
},
{
"tag": "DeleteDirective",
"location": {
"path": {
"tag": "Field",
"model": "Following",
"field": "id"
},
"directive": "unique"
}
}
]
} // migrations/20200717142452-my-migration/data-migration.ts
import { migrate } from '@prisma/migrate`
migrate({
async up(prisma) {
await prisma.someModel.rows.forEach(r => {
// move data around with typescript
});
await prisma.executeRaw(`some SQL stuff`) // ...or just use SQL
},
async down(prisma) {
await prisma.executeRaw('reverse the previous changes')
}
}) It could even come with a json stub like As for custom indexing and an escape hatch for things prisma doesn't support -- I think those belong in the declarative |
We've got a PR for adding Data Migrations natively to RedwoodJS (here are the docs): https://deploy-preview-256--redwoodjs.netlify.app/docs/data-migrations It was inspired by the data-migrate gem for Rails. We now have three tasks that run during a deploy (
It never occurred to me that Prisma could add this functionality natively so I just started building our own. 😬 But ideally these would tie into the migrate workflow so they can run in the proper order. Right now our process does have the downside that Prisma migrations and these data migrations are run completely separately, so you need to be careful if you're migrating data that could result in data loss, like removing a column. You need to deploy in two steps:
|
Have similar needs, one example (similar to @ryanking1809's) is detailed in #3470 |
Same here, the possibility to use imperative sql alongside declarative migrations would solve all my problems, and I would use prisma for every project. As of now I'm kind of confused on how to do custom migrations in order. Would br awesome |
This is a blocker for us. We're rolling our own post-deploy data migration scripts. Having something similar to Rails's |
I'm interested in this too! So I can declare views/indexes/on cascade statements and have my db ready with one command. |
Hello, I'm a product manager at Prisma, currently working on schema migrations. As previously mentioned in this issue, we are working hard on taking Prisma Migrate to General Availability. As part of this work, we are making some important changes to how it works. We have an Early Access program for the Prisma Migrate with these set of changes and are looking for users who can test it and provide feedback. You can find more info and join the conversation in our #product-feedback channel on our public Slack community. You can join the Slack community here. |
As of 2.13.0, the new migrate with imperative migrations (as SQL, currently) in in preview. Feedback is very much wanted :) |
I'm using it already and I'll report as needed |
@albertoperdomo are you currently working on migrations using DSL of typescript? Now, as for me, the absence of this possibility is the only thing that stops from introducing the prism to large projects. Since without it we can't applying to DB changed, based on the typescript, for example, inserting values from dictionary enums in automatic mode. Or, was the work on migrations finished after the introduction of migrations based on the SQL? |
I'd also love some info on this. I really like working with prisma so far but the fact that this seems to be a limitation of the migrations system could be a real concern / show-stopper for me. While it might be sufficient for small projects to use migrations that are .sql only, this introduces pretty drastic contrains for larger projects. It would be great to know if the Prisma team conciders .sql migrations as sufficient or wants to move to a solution that allows for scripting. |
It's a complex question, there is no written stance at Prisma at the moment, the discussion is still definitely open. This issue was opened at the time of the experimental migrate version, where migrations were My individual, personal view is that since the SQL migrations are generated, you only need to review them and maybe customize them, which is easier than writing them yourself from scratch, so I think they are fine. These are a few questions that come to mind when discussing a Typescript DSL for migrations on SQL databases:
|
Thanks @tomhoule, what you say definitely makes sense and is very interesting to think about. My current use-case is as follows: I'm working with a simple text-based field named This is a relatively simple use-case but around 80% of my migration scripting use-cases are in that ballpark. Both migrations hooks as well as DSL migrations would potentially solve this (although I'd prefer DSL migrations). I understand the need for deterministic migrations but don't think this necessarly needs to be enforced programatically if it leads to limitations as such. What is wrong with advising users to not apply db schema migrations conditionally? I think many of us have experience with other migrations systems that wouldn't stop us from doing this, yet I'm assuming that very few of us have ever mutated our db schema conditionally as it screams bad practise. I also understand that your resources are limited and am super thankful for all the work you've put in already. I just think prisma would really become so much more useful if the migration system would support:
|
Thanks for the context @florianmartena, that definitely makes a lot of sense. You raised the idea that migration hooks would also be OK for that, and I think we're more aligned than it looks like at first. If I understand your use case correctly, it's about using a JS/TS library to migrate data, rather than the database schema. There is room for improvement in migrate to support data migrations, and there a DSL would totally be the way to go — the question is whether that DSL should also be used for schema migrations. There is a lot to be said for keeping them separate (see this classic blog post from the Rails world. If you have input for designs in that direction, it is super welcome and will definitely be taken into account — there is a lot of feedback in GitHub issues and a lot of different projects running at the same time, but we always read and discuss all of it. This problem could also benefit from its own issue. |
Hi y'all, what about generating imperative .sql migrations with code? This way only the generated .sql needs to be correct and it can use the existing migration history system. |
No, it means a user asked if someone was interested and I replied for all the people in this thread that there indeed are. |
Tossing in my hat to say that my org is fairly deep into Prisma in production and I'm having to hand-build a data migrations pipeline because it's missing in Prisma. I'm going to go with a similar solution to @marek-hanzal to use umzug for managing which migrations have run or not, but it seems a heck of a feature to be missing. |
I was in need of this feature today and was surprised to find out that I couldn't do this in Prisma as I had been using it back in the day with Drupal (using hook updates) for the longest time. I would love to see this officially supported by Prisma. |
The easiest way would be to differentiate between schema migrations and data migrationsSome may argue that other ORMs like TypeORM do not differentiate between different types of migrations but that would always lead to type inconsistency in Prisma’s context. In addition, Redwood also took a similar approach (https://redwoodjs.com/docs/data-migrations). prisma.user.addColumn('company');
prisma.user.create({
data: {
company: 'example company name', // <-- this entry does not exist yet!
// .. other entries
},
}); The new data migration partTo create a new data migration I would suggest extending the migration command with a new --data flag like This command creates a folder in the default migrations folder containing one TypeScript file following the naming convention (unix Timestamp + migration name in snake case). The generated TypeScript file exports a class implementing the PrismaDataMigration interface which provides two functions with the prisma client as parameter. Here is an example: export class TimestampMigrationName implements PrismaDataMigration {
// called when migration gets applied
async up(prisma: PrismaClient) {
await prisma.user.create({
data: {
company: 'example company name',
// .. other entries
}.
});
// more data migration logic here..
}
// called when migration gets reverted
async down(prisma: PrismaClient) {
await prisma.user.deleteMany({
where: {
company: 'example company name',
},
});
// more revert logic here..
}
} All data migrations should be tracked in the migration history (_prisma_migrations table) too and executed in the default order (oldest first, newest last). What are you thoughts on this concept? |
@leohaarmann migrations should not depend on |
Not necessarily. Take a look at the long-term maintainability section from Redwood (https://redwoodjs.com/docs/data-migrations#long-term-maintainability). Sure, it is a trade-off but using the PrismaClient ensures typesafety in all migrations which is more important in my opinion. Especially because one of Prisma's biggest claims is being typesafe. |
@leohaarmann What if:
It simply won't work unless you keep multiple versions of the |
This problem already has been discussed here and yes, you definitely have to write some extra code to get no TypeScript errors. That is what I meant with trade-off. I think @franky47 made it quite clear here:
By the way, another trade-off would be that you can not squash data and schema migrations (https://www.prisma.io/docs/guides/database/developing-with-prisma-migrate/squashing-migrations). So coming to your example you would have to modify the first migration: import { Prisma } from '@prisma/client';
prisma.user.create({
data: {
laterDeletedColumn: 'value123',
} as Prisma.UserCreateInput & { laterDeletedColumn: string }, // or just as any
}); So just to be super clear: You definitely have to modify your TypeScript code in a rather ugly way (with type assertions) or just ignore it with But since migrations usually only run once and almost no one will ever rewrite old (already executed) migrations it is a conscious trade-off. I would definitely prefer having typesafety while implementing a new migration and add a few @elderapo besides this concept, how would you concept a solution for this issue? |
This same discussion came up at my organization and we landed on a similar conclusion to @leohaarmann . You can't easily make it so that every data migration will always work forever, but who cares? I want the migration to work now because data migrations are (IMO) by definition run once. If you need a data migration in order for your tables to be consistent, that's not a data migration, that's a schema migration and transactional guarantees become more important than things like keeping the tables from locking or type safety. Similarly, as @leohaarmann pointed out you actually can maintain those old migrations if you really care but just commenting them out or adding some ignores does the trick too |
@leohaarmann Migrations are kind of "write and forget" so I think manually writing/generating ( await prisma.db.alterColumn(...);
await prisma.db.dropColumn(...); then making sure they work, and "forgetting about them" is the way to go. Editing old migrations sounds like a really bad idea. If you want to have type-safety when writing migrations then there is a possibility of writing migrations using prisma.user.create({
data: {
username: "Tom",
age: 123
}
}); with prisma.$queryRaw`INSERT INTO user ...`; either manually or maybe using some kind of codegen. Another problem with using Example (wouldn't do that in the real world but you get the point...):
So by changing |
I don't see the major benefits to this versus just doing data migrations through the existing Prisma schema migration tool? You get TS and query building but if you're already in raw query land why not just write raw queries and have them be nice and centralized in your migrations folder? My org also ran into this exact problem in that nobody could nail down a sufficiently precise definition of "data migration". I interviewed a number of devs and that was the problem I kept running into that people kept placing whatever their particular immediate or non-immediate need was on top of the idea of a "data migration". For example, does a data migration need to have:
People having different answers to those sorts of questions leads to wildly different opinions on what is obviously going to work or not. To me a data migration is:
To me, that makes using the PrismaClient in an umzug style up/down function a no-brainer. That being said, I work in an org where there is one live version of the schema at any given time. If you were designing for being able to handle many different schemas all being considered equally valid across the org, this requires additional coding to make things backwards compatible, but I don't think there's any way to get backwards compatibility for "free" other than to ignore type safety relative to the DB schema. |
I don't think the existing Prisma schema migration tool works for all the migration cases that's why this issue was created. How else would you write a migration for adding a non-nullable column assuming there are already production environments in the wild? You need to:
The above migration is possible to create using raw SQL queries assuming you have somewhere to automatically pull data from (for point 2). Using I'd add |
To data-migrate non-nullable new columns based on computed data (whether from the database or from outside sources), while keeping production downtime to a minimum (ideally zero), there's quite a little dance to set up. In a previous comment, I linked to the expand and contract pattern, which involves not only database operations, but also adapting the application code for correct reads and writes based on what phase we're at. It's not something that can be automated away with a tool. Now, coming back to doing data migrations with Prisma Client, there are a few roadblocks in place, aside from typesafety:
|
No perfect technical solutionI think the goal of many here is to create a solution with build-in mechanisms which prevents developers from writing "bad" code. But as we discussed before - there is not one specific definition for bad code, it heavily depends on the developer you ask and the current environment (like dev team size, ...). @JulianAtPave made it quite clear with his statement here:
I do not think we will get all on the same page when discussing the question of good/bad code. Code migrationsA code migration can be created by using the following CLI command It created a folder containing a TypeScript file, which implements the PrismaCodeMigration interface and follows the given naming conventions (unix Timestamp + migration name in snake case). It would look like this: export class TimestampMigrationName implements PrismaCodeMigration {
// called when migration gets applied
async up() {
// do whatever you want to do here
}
// called when migration gets reverted
async down() {
// do whatever you want to do here
}
} No build-in mechanisms or stuff like that. In order to make data migrations like in my previous example I would just have to import the prisma client and run the generate command upfront (which is another issue mentioned here #4703). By the way - right now nothing really prevents you from writing SQL code which breaks the schema and typesafety when running co-existenceAt then end both ways (new code migration and raw sql only) would just co-exist. The raw SQL approach already exists with Easy maintenance and competitorsIn addition, this feature would be super easy to maintain since it does not implement any constrains.
What are your thoughts on the code migration approach? |
Quick note from the Prisma side (who is obviously watching this conversation): The most important part of the issue here is you all leaving a precise description of your use case. As you also highlighted "data migrations" means all kinds of things to different people, so having an as extensive as possible list of these is good start for us. The design suggestions are of course also welcome, but the use case description are essential to use to be able to figure this out later. Thanks. |
My use case for this is that I use Postgres JSONB field types to sweep up complex data structures. There's a typescript interfaces that describes the format (and often helper/utility functions). From time to time I need to change the format of the JSON and want to run a migration. This is possible at the database level with postgres JSON support but much more painful compared to writing something in typescript where I can leverage the types and other code that I have. So as part of running the migration, I want to be able to import code from my app. |
I have a very simple use case: I need to be able to generate CUIDs while splitting a table and populating data from old table to the new one. I don't want to copy the id from table to another. I think the default and the first class method could still very well be an sql file as it definitely covers most of the cases and is usually safer, but a possibility to run raw sql mixed with JS would be helpful in many more complex cases. To keep it simple, only forward migrations should be supported, and there could be a special restricted prisma client that would only expose So I would treat code based migrations as an escape hatch only. |
My use case is a column containing some kind of instructions (in a given DSL) as strings. |
I basically don't understand any argument here, nor am able to find a legit one against improving migrations with a DSL language for this. Prisma is a fantastic piece of library, such a wonderful ORM. But everytime I ask myself "which ORM should I choose this time ?" (On every project I start), every. single. time the migration topic is in the "cons" column for Prisma. Almost 400 issues with a title containing the word "migration" should be a strong hint that it's time to face the reality : it was a nice try, but the migration system is bad. As simple as that. The above comment states that "what needs the Prisma teams are some piece of use-cases"; well here it is, as a user-story :
...is a total nonsense. In the actual world, when you migrate the structure, 50% of the time you'll migrate the data. The only correct way to do this, is to do this in a transaction. Oh wait... not every DB systems can use migration for data structure ? Guess what : you just have to tell them to switch to a non-crappy DB engine ! Examples :
Well refactor it. Underlying low level proxy methods should just not be part of it; and should not change when generated.
Yeah for sure. Let's rather encourage them writing SQL by hand, and bypassing the migration flow when migrating data outside of migration transactions. Total nonsense. This might sound spicy, and I'm sorry about that. But please take note that I LOVE that ORM; deeply respect the hard work done on it. And all of this are just IMHO legit arguments from someone (just like most backend developers) who has written hundreds of migrations over the years, have worked with tens of ORMs on tens of environments, and is dying to finally be able to use this fantastically promising ORM without thinking "ha, I had forgotten that strange migration system"... |
With Prisma I started living the thug life and runnin ma |
I have kept a close watch on this topic and aware the prisma community cares a lot about it. Db migration is a difficult problem to solve but the cleanest I have seen is with edgedb - https://www.edgedb.com/docs/intro/migrations. It not perfect but it's the best I've see so far. I use prisma and hope it gets inspired by edgedb in improving its migration strategy. |
@emmanuelbuah yes, and the same goes with umzug; when used programmatically which can become a solid brick for simple and powerful migration systems too. Here is an example from a personal repository : README.md(notice how things are simple-and-stupid, without any magic in it) yarn mig --help
# Create a migrations
yarn mig create <name>
# Execute all pending migrations
yarn mig up
# Execute next migration
yarn mig up --one
# Revert all migrations
yarn mig down
# Revert last migration
yarn mig down --one
# Get migrations status
# Success if up-to-date
# Errors if pending found
yarn mig status
migrator.tsimport { Command } from '@commander-js/extra-typings'
import { appEnv } from '../lib/env/app-env.js'
import { createWhateverDbConnector } from '../lib/whatever-db-connector.js'
import { createUmzug } from './umzug.js'
const whateverDbConnector = createWhateverDbConnector(appEnv.DATABASE_URL)
const umzug = createUmzug(whateverDbConnector, 'migration')
const program = new Command()
program.name('mig').description('Perform database migrations powered by Umzug')
program
.command('create')
.description('Create a new migration')
.argument('<name>', 'name of migration to create')
.action(async (name) => {
await umzug.create({
name: name,
allowExtension: '.ts',
})
})
program
.command('up')
.description('Execute pending migrations')
.option('--one', 'just execute next migration')
.action(async (options) => {
await umzug.up(!!options.one ? { step: 1 } : undefined)
})
program
.command('down')
.description('Revert executed migrations')
.option('--one', 'just revert previous migration')
.action(async (options) => {
await umzug.down(!!options.one ? undefined : { to: 0 })
})
program
.command('status')
.description('Get migrations status')
.action(async () => {
const pendingMigrations = await umzug.pending()
if (pendingMigrations.length > 0) {
program.error(
`Found pending migrations, execute \`mig up\` to run them.\n${JSON.stringify(
pendingMigrations.map((mig) => mig.name),
)}`,
{
exitCode: 1,
code: 'pending.migrations',
},
)
}
})
const go = async () => {
try {
await program.parseAsync()
} finally {
await createWhateverDbConnector.disconnect()
}
}
void go() migration-xxx.ts import type { Migration } from '../umzug.js'
export const up: Migration = async ({ context: { whateverDbConnector } }) => {
await whateverDbConnector.transaction(async (tx) => {
await tx.createTable('stuff')
// await some data migration
await tx.dropTable('other-stuff')
})
}
export const down: Migration = async ({ context: { whateverDbConnector } }) => {
await whateverDbConnector.transaction(async (tx) => {
await tx.createTable('other-stuff')
// await revert data migration
await tx.dropTable('stuff')
})
} Things are so simple here, understandable, flexible. What is the point of missing that ? |
I was trying one of the ways with umzug. After using it, the Prisma migration tool (when I want to create new SQL migration) detects drift. So basically it's not possible to use umzug and prisma migration together right? |
You just need to tell Prisma about the change you did with the external tool. That would mean creating the SQL migration file that affects anything schema related, and marking it as already applied to the database with
We at Prisma agree, which is why this issue exists. We will work on this when we have capacity. |
@janpio , that's so great to hear ❤️ If I could add just one thing here, is I also tried myself to just-don't-use-prisma-migration-tool and go with another one like Umzug; and faced the same issue. It would be a great addition, maybe, to allow Prisma just to report the DB "state" versus Schema. Just that; and print this to the console; maybe with an option to process.exit(1) to run this in CIs. |
We got you: https://www.prisma.io/docs/reference/api-reference/command-reference#migrate-diff (If you have questions on how exactly to do that, best to it in a discussion so we do not spam all the people subscribed here with notifications. Thanks.) |
Problem
Not all migrations can be declarative. For example, we need to add a custom index for performance reasons. We also need this as an escape hatch for anything prisma schema doesn't support. Lastly, we also need this for data migrations.
This is very important for any serious apps using prisma migrate.
Suggested solution
A new command:
prisma migrate create
This will create the necessary migration files. Similar as
prisma migrate save
but the files are just stubbed out.Then the user will open the stubbed migration file and add their custom migration code.
Perhaps the imperative migration file can look something like this:
The text was updated successfully, but these errors were encountered: