Skip to content
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

Ensure all data is removed during the teardown step #1714

Merged
merged 12 commits into from
Feb 11, 2021

Conversation

tctrautman
Copy link
Contributor

@tctrautman tctrautman commented Feb 2, 2021

This PR attempts to fix three issues with the removeScenario function that's run after a backend test is run:

  1. Tests that use scenarios currently fail on Postgres databases that have database tables with PascalCasing.
  2. removeScenario does not teardown records of data models that are not included in a scenario.
  3. The removeScenario function would only remove records from the database if a scenario is present

We fix these issues by getting all prisma data models with their exact casing and issuing DELETE statements for each each of them, wrapping the model name in double quotes to maintain exact casing in Postgres. We do this regardless of whether or not a scenario is present, so I propose we rename this function from removeScenario to teardown.

At some point in the future we will likely want to replace this with a future Prisma implementation of transactions since issuing DELETE statements for every data model on every test run feels wasteful.

@github-actions
Copy link

github-actions bot commented Feb 2, 2021

@tctrautman
Copy link
Contributor Author

Hm. After using this change in development for the past few hours I believe I now understand why we are deleting data from tables using queryRaw -- it seems that the above change doesn't work with scenarios that contain relationships between models.

For example, in testing a signup flow (where a "signup" is an intermediate data model that gets updated through a multi-step process) I received the following error:

Invalid `prisma.signup.deleteMany()` invocation:


      The change you are trying to make would violate the required relation 'SignupToUser' between the `Signup` and `User` models.

      at PrismaClientFetcher.request (node_modules/@prisma/client/runtime/index.js:78130:15)
      at removeScenario (../node_modules/@redwoodjs/core/dist/configs/node/jest.setup.js:88:7)
      at Object.<anonymous> (../node_modules/@redwoodjs/core/dist/configs/node/jest.setup.js:136:7)

I'll give this some more thought and see if I can come up with a better solution. Thoughts / preferences for how to solve would be welcome!

@thedavidprice thedavidprice requested review from cannikin and removed request for cannikin February 4, 2021 19:41
@thedavidprice thedavidprice marked this pull request as draft February 4, 2021 19:41
@thedavidprice
Copy link
Contributor

Ah, understood @tctrautman Thanks for getting this started!

Looping in @cannikin for all the potentially amazing ideas he (probably) has about this.

@cannikin
Copy link
Member

cannikin commented Feb 4, 2021

So I would love it if this behaved the way that Rails does—the fixtures are put into the database inside of a transaction, and then at the end of the test the transaction is rolled back, leaving the database in the same state it was in when we started. But I don't think transactions were supported in Prisma when I wrote that code, or at least not in the form we would need them to be in (some way to save the transaction to a variable so it could be rolled back later). Maybe there's a way to define a named transaction in queryRaw() and then reference that same name later in a rollback? I haven't had to manually do transactions in SQL in years (Rails handles them really well automatically).

So I went more heavy handed and just tracked all models that had records created for them at the start of the test, and then delete all the records in those tables at the end of the test. I can't remember the exact reason I went with queryRaw() instead of deleteMany(), but being able to delete records without Prisma's integrity checks getting in the way may have been the reason. Also performance?

But yeah now that I'm looking I can see this being an issue in databases that enforce case sensitivity in table/column names (I ran all of this against SQLite and it didn't complain when I tried to DELETE FROM post even though the table name is Post). HMMM There's a function we use from prisma called getDMMF() that actually parses the schema file and gets all the stats out...maybe there's a way to use that to figure out the actual table name's case from the camelCase model name we use in the scenario itself?

@tctrautman
Copy link
Contributor Author

tctrautman commented Feb 6, 2021

Thanks for such a thoughtful response @cannikin! And thanks for connecting, @thedavidprice -- this was really helpful.

I don't think transactions were supported in Prisma when I wrote that code, or at least not in the form we would need them to be in (some way to save the transaction to a variable so it could be rolled back later).

I just took a look at the Prisma docs around and as far as I can tell this is still the case 😞

I can't remember the exact reason I went with queryRaw() instead of deleteMany(), but being able to delete records without Prisma's integrity checks getting in the way may have been the reason. Also performance?

Ah, yeah I assume this is what I ran into with the above error.

There's a function we use from prisma called getDMMF() that actually parses the schema file and gets all the stats out...maybe there's a way to use that to figure out the actual table name's case from the camelCase model name we use in the scenario itself?

Edit: The below conclusions are a bit out-dated after I had a chance to take a closer look at this.

Thanks, this was a great idea. I just dug into this a bit and as far as I can tell we can easily get an array of the Prisma data models (which, in Postgres, will be the names of the tables, but not other databases as you mentioned.). I haven't yet found a way to get table names specifically, but I'll take a closer look this weekend with a sqlite project to see if that turns anything else up.

If I don't have any luck with that, I could take stab at writing a function to turn prisma data models into table names like so:

  • In postgres: the data model "BlogPost" would return the table name "BlogPost"
  • In sqlite: the data model "BlogPost" would return the table name "blog_post"
  • etc..

This assumes that I can find a way to get the database type that a user is using (seems like a safe assumption, but it's not entirely clear to me where to get that.)

It would be nice to avoid this, though, so hopefully I can just find a way to get table names from Prisma.

@tctrautman
Copy link
Contributor Author

@cannikin whenever you have a moment I'd love to get your feedback on this. I've just updated the PR description above with my updated approach.

@tctrautman
Copy link
Contributor Author

Ah, I believe I've discovered another situation where removeScenario doesn't quite do what we want it to: cases where the tested function creates a record of a model that is not in any of your fixtures. This happens because (if I understand correctly,) we're only iterating through and deleting models that appear in you fixtures.

It wouldn't be the most performant, but a simple solution to this would be to issue a DELETE statement for every Prisma model. If that's what we'd like to do I'd be happy to include that in this PR.

And I'm rusty here, but maybe there's a way to do this in a single statement?

@jvanbaarsen
Copy link
Contributor

@tctrautman I can confirm two of your assumptions:

  • If you don't have a scenario for every model in your system the DB is not fully cleared
  • If you have a model that starts with a capital letter (Or PascalCasing as you said), things don't work for postgresql, in sqlite that isn't a problem.

@tctrautman
Copy link
Contributor Author

Thanks @jvanbaarsen! I've updated this PR to fix the second issue with a change I've been using in local development for the last few days.

@tctrautman tctrautman marked this pull request as ready for review February 9, 2021 16:33
@tctrautman tctrautman changed the title Improve scenario teardown casing resiliency Improve scenario teardown resiliency Feb 9, 2021
@jvanbaarsen
Copy link
Contributor

@tctrautman Thanks! I'll make sure to check that out against my codebase to see if this solves the problem.

@@ -50,8 +51,12 @@ const removeScenario = async (scenario) => {
// get unique model names only
models = Array.from(new Set(models))

for (const model of models) {
await db.$queryRaw(`DELETE FROM ${model}`)
const prismaModelNames = (
Copy link
Contributor

Choose a reason for hiding this comment

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

We are now running this for every scenario, would it make sense to instead of attaching this to if a scenario is present, run this every time a test has ran? So basically make it a poor mans transaction?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great catch -- I hadn't realized it would miss cases where we weren't using scenarios, but were adding records to the database. I believe that case is now taken care of, and I've renamed the function to reflect this.

@tctrautman tctrautman changed the title Improve scenario teardown resiliency Ensure all data is removed during the teardown step Feb 10, 2021
@cannikin
Copy link
Member

@tctrautman I still see code changes coming in so I've been holding off on reviewing. Let me know when you think it's ready!

@tctrautman
Copy link
Contributor Author

Thanks @cannikin!

The scope of the PR is now a little wider than it was last week (description above is up to date,) but it's ready for your review whenever you have a moment.

And for what it's worth, consider it battle tested on ~35 tests I have running locally on Postgres.

@cannikin
Copy link
Member

Oh damn that's way simpler. Thanks!

@cannikin cannikin merged commit b78132d into redwoodjs:main Feb 11, 2021
@cannikin cannikin added next release bug/confirmed We have confirmed this is a bug labels Feb 11, 2021
@tctrautman
Copy link
Contributor Author

@cannikin happy to do it -- thanks for the direction!

@thedavidprice
Copy link
Contributor

Ah, I see this one got merged. All good -- was just going to ask if it was appropriate to use the new Prisma migrate reset command for this in the place of what is currently the teardown().

No need to change at this point if it's even applicable.

@thedavidprice thedavidprice added this to the v0.25.0 milestone Feb 11, 2021
@thedavidprice thedavidprice removed bug/confirmed We have confirmed this is a bug next release labels Feb 11, 2021
@cannikin
Copy link
Member

This code runs after every test to remove any records that were created. We definitely don’t want to do a migrate reset at that point, it would take forever. I was suggesting doing it when the test suite starts to make sure the DB starts in the right state.

@thedavidprice
Copy link
Contributor

thedavidprice commented Feb 11, 2021

Roger that.

I was suggesting doing it when the test suite starts to make sure the DB starts in the right state.

I start the test DB setup now with prisma db push so we never have to mess with migration steps / shadow DB. Been working well in local testing.

dac09 added a commit to dac09/redwood that referenced this pull request Feb 12, 2021
…ender-p1

* 'main' of github.com:redwoodjs/redwood:
  v0.25.0
  yarn rw prisma commands now show a little tip (redwoodjs#1772)
  FIx typo (redwoodjs#1779)
  Ensure all data is removed during the teardown step (redwoodjs#1714)
  [v0.25] revert to @prisma/cli; recreate yarn.lock (redwoodjs#1774)
  use prisma db push for test DB (redwoodjs#1768)
  Disable --version and -v for prisma alias. (redwoodjs#1767)
  Rebuild Prisma Client between upgrades (redwoodjs#1155)
  upgrade Prisma v2.16.1 (redwoodjs#1766)
  [v0.25] Remove warning if you scaffold a model with a relation (redwoodjs#1757)
  [v0.25] Scaffolded many-to-many relationship fixes (redwoodjs#1758)
  add forrest and kim-adeline; update all contribs (redwoodjs#1763)
  Run clean and then build the js.
  Fix: applying afterQuery in storybook mocks (redwoodjs#1740)
  restructure and update Deploy command and change Setup Deploy command template [breaking] (redwoodjs#1747)
  Fixes prisma command when path has spaces
  Make tutorial work with workspaces.
  Add "packages/*" to the template.
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.

4 participants