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

Initial database_setup RPC functions #3665

Merged
merged 17 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/settings/common_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ def pipe_delim(pipe_string):
'mathesar.rpc.constraints',
'mathesar.rpc.columns',
'mathesar.rpc.columns.metadata',
'mathesar.rpc.database_setup',
'mathesar.rpc.databases',
'mathesar.rpc.roles',
'mathesar.rpc.schemas',
'mathesar.rpc.tables',
Expand Down
19 changes: 18 additions & 1 deletion docs/docs/api/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,24 @@ To use an RPC function:
- add_from_known_connection
- add_from_scratch
- grant_access_to_user
- DBModelReturn
- ConnectionReturn

## Databases

::: databases
options:
members:
- list_
- DatabaseInfo

## Database Setup

::: database_setup
options:
members:
- create_new
- connect_existing
- DatabaseConnectionResult

## Schemas

Expand Down
30 changes: 14 additions & 16 deletions mathesar/examples/library_dataset.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
"""This module contains functions to load the Library Management dataset."""

from sqlalchemy import text
from psycopg import sql
from mathesar.examples.base import LIBRARY_MANAGEMENT, LIBRARY_ONE, LIBRARY_TWO


def load_library_dataset(engine, safe_mode=False):
def load_library_dataset(conn):
"""
Load the library dataset into a "Library Management" schema.

Args:
engine: an SQLAlchemy engine defining the connection to load data into.
safe_mode: When True, we will throw an error if the "Library Management"
schema already exists instead of dropping it.
conn: a psycopg (3) connection for loading the data.

Uses given engine to define database to load into.
Destructive, and will knock out any previous "Library Management"
schema in the given database, unless safe_mode=True.
Uses given connection to define database to load into. Raises an
Exception if the "Library Management" schema already exists.
"""
drop_schema_query = text(f"""DROP SCHEMA IF EXISTS "{LIBRARY_MANAGEMENT}" CASCADE;""")
create_schema_query = text(f"""CREATE SCHEMA "{LIBRARY_MANAGEMENT}";""")
set_search_path = text(f"""SET search_path="{LIBRARY_MANAGEMENT}";""")
with engine.begin() as conn, open(LIBRARY_ONE) as f1, open(LIBRARY_TWO) as f2:
if safe_mode is False:
conn.execute(drop_schema_query)
create_schema_query = sql.SQL("CREATE SCHEMA {}").format(
sql.Identifier(LIBRARY_MANAGEMENT)
)
set_search_path = sql.SQL("SET search_path={}").format(
sql.Identifier(LIBRARY_MANAGEMENT)
)
with open(LIBRARY_ONE) as f1, open(LIBRARY_TWO) as f2:
conn.execute(create_schema_query)
conn.execute(set_search_path)
conn.execute(text(f1.read()))
conn.execute(text(f2.read()))
conn.execute(f1.read())
conn.execute(f2.read())
32 changes: 16 additions & 16 deletions mathesar/examples/movies_dataset.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
"""This module contains functions to load the Movie Collection dataset."""
import os
from sqlalchemy import text
from psycopg import sql

from mathesar.examples.base import (
MOVIE_COLLECTION, MOVIES_SQL_TABLES, MOVIES_CSV, MOVIES_SQL_FKS
)


def load_movies_dataset(engine, safe_mode=False):
def load_movies_dataset(conn):
"""
Load the movie example data set.

Args:
engine: an SQLAlchemy engine defining the connection to load data into.
safe_mode: When True, we will throw an error if the "Movie Collection"
schema already exists instead of dropping it.
conn: a psycopg (3) connection for loading the data.

Uses given connection to define database to load into. Raises an
Exception if the "Movie Collection" schema already exists.
"""
drop_schema_query = text(f"""DROP SCHEMA IF EXISTS "{MOVIE_COLLECTION}" CASCADE;""")
with engine.begin() as conn, open(MOVIES_SQL_TABLES) as f, open(MOVIES_SQL_FKS) as f2:
if safe_mode is False:
conn.execute(drop_schema_query)
conn.execute(text(f.read()))
with open(MOVIES_SQL_TABLES) as f, open(MOVIES_SQL_FKS) as f2:
conn.execute(f.read())
for file in os.scandir(MOVIES_CSV):
table_name = file.name.split('.csv')[0]
with open(file, 'r') as csv_file:
conn.connection.cursor().copy_expert(
f"""COPY "{MOVIE_COLLECTION}"."{table_name}" FROM STDIN DELIMITER ',' CSV HEADER""",
csv_file
)
conn.execute(text(f2.read()))
copy_sql = sql.SQL(
"COPY {}.{} FROM STDIN DELIMITER ',' CSV HEADER"
).format(
sql.Identifier(MOVIE_COLLECTION), sql.Identifier(table_name)
)
with open(file, 'r') as csv, conn.cursor().copy(copy_sql) as copy:
copy.write(csv.read())
conn.execute(f2.read())
34 changes: 17 additions & 17 deletions mathesar/rpc/connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
from mathesar.rpc.exceptions.handlers import handle_rpc_exceptions


class DBModelReturn(TypedDict):
class ConnectionReturn(TypedDict):
"""
Information about a database model.
Information about a connection model.

Attributes:
id (int): The Django id of the Database object added.
id (int): The Django id of the Connection object added.
nickname (str): Used to identify the added connection.
database (str): The name of the database on the server.
username (str): The username of the role for the connection.
Expand All @@ -30,14 +30,14 @@ class DBModelReturn(TypedDict):
port: int

@classmethod
def from_db_model(cls, db_model):
def from_model(cls, connection):
return cls(
id=db_model.id,
nickname=db_model.name,
database=db_model.db_name,
username=db_model.username,
host=db_model.host,
port=db_model.port
id=connection.id,
nickname=connection.name,
database=connection.db_name,
username=connection.username,
host=connection.host,
port=connection.port
)


Expand All @@ -51,7 +51,7 @@ def add_from_known_connection(
create_db: bool = False,
connection_id: int = None,
sample_data: list[str] = [],
) -> DBModelReturn:
) -> ConnectionReturn:
"""
Add a new connection from an already existing one.

Expand Down Expand Up @@ -80,10 +80,10 @@ def add_from_known_connection(
'connection_type': connection_type,
'connection_id': connection_id
}
db_model = connections.copy_connection_from_preexisting(
connection_model = connections.copy_connection_from_preexisting(
connection, nickname, database, create_db, sample_data
)
return DBModelReturn.from_db_model(db_model)
return ConnectionReturn.from_model(connection_model)


@rpc_method(name='connections.add_from_scratch')
Expand All @@ -98,7 +98,7 @@ def add_from_scratch(
host: str,
port: int,
sample_data: list[str] = [],
) -> DBModelReturn:
) -> ConnectionReturn:
"""
Add a new connection to a PostgreSQL server from scratch.

Expand All @@ -121,10 +121,10 @@ def add_from_scratch(
Returns:
Metadata about the Database associated with the connection.
"""
db_model = connections.create_connection_from_scratch(
connection_model = connections.create_connection_from_scratch(
user, password, host, port, nickname, database, sample_data
)
return DBModelReturn.from_db_model(db_model)
return ConnectionReturn.from_model(connection_model)


@rpc_method(name='connections.grant_access_to_user')
Expand All @@ -143,4 +143,4 @@ def grant_access_to_user(*, connection_id: int, user_id: int):
connection_id: The Django id of an old-style connection.
user_id: The Django id of a user.
"""
permissions.create_user_database_role_map(connection_id, user_id)
permissions.migrate_connection_for_user(connection_id, user_id)
99 changes: 99 additions & 0 deletions mathesar/rpc/database_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""
RPC functions for setting up database connections.
"""
from typing import TypedDict

from modernrpc.core import rpc_method, REQUEST_KEY
from modernrpc.auth.basic import http_basic_auth_superuser_required

from mathesar.utils import permissions
from mathesar.rpc.exceptions.handlers import handle_rpc_exceptions


class DatabaseConnectionResult(TypedDict):
"""
Info about the objects resulting from calling the setup functions.

These functions will get or create an instance of the Server,
Database, and Role models, as well as a UserDatabaseRoleMap entry.

Attributes:
server_id: The Django ID of the Server model instance.
database_id: The Django ID of the Database model instance.
role_id: The Django ID of the Role model instance.
"""
server_id: int
database_id: int
role_id: int

@classmethod
def from_model(cls, model):
return cls(
server_id=model.server.id,
database_id=model.database.id,
role_id=model.role.id,
)


@rpc_method(name='database_setup.create_new')
@http_basic_auth_superuser_required
@handle_rpc_exceptions
def create_new(
*,
database: str,
sample_data: list[str] = [],
**kwargs
) -> DatabaseConnectionResult:
"""
Set up a new database on the internal server.

The calling user will get access to that database using the default
role stored in Django settings.

Args:
database: The name of the new database.
sample_data: A list of strings requesting that some example data
sets be installed on the underlying database. Valid list
members are 'library_management' and 'movie_collection'.
"""
user = kwargs.get(REQUEST_KEY).user
result = permissions.set_up_new_database_for_user_on_internal_server(
database, user, sample_data=sample_data
)
return DatabaseConnectionResult.from_model(result)


@rpc_method(name='database_setup.connect_existing')
@http_basic_auth_superuser_required
@handle_rpc_exceptions
def connect_existing(
*,
host: str,
port: int,
database: str,
role: str,
password: str,
sample_data: list[str] = [],
**kwargs
) -> DatabaseConnectionResult:
"""
Connect Mathesar to an existing database on a server.

The calling user will get access to that database using the
credentials passed to this function.

Args:
host: The host of the database server.
port: The port of the database server.
database: The name of the database on the server.
role: The role on the server to use for the connection.
password: A password valid for the role.
sample_data: A list of strings requesting that some example data
sets be installed on the underlying database. Valid list
members are 'library_management' and 'movie_collection'.
"""
user = kwargs.get(REQUEST_KEY).user
result = permissions.set_up_preexisting_database_for_user(
host, port, database, role, password, user, sample_data=sample_data
)
return DatabaseConnectionResult.from_model(result)
1 change: 1 addition & 0 deletions mathesar/rpc/databases/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .base import * # noqa
52 changes: 52 additions & 0 deletions mathesar/rpc/databases/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from typing import TypedDict

from modernrpc.core import rpc_method
from modernrpc.auth.basic import http_basic_auth_login_required

from mathesar.models.base import Database
from mathesar.rpc.exceptions.handlers import handle_rpc_exceptions


class DatabaseInfo(TypedDict):
"""
Information about a database.

Attributes:
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.
"""
id: int
name: str
server_id: int

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


@rpc_method(name="databases.list")
@http_basic_auth_login_required
@handle_rpc_exceptions
def list_(*, server_id: int = None, **kwargs) -> list[DatabaseInfo]:
"""
List information about databases for a server. Exposed as `list`.

If called with no `server_id`, all databases for all servers are listed.

Args:
server_id: The Django id of the server containing the databases.

Returns:
A list of database details.
"""
if server_id is not None:
database_qs = Database.objects.filter(server__id=server_id)
else:
database_qs = Database.objects.all()

return [DatabaseInfo.from_model(db_model) for db_model in database_qs]
4 changes: 2 additions & 2 deletions mathesar/tests/rpc/test_connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
]
)
def test_add_from_known_connection(create_db, connection_id, sample_data):
with patch.object(rpc_conn, 'DBModelReturn'):
with patch.object(rpc_conn, 'ConnectionReturn'):
with patch.object(
rpc_conn.connections,
'copy_connection_from_preexisting'
Expand Down Expand Up @@ -56,7 +56,7 @@ def test_add_from_known_connection(create_db, connection_id, sample_data):
]
)
def test_add_from_scratch(port, sample_data):
with patch.object(rpc_conn, 'DBModelReturn'):
with patch.object(rpc_conn, 'ConnectionReturn'):
with patch.object(
rpc_conn.connections,
'create_connection_from_scratch'
Expand Down
Loading
Loading