diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/files.manifest b/waspc/e2e-test/test-outputs/waspMigrate-golden/files.manifest index f2d313edd..5e12dd881 100644 --- a/waspc/e2e-test/test-outputs/waspMigrate-golden/files.manifest +++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/files.manifest @@ -6,7 +6,7 @@ waspMigrate/.wasp/out/db/migrations/no-date-foo/migration.sql waspMigrate/.wasp/out/db/package.json waspMigrate/.wasp/out/db/schema.prisma 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/server/.npmrc waspMigrate/.wasp/out/server/README.md diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/db/schema.prisma.wasp-migrate-checksum b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/db/schema.prisma.wasp-last-db-concurrence-checksum similarity index 100% rename from waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/db/schema.prisma.wasp-migrate-checksum rename to waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/db/schema.prisma.wasp-last-db-concurrence-checksum diff --git a/waspc/src/Wasp/Generator/DbGenerator.hs b/waspc/src/Wasp/Generator/DbGenerator.hs index e56f8dde4..0790caeb3 100644 --- a/waspc/src/Wasp/Generator/DbGenerator.hs +++ b/waspc/src/Wasp/Generator/DbGenerator.hs @@ -2,7 +2,7 @@ module Wasp.Generator.DbGenerator ( genDb, - warnIfDbSchemaChangedSinceLastMigration, + warnIfDbNeedsMigration, genPrismaClient, postWriteDbGeneratorActions, ) @@ -23,8 +23,8 @@ import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.DbGenerator.Common ( dbMigrationsDirInDbRootDir, dbRootDirInProjectRootDir, + dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir, dbSchemaChecksumOnLastGenerateFileProjectRootDir, - dbSchemaChecksumOnLastMigrateFileProjectRootDir, dbSchemaFileInDbTemplatesDir, dbSchemaFileInProjectRootDir, dbTemplatesDirInTemplatesDir, @@ -83,42 +83,70 @@ genMigrationsDir spec = -- | 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 spec dstDir = do - dbGeneratorWarnings <- maybeToList <$> warnIfDbSchemaChangedSinceLastMigration spec dstDir + dbGeneratorWarnings <- maybeToList <$> warnIfDbNeedsMigration spec dstDir dbGeneratorErrors <- maybeToList <$> genPrismaClient spec dstDir 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. -- -- This function makes following assumptions: -- - 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. --- - 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`: --- (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. --- (2) If schema.prisma.wasp-checksum does not exist, but the user has entities defined in schema.prisma (and thus, AppSpec). +-- (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-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. -- 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. -warnIfDbSchemaChangedSinceLastMigration :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO (Maybe GeneratorWarning) -warnIfDbSchemaChangedSinceLastMigration spec projectRootDir = do +-- In either of those scenarios, validate against DB itself to avoid redundant warnings. +-- +-- 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 - if dbSchemaChecksumFileExists - then do - dbSchemaFileChecksum <- hexToString <$> checksumFromFilePath dbSchemaFp - dbChecksumFileContents <- readFile dbSchemaChecksumFp - return $ warnIf (dbSchemaFileChecksum /= dbChecksumFileContents) "Your Prisma schema has changed, you should run `wasp db migrate-dev`." - else return $ warnIf entitiesExist "Please run `wasp db migrate-dev` to ensure the local project is fully initialized." + then warnIfSchemaDiffersFromChecksum dbSchemaFp dbSchemaChecksumFp + else + if entitiesExist + then warnIfSchemaDiffersFromDb projectRootDir + else return Nothing where dbSchemaFp = SP.fromAbsFile $ projectRootDir dbSchemaFileInProjectRootDir - dbSchemaChecksumFp = SP.fromAbsFile $ projectRootDir dbSchemaChecksumOnLastMigrateFileProjectRootDir + dbSchemaChecksumFp = SP.fromAbsFile $ projectRootDir dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir entitiesExist = not . null $ getEntities spec - warnIf :: Bool -> String -> Maybe GeneratorWarning - warnIf b msg = if b then Just $ GeneratorNeedsMigrationWarning msg else Nothing +warnIfSchemaDiffersFromChecksum :: FilePath -> FilePath -> IO (Maybe GeneratorWarning) +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 spec projectRootDir = do diff --git a/waspc/src/Wasp/Generator/DbGenerator/Common.hs b/waspc/src/Wasp/Generator/DbGenerator/Common.hs index 0bff4b980..91d2703c6 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Common.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Common.hs @@ -1,7 +1,7 @@ module Wasp.Generator.DbGenerator.Common ( dbMigrationsDirInDbRootDir, dbRootDirInProjectRootDir, - dbSchemaChecksumOnLastMigrateFileProjectRootDir, + dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir, dbSchemaChecksumOnLastGenerateFileProjectRootDir, dbSchemaFileInDbTemplatesDir, dbSchemaFileInProjectRootDir, @@ -19,10 +19,10 @@ data DbRootDir data DbTemplatesDir --- | This file represents the checksum of schema.prisma --- at the point at which `prisma db migrate-dev` was last run. It is used --- to help warn the user of instances when they may need to migrate. -data DbSchemaChecksumOnLastMigrateFile +-- | This file represents the checksum of schema.prisma at the point +-- at which we last interacted with the DB to ensure they matched. +-- It is used to help warn the user of instances when they may need to migrate. +data DbSchemaChecksumOnLastDbConcurrenceFile -- | This file represents the checksum of schema.prisma -- 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 = [reldir|migrations|] -dbSchemaChecksumOnLastMigrateFileInDbRootDir :: Path' (Rel DbRootDir) (File DbSchemaChecksumOnLastMigrateFile) -dbSchemaChecksumOnLastMigrateFileInDbRootDir = [relfile|schema.prisma.wasp-migrate-checksum|] +dbSchemaChecksumOnLastDbConcurrenceFileInDbRootDir :: Path' (Rel DbRootDir) (File DbSchemaChecksumOnLastDbConcurrenceFile) +dbSchemaChecksumOnLastDbConcurrenceFileInDbRootDir = [relfile|schema.prisma.wasp-last-db-concurrence-checksum|] -dbSchemaChecksumOnLastMigrateFileProjectRootDir :: Path' (Rel ProjectRootDir) (File DbSchemaChecksumOnLastMigrateFile) -dbSchemaChecksumOnLastMigrateFileProjectRootDir = dbRootDirInProjectRootDir dbSchemaChecksumOnLastMigrateFileInDbRootDir +dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir :: Path' (Rel ProjectRootDir) (File DbSchemaChecksumOnLastDbConcurrenceFile) +dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir = dbRootDirInProjectRootDir dbSchemaChecksumOnLastDbConcurrenceFileInDbRootDir dbSchemaChecksumOnLastGenerateFileInDbRootDir :: Path' (Rel DbRootDir) (File DbSchemaChecksumOnLastGenerateFile) dbSchemaChecksumOnLastGenerateFileInDbRootDir = [relfile|schema.prisma.wasp-generate-checksum|] diff --git a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs index a63a2fc30..36b6afd42 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs @@ -1,5 +1,6 @@ module Wasp.Generator.DbGenerator.Jobs ( migrateDev, + migrateDiff, generatePrismaClient, runStudio, ) @@ -49,6 +50,25 @@ migrateDev projectDir maybeMigrationName = do 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. runStudio :: Path' Abs (Dir ProjectRootDir) -> J.Job runStudio projectDir = do diff --git a/waspc/src/Wasp/Generator/DbGenerator/Operations.hs b/waspc/src/Wasp/Generator/DbGenerator/Operations.hs index dd47c94b0..695dfd111 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Operations.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Operations.hs @@ -1,6 +1,8 @@ module Wasp.Generator.DbGenerator.Operations ( migrateDevAndCopyToSource, generatePrismaClient, + doesSchemaMatchDb, + writeDbSchemaChecksumToFile, ) where @@ -18,8 +20,8 @@ import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.DbGenerator.Common ( dbMigrationsDirInDbRootDir, dbRootDirInProjectRootDir, + dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir, dbSchemaChecksumOnLastGenerateFileProjectRootDir, - dbSchemaChecksumOnLastMigrateFileProjectRootDir, dbSchemaFileInProjectRootDir, ) 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. Generator.WriteFileDrafts.removeFromChecksumFile genProjectRootDirAbs [Right $ SP.castDir dbMigrationsDirInProjectRootDir] res <- copyMigrationsBackToSource genProjectRootDirAbs dbMigrationsDirInWaspProjectDirAbs - writeDbSchemaChecksumToFile genProjectRootDirAbs (SP.castFile dbSchemaChecksumOnLastMigrateFileProjectRootDir) + writeDbSchemaChecksumToFile genProjectRootDirAbs (SP.castFile dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir) return res where dbMigrationsDirInProjectRootDir = dbRootDirInProjectRootDir SP. dbMigrationsDirInDbRootDir @@ -96,3 +98,21 @@ generatePrismaClient genProjectRootDirAbs = do writeDbSchemaChecksumToFile genProjectRootDirAbs (SP.castFile dbSchemaChecksumOnLastGenerateFileProjectRootDir) return $ Right () 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