We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
bug: `meltano install` exits success even if migrations fail
I would expect `meltano install` to fail and exit with a non-zero exit code if the migrations fail. Instead, it proceeds to install plugins.

To reproduce:

```
$ meltano --version && python --version
meltano, version 2.2.0
Python 3.9.13
$ meltano init example
$ cd example
# this is just an example of a way to make migrations fail
$ sqlite3 .meltano/meltano.db "UPDATE alembic_version SET version_num = 'garbage';"
$ meltano install                                                                   
2022-07-12T16:35:37.771519Z [info     ] Environment 'dev' is active
2022-07-12T16:35:38.860146Z [error    ] No such revision or branch 'garbage' 
Traceback (most recent call last):
  File ".venv/lib/python3.9/site-packages/meltano/core/migration_service.py", line 89, in upgrade
    self.ensure_migration_needed(script, context, head)
  File ".venv/lib/python3.9/site-packages/meltano/core/migration_service.py", line 55, in ensure_migration_needed
    for rev in script.iterate_revisions(current_head, "base"):
  File ".venv/lib/python3.9/site-packages/alembic/script/revision.py", line 799, in iterate_revisions
    revisions, heads = fn(
  File ".venv/lib/python3.9/site-packages/alembic/script/revision.py", line 1426, in _collect_upgrade_revisions
    for rev in self._parse_upgrade_target(
  File ".venv/lib/python3.9/site-packages/alembic/script/revision.py", line 1218, in _parse_upgrade_target
    return self.get_revisions(target)
  File ".venv/lib/python3.9/site-packages/alembic/script/revision.py", line 552, in get_revisions
    return tuple(
  File ".venv/lib/python3.9/site-packages/alembic/script/revision.py", line 553, in <genexpr>
    self._revision_for_ident(rev_id, branch_label)
  File ".venv/lib/python3.9/site-packages/alembic/script/revision.py", line 624, in _revision_for_ident
    raise ResolutionError(
alembic.script.revision.ResolutionError: No such revision or branch 'garbage'
Cannot upgrade the system database. It might be corrupted or was created before database migrations where introduced (v0.34.0)
Installing 0 plugins...
$ echo $?                                                                           
0


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/meltano/core/migration_service.py`
Content:
```
1 """Migration and system db management."""
2 from __future__ import annotations
3 
4 import logging
5 
6 import click
7 import sqlalchemy
8 from alembic import command
9 from alembic.config import Config
10 from alembic.runtime.migration import MigrationContext
11 from alembic.script import ScriptDirectory
12 from sqlalchemy.engine import Engine
13 from sqlalchemy.orm import Session
14 
15 from meltano.api.models.security import Role, RolePermissions
16 from meltano.core.db import project_engine
17 from meltano.core.project import Project
18 from meltano.migrations import LOCK_PATH, MIGRATION_DIR
19 
20 SPLAT = "*"
21 
22 
23 class MigrationError(Exception):
24     """Generic class for migration errors."""
25 
26 
27 class MigrationUneededException(Exception):
28     """Occurs when no migrations are needed."""
29 
30 
31 class MigrationService:
32     """Migration service."""
33 
34     def __init__(self, engine: Engine) -> None:
35         """Initialize the migration service.
36 
37         Args:
38             engine: The sqlalchemy engine to use for the migration and checks.
39         """
40         self.engine = engine
41 
42     def ensure_migration_needed(
43         self, script: ScriptDirectory, context: MigrationContext, target_revision: str
44     ) -> None:
45         """Ensure that a migration of the system database is actually needed.
46 
47         Args:
48             script: The alembic script directory.
49             context: The migration context.
50             target_revision: The desired target revision.
51 
52         Raises:
53             MigrationUneededException: If no migration is needed.
54         """
55         current_head = context.get_current_revision()
56 
57         for rev in script.iterate_revisions(current_head, "base"):
58             if rev.revision == target_revision:
59                 raise MigrationUneededException
60 
61     def upgrade(  # noqa: WPS213, WPS231 too many expression and too complex
62         self, silent: bool = False
63     ) -> None:
64         """Upgrade to the latest revision.
65 
66         Args:
67             silent: If true, don't print anything.
68 
69         Raises:
70             MigrationError: If the upgrade fails.
71         """
72         conn = self.engine.connect()
73         cfg = Config()
74 
75         # this connection is used in `env.py` for the migrations
76         cfg.attributes["connection"] = conn
77         cfg.set_main_option("script_location", str(MIGRATION_DIR))
78         script = ScriptDirectory.from_config(cfg)
79         # let's make sure we actually need to migrate
80 
81         migration_logger = logging.getLogger("alembic.runtime.migration")
82         original_log_level = migration_logger.getEffectiveLevel()
83         if silent:
84             migration_logger.setLevel(logging.ERROR)
85 
86         context = MigrationContext.configure(conn)
87 
88         try:  # noqa: WPS229
89             # try to find the locked version
90             head = LOCK_PATH.open().read().strip()
91             self.ensure_migration_needed(script, context, head)
92 
93             if not silent:
94                 click.secho(f"Upgrading database to {head}")
95             command.upgrade(cfg, head)
96 
97             if silent:
98                 migration_logger.setLevel(original_log_level)
99         except FileNotFoundError:
100             raise MigrationError(
101                 "Cannot upgrade the system database, revision lock not found."
102             )
103         except MigrationUneededException:
104             if not silent:
105                 click.secho("System database up-to-date.")
106         except Exception as err:
107             logging.exception(str(err))
108             click.secho(
109                 "Cannot upgrade the system database. It might be corrupted or was created before database migrations where introduced (v0.34.0)",
110                 fg="yellow",
111                 err=True,
112             )
113         finally:
114             conn.close()
115 
116     def seed(self, project: Project) -> None:
117         """Seed the database with the default roles and permissions.
118 
119         Args:
120             project: The project to seed the database for.
121         """
122         _, session_maker = project_engine(project)
123         session = session_maker()
124         try:  # noqa: WPS501, WPS229 Found too long try body length and finally without except
125             self._create_user_role(session)
126             session.commit()
127         finally:
128             session.close()
129 
130     def _create_user_role(self, session: Session) -> None:
131         """Actually perform the database seeding creating users/roles.
132 
133         Args:
134             session: The session to use.
135         """
136         if not session.query(Role).filter_by(name="admin").first():
137 
138             session.add(
139                 Role(
140                     name="admin",
141                     description="Meltano Admin",
142                     permissions=[
143                         RolePermissions(type="view:design", context=SPLAT),
144                         RolePermissions(type="view:reports", context=SPLAT),
145                         RolePermissions(type="modify:acl", context=SPLAT),
146                     ],
147                 )
148             )
149 
150         if not session.query(Role).filter_by(name="regular").first():
151             session.merge(Role(name="regular", description="Meltano User"))
152 
153         # add the universal permissions to Admin
154         admin = session.query(Role).filter_by(name="admin").one()
155         try:
156             session.query(RolePermissions).filter_by(
157                 role=admin, type=SPLAT, context=SPLAT
158             ).one()
159         except sqlalchemy.orm.exc.NoResultFound:
160             admin.permissions.append(RolePermissions(type=SPLAT, context=SPLAT))
161
```

