Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
48 changes: 33 additions & 15 deletions db/databases.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from psycopg import sql

from config.database_config import get_internal_database_config
from db import connection as db_conn


Expand All @@ -8,22 +8,40 @@ def get_database(conn):


def drop_database(database_oid, conn):
cursor = conn.cursor()
icfg = get_internal_database_config()
conn.commit()
conn.autocommit = True
drop_database_query = db_conn.exec_msar_func(
conn,
'drop_database_query',
database_oid
).fetchone()[0]
cursor.execute(sql.SQL(drop_database_query))
cursor.close()
conn.autocommit = False

with conn.cursor() as c:
c.execute("SELECT datname FROM pg_database WHERE oid = %s", (database_oid,))
dbname = c.fetchone()
if not dbname:
raise ValueError("Database OID not found")
c.execute(sql.SQL("ALTER DATABASE {} OWNER TO {}")
.format(sql.Identifier(dbname[0]), sql.Identifier(icfg.role)))

with db_conn.mathesar_connection(
host=icfg.host, port=icfg.port, dbname=icfg.dbname,
user=icfg.role, password=icfg.password, sslmode=icfg.sslmode,
application_name='db.databases.drop_database',
) as c2:
# Set autocommit immediately after connection
c2.autocommit = True
with c2.cursor() as cur:
cur.execute(
sql.SQL("""
SELECT pg_terminate_backend(pg_stat_activity.pid)
FROM pg_stat_activity
WHERE pg_stat_activity.datname = %s
AND pid <> pg_backend_pid()
"""),
(dbname[0],)
)
cur.execute(sql.SQL("DROP DATABASE {}").format(sql.Identifier(dbname[0])))


def create_database(database_name, conn):
"""Use the given connection to create a database."""
conn.commit()
conn.autocommit = True
conn.execute(
sql.SQL('CREATE DATABASE {}').format(sql.Identifier(database_name))
)
conn.autocommit = False
with conn.cursor() as c:
c.execute(sql.SQL('CREATE DATABASE {}').format(sql.Identifier(database_name)))
13 changes: 9 additions & 4 deletions mathesar/rpc/databases/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from typing import Literal, TypedDict

from modernrpc.core import REQUEST_KEY

from config.database_config import get_internal_database_config
from db.databases import get_database, drop_database
from mathesar.models.base import Database
from mathesar.rpc.utils import connect
Expand Down Expand Up @@ -63,8 +62,14 @@ def delete(*, database_oid: int, database_id: int, **kwargs) -> None:
database_id: The Django id of the database to connect to.
"""
user = kwargs.get(REQUEST_KEY).user
with connect(database_id, user) as conn:
drop_database(database_oid, conn)
if not user.is_superuser:
raise Exception("Only Mathesar admins can delete databases")
db = Database.objects.get(id=database_id)
icfg = get_internal_database_config()
if db.server.host != icfg.host or db.server.port != icfg.port:
raise Exception("Only databases on the internal server can be deleted")
with connect(database_id, user) as c:
drop_database(database_oid, c)


@mathesar_rpc_method(name="databases.upgrade_sql")
Expand Down
44 changes: 39 additions & 5 deletions mathesar/rpc/databases/configured.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

from modernrpc.core import REQUEST_KEY

from config.database_config import get_internal_database_config
from db.databases import drop_database as drop_database_from_server
from mathesar.models.base import Database
from mathesar.models import exceptions as db_exceptions
from mathesar.rpc.decorators import mathesar_rpc_method
from mathesar.rpc.utils import connect


class ConfiguredDatabaseInfo(TypedDict):
Expand Down Expand Up @@ -109,8 +112,10 @@ class DisconnectResult(TypedDict):
Attributes:
sql_cleaned: Whether Mathesar schemas were successfully removed from the database.
False indicates the connection was unavailable and cleanup was skipped.
database_dropped: Whether the database was dropped from the server.
"""
sql_cleaned: bool
database_dropped: bool


@mathesar_rpc_method(name="databases.configured.disconnect")
Expand All @@ -121,7 +126,9 @@ def disconnect(
strict: bool = True,
role_name: str = None,
password: str = None,
disconnect_db_server: bool = False
disconnect_db_server: bool = False,
drop_database: bool = False,
**kwargs
) -> DisconnectResult:
"""
Disconnect a configured database, after removing Mathesar SQL from it.
Expand All @@ -148,14 +155,25 @@ def disconnect(
metadata(host, port, role credentials) from Mathesar.
This is intended for optional use while disconnecting the
last database on the server.
drop_database: If True, will drop the database from the server.
Only works for databases on the internal server and requires
Mathesar admin privileges.

Returns:
The result of the disconnect operation.
"""
user = kwargs.get(REQUEST_KEY).user
database = Database.objects.get(id=database_id)
database_dropped = False

