Skip to content

Commit

Permalink
Merge pull request #4021 from mathesar-foundation/rpc_sql_install
Browse files Browse the repository at this point in the history
Add RPC functions to install/update SQL on configured databases
  • Loading branch information
mathemancer authored Nov 11, 2024
2 parents e83db0c + 5c2a97d commit 7f2b6d0
Show file tree
Hide file tree
Showing 12 changed files with 179 additions and 132 deletions.
10 changes: 10 additions & 0 deletions db/databases.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,13 @@ def drop_database(database_oid, conn):
).fetchone()[0]
cursor.execute(sql.SQL(drop_database_query))
cursor.close()
conn.autocommit = False


def create_database(database_name, conn):
"""Use the given connection to create a database."""
conn.autocommit = True
conn.execute(
sql.SQL('CREATE DATABASE {}').format(sql.Identifier(database_name))
)
conn.autocommit = False
104 changes: 0 additions & 104 deletions db/install.py

This file was deleted.

2 changes: 1 addition & 1 deletion db/sql/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
def _install_sql_file(file_name):
"""
Return a function that installs the SQL file with the given name. The
returned function accepts a psycopg2 connection as its only argument.
returned function accepts a psycopg connection as its only argument.
"""

def _install(conn):
Expand Down
1 change: 1 addition & 0 deletions docs/docs/api/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ Unrecognized errors from a given library return a "round number" code, so an unk
members:
- get
- delete
- upgrade_sql
- DatabaseInfo

## Database Privileges
Expand Down
18 changes: 18 additions & 0 deletions mathesar/migrations/0021_database_last_confirmed_sql_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.16 on 2024-11-07 15:35

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('mathesar', '0020_remove_tablemetadata_record_summary_customized_and_more'),
]

operations = [
migrations.AddField(
model_name='database',
name='last_confirmed_sql_version',
field=models.CharField(default='0.0.0'),
),
]
71 changes: 71 additions & 0 deletions mathesar/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
from encrypted_fields.fields import EncryptedCharField
import psycopg

from db.sql.install import install as install_sql
from mathesar import __version__
from mathesar.models import exceptions


class BaseModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
Expand All @@ -31,6 +35,7 @@ class Database(BaseModel):
server = models.ForeignKey(
'Server', on_delete=models.CASCADE, related_name='databases'
)
last_confirmed_sql_version = models.CharField(default='0.0.0')

class Meta:
constraints = [
Expand All @@ -42,6 +47,72 @@ class Meta:
)
]

@property
def needs_upgrade_attention(self):
return self.last_confirmed_sql_version != __version__

def install_sql(self, username=None, password=None):
if username is not None and password is not None:
with self.connect_manually(username, password) as conn:
install_sql(conn)
else:
with self.connect_admin() as conn:
install_sql(conn)

self.last_confirmed_sql_version = __version__
self.save()

def connect_user(self, user):
"""Return the given user's connection to the database."""
try:
role_map = UserDatabaseRoleMap.objects.get(user=user, database=self)
except UserDatabaseRoleMap.DoesNotExist:
raise exceptions.NoConnectionAvailable
return role_map.connection

def connect_manually(self, role, password):
"""Return a connection to the Database using the role and password."""
return psycopg.connect(
host=self.server.host,
port=self.server.port,
dbname=self.name,
user=role,
password=password,
)

def connect_admin(self):
"""
Return a connection using the role that installed Mathesar.
Note that this function should be used with care, since the
connection has privileges to modify Mathesar's system schemata.
"""
admin_role_query = """
SELECT nspowner::regrole::text
FROM pg_catalog.pg_namespace
WHERE nspname='msar';
"""

for role_map in UserDatabaseRoleMap.objects.filter(database=self):
try:
with role_map.connection as conn:
admin_role_name = conn.execute(admin_role_query).fetchone()[0]
assert admin_role_name is not None
break
except Exception:
pass
else:
raise exceptions.NoConnectionAvailable

try:
role = ConfiguredRole.objects.get(
name=admin_role_name, server=self.server
)
except ConfiguredRole.DoesNotExist:
raise exceptions.NoAdminConnectionAvailable

return self.connect_manually(role.name, role.password)


class ConfiguredRole(BaseModel):
name = models.CharField(max_length=255)
Expand Down
6 changes: 6 additions & 0 deletions mathesar/models/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class NoConnectionAvailable(Exception):
pass


class NoAdminConnectionAvailable(Exception):
pass
30 changes: 28 additions & 2 deletions mathesar/rpc/databases/base.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from typing import Literal, TypedDict

from modernrpc.core import rpc_method, REQUEST_KEY
from modernrpc.auth.basic import http_basic_auth_login_required
from modernrpc.auth.basic import (
http_basic_auth_login_required,
http_basic_auth_superuser_required,
)

from mathesar.rpc.utils import connect
from db.databases import get_database, drop_database
from mathesar.models.base import Database
from mathesar.rpc.utils import connect
from mathesar.rpc.exceptions.handlers import handle_rpc_exceptions


Expand Down Expand Up @@ -69,3 +73,25 @@ def delete(*, database_oid: int, database_id: int, **kwargs) -> None:
user = kwargs.get(REQUEST_KEY).user
with connect(database_id, user) as conn:
drop_database(database_oid, conn)


@rpc_method(name="databases.upgrade_sql")
@http_basic_auth_superuser_required
@handle_rpc_exceptions
def upgrade_sql(
*, database_id: int, username: str = None, password: str = None
) -> None:
"""
Install, Upgrade, or Reinstall the Mathesar SQL on a database.
If no `username` and `password` are submitted, we will determine the
role which owns the `msar` schema on the database, then use that role
for the upgrade.
Args:
database_id: The Django id of the database.
username: The username of the role used for upgrading.
password: The password of the role used for upgrading.
"""
database = Database.objects.get(id=database_id)
database.install_sql(username=username, password=password)
10 changes: 9 additions & 1 deletion mathesar/rpc/databases/configured.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,25 @@ class ConfiguredDatabaseInfo(TypedDict):
id: the Django ID of the database model instance.
name: The name of the database on the server.
server_id: the Django ID of the server model instance for the database.
last_confirmed_sql_version: The last version of the SQL scripts which
were confirmed to have been run on this database.
needs_upgrade_attention: This is `True` if the SQL version isn't the
same as the service version.
"""
id: int
name: str
server_id: int
last_confirmed_sql_version: str
needs_upgrade_attention: bool

@classmethod
def from_model(cls, model):
return cls(
id=model.id,
name=model.name,
server_id=model.server.id
server_id=model.server.id,
last_confirmed_sql_version=model.last_confirmed_sql_version,
needs_upgrade_attention=model.needs_upgrade_attention,
)


Expand Down
7 changes: 2 additions & 5 deletions mathesar/rpc/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from mathesar.models.base import UserDatabaseRoleMap
from mathesar.models.base import Database


def connect(database_id, user):
Expand All @@ -9,7 +9,4 @@ def connect(database_id, user):
database_id: The Django id of the Database used for connecting.
user: A user model instance who'll connect to the database.
"""
user_database_role = UserDatabaseRoleMap.objects.get(
user=user, database__id=database_id
)
return user_database_role.connection
return Database.objects.get(id=database_id).connect_user(user)
5 changes: 5 additions & 0 deletions mathesar/tests/rpc/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@
"databases.delete",
[user_is_authenticated]
),
(
databases.upgrade_sql,
"databases.upgrade_sql",
[user_is_superuser]
),

(
databases.configured.list_,
Expand Down
Loading

0 comments on commit 7f2b6d0

Please sign in to comment.