Path: `src/meltano/cli/params.py`
Content:
```
1 from __future__ import annotations
2 
3 import functools
4 
5 import click
6 from click.globals import get_current_context as get_current_click_context
7 
8 from meltano.core.db import project_engine
9 from meltano.core.project_settings_service import ProjectSettingsService
10 
11 from .utils import CliError
12 
13 
14 def database_uri_option(func):
15     @click.option("--database-uri", help="System database URI.")
16     def decorate(*args, database_uri=None, **kwargs):
17         if database_uri:
18             ProjectSettingsService.config_override["database_uri"] = database_uri
19 
20         return func(*args, **kwargs)
21 
22     return functools.update_wrapper(decorate, func)
23 
24 
25 class pass_project:  # noqa: N801
26     """Pass current project to decorated CLI command function."""
27 
28     __name__ = "project"
29 
30     def __init__(self, migrate=False):
31         self.migrate = migrate
32 
33     def __call__(self, func):
34         @database_uri_option
35         def decorate(*args, **kwargs):
36             ctx = get_current_click_context()
37 
38             project = ctx.obj["project"]
39             if not project:
40                 raise CliError(
41                     f"`{ctx.command_path}` must be run inside a Meltano project.\n"
42                     + "Use `meltano init <project_name>` to create one."
43                 )
44 
45             # register the system database connection
46             engine, _ = project_engine(project, default=True)
47 
48             if self.migrate:
49                 from meltano.core.migration_service import MigrationService
50 
51                 migration_service = MigrationService(engine)
52                 migration_service.upgrade(silent=True)
53                 migration_service.seed(project)
54 
55             func(project, *args, **kwargs)
56 
57         return functools.update_wrapper(decorate, func)
58
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/meltano/cli/params.py b/src/meltano/cli/params.py
--- a/src/meltano/cli/params.py
+++ b/src/meltano/cli/params.py
@@ -1,3 +1,5 @@
+"""Click parameter helper decorators."""
+
 from __future__ import annotations
 
 import functools
@@ -6,12 +8,19 @@
 from click.globals import get_current_context as get_current_click_context
 
 from meltano.core.db import project_engine
+from meltano.core.migration_service import MigrationError
 from meltano.core.project_settings_service import ProjectSettingsService
 
 from .utils import CliError
 
 
 def database_uri_option(func):
+    """Database URI Click option decorator.
+
+    args:
+        func: The function to decorate.
+    """
+
     @click.option("--database-uri", help="System database URI.")
     def decorate(*args, database_uri=None, **kwargs):
         if database_uri:
@@ -28,9 +37,20 @@
     __name__ = "project"
 
     def __init__(self, migrate=False):
+        """Instantiate decorator.
+
+        args:
+            migrate: Flag to perform database migration before passing the project.
+        """
         self.migrate = migrate
 
     def __call__(self, func):
+        """Return decorated function.
+
+        args:
+            func: The function to decorate.
+        """
+
         @database_uri_option
         def decorate(*args, **kwargs):
             ctx = get_current_click_context()
@@ -48,9 +68,12 @@
             if self.migrate:
                 from meltano.core.migration_service import MigrationService
 
-                migration_service = MigrationService(engine)
-                migration_service.upgrade(silent=True)
-                migration_service.seed(project)
+                try:
+                    migration_service = MigrationService(engine)
+                    migration_service.upgrade(silent=True)
+                    migration_service.seed(project)
+                except MigrationError as err:
+                    raise CliError(str(err))
 
             func(project, *args, **kwargs)
 
diff --git a/src/meltano/core/migration_service.py b/src/meltano/core/migration_service.py
--- a/src/meltano/core/migration_service.py
+++ b/src/meltano/core/migration_service.py
@@ -105,10 +105,8 @@
                 click.secho("System database up-to-date.")
         except Exception as err:
             logging.exception(str(err))
-            click.secho(
-                "Cannot upgrade the system database. It might be corrupted or was created before database migrations where introduced (v0.34.0)",
-                fg="yellow",
-                err=True,
+            raise MigrationError(
+                "Cannot upgrade the system database. It might be corrupted or was created before database migrations where introduced (v0.34.0)"
             )
         finally:
             conn.close()

{"golden_diff": "diff --git a/src/meltano/cli/params.py b/src/meltano/cli/params.py\n--- a/src/meltano/cli/params.py\n+++ b/src/meltano/cli/params.py\n@@ -1,3 +1,5 @@\n+\"\"\"Click parameter helper decorators.\"\"\"\n+\n from __future__ import annotations\n \n import functools\n@@ -6,12 +8,19 @@\n from click.globals import get_current_context as get_current_click_context\n \n from meltano.core.db import project_engine\n+from meltano.core.migration_service import MigrationError\n from meltano.core.project_settings_service import ProjectSettingsService\n \n from .utils import CliError\n \n \n def database_uri_option(func):\n+    \"\"\"Database URI Click option decorator.\n+\n+    args:\n+        func: The function to decorate.\n+    \"\"\"\n+\n     @click.option(\"--database-uri\", help=\"System database URI.\")\n     def decorate(*args, database_uri=None, **kwargs):\n         if database_uri:\n@@ -28,9 +37,20 @@\n     __name__ = \"project\"\n \n     def __init__(self, migrate=False):\n+        \"\"\"Instantiate decorator.\n+\n+        args:\n+            migrate: Flag to perform database migration before passing the project.\n+        \"\"\"\n         self.migrate = migrate\n \n     def __call__(self, func):\n+        \"\"\"Return decorated function.\n+\n+        args:\n+            func: The function to decorate.\n+        \"\"\"\n+\n         @database_uri_option\n         def decorate(*args, **kwargs):\n             ctx = get_current_click_context()\n@@ -48,9 +68,12 @@\n             if self.migrate:\n                 from meltano.core.migration_service import MigrationService\n \n-                migration_service = MigrationService(engine)\n-                migration_service.upgrade(silent=True)\n-                migration_service.seed(project)\n+                try:\n+                    migration_service = MigrationService(engine)\n+                    migration_service.upgrade(silent=True)\n+                    migration_service.seed(project)\n+                except MigrationError as err:\n+                    raise CliError(str(err))\n \n             func(project, *args, **kwargs)\n \ndiff --git a/src/meltano/core/migration_service.py b/src/meltano/core/migration_service.py\n--- a/src/meltano/core/migration_service.py\n+++ b/src/meltano/core/migration_service.py\n@@ -105,10 +105,8 @@\n                 click.secho(\"System database up-to-date.\")\n         except Exception as err:\n             logging.exception(str(err))\n-            click.secho(\n-                \"Cannot upgrade the system database. It might be corrupted or was created before database migrations where introduced (v0.34.0)\",\n-                fg=\"yellow\",\n-                err=True,\n+            raise MigrationError(\n+                \"Cannot upgrade the system database. It might be corrupted or was created before database migrations where introduced (v0.34.0)\"\n             )\n         finally:\n             conn.close()\n", "issue": "bug: `meltano install` exits success even if migrations fail\nI would expect `meltano install` to fail and exit with a non-zero exit code if the migrations fail. Instead, it proceeds to install plugins.\r\n\r\nTo reproduce:\r\n\r\n```\r\n$ meltano --version && python --version\r\nmeltano, version 2.2.0\r\nPython 3.9.13\r\n$ meltano init example\r\n$ cd example\r\n# this is just an example of a way to make migrations fail\r\n$ sqlite3 .meltano/meltano.db \"UPDATE alembic_version SET version_num = 'garbage';\"\r\n$ meltano install                                                                   \r\n2022-07-12T16:35:37.771519Z [info     ] Environment 'dev' is active\r\n2022-07-12T16:35:38.860146Z [error    ] No such revision or branch 'garbage' \r\nTraceback (most recent call last):\r\n  File \".venv/lib/python3.9/site-packages/meltano/core/migration_service.py\", line 89, in upgrade\r\n    self.ensure_migration_needed(script, context, head)\r\n  File \".venv/lib/python3.9/site-packages/meltano/core/migration_service.py\", line 55, in ensure_migration_needed\r\n    for rev in script.iterate_revisions(current_head, \"base\"):\r\n  File \".venv/lib/python3.9/site-packages/alembic/script/revision.py\", line 799, in iterate_revisions\r\n    revisions, heads = fn(\r\n  File \".venv/lib/python3.9/site-packages/alembic/script/revision.py\", line 1426, in _collect_upgrade_revisions\r\n    for rev in self._parse_upgrade_target(\r\n  File \".venv/lib/python3.9/site-packages/alembic/script/revision.py\", line 1218, in _parse_upgrade_target\r\n    return self.get_revisions(target)\r\n  File \".venv/lib/python3.9/site-packages/alembic/script/revision.py\", line 552, in get_revisions\r\n    return tuple(\r\n  File \".venv/lib/python3.9/site-packages/alembic/script/revision.py\", line 553, in <genexpr>\r\n    self._revision_for_ident(rev_id, branch_label)\r\n  File \".venv/lib/python3.9/site-packages/alembic/script/revision.py\", line 624, in _revision_for_ident\r\n    raise ResolutionError(\r\nalembic.script.revision.ResolutionError: No such revision or branch 'garbage'\r\nCannot upgrade the system database. It might be corrupted or was created before database migrations where introduced (v0.34.0)\r\nInstalling 0 plugins...\r\n$ echo $?                                                                           \r\n0\r\n\n", "before_files": [{"content": "\"\"\"Migration and system db management.\"\"\"\nfrom __future__ import annotations\n\nimport logging\n\nimport click\nimport sqlalchemy\nfrom alembic import command\nfrom alembic.config import Config\nfrom alembic.runtime.migration import MigrationContext\nfrom alembic.script import ScriptDirectory\nfrom sqlalchemy.engine import Engine\nfrom sqlalchemy.orm import Session\n\nfrom meltano.api.models.security import Role, RolePermissions\nfrom meltano.core.db import project_engine\nfrom meltano.core.project import Project\nfrom meltano.migrations import LOCK_PATH, MIGRATION_DIR\n\nSPLAT = \"*\"\n\n\nclass MigrationError(Exception):\n    \"\"\"Generic class for migration errors.\"\"\"\n\n\nclass MigrationUneededException(Exception):\n    \"\"\"Occurs when no migrations are needed.\"\"\"\n\n\nclass MigrationService:\n    \"\"\"Migration service.\"\"\"\n\n    def __init__(self, engine: Engine) -> None:\n        \"\"\"Initialize the migration service.\n\n        Args:\n            engine: The sqlalchemy engine to use for the migration and checks.\n        \"\"\"\n        self.engine = engine\n\n    def ensure_migration_needed(\n        self, script: ScriptDirectory, context: MigrationContext, target_revision: str\n    ) -> None:\n        \"\"\"Ensure that a migration of the system database is actually needed.\n\n        Args:\n            script: The alembic script directory.\n            context: The migration context.\n            target_revision: The desired target revision.\n\n        Raises:\n            MigrationUneededException: If no migration is needed.\n        \"\"\"\n        current_head = context.get_current_revision()\n\n        for rev in script.iterate_revisions(current_head, \"base\"):\n            if rev.revision == target_revision:\n                raise MigrationUneededException\n\n    def upgrade(  # noqa: WPS213, WPS231 too many expression and too complex\n        self, silent: bool = False\n    ) -> None:\n        \"\"\"Upgrade to the latest revision.\n\n        Args:\n            silent: If true, don't print anything.\n\n        Raises:\n            MigrationError: If the upgrade fails.\n        \"\"\"\n        conn = self.engine.connect()\n        cfg = Config()\n\n        # this connection is used in `env.py` for the migrations\n        cfg.attributes[\"connection\"] = conn\n        cfg.set_main_option(\"script_location\", str(MIGRATION_DIR))\n        script = ScriptDirectory.from_config(cfg)\n        # let's make sure we actually need to migrate\n\n        migration_logger = logging.getLogger(\"alembic.runtime.migration\")\n        original_log_level = migration_logger.getEffectiveLevel()\n        if silent:\n            migration_logger.setLevel(logging.ERROR)\n\n        context = MigrationContext.configure(conn)\n\n        try:  # noqa: WPS229\n            # try to find the locked version\n            head = LOCK_PATH.open().read().strip()\n            self.ensure_migration_needed(script, context, head)\n\n            if not silent:\n                click.secho(f\"Upgrading database to {head}\")\n            command.upgrade(cfg, head)\n\n            if silent:\n                migration_logger.setLevel(original_log_level)\n        except FileNotFoundError:\n            raise MigrationError(\n                \"Cannot upgrade the system database, revision lock not found.\"\n            )\n        except MigrationUneededException:\n            if not silent:\n                click.secho(\"System database up-to-date.\")\n        except Exception as err:\n            logging.exception(str(err))\n            click.secho(\n                \"Cannot upgrade the system database. It might be corrupted or was created before database migrations where introduced (v0.34.0)\",\n                fg=\"yellow\",\n                err=True,\n            )\n        finally:\n            conn.close()\n\n    def seed(self, project: Project) -> None:\n        \"\"\"Seed the database with the default roles and permissions.\n\n        Args:\n            project: The project to seed the database for.\n        \"\"\"\n        _, session_maker = project_engine(project)\n        session = session_maker()\n        try:  # noqa: WPS501, WPS229 Found too long try body length and finally without except\n            self._create_user_role(session)\n            session.commit()\n        finally:\n            session.close()\n\n    def _create_user_role(self, session: Session) -> None:\n        \"\"\"Actually perform the database seeding creating users/roles.\n\n        Args:\n            session: The session to use.\n        \"\"\"\n        if not session.query(Role).filter_by(name=\"admin\").first():\n\n            session.add(\n                Role(\n                    name=\"admin\",\n                    description=\"Meltano Admin\",\n                    permissions=[\n                        RolePermissions(type=\"view:design\", context=SPLAT),\n                        RolePermissions(type=\"view:reports\", context=SPLAT),\n                        RolePermissions(type=\"modify:acl\", context=SPLAT),\n                    ],\n                )\n            )\n\n        if not session.query(Role).filter_by(name=\"regular\").first():\n            session.merge(Role(name=\"regular\", description=\"Meltano User\"))\n\n        # add the universal permissions to Admin\n        admin = session.query(Role).filter_by(name=\"admin\").one()\n        try:\n            session.query(RolePermissions).filter_by(\n                role=admin, type=SPLAT, context=SPLAT\n            ).one()\n        except sqlalchemy.orm.exc.NoResultFound:\n            admin.permissions.append(RolePermissions(type=SPLAT, context=SPLAT))\n", "path": "src/meltano/core/migration_service.py"}, {"content": "from __future__ import annotations\n\nimport functools\n\nimport click\nfrom click.globals import get_current_context as get_current_click_context\n\nfrom meltano.core.db import project_engine\nfrom meltano.core.project_settings_service import ProjectSettingsService\n\nfrom .utils import CliError\n\n\ndef database_uri_option(func):\n    @click.option(\"--database-uri\", help=\"System database URI.\")\n    def decorate(*args, database_uri=None, **kwargs):\n        if database_uri:\n            ProjectSettingsService.config_override[\"database_uri\"] = database_uri\n\n        return func(*args, **kwargs)\n\n    return functools.update_wrapper(decorate, func)\n\n\nclass pass_project:  # noqa: N801\n    \"\"\"Pass current project to decorated CLI command function.\"\"\"\n\n    __name__ = \"project\"\n\n    def __init__(self, migrate=False):\n        self.migrate = migrate\n\n    def __call__(self, func):\n        @database_uri_option\n        def decorate(*args, **kwargs):\n            ctx = get_current_click_context()\n\n            project = ctx.obj[\"project\"]\n            if not project:\n                raise CliError(\n                    f\"`{ctx.command_path}` must be run inside a Meltano project.\\n\"\n                    + \"Use `meltano init <project_name>` to create one.\"\n                )\n\n            # register the system database connection\n            engine, _ = project_engine(project, default=True)\n\n            if self.migrate:\n                from meltano.core.migration_service import MigrationService\n\n                migration_service = MigrationService(engine)\n                migration_service.upgrade(silent=True)\n                migration_service.seed(project)\n\n            func(project, *args, **kwargs)\n\n        return functools.update_wrapper(decorate, func)\n", "path": "src/meltano/cli/params.py"}], "after_files": [{"content": "\"\"\"Migration and system db management.\"\"\"\nfrom __future__ import annotations\n\nimport logging\n\nimport click\nimport sqlalchemy\nfrom alembic import command\nfrom alembic.config import Config\nfrom alembic.runtime.migration import MigrationContext\nfrom alembic.script import ScriptDirectory\nfrom sqlalchemy.engine import Engine\nfrom sqlalchemy.orm import Session\n\nfrom meltano.api.models.security import Role, RolePermissions\nfrom meltano.core.db import project_engine\nfrom meltano.core.project import Project\nfrom meltano.migrations import LOCK_PATH, MIGRATION_DIR\n\nSPLAT = \"*\"\n\n\nclass MigrationError(Exception):\n    \"\"\"Generic class for migration errors.\"\"\"\n\n\nclass MigrationUneededException(Exception):\n    \"\"\"Occurs when no migrations are needed.\"\"\"\n\n\nclass MigrationService:\n    \"\"\"Migration service.\"\"\"\n\n    def __init__(self, engine: Engine) -> None:\n        \"\"\"Initialize the migration service.\n\n        Args:\n            engine: The sqlalchemy engine to use for the migration and checks.\n        \"\"\"\n        self.engine = engine\n\n    def ensure_migration_needed(\n        self, script: ScriptDirectory, context: MigrationContext, target_revision: str\n    ) -> None:\n        \"\"\"Ensure that a migration of the system database is actually needed.\n\n        Args:\n            script: The alembic script directory.\n            context: The migration context.\n            target_revision: The desired target revision.\n\n        Raises:\n            MigrationUneededException: If no migration is needed.\n        \"\"\"\n        current_head = context.get_current_revision()\n\n        for rev in script.iterate_revisions(current_head, \"base\"):\n            if rev.revision == target_revision:\n                raise MigrationUneededException\n\n    def upgrade(  # noqa: WPS213, WPS231 too many expression and too complex\n        self, silent: bool = False\n    ) -> None:\n        \"\"\"Upgrade to the latest revision.\n\n        Args:\n            silent: If true, don't print anything.\n\n        Raises:\n            MigrationError: If the upgrade fails.\n        \"\"\"\n        conn = self.engine.connect()\n        cfg = Config()\n\n        # this connection is used in `env.py` for the migrations\n        cfg.attributes[\"connection\"] = conn\n        cfg.set_main_option(\"script_location\", str(MIGRATION_DIR))\n        script = ScriptDirectory.from_config(cfg)\n        # let's make sure we actually need to migrate\n\n        migration_logger = logging.getLogger(\"alembic.runtime.migration\")\n        original_log_level = migration_logger.getEffectiveLevel()\n        if silent:\n            migration_logger.setLevel(logging.ERROR)\n\n        context = MigrationContext.configure(conn)\n\n        try:  # noqa: WPS229\n            # try to find the locked version\n            head = LOCK_PATH.open().read().strip()\n            self.ensure_migration_needed(script, context, head)\n\n            if not silent:\n                click.secho(f\"Upgrading database to {head}\")\n            command.upgrade(cfg, head)\n\n            if silent:\n                migration_logger.setLevel(original_log_level)\n        except FileNotFoundError:\n            raise MigrationError(\n                \"Cannot upgrade the system database, revision lock not found.\"\n            )\n        except MigrationUneededException:\n            if not silent:\n                click.secho(\"System database up-to-date.\")\n        except Exception as err:\n            logging.exception(str(err))\n            raise MigrationError(\n                \"Cannot upgrade the system database. It might be corrupted or was created before database migrations where introduced (v0.34.0)\"\n            )\n        finally:\n            conn.close()\n\n    def seed(self, project: Project) -> None:\n        \"\"\"Seed the database with the default roles and permissions.\n\n        Args:\n            project: The project to seed the database for.\n        \"\"\"\n        _, session_maker = project_engine(project)\n        session = session_maker()\n        try:  # noqa: WPS501, WPS229 Found too long try body length and finally without except\n            self._create_user_role(session)\n            session.commit()\n        finally:\n            session.close()\n\n    def _create_user_role(self, session: Session) -> None:\n        \"\"\"Actually perform the database seeding creating users/roles.\n\n        Args:\n            session: The session to use.\n        \"\"\"\n        if not session.query(Role).filter_by(name=\"admin\").first():\n\n            session.add(\n                Role(\n                    name=\"admin\",\n                    description=\"Meltano Admin\",\n                    permissions=[\n                        RolePermissions(type=\"view:design\", context=SPLAT),\n                        RolePermissions(type=\"view:reports\", context=SPLAT),\n                        RolePermissions(type=\"modify:acl\", context=SPLAT),\n                    ],\n                )\n            )\n\n        if not session.query(Role).filter_by(name=\"regular\").first():\n            session.merge(Role(name=\"regular\", description=\"Meltano User\"))\n\n        # add the universal permissions to Admin\n        admin = session.query(Role).filter_by(name=\"admin\").one()\n        try:\n            session.query(RolePermissions).filter_by(\n                role=admin, type=SPLAT, context=SPLAT\n            ).one()\n        except sqlalchemy.orm.exc.NoResultFound:\n            admin.permissions.append(RolePermissions(type=SPLAT, context=SPLAT))\n", "path": "src/meltano/core/migration_service.py"}, {"content": "\"\"\"Click parameter helper decorators.\"\"\"\n\nfrom __future__ import annotations\n\nimport functools\n\nimport click\nfrom click.globals import get_current_context as get_current_click_context\n\nfrom meltano.core.db import project_engine\nfrom meltano.core.migration_service import MigrationError\nfrom meltano.core.project_settings_service import ProjectSettingsService\n\nfrom .utils import CliError\n\n\ndef database_uri_option(func):\n    \"\"\"Database URI Click option decorator.\n\n    args:\n        func: The function to decorate.\n    \"\"\"\n\n    @click.option(\"--database-uri\", help=\"System database URI.\")\n    def decorate(*args, database_uri=None, **kwargs):\n        if database_uri:\n            ProjectSettingsService.config_override[\"database_uri\"] = database_uri\n\n        return func(*args, **kwargs)\n\n    return functools.update_wrapper(decorate, func)\n\n\nclass pass_project:  # noqa: N801\n    \"\"\"Pass current project to decorated CLI command function.\"\"\"\n\n    __name__ = \"project\"\n\n    def __init__(self, migrate=False):\n        \"\"\"Instantiate decorator.\n\n        args:\n            migrate: Flag to perform database migration before passing the project.\n        \"\"\"\n        self.migrate = migrate\n\n    def __call__(self, func):\n        \"\"\"Return decorated function.\n\n        args:\n            func: The function to decorate.\n        \"\"\"\n\n        @database_uri_option\n        def decorate(*args, **kwargs):\n            ctx = get_current_click_context()\n\n            project = ctx.obj[\"project\"]\n            if not project:\n                raise CliError(\n                    f\"`{ctx.command_path}` must be run inside a Meltano project.\\n\"\n                    + \"Use `meltano init <project_name>` to create one.\"\n                )\n\n            # register the system database connection\n            engine, _ = project_engine(project, default=True)\n\n            if self.migrate:\n                from meltano.core.migration_service import MigrationService\n\n                try:\n                    migration_service = MigrationService(engine)\n                    migration_service.upgrade(silent=True)\n                    migration_service.seed(project)\n                except MigrationError as err:\n                    raise CliError(str(err))\n\n            func(project, *args, **kwargs)\n\n        return functools.update_wrapper(decorate, func)\n", "path": "src/meltano/cli/params.py"}]}