Skip to content

Commit

Permalink
Merge branch 'develop' into table_add
Browse files Browse the repository at this point in the history
  • Loading branch information
Anish9901 committed Jun 10, 2024
2 parents 88b8e20 + f49bb47 commit fe7b73a
Show file tree
Hide file tree
Showing 113 changed files with 3,270 additions and 2,120 deletions.
84 changes: 79 additions & 5 deletions db/columns/operations/alter.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def alter_column(engine, table_oid, column_attnum, column_data, connection=None)
"description": <str>
}
"""
column_alter_def = _process_column_alter_dict(column_data, column_attnum)
column_alter_def = _process_column_alter_dict_dep(column_data, column_attnum)
requested_type = column_alter_def.get("type", {}).get("name")
if connection is None:
try:
Expand Down Expand Up @@ -154,7 +154,7 @@ def batch_update_columns(table_oid, engine, column_data_list):
"""
Alter the given columns of the table.
For details on the column_data_list format, see _process_column_alter_dict.
For details on the column_data_list format, see _process_column_alter_dict_dep.
Args:
table_oid: the OID of the table whose columns we'll alter.
Expand All @@ -167,7 +167,7 @@ def batch_update_columns(table_oid, engine, column_data_list):
engine, 'alter_columns',
table_oid,
json.dumps(
[_process_column_alter_dict(column) for column in column_data_list]
[_process_column_alter_dict_dep(column) for column in column_data_list]
)
)
except InvalidParameterValue:
Expand All @@ -180,7 +180,81 @@ def batch_update_columns(table_oid, engine, column_data_list):
raise InvalidTypeOptionError


def _process_column_alter_dict(column_data, column_attnum=None):
def alter_columns_in_table(table_oid, column_data_list, conn):
"""
Alter columns of the given table in bulk.
For a description of column_data_list, see _transform_column_alter_dict
Args:
table_oid: The OID of the table whose columns we'll alter.
column_data_list: a list of dicts describing the alterations to make.
"""
transformed_column_data = [
_transform_column_alter_dict(column) for column in column_data_list
]
db_conn.exec_msar_func(
conn, 'alter_columns', table_oid, json.dumps(transformed_column_data)
)
return len(column_data_list)


# TODO This function wouldn't be needed if we had the same form in the DB
# as the RPC API function.
def _transform_column_alter_dict(data):
"""
Transform the data dict into the form needed for the DB functions.
Input data form:
{
"id": <int>,
"name": <str>,
"type": <str>,
"type_options": <dict>,
"nullable": <bool>,
"default": {"value": <any>}
"description": <str>
}
Output form:
{
"attnum": <int>,
"type": {"name": <str>, "options": <dict>},
"name": <str>,
"not_null": <bool>,
"default": <any>,
"description": <str>
}
Note that keys with empty values will be dropped, except "default"
and "description". Explicitly setting these to None requests dropping
the associated property of the underlying column.
"""
type_ = {"name": data.get('type'), "options": data.get('type_options')}
new_type = {k: v for k, v in type_.items() if v} or None
nullable = data.get(NULLABLE)
not_null = not nullable if nullable is not None else None
column_name = (data.get(NAME) or '').strip() or None
raw_alter_def = {
"attnum": data["id"],
"type": new_type,
"not_null": not_null,
"name": column_name,
"description": data.get("description")
}
alter_def = {k: v for k, v in raw_alter_def.items() if v is not None}

default_dict = data.get("default", {})
if default_dict is None:
alter_def.update(default=None)
elif "value" in default_dict:
alter_def.update(default=default_dict["value"])

return alter_def


# TODO This function is deprecated. Remove it when possible.
def _process_column_alter_dict_dep(column_data, column_attnum=None):
"""
Transform the column_data dict into the form needed for the DB functions.
Expand Down Expand Up @@ -221,7 +295,7 @@ def _process_column_alter_dict(column_data, column_attnum=None):
column_not_null = not column_nullable if column_nullable is not None else None
column_name = (column_data.get(NAME) or '').strip() or None
raw_col_alter_def = {
"attnum": column_attnum or column_data.get("attnum"),
"attnum": column_attnum or column_data.get("attnum") or column_data.get("id"),
"type": new_type,
"not_null": column_not_null,
"name": column_name,
Expand Down
41 changes: 41 additions & 0 deletions db/tests/columns/operations/test_alter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import json
from unittest.mock import patch
from sqlalchemy import Column, select, Table, MetaData, VARCHAR, INTEGER

from db import constants
from db.columns.operations import alter as col_alt
from db.columns.operations.alter import batch_update_columns, rename_column
from db.columns.operations.select import (
get_column_attnum_from_name, get_column_name_from_attnum,
Expand All @@ -18,6 +21,44 @@
from db.schemas.utils import get_schema_oid_from_name


def test_alter_columns_in_table_basic():
with patch.object(col_alt.db_conn, 'exec_msar_func') as mock_exec:
col_alt.alter_columns_in_table(
123,
[
{
"id": 3, "name": "colname3", "type": "numeric",
"type_options": {"precision": 8}, "nullable": True,
"default": {"value": 8, "is_dynamic": False},
"description": "third column"
}, {
"id": 6, "name": "colname6", "type": "character varying",
"type_options": {"length": 32}, "nullable": True,
"default": {"value": "blahblah", "is_dynamic": False},
"description": "textual column"
}
],
'conn'
)
expect_json_arg = [
{
"attnum": 3, "name": "colname3",
"type": {"name": "numeric", "options": {"precision": 8}},
"not_null": False, "default": 8, "description": "third column",
}, {
"attnum": 6, "name": "colname6",
"type": {
"name": "character varying", "options": {"length": 32},
},
"not_null": False, "default": "blahblah",
"description": "textual column"
}
]
assert mock_exec.call_args.args[:3] == ('conn', 'alter_columns', 123)
# Necessary since `json.dumps` mangles dict ordering, but we don't care.
assert json.loads(mock_exec.call_args.args[3]) == expect_json_arg


def _rename_column_and_assert(table, old_col_name, new_col_name, engine):
"""
Renames the colum of a table and assert the change went through
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/api/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,11 @@ To use an RPC function:
options:
members:
- list_
- patch
- delete
- ColumnListReturn
- ColumnInfo
- SettableColumnInfo
- TypeOptions
- ColumnDefault

Expand Down
69 changes: 66 additions & 3 deletions mathesar/rpc/columns.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""
Classes and functions exposed to the RPC endpoint for managing table columns.
"""
from typing import TypedDict
from typing import Optional, TypedDict

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

from db.columns.operations.select import get_column_info_for_table
from db.columns.operations.alter import alter_columns_in_table
from db.columns.operations.drop import drop_columns_from_table
from db.columns.operations.select import get_column_info_for_table
from mathesar.rpc.exceptions.handlers import handle_rpc_exceptions
from mathesar.rpc.utils import connect
from mathesar.utils.columns import get_raw_display_options
Expand Down Expand Up @@ -73,9 +74,43 @@ def from_dict(cls, col_default):
)


class SettableColumnInfo(TypedDict):
"""
Information about a column, restricted to settable fields.
When possible, Passing `null` for a key will clear the underlying
setting. E.g.,
- `default = null` clears the column default setting.
- `type_options = null` clears the type options for the column.
- `description = null` clears the column description.
Setting any of `name`, `type`, or `nullable` is a noop.
Only the `id` key is required.
Attributes:
id: The `attnum` of the column in the table.
name: The name of the column.
type: The type of the column on the database.
type_options: The options applied to the column type.
nullable: Whether or not the column is nullable.
default: The default value.
description: The description of the column.
"""
id: int
name: Optional[str]
type: Optional[str]
type_options: Optional[TypeOptions]
nullable: Optional[bool]
default: Optional[ColumnDefault]
description: Optional[str]


class ColumnInfo(TypedDict):
"""
Information about a column.
Information about a column. Extends the settable fields.
Attributes:
id: The `attnum` of the column in the table.
Expand Down Expand Up @@ -158,6 +193,34 @@ def list_(*, table_oid: int, database_id: int, **kwargs) -> ColumnListReturn:
)


@rpc_method(name="columns.patch")
@http_basic_auth_login_required
@handle_rpc_exceptions
def patch(
*,
column_data_list: list[SettableColumnInfo],
table_oid: int,
database_id: int,
**kwargs
) -> int:
"""
Alter details of preexisting columns in a table.
Does not support altering the type or type options of array columns.
Args:
column_data_list: A list describing desired column alterations.
table_oid: Identity of the table whose columns we'll modify.
database_id: The Django id of the database containing the table.
Returns:
The number of columns altered.
"""
user = kwargs.get(REQUEST_KEY).user
with connect(database_id, user) as conn:
return alter_columns_in_table(table_oid, column_data_list, conn)


@rpc_method(name="columns.delete")
@http_basic_auth_login_required
@handle_rpc_exceptions
Expand Down
33 changes: 33 additions & 0 deletions mathesar/tests/rpc/test_columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,39 @@ def mock_display_options(_database_id, _table_oid, attnums, user):
assert actual_col_list == expect_col_list


def test_columns_patch(rf, monkeypatch):
request = rf.post('/api/rpc/v0/', data={})
request.user = User(username='alice', password='pass1234')
table_oid = 23457
database_id = 2
column_data_list = [{"id": 3, "name": "newname"}]

@contextmanager
def mock_connect(_database_id, user):
if _database_id == 2 and user.username == 'alice':
try:
yield True
finally:
pass
else:
raise AssertionError('incorrect parameters passed')

def mock_column_alter(_table_oid, _column_data_list, conn):
if _table_oid != table_oid or _column_data_list != column_data_list:
raise AssertionError('incorrect parameters passed')
return 1

monkeypatch.setattr(columns, 'connect', mock_connect)
monkeypatch.setattr(columns, 'alter_columns_in_table', mock_column_alter)
actual_result = columns.patch(
column_data_list=column_data_list,
table_oid=table_oid,
database_id=database_id,
request=request
)
assert actual_result == 1


def test_columns_delete(rf, monkeypatch):
request = rf.post('/api/rpc/v0/', data={})
request.user = User(username='alice', password='pass1234')
Expand Down
5 changes: 5 additions & 0 deletions mathesar/tests/rpc/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
"columns.list",
[user_is_authenticated]
),
(
columns.patch,
"columns.patch",
[user_is_authenticated]
),
(
connections.add_from_known_connection,
"connections.add_from_known_connection",
Expand Down
1 change: 1 addition & 0 deletions mathesar_ui/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module.exports = {
rules: {
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
'no-console': ['warn', { allow: ['error'] }],
'generator-star-spacing': 'off',
'no-continue': 'off',
'@typescript-eslint/explicit-member-accessibility': 'off',
'@typescript-eslint/ban-ts-comment': [
Expand Down
8 changes: 4 additions & 4 deletions mathesar_ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion mathesar_ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"dayjs": "^1.11.5",
"fast-diff": "^1.2.0",
"flatpickr": "^4.6.13",
"iter-tools": "^7.4.0",
"iter-tools": "^7.5.3",
"js-cookie": "^3.0.1",
"papaparse": "^5.4.1",
"perfect-scrollbar": "^1.5.5",
Expand Down
2 changes: 2 additions & 0 deletions mathesar_ui/src/component-library/common/actions/slider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ export default function slider(
}

function start(e: MouseEvent | TouchEvent) {
e.stopPropagation();
e.preventDefault();
opts.onStart();
startingValue = opts.getStartingValue();
startingPosition = getPosition(e, opts.axis);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ export default class ImmutableSet<T> {
return this.getNewInstance(set);
}

/**
* @returns a new ImmutableSet that contains only the items that are present
* in both this set and the other set. The order of the items in the returned
* set is taken from this set (not the supplied set).
*/
intersect(other: { has: (v: T) => boolean }): this {
return this.getNewInstance([...this].filter((v) => other.has(v)));
}

without(itemOrItems: T | T[]): this {
const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
const set = new Set(this.set);
Expand Down
Loading

0 comments on commit fe7b73a

Please sign in to comment.