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

Type logic refactor #1222

Merged
merged 84 commits into from
May 24, 2022
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
af4fa7c
add db type alias information to db types endpoint
dmos62 Mar 17, 2022
41c6623
fix set membership test
dmos62 Mar 17, 2022
9b294c8
Merge branch 'master' into add_alias_info_to_db_types_endpoint
dmos62 Mar 17, 2022
10bf354
Exploratory comments and minor refactor
dmos62 Mar 17, 2022
d644c37
Some more slight refactors and comments
dmos62 Mar 17, 2022
b52b3ab
+
dmos62 Mar 18, 2022
57cf327
Fix None filtering
dmos62 Mar 18, 2022
95595e3
Merge branch 'add_alias_info_to_db_types_endpoint' into some-type-log…
dmos62 Mar 18, 2022
d98271d
Add utilities to DatabaseType for looking up its ischema key
dmos62 Mar 18, 2022
9264c0d
Linter fixes
dmos62 Mar 18, 2022
9576f27
Minor refactor
dmos62 Mar 21, 2022
430c8af
Add get_sa_class method to DatabaseType
dmos62 Mar 21, 2022
f0517e8
Get rid of aliases information from DatabaseType
dmos62 Mar 21, 2022
754e793
Access ischema_names dict directly
dmos62 Mar 21, 2022
da659d3
Add DatabaseType::is_available_on_engine
dmos62 Mar 21, 2022
e5023cb
Delete get_available_types, some constants
dmos62 Mar 21, 2022
69fc2b2
Various type-related refactors
dmos62 Mar 23, 2022
c2357fe
Remove leftover aliases namespace
dmos62 Mar 23, 2022
0fd467e
Minor refactor
dmos62 Mar 23, 2022
3a73895
Linter fixes
dmos62 Mar 23, 2022
4a9aa6d
Merge branch 'master' into some-type-logic-refactor
dmos62 Mar 24, 2022
e614e2c
Remove other aliases remnants
dmos62 Mar 24, 2022
278d885
Minor changes
dmos62 Mar 24, 2022
50a4425
Various fixes
dmos62 Mar 24, 2022
5f3044a
Convert group of tests into a single parametrized test
dmos62 Mar 24, 2022
d39a436
Revert to using multiple tests
dmos62 Mar 24, 2022
898cd73
More refactoring
dmos62 Mar 25, 2022
4f4cb7a
Various refactors
dmos62 Mar 25, 2022
1ad0fb3
columns test_alter.py passing
dmos62 Mar 29, 2022
38156a4
A few more suites are passing; currently working on types test_cast
dmos62 Mar 31, 2022
dd8b004
Merge branch 'master' into some-type-logic-refactor
dmos62 Apr 14, 2022
6ff9d87
db/tests/types/ are passing
dmos62 Apr 21, 2022
ecb508d
db/tests/records/ is passing
dmos62 Apr 21, 2022
0b224ef
db/tests/tables/ is passing
dmos62 Apr 21, 2022
dab7c5c
add previously renamed file
dmos62 Apr 21, 2022
79db6be
db/tests/ passing
dmos62 Apr 21, 2022
29d792a
some initial changes to mathesar ns
dmos62 Apr 21, 2022
d0e8a8c
mathesar/tests/api/test_column_api.py passing
dmos62 Apr 27, 2022
d23a2d3
mathesar/tests/api/test_data_file_api.py is passing
dmos62 Apr 27, 2022
8752637
fix more mathesar namespace tests
dmos62 Apr 29, 2022
350e03a
All mathesar namespace tests are passing
dmos62 May 1, 2022
050534b
Fix last failing (non-integration) test
dmos62 May 1, 2022
7892e3e
Satisfy flake8
dmos62 May 1, 2022
88844cf
Merge branch 'master' into some-type-logic-refactor
dmos62 May 1, 2022
9047c2b
Refactor/fix number validation
dmos62 May 2, 2022
a81398c
Some post-merge test fixes
dmos62 May 2, 2022
6540b3b
Satisfy flake8
dmos62 May 2, 2022
beb365f
Undo accidental search and replace on a mathesar_ui file
dmos62 May 2, 2022
b2e988b
Fix whitespace
dmos62 May 2, 2022
cedd891
Remove dead code
dmos62 May 2, 2022
7f5d331
Typo
dmos62 May 2, 2022
8317427
Update integ. tests and fixtures
dmos62 May 3, 2022
d107bf8
Update frontend UI type configs
dmos62 May 3, 2022
902267a
Merge branch 'master' into some-type-logic-refactor
dmos62 May 3, 2022
58887d2
Have Dockerfile setting survive rebuilds
dmos62 May 5, 2022
952aeea
Fix some tests
dmos62 May 6, 2022
25c9f6f
Minor changes
dmos62 May 9, 2022
ff23257
Some merge conflict and bug fixes
dmos62 May 16, 2022
6c2ef2c
Some minor changes; enabled model reset for all mathesar tests
dmos62 May 16, 2022
d08a5af
Reduce fixture scope
dmos62 May 17, 2022
d33efb8
Add sqlalchemy_utils to requirements
dmos62 May 17, 2022
e0d9fd3
Make django test database short lived
dmos62 May 17, 2022
0873b11
Support xdist testing; optimize fixture scopes for performance
dmos62 May 19, 2022
6b5561f
Merge branch 'master' into some-type-logic-refactor
dmos62 May 20, 2022
cf911cc
Add xdist to requirements-dev.txt
dmos62 May 20, 2022
07fc8fc
Many merge conflict fixes and refactors
dmos62 May 20, 2022
3696692
Fix display options serializer
dmos62 May 23, 2022
f2cec65
Various fixes
dmos62 May 23, 2022
305556f
Merge branch 'master' into some-type-logic-refactor
dmos62 May 23, 2022
3bd9c7a
Remove logging from fixtures
dmos62 May 23, 2022
b4dc592
Minor changes
dmos62 May 23, 2022
eecdfae
Linter fixes
dmos62 May 23, 2022
8357b6e
Skip a hard-to-fix e2e test
dmos62 May 23, 2022
ec16cc3
Vulture fixes
dmos62 May 23, 2022
f8ff2cb
Linter fixes
dmos62 May 23, 2022
0708bc0
Add -n auto to setup.cfg so that tests run in parallel automatically
dmos62 May 23, 2022
dc03636
Disable xdist for e2e tests
dmos62 May 24, 2022
b133c4f
Get rid of non-canonical DatabaseTypes
dmos62 May 24, 2022
62fa079
Remove is_alias DatabaseType property
dmos62 May 24, 2022
7cdfa7c
Update E2E setup docs
seancolsen May 24, 2022
91adbb7
Use correct case for URI type
seancolsen May 24, 2022
bfcf549
Disable automatic test parallelization: fails with >7 threads
dmos62 May 24, 2022
80b368e
Merge branch 'some-type-logic-refactor' of github.com:centerofci/math…
dmos62 May 24, 2022
1b53d4c
Merge branch 'master' into some-type-logic-refactor
dmos62 May 24, 2022
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 .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ SECRET_KEY=2gr6ud88x=(p855_5nbj_+7^bw-iz&n7ldqv%94mjaecl+b9=4
DJANGO_DATABASE_KEY=default
DJANGO_DATABASE_URL=postgres://mathesar:mathesar@mathesar_db:5432/mathesar_django
MATHESAR_DATABASES=(mathesar_tables|postgresql://mathesar:mathesar@mathesar_db:5432/mathesar)
# Change this to Dockerfile.integ-tests to be able to run e2e integ tests
DOCKERFILE=Dockerfile
6 changes: 3 additions & 3 deletions .github/workflows/run-e2e-integ-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,23 @@ jobs:

- name: Build the stack
run: docker-compose up --build -d

- name: Sleep for 60 seconds
run: sleep 60s
shell: bash

# TODO: This needs to be handled inside the tests
- name: Run migrations
run: docker exec mathesar_service python manage.py migrate

- name: Run type installation
run: docker exec mathesar_service python install.py --skip-confirm

- name: Build front end
run: docker exec -w /code/mathesar_ui mathesar_service npx vite build

- name: Run integ tests with pytest
run: docker exec mathesar_service pytest --browser chromium --browser webkit --browser firefox --video="on" --output="./videos/" mathesar/tests/integration/
run: docker exec mathesar_service pytest --browser chromium --browser webkit --browser firefox --video="on" --output="./videos/" mathesar/tests/integration/ -n0

- uses: actions/upload-artifact@v2
if: ${{ failure() || success() }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/run-pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
- name: Build the stack
run: docker-compose up --build -d

- name: create coverage directory
- name: Create coverage directory
run: docker exec mathesar_service mkdir coverage_report

- name: Run tests with pytest
Expand Down
238 changes: 163 additions & 75 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,109 +4,197 @@
environment (e.g., the login info for the Postgres instance for testing)
"""
import pytest
from sqlalchemy import text
from config.settings import DATABASES
import random
import string

from django.db import connection as dj_connection

from sqlalchemy_utils import database_exists, create_database, drop_database
from sqlalchemy.exc import OperationalError

from db.engine import add_custom_types_to_ischema_names, create_engine
from db.types import install
from db.schemas.operations.create import create_schema
from db.schemas.operations.drop import drop_schema
from db.schemas.operations.drop import drop_schema as drop_sa_schema
from db.schemas.operations.create import create_schema as create_sa_schema
from db.schemas.utils import get_schema_oid_from_name, get_schema_name_from_oid


@pytest.fixture(scope="session")
def test_db_name():
test_db_name = "mathesar_db_test"
superuser_engine = _get_superuser_engine()
with superuser_engine.connect() as conn:
conn.execution_options(isolation_level="AUTOCOMMIT")
conn.execute(text(f"DROP DATABASE IF EXISTS {test_db_name} WITH (FORCE)"))
conn.execute(text(f"CREATE DATABASE {test_db_name}"))
yield test_db_name
with superuser_engine.connect() as conn:
conn.execution_options(isolation_level="AUTOCOMMIT")
conn.execute(text(f"DROP DATABASE {test_db_name} WITH (FORCE)"))
def worker_id(worker_id):
"""
Guaranteed to always be a non-empty string.

Returns 'master' when we're not parallelizing, 'gw0', 'gw1', etc., otherwise.
"""
return worker_id


@pytest.fixture(scope="session")
def engine(test_db_name):
engine = _create_engine(test_db_name)
def get_uid(worker_id):
"""
A factory of session-unique 4 letter strings.
"""
used_uids = set()

def _get_uid():
letters = string.ascii_letters
candidate = "".join(random.sample(letters, 4))
if worker_id:
candidate = worker_id + '_' + candidate
if candidate not in used_uids:
used_uids.add(candidate)
return candidate
else:
return _get_uid()
yield _get_uid


@pytest.fixture(scope="function")
def uid(get_uid):
"""
A session-unique string.
"""
return get_uid()


def _get_connection_string(username, password, hostname, database):
return f"postgresql://{username}:{password}@{hostname}/{database}"


def _create_engine(db_name):
dj_connection_settings = dj_connection.settings_dict
engine = create_engine(
_get_connection_string(
username=dj_connection_settings["USER"],
password=dj_connection_settings["PASSWORD"],
hostname=dj_connection_settings["HOST"],
database=db_name,
),
future=True,
# Setting a fixed timezone makes the timezone aware test cases predictable.
connect_args={"options": "-c timezone=utc -c lc_monetary=en_US.UTF-8"}
)
return engine


@pytest.fixture(scope="session")
def engine_with_ischema_names_updated(test_db_name):
def _create_db():
"""
This fixture does not inherit from the fixture `engine`, because it mutates the engine, which
would otherwise taint tests depending on `engine`.
A factory for Postgres mathesar-installed databases. A fixture made of this method tears down
created dbs when leaving scope.

This method is used to create two fixtures with different scopes, that's why it's not a fixture
itself.
"""
engine = _create_engine(test_db_name)
add_custom_types_to_ischema_names(engine)
return engine
created_dbs = set()

def __create_db(db_name):
engine = _create_engine(db_name)
if database_exists(engine.url):
drop_database(engine.url)
create_database(engine.url)
created_dbs.add(db_name)
# Our default testing database has our types and functions preinstalled.
install.install_mathesar_on_database(engine)
return db_name
yield __create_db
for db_name in created_dbs:
engine = _create_engine(db_name)
if database_exists(engine.url):
drop_database(engine.url)

@pytest.fixture
def test_schema_name():
return "test_schema"

# This factory will clean up its created dbs after each test function that it's used in.
# Useful when doing API stuff that would otherwise be bothersome to clean up.
# TODO padaryti kad duombaze butu ideta ir isimta is settings.DATABASES
# kaip tai daroma kituose fixtures?
create_temp_db = pytest.fixture(_create_db, scope="function")

@pytest.fixture
def engine_with_schema(engine, test_schema_name):
schema = test_schema_name
_create_schema(engine, schema)
yield engine, schema
_drop_schema(engine, schema)

# This factory will clean up its created dbs after its module is finished testing.
create_module_db = pytest.fixture(_create_db, scope="module")

@pytest.fixture
def engine_with_schema_with_ischema_names_updated(engine_with_ischema_names_updated, test_schema_name):
engine = engine_with_ischema_names_updated
schema = test_schema_name
_create_schema(engine, schema)
yield engine, schema
_drop_schema(engine, schema)

# This factory will clean up its created dbs after the whole testing session is finished.
create_session_db = pytest.fixture(_create_db, scope="session")

@pytest.fixture
def engine_with_mathesar(engine_with_schema_with_ischema_names_updated):
engine, schema = engine_with_schema_with_ischema_names_updated
install.install_mathesar_on_database(engine)
yield engine, schema
install.uninstall_mathesar_from_database(engine)

@pytest.fixture(scope="session", autouse=True)
def test_db_name(worker_id, create_session_db):
"""
A dynamic, yet non-random, db_name is used so that subsequent runs would automatically clean up
test databases that we failed to tear down.
"""
default_test_db_name = "mathesar_db_test"
db_name = f"{default_test_db_name}_{worker_id}"
create_session_db(db_name)
yield db_name

def _create_schema(engine, schema):
create_schema(schema, engine)

# TODO does testing this make sense?
@pytest.fixture(scope="session")
def engine_without_ischema_names_updated(test_db_name):
"""
For testing environments where an engine might not be fully setup.
"""
engine = _create_engine(test_db_name)
yield engine
engine.dispose()

def _drop_schema(engine, schema):
drop_schema(schema, engine, cascade=True, if_exists=False)

@pytest.fixture(scope="session")
def engine(test_db_name):
engine = _create_engine(test_db_name)
add_custom_types_to_ischema_names(engine)
yield engine
engine.dispose()

def _get_superuser_engine():
return create_engine(
_get_connection_string(
username=DATABASES["default"]["USER"],
password=DATABASES["default"]["PASSWORD"],
hostname=DATABASES["default"]["HOST"],
database=DATABASES["default"]["NAME"],
),
future=True,
)

@pytest.fixture(scope="session")
def _test_schema_name():
return "_test_schema_name"

def _get_connection_string(username, password, hostname, database):
return f"postgresql://{username}:{password}@{hostname}/{database}"

@pytest.fixture
def engine_with_schema_without_updated_ischema_names(
engine_without_ischema_names_updated, _test_schema_name, create_db_schema
):
engine = engine_without_ischema_names_updated
schema_name = _test_schema_name
create_db_schema(schema_name, engine)
yield engine, schema_name

def _create_engine(db_name):
engine = create_engine(
_get_connection_string(
DATABASES["default"]["USER"],
DATABASES["default"]["PASSWORD"],
DATABASES["default"]["HOST"],
db_name,
),
future=True,
# Setting a fixed timezone makes the timezone aware test cases predictable.
connect_args={"options": "-c timezone=utc -c lc_monetary=en_US.UTF-8"}
)
return engine

@pytest.fixture
def engine_with_schema(engine, _test_schema_name, create_db_schema):
schema_name = _test_schema_name
create_db_schema(schema_name, engine)
yield engine, schema_name


@pytest.fixture
def create_db_schema():
"""
Creates a DB schema factory, making sure to track and clean up new instances
"""
created_schemas = {}

def _create_schema(schema_name, engine, schema_mustnt_exist=True):
if schema_mustnt_exist:
assert schema_name not in created_schemas
create_sa_schema(schema_name, engine)
schema_oid = get_schema_oid_from_name(schema_name, engine)
engine_url = engine.url
created_schemas_in_this_engine = created_schemas.setdefault(engine_url, {})
created_schemas_in_this_engine[schema_name] = schema_oid
return schema_name
yield _create_schema
for engine_url, created_schemas_in_this_engine in created_schemas.items():
engine = create_engine(engine_url)
try:
for _, schema_oid in created_schemas_in_this_engine.items():
# Handle schemas being renamed during test
schema_name = get_schema_name_from_oid(schema_oid, engine)
if schema_name:
drop_sa_schema(schema_name, engine, cascade=True, if_exists=True)
except OperationalError:
pass
7 changes: 2 additions & 5 deletions db/columns/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
)
from db.tables.operations.select import get_oid_from_table
from db.types.operations.cast import get_full_cast_map
from db.types.base import get_db_type_enum_from_class, DatabaseType

from collections.abc import Collection
from typing import Optional
from db.types.base import get_db_type_enum_from_class


# TODO consider renaming to DbColumn or DatabaseColumn
Expand Down Expand Up @@ -112,7 +109,7 @@ def add_engine(self, engine):
self.engine = engine

@property
def valid_target_types(self) -> Optional[Collection[DatabaseType]]:
def valid_target_types(self):
"""
Returns a set of valid types to which the type of the column can be
altered.
Expand Down
11 changes: 6 additions & 5 deletions db/columns/operations/alter.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
)
from db.columns.utils import get_mathesar_column_with_engine, get_type_options
from db.tables.operations.select import get_oid_from_table, reflect_table_from_oid
from db.types.base import get_db_type_enum_from_class, DatabaseType, get_db_type_enum_from_id
from db.types.base import get_db_type_enum_from_class, get_db_type_enum_from_id
from db.types.operations.cast import get_cast_function_name
from db.utils import execute_statement

Expand Down Expand Up @@ -60,7 +60,7 @@ def alter_column(engine, table_oid, column_attnum, column_data):


def retype_column(
table_oid, column_attnum, engine, connection, new_type: DatabaseType = None, type_options={},
table_oid, column_attnum, engine, connection, new_type=None, type_options={},
):
table = reflect_table_from_oid(table_oid, engine, connection)
column_name = get_column_name_from_attnum(table_oid, column_attnum, engine)
Expand Down Expand Up @@ -96,7 +96,7 @@ def retype_column(


def alter_column_type(
table_oid, column_name, engine, connection, target_type: DatabaseType, type_options={}
table_oid, column_name, engine, connection, target_type, type_options={}
):
type_options = type_options if type_options is not None else {}
table = reflect_table_from_oid(table_oid, engine, connection)
Expand Down Expand Up @@ -182,7 +182,7 @@ def _check_type_option_equivalence(type_options_1, type_options_2):


def _validate_columns_for_batch_update(table, column_data):
ALLOWED_KEYS = ['name', 'type', 'type_options']
ALLOWED_KEYS = ['attnum', 'name', 'type', 'type_options']
if len(column_data) != len(table.columns):
raise ValueError('Number of columns passed in must equal number of columns in table')
for single_column_data in column_data:
Expand All @@ -194,7 +194,8 @@ def _validate_columns_for_batch_update(table, column_data):

def _batch_update_column_types(table_oid, column_data_list, connection, engine):
for index, column_data in enumerate(column_data_list):
if 'type' in column_data:
column_attnum = column_data.get('attnum', None)
if 'type' in column_data and column_attnum is not None:
new_type = get_db_type_enum_from_id(column_data['type'])
type_options = column_data.get('type_options', {})
if type_options is None:
Expand Down
2 changes: 2 additions & 0 deletions db/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def create_future_engine_with_custom_types(
return engine


# TODO would an engine without ischema names updated ever be used? make it private if not
def create_future_engine(
username, password, hostname, database, port, *args, **kwargs
):
Expand All @@ -31,6 +32,7 @@ def create_future_engine(
return create_engine(conn_str, *args, **kwargs)


# NOTE: used in testing, hence public
def create_engine(conn_str, *args, **kwargs):
"""
Wrapper over sqlalchemy.create_engine that stops SA from propagating changes to ischema_names
Expand Down
Loading