[CLI] Prisma migration warning improvements (#682)

This commit is contained in:
Shayne Czyzewski 2022-09-16 10:12:15 -04:00 committed by GitHub
parent 1a7a04b87c
commit 40041ff42b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 99 additions and 31 deletions

View File

@ -6,7 +6,7 @@ waspMigrate/.wasp/out/db/migrations/no-date-foo/migration.sql
waspMigrate/.wasp/out/db/package.json waspMigrate/.wasp/out/db/package.json
waspMigrate/.wasp/out/db/schema.prisma waspMigrate/.wasp/out/db/schema.prisma
waspMigrate/.wasp/out/db/schema.prisma.wasp-generate-checksum waspMigrate/.wasp/out/db/schema.prisma.wasp-generate-checksum
waspMigrate/.wasp/out/db/schema.prisma.wasp-migrate-checksum waspMigrate/.wasp/out/db/schema.prisma.wasp-last-db-concurrence-checksum
waspMigrate/.wasp/out/installedFullStackNpmDependencies.json waspMigrate/.wasp/out/installedFullStackNpmDependencies.json
waspMigrate/.wasp/out/server/.npmrc waspMigrate/.wasp/out/server/.npmrc
waspMigrate/.wasp/out/server/README.md waspMigrate/.wasp/out/server/README.md

View File

@ -2,7 +2,7 @@
module Wasp.Generator.DbGenerator module Wasp.Generator.DbGenerator
( genDb, ( genDb,
warnIfDbSchemaChangedSinceLastMigration, warnIfDbNeedsMigration,
genPrismaClient, genPrismaClient,
postWriteDbGeneratorActions, postWriteDbGeneratorActions,
) )
@ -23,8 +23,8 @@ import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.DbGenerator.Common import Wasp.Generator.DbGenerator.Common
( dbMigrationsDirInDbRootDir, ( dbMigrationsDirInDbRootDir,
dbRootDirInProjectRootDir, dbRootDirInProjectRootDir,
dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir,
dbSchemaChecksumOnLastGenerateFileProjectRootDir, dbSchemaChecksumOnLastGenerateFileProjectRootDir,
dbSchemaChecksumOnLastMigrateFileProjectRootDir,
dbSchemaFileInDbTemplatesDir, dbSchemaFileInDbTemplatesDir,
dbSchemaFileInProjectRootDir, dbSchemaFileInProjectRootDir,
dbTemplatesDirInTemplatesDir, dbTemplatesDirInTemplatesDir,
@ -83,42 +83,70 @@ genMigrationsDir spec =
-- | This function operates on generated code, and thus assumes the file drafts were written to disk -- | This function operates on generated code, and thus assumes the file drafts were written to disk
postWriteDbGeneratorActions :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO ([GeneratorWarning], [GeneratorError]) postWriteDbGeneratorActions :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO ([GeneratorWarning], [GeneratorError])
postWriteDbGeneratorActions spec dstDir = do postWriteDbGeneratorActions spec dstDir = do
dbGeneratorWarnings <- maybeToList <$> warnIfDbSchemaChangedSinceLastMigration spec dstDir dbGeneratorWarnings <- maybeToList <$> warnIfDbNeedsMigration spec dstDir
dbGeneratorErrors <- maybeToList <$> genPrismaClient spec dstDir dbGeneratorErrors <- maybeToList <$> genPrismaClient spec dstDir
return (dbGeneratorWarnings, dbGeneratorErrors) return (dbGeneratorWarnings, dbGeneratorErrors)
-- | Checks if user needs to run `wasp db migrate-dev` due to changes they did in schema.prisma, and if so, returns a warning. -- | Checks if user needs to run `wasp db migrate-dev` due to changes in schema.prisma, and if so, returns a warning.
-- When doing this, it looks at schema.prisma in the generated project. -- When doing this, it looks at schema.prisma in the generated project.
-- --
-- This function makes following assumptions: -- This function makes following assumptions:
-- - schema.prisma will exist in the generated project even if no Entities were defined. -- - schema.prisma will exist in the generated project even if no Entities were defined.
-- Due to how Prisma itself works, this assumption is currently fulfilled. -- Due to how Prisma itself works, this assumption is currently fulfilled.
-- - schema.prisma.wasp-checksum contains the checksum of the schema.prisma as it was during the last `wasp db migrate-dev`. -- - schema.prisma.wasp-last-db-concurrence-checksum contains the checksum of the schema.prisma as it was when we last ensured it matched the DB.
-- --
-- Given that, there are two cases in which we wish to warn the user to run `wasp db migrate-dev`: -- Given that, there are two cases in which we wish to warn the user to run `wasp db migrate-dev`:
-- (1) If schema.prisma.wasp-checksum exists, but is not equal to checksum(schema.prisma), we know they made changes to schema.prisma and should migrate. -- (1) If schema.prisma.wasp-last-db-concurrence-checksum exists, but is not equal to checksum(schema.prisma), we know there were changes to schema.prisma and they should migrate.
-- (2) If schema.prisma.wasp-checksum does not exist, but the user has entities defined in schema.prisma (and thus, AppSpec). -- (2) If schema.prisma.wasp-last-db-concurrence-checksum does not exist, but the user has entities defined in schema.prisma (and thus, AppSpec).
-- This could imply they have never migrated locally, or that they have but are simply missing their generated project dir. -- This could imply they have never migrated locally, or that they have but are simply missing their generated project dir.
-- Common scenarios for the second warning include: -- Common scenarios for the second warning include:
-- - After a fresh checkout, or after `wasp clean`; possible false positives in these cases, but for safety, it's still preferable to warn. -- - After a fresh checkout, or after `wasp clean`.
-- - When they previously had no entities and just added their first. -- - When they previously had no entities and just added their first.
warnIfDbSchemaChangedSinceLastMigration :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO (Maybe GeneratorWarning) -- In either of those scenarios, validate against DB itself to avoid redundant warnings.
warnIfDbSchemaChangedSinceLastMigration spec projectRootDir = do --
-- NOTE: As one final optimization, if they do not have a schema.prisma.wasp-last-db-concurrence-checksum but the schema is
-- in sync with the databse, we generate that file to avoid future checks.
--
-- NOTE: Because we currently only allow devs to migrate-dev, we only compare the schema to the DB since
-- there are no likely scenarios where schema == db but schema != migrations dir. In the future, as we add more DB commands,
-- we may wish to also compare the migrations dir to the DB as well.
warnIfDbNeedsMigration :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO (Maybe GeneratorWarning)
warnIfDbNeedsMigration spec projectRootDir = do
dbSchemaChecksumFileExists <- doesFileExist dbSchemaChecksumFp dbSchemaChecksumFileExists <- doesFileExist dbSchemaChecksumFp
if dbSchemaChecksumFileExists if dbSchemaChecksumFileExists
then do then warnIfSchemaDiffersFromChecksum dbSchemaFp dbSchemaChecksumFp
dbSchemaFileChecksum <- hexToString <$> checksumFromFilePath dbSchemaFp else
dbChecksumFileContents <- readFile dbSchemaChecksumFp if entitiesExist
return $ warnIf (dbSchemaFileChecksum /= dbChecksumFileContents) "Your Prisma schema has changed, you should run `wasp db migrate-dev`." then warnIfSchemaDiffersFromDb projectRootDir
else return $ warnIf entitiesExist "Please run `wasp db migrate-dev` to ensure the local project is fully initialized." else return Nothing
where where
dbSchemaFp = SP.fromAbsFile $ projectRootDir </> dbSchemaFileInProjectRootDir dbSchemaFp = SP.fromAbsFile $ projectRootDir </> dbSchemaFileInProjectRootDir
dbSchemaChecksumFp = SP.fromAbsFile $ projectRootDir </> dbSchemaChecksumOnLastMigrateFileProjectRootDir dbSchemaChecksumFp = SP.fromAbsFile $ projectRootDir </> dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir
entitiesExist = not . null $ getEntities spec entitiesExist = not . null $ getEntities spec
warnIf :: Bool -> String -> Maybe GeneratorWarning warnIfSchemaDiffersFromChecksum :: FilePath -> FilePath -> IO (Maybe GeneratorWarning)
warnIf b msg = if b then Just $ GeneratorNeedsMigrationWarning msg else Nothing warnIfSchemaDiffersFromChecksum dbSchemaFp dbSchemaChecksumFp = do
dbSchemaFileChecksum <- hexToString <$> checksumFromFilePath dbSchemaFp
dbChecksumFileContents <- readFile dbSchemaChecksumFp
if dbSchemaFileChecksum /= dbChecksumFileContents
then return . Just $ GeneratorNeedsMigrationWarning "Your Prisma schema has changed, please run `wasp db migrate-dev` when ready."
else return Nothing
warnIfSchemaDiffersFromDb :: Path' Abs (Dir ProjectRootDir) -> IO (Maybe GeneratorWarning)
warnIfSchemaDiffersFromDb projectRootDir = do
-- NOTE: If we wanted to, we could also check that the migrations dir == db,
-- but a schema check should handle all most likely cases.
schemaMatchesDb <- DbOps.doesSchemaMatchDb projectRootDir
case schemaMatchesDb of
Just True -> do
-- NOTE: Since we know schema == db, writing this file prevents future redundant Prisma checks.
DbOps.writeDbSchemaChecksumToFile projectRootDir (SP.castFile dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir)
return Nothing
Just False -> return . Just $ GeneratorNeedsMigrationWarning "Your Prisma schema does not match your database, please run `wasp db migrate-dev`."
-- NOTE: If there was an error, it could mean we could not connect to the SQLite db, since it does not exist.
-- Or it could mean their DATABASE_URL is wrong, or database is down, or any other number of causes.
-- In any case, migrating will either solve it (in the SQLite case), or allow Prisma to give them enough info to troubleshoot.
Nothing -> return . Just $ GeneratorNeedsMigrationWarning "Wasp was unable to verify your database is up to date. Running `wasp db migrate-dev` may fix this and will provide more info."
genPrismaClient :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO (Maybe GeneratorError) genPrismaClient :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO (Maybe GeneratorError)
genPrismaClient spec projectRootDir = do genPrismaClient spec projectRootDir = do

View File

@ -1,7 +1,7 @@
module Wasp.Generator.DbGenerator.Common module Wasp.Generator.DbGenerator.Common
( dbMigrationsDirInDbRootDir, ( dbMigrationsDirInDbRootDir,
dbRootDirInProjectRootDir, dbRootDirInProjectRootDir,
dbSchemaChecksumOnLastMigrateFileProjectRootDir, dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir,
dbSchemaChecksumOnLastGenerateFileProjectRootDir, dbSchemaChecksumOnLastGenerateFileProjectRootDir,
dbSchemaFileInDbTemplatesDir, dbSchemaFileInDbTemplatesDir,
dbSchemaFileInProjectRootDir, dbSchemaFileInProjectRootDir,
@ -19,10 +19,10 @@ data DbRootDir
data DbTemplatesDir data DbTemplatesDir
-- | This file represents the checksum of schema.prisma -- | This file represents the checksum of schema.prisma at the point
-- at the point at which `prisma db migrate-dev` was last run. It is used -- at which we last interacted with the DB to ensure they matched.
-- to help warn the user of instances when they may need to migrate. -- It is used to help warn the user of instances when they may need to migrate.
data DbSchemaChecksumOnLastMigrateFile data DbSchemaChecksumOnLastDbConcurrenceFile
-- | This file represents the checksum of schema.prisma -- | This file represents the checksum of schema.prisma
-- at the point at which `prisma generate` was last run. It is used -- at the point at which `prisma generate` was last run. It is used
@ -49,11 +49,11 @@ dbSchemaFileInProjectRootDir = dbRootDirInProjectRootDir </> dbSchemaFileInDbRoo
dbMigrationsDirInDbRootDir :: Path' (Rel DbRootDir) (Dir DbMigrationsDir) dbMigrationsDirInDbRootDir :: Path' (Rel DbRootDir) (Dir DbMigrationsDir)
dbMigrationsDirInDbRootDir = [reldir|migrations|] dbMigrationsDirInDbRootDir = [reldir|migrations|]
dbSchemaChecksumOnLastMigrateFileInDbRootDir :: Path' (Rel DbRootDir) (File DbSchemaChecksumOnLastMigrateFile) dbSchemaChecksumOnLastDbConcurrenceFileInDbRootDir :: Path' (Rel DbRootDir) (File DbSchemaChecksumOnLastDbConcurrenceFile)
dbSchemaChecksumOnLastMigrateFileInDbRootDir = [relfile|schema.prisma.wasp-migrate-checksum|] dbSchemaChecksumOnLastDbConcurrenceFileInDbRootDir = [relfile|schema.prisma.wasp-last-db-concurrence-checksum|]
dbSchemaChecksumOnLastMigrateFileProjectRootDir :: Path' (Rel ProjectRootDir) (File DbSchemaChecksumOnLastMigrateFile) dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir :: Path' (Rel ProjectRootDir) (File DbSchemaChecksumOnLastDbConcurrenceFile)
dbSchemaChecksumOnLastMigrateFileProjectRootDir = dbRootDirInProjectRootDir </> dbSchemaChecksumOnLastMigrateFileInDbRootDir dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir = dbRootDirInProjectRootDir </> dbSchemaChecksumOnLastDbConcurrenceFileInDbRootDir
dbSchemaChecksumOnLastGenerateFileInDbRootDir :: Path' (Rel DbRootDir) (File DbSchemaChecksumOnLastGenerateFile) dbSchemaChecksumOnLastGenerateFileInDbRootDir :: Path' (Rel DbRootDir) (File DbSchemaChecksumOnLastGenerateFile)
dbSchemaChecksumOnLastGenerateFileInDbRootDir = [relfile|schema.prisma.wasp-generate-checksum|] dbSchemaChecksumOnLastGenerateFileInDbRootDir = [relfile|schema.prisma.wasp-generate-checksum|]

View File

@ -1,5 +1,6 @@
module Wasp.Generator.DbGenerator.Jobs module Wasp.Generator.DbGenerator.Jobs
( migrateDev, ( migrateDev,
migrateDiff,
generatePrismaClient, generatePrismaClient,
runStudio, runStudio,
) )
@ -49,6 +50,25 @@ migrateDev projectDir maybeMigrationName = do
runNodeCommandAsJob serverDir "script" scriptArgs J.Db runNodeCommandAsJob serverDir "script" scriptArgs J.Db
-- | Diffs the Prisma schema file against the db.
-- Because of the --exit-code flag, it changes the exit code behavior
-- to signal if the diff is empty or not (Empty: 0, Error: 1, Not empty: 2)
migrateDiff :: Path' Abs (Dir ProjectRootDir) -> J.Job
migrateDiff projectDir = do
let serverDir = projectDir </> serverRootDirInProjectRootDir
let schemaFileFp = SP.toFilePath $ projectDir </> dbSchemaFileInProjectRootDir
let prismaMigrateDiffCmdArgs =
[ "migrate",
"diff",
"--from-schema-datamodel",
schemaFileFp,
"--to-schema-datasource",
schemaFileFp,
"--exit-code"
]
runNodeCommandAsJob serverDir (absPrismaExecutableFp projectDir) prismaMigrateDiffCmdArgs J.Db
-- | Runs `prisma studio` - Prisma's db inspector. -- | Runs `prisma studio` - Prisma's db inspector.
runStudio :: Path' Abs (Dir ProjectRootDir) -> J.Job runStudio :: Path' Abs (Dir ProjectRootDir) -> J.Job
runStudio projectDir = do runStudio projectDir = do

View File

@ -1,6 +1,8 @@
module Wasp.Generator.DbGenerator.Operations module Wasp.Generator.DbGenerator.Operations
( migrateDevAndCopyToSource, ( migrateDevAndCopyToSource,
generatePrismaClient, generatePrismaClient,
doesSchemaMatchDb,
writeDbSchemaChecksumToFile,
) )
where where
@ -18,8 +20,8 @@ import Wasp.Generator.Common (ProjectRootDir)
import Wasp.Generator.DbGenerator.Common import Wasp.Generator.DbGenerator.Common
( dbMigrationsDirInDbRootDir, ( dbMigrationsDirInDbRootDir,
dbRootDirInProjectRootDir, dbRootDirInProjectRootDir,
dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir,
dbSchemaChecksumOnLastGenerateFileProjectRootDir, dbSchemaChecksumOnLastGenerateFileProjectRootDir,
dbSchemaChecksumOnLastMigrateFileProjectRootDir,
dbSchemaFileInProjectRootDir, dbSchemaFileInProjectRootDir,
) )
import qualified Wasp.Generator.DbGenerator.Jobs as DbJobs import qualified Wasp.Generator.DbGenerator.Jobs as DbJobs
@ -57,7 +59,7 @@ finalizeMigration genProjectRootDirAbs dbMigrationsDirInWaspProjectDirAbs = do
-- NOTE: We are updating a managed CopyDirFileDraft outside the normal generation process, so we must invalidate the checksum entry for it. -- NOTE: We are updating a managed CopyDirFileDraft outside the normal generation process, so we must invalidate the checksum entry for it.
Generator.WriteFileDrafts.removeFromChecksumFile genProjectRootDirAbs [Right $ SP.castDir dbMigrationsDirInProjectRootDir] Generator.WriteFileDrafts.removeFromChecksumFile genProjectRootDirAbs [Right $ SP.castDir dbMigrationsDirInProjectRootDir]
res <- copyMigrationsBackToSource genProjectRootDirAbs dbMigrationsDirInWaspProjectDirAbs res <- copyMigrationsBackToSource genProjectRootDirAbs dbMigrationsDirInWaspProjectDirAbs
writeDbSchemaChecksumToFile genProjectRootDirAbs (SP.castFile dbSchemaChecksumOnLastMigrateFileProjectRootDir) writeDbSchemaChecksumToFile genProjectRootDirAbs (SP.castFile dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir)
return res return res
where where
dbMigrationsDirInProjectRootDir = dbRootDirInProjectRootDir SP.</> dbMigrationsDirInDbRootDir dbMigrationsDirInProjectRootDir = dbRootDirInProjectRootDir SP.</> dbMigrationsDirInDbRootDir
@ -96,3 +98,21 @@ generatePrismaClient genProjectRootDirAbs = do
writeDbSchemaChecksumToFile genProjectRootDirAbs (SP.castFile dbSchemaChecksumOnLastGenerateFileProjectRootDir) writeDbSchemaChecksumToFile genProjectRootDirAbs (SP.castFile dbSchemaChecksumOnLastGenerateFileProjectRootDir)
return $ Right () return $ Right ()
ExitFailure code -> return $ Left $ "Prisma client generation failed with exit code: " ++ show code ExitFailure code -> return $ Left $ "Prisma client generation failed with exit code: " ++ show code
-- | Checks `prisma migrate diff` exit code to determine if schema.prisma is
-- different than the DB. Returns Nothing on error as we do not know the current state.
-- Returns Just True if schema.prisma is the same as DB, Just False if it is different, and
-- Nothing if the check itself failed (exe: if a connection to the DB could not be established).
-- NOTE: Here we only compare the schema to the DB, and not the migrations dir.
doesSchemaMatchDb :: Path' Abs (Dir ProjectRootDir) -> IO (Maybe Bool)
doesSchemaMatchDb genProjectRootDirAbs = do
chan <- newChan
(_, dbExitCode) <-
concurrently
(readJobMessagesAndPrintThemPrefixed chan)
(DbJobs.migrateDiff genProjectRootDirAbs chan)
-- Schema in sync: 0, Error: 1, Schema differs: 2
case dbExitCode of
ExitSuccess -> return $ Just True
ExitFailure 2 -> return $ Just False
ExitFailure _ -> return Nothing