if drop_database:
if not user.is_superuser:
raise Exception("Only Mathesar admins can drop databases")

icfg = get_internal_database_config()
if database.server.host != icfg.host or database.server.port != icfg.port:
raise Exception("Only databases on the internal server can be dropped")

# Try to uninstall SQL, but if connection is unavailable, skip it
# This allows disconnecting databases with broken connections
sql_cleaned = True
try:
database.uninstall_sql(
Expand All @@ -165,12 +183,28 @@ def disconnect(
password=password,
)
except db_exceptions.NoConnectionAvailable:
# Connection is broken, skip SQL cleanup and just remove the database record
sql_cleaned = False

if drop_database:
try:
with connect(database_id, user) as conn:
conn.commit()
conn.autocommit = True
with conn.cursor() as c:
c.execute("SELECT oid FROM pg_database WHERE datname = %s", (database.name,))
result = c.fetchone()
if result:
database_oid = result[0]
drop_database_from_server(database_oid, conn)
database_dropped = True
else:
raise Exception(f"Database {database.name} not found on server")
except Exception as e:
raise Exception(f"Failed to drop database: {str(e)}")

database.delete()
server_db_count = len(Database.objects.filter(server=database.server))
if disconnect_db_server and server_db_count == 0:
database.server.delete()

return DisconnectResult(sql_cleaned=sql_cleaned)
return DisconnectResult(sql_cleaned=sql_cleaned, database_dropped=database_dropped)
2 changes: 2 additions & 0 deletions mathesar_ui/src/api/rpc/databases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,11 @@ export const databases = {
role_name?: string;
password?: string;
disconnect_db_server?: boolean;
drop_database?: boolean;
},
{
sql_cleaned: boolean;
database_dropped: boolean;
}
>(),
},
Expand Down
3 changes: 3 additions & 0 deletions mathesar_ui/src/i18n/languages/en/dict.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@
"database_disconnect_form_into": "This will disconnect the database from Mathesar.",
"database_disconnected_successfully": "Database disconnected successfully",
"database_disconnected_without_sql_cleanup": "Database disconnected successfully. Mathesar was unable to connect to the database to remove internal schemas.",
"database_dropped_successfully": "Database dropped successfully",
"database_dropped_without_sql_cleanup": "Database dropped successfully. Mathesar was unable to connect to the database to remove internal schemas.",
"database_name": "Database Name",
"database_new_items_scroll_hint": "Scroll or click here to see the database.",
"database_not_found": "Database with id [connectionId] is not found.",
Expand Down Expand Up @@ -255,6 +257,7 @@
"donate_blurb": "Help us build and maintain Mathesar with a donation to the Mathesar Foundation, a 501(c)(3) nonprofit organization.",
"donate_to_mathesar": "Donate to Mathesar",
"download": "Download",
"drop_database_warning": "Warning: This will permanently delete the database and all its data from the PostgreSQL server. This action cannot be undone.",
"drop_not_yet_implemented": "Dropping databases from within Mathesar is not yet implemented. You can use PostgreSQL directly to drop the database if needed. See this [link](issue) for more information.",
"drop_role": "Drop Role",
"drop_role_name_question": "Drop role [name]?",
Expand Down
29 changes: 7 additions & 22 deletions mathesar_ui/src/pages/database/DatabasePageWrapper.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,6 @@
$: ({ database, underlyingDatabase } = $databaseRouteContext);
$: void underlyingDatabase.runConservatively();

// // TODO Allow dropping databases
// const commonData = preloadCommonData();
// $: currentRoleOwnsDatabase =
// $underlyingDatabase.resolvedValue?.currentAccess.currentRoleOwns;
// $: isDatabaseInInternalServer =
// database.server.host === commonData.internal_db.host &&
// database.server.port === commonData.internal_db.port;

const permissionsModal = modal.spawnModalController();
const disconnectModal = modal.spawnModalController<Database>();
const reinstallModal = modal.spawnModalController<Database>();
Expand Down Expand Up @@ -146,19 +138,6 @@
>
{$_('reinstall_mathesar_schemas')}
</ButtonMenuItem>
<!--
TODO: Allow dropping databases
https://github.com/mathesar-foundation/mathesar/issues/3862
-->
<!-- {#if isDatabaseInInternalServer}
<ButtonMenuItem
icon={iconDeleteMajor}
danger
disabled={!$currentRoleOwnsDatabase}
>
{$_('delete_database')}
</ButtonMenuItem>
{/if} -->
</DropdownMenu>
</div>
{/if}
Expand Down Expand Up @@ -203,7 +182,13 @@
controller={disconnectModal}
disconnect={async (opts) => {
const result = await databasesStore.disconnectDatabase(opts);
if (result.sql_cleaned) {
if (result.database_dropped) {
toast.success(
result.sql_cleaned
? $_('database_dropped_successfully')
: $_('database_dropped_without_sql_cleanup'),
);
} else if (result.sql_cleaned) {
toast.success($_('database_disconnected_successfully'));
} else {
toast.success($_('database_disconnected_without_sql_cleanup'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
type DatabaseDisconnectFn,
databasesStore,
} from '@mathesar/stores/databases';
import { getUserProfileStoreFromContext } from '@mathesar/stores/userProfile';
import { preloadCommonData } from '@mathesar/utils/preloadData';
import {
Checkbox,
Fieldset,
Expand All @@ -35,6 +37,23 @@
export let disconnect: DatabaseDisconnectFn;
export let cancel: () => void;

const userProfileStore = getUserProfileStoreFromContext();
const commonData = preloadCommonData();

$: isAdmin = $userProfileStore?.isMathesarAdmin ?? false;
$: isInternalDatabase = (() => {
if (commonData.routing_context === 'anonymous') {
return false;
}
const internalDb = commonData.internal_db;
return (
internalDb &&
database.server.host === internalDb.host &&
database.server.port === internalDb.port
);
})();
$: canDropDatabase = isAdmin && isInternalDatabase;

const dropOptions = {
keep: $_('keep_the_database'),
drop: $_('drop_the_database'),
Expand Down Expand Up @@ -85,6 +104,7 @@
schemas_to_remove: schemasToRemove,
role: $useRole ? { name: $roleName, password: $rolePassword } : undefined,
disconnect_db_server: isLastDbInServer && $removeDbServer,
drop_database: $dropOption === 'drop',
});
}
</script>
Expand Down Expand Up @@ -192,23 +212,31 @@
/>
</FieldLayout>
{#if $dropOption === 'drop'}
<FieldLayout>
<WarningBox>
<RichText
text={$_('drop_not_yet_implemented')}
let:slotName
let:translatedArg
>
{#if slotName === 'link'}
<a
href="https://github.com/mathesar-foundation/mathesar/issues/3862"
>
{translatedArg}
</a>
{/if}
</RichText>
</WarningBox>
</FieldLayout>
{#if !canDropDatabase}
<FieldLayout>
<WarningBox>
<RichText
text={$_('drop_not_yet_implemented')}
let:slotName
let:translatedArg
>
{#if slotName === 'link'}
<a
href="https://github.com/mathesar-foundation/mathesar/issues/3862"
>
{translatedArg}
</a>
{/if}
</RichText>
</WarningBox>
</FieldLayout>
{:else}
<FieldLayout>
<WarningBox>
{$_('drop_database_warning')}
</WarningBox>
</FieldLayout>
{/if}
{:else if $dropOption === 'keep'}
<FieldLayout>
<LabeledInput layout="inline-input-first">
Expand Down Expand Up @@ -309,7 +337,8 @@
form.reset();
cancel();
}}
canProceed={$dropOption === 'keep'}
canProceed={$dropOption === 'keep' ||
($dropOption === 'drop' && canDropDatabase)}
onProceed={submit}
proceedButton={{ label: $_('disconnect') }}
cancelButton={{ label: $_('cancel') }}
Expand Down
8 changes: 7 additions & 1 deletion mathesar_ui/src/pages/home/database-card/DatabaseCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,13 @@
controller={disconnectModalController}
disconnect={async (opts) => {
const result = await databasesStore.disconnectDatabase(opts);
if (result.sql_cleaned) {
if (result.database_dropped) {
toast.success(
result.sql_cleaned
? $_('database_dropped_successfully')
: $_('database_dropped_without_sql_cleanup'),
);
} else if (result.sql_cleaned) {
toast.success($_('database_disconnected_successfully'));
} else {
toast.success($_('database_disconnected_without_sql_cleanup'));
Expand Down
4 changes: 3 additions & 1 deletion mathesar_ui/src/stores/databases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ class DatabasesStore {
schemas_to_remove?: SystemSchema[];
role?: { name: string; password: string };
disconnect_db_server: boolean;
}): Promise<{ sql_cleaned: boolean }> {
drop_database?: boolean;
}): Promise<{ sql_cleaned: boolean; database_dropped: boolean }> {
const result = await api.databases.configured
.disconnect({
database_id: p.database.id,
Expand All @@ -112,6 +113,7 @@ class DatabasesStore {
role_name: p.role?.name,
password: p.role?.password,
disconnect_db_server: p.disconnect_db_server,
drop_database: p.drop_database,
})
.run();
this.unsortedDatabases.delete(p.database.id);
Expand Down
Loading