Repository: datajoint/datajoint-python (language: python)

Issue:
Add release 0.13 features and bug fixes
Depends on #889, #890
Cannot query a nullable field with `is NULL`
## Bug Report

### Description
Cannot query a nullable field with `is NULL`

### Reproducibility
Include:
- OS: MACOS
- Python Version 3.7, 3.8
- DataJoint Version: 0.13.dev2
- Code to reproduce
```python
schema = dj.schema('test')
@schema
class Subject(dj.Manual):
    definition = """
    subject_id   : varchar(12)
    ---
    dob=null          : date
    """
Subject & 'dob is NULL'
```
gives the error:

```
DataJointError                            Traceback (most recent call last)
<ipython-input-13-36e3953f7e60> in <module>
----> 1 Subject & 'dob is NULL'

~/anaconda3/lib/python3.7/site-packages/datajoint/user_tables.py in __and__(cls, arg)
     47 
     48     def __and__(cls, arg):
---> 49         return cls() & arg
     50 
     51     def __xor__(cls, arg):

~/anaconda3/lib/python3.7/site-packages/datajoint/expression.py in __and__(self, restriction)
    194         See QueryExpression.restrict for more detail.
    195         """
--> 196         return self.restrict(restriction)
    197 
    198     def __xor__(self, restriction):

~/anaconda3/lib/python3.7/site-packages/datajoint/expression.py in restrict(self, restriction)
    169         try:
    170             raise DataJointError("Attribute `%s` is not found in query." % next(
--> 171                 attr for attr in attributes if attr not in self.heading.names))
    172         except StopIteration:
    173             pass  # all ok

DataJointError: Attribute `is` is not found in query.
```

Index key error when using fetch1('KEY')
## Bug Report

### Description
When using fetch1('KEY') in datajoint 0.12.8, it gives this error when using fetch1('KEY'):
![image](https://user-images.githubusercontent.com/2539832/110170128-91dea080-7dae-11eb-9052-3a27f802b212.png)



Index key error when using fetch1('KEY')
## Bug Report

### Description
When using fetch1('KEY') in datajoint 0.12.8, it gives this error when using fetch1('KEY'):
![image](https://user-images.githubusercontent.com/2539832/110170128-91dea080-7dae-11eb-9052-3a27f802b212.png)





Discussion / hints:

Fix patch:
diff --git a/.coveragerc b/.coveragerc
index 8e9b6dd17..df41fbcc8 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -3,3 +3,4 @@ branch = False
 source = datajoint
 
 [report]
+show_missing = True
\ No newline at end of file
diff --git a/.github/workflows/development.yaml b/.github/workflows/development.yaml
index 55ced7eb9..9785580b3 100644
--- a/.github/workflows/development.yaml
+++ b/.github/workflows/development.yaml
@@ -21,8 +21,6 @@ jobs:
             mysql_ver: "5.7"
           - py_ver: "3.6"
             mysql_ver: "5.7"
-          - py_ver: "3.5"
-            mysql_ver: "5.7"
     steps:
       - uses: actions/checkout@v2
       - name: Set up Python ${{matrix.py_ver}}
@@ -38,7 +36,7 @@ jobs:
       - name: Run primary tests
         env:
           UID: "1001"
-          GID: "116"
+          GID: "121"
           PY_VER: ${{matrix.py_ver}}
           MYSQL_VER: ${{matrix.mysql_ver}}
           ALPINE_VER: "3.10"
@@ -50,4 +48,4 @@ jobs:
       - name: Run style tests
         run: |
           flake8 --ignore=E121,E123,E126,E226,E24,E704,W503,W504,E722,F401,W605 datajoint \
-                 --count --max-complexity=62 --max-line-length=127 --statistics
\ No newline at end of file
+                 --count --max-complexity=62 --max-line-length=127 --statistics
diff --git a/.gitignore b/.gitignore
index cc979da25..7b6b58d7d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,4 +24,5 @@ notebook
 .vscode
 __main__.py
 jupyter_custom.js
-apk_requirements.txt
\ No newline at end of file
+apk_requirements.txt
+.eggs
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3ee92a390..6625d1d5b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,22 @@
 ## Release notes
 
+### 0.13.0 -- Mar 24, 2021
+* Re-implement query transpilation into SQL, fixing issues (#386, #449, #450, #484). PR #754
+* Re-implement cascading deletes for better performance. PR #839.
+* Add table method `.update1` to update a row in the table with new values PR #763
+* Python datatypes are now enabled by default in blobs (#761). PR #785
+* Added permissive join and restriction operators `@` and `^` (#785) PR #754
+* Support DataJoint datatype and connection plugins (#715, #729) PR 730, #735
+* Add `dj.key_hash` alias to `dj.hash.key_hash`
+* Default enable_python_native_blobs to True
+* Bugfix - Regression error on joins with same attribute name (#857) PR #878
+* Bugfix - Error when `fetch1('KEY')` when `dj.config['fetch_format']='frame'` set (#876) PR #880, #878
+* Bugfix - Error when cascading deletes in tables with many, complex keys (#883, #886) PR #839
+* Add deprecation warning for `_update`. PR #889
+* Add `purge_query_cache` utility. PR #889
+* Add tests for query caching and permissive join and restriction. PR #889
+* Drop support for Python 3.5
+
 ### 0.12.9 -- Mar 12, 2021
 * Fix bug with fetch1 with `dj.config['fetch_format']="frame"`. (#876) PR #880
 
diff --git a/LNX-docker-compose.yml b/LNX-docker-compose.yml
index 64d8b7ba3..448ab6f02 100644
--- a/LNX-docker-compose.yml
+++ b/LNX-docker-compose.yml
@@ -1,4 +1,4 @@
-# docker-compose -f LNX-docker-compose.yml --env-file LNX.env up --build --exit-code-from app
+# docker-compose -f LNX-docker-compose.yml --env-file LNX.env up --exit-code-from app --build
 version: '2.2'
 x-net: &net
   networks:
@@ -32,7 +32,7 @@ services:
       interval: 1s
   fakeservices.datajoint.io:
     <<: *net
-    image: raphaelguzman/nginx:v0.0.13
+    image: datajoint/nginx:v0.0.16
     environment:
     - ADD_db_TYPE=DATABASE
     - ADD_db_ENDPOINT=db:3306
@@ -72,14 +72,17 @@ services:
       - COVERALLS_SERVICE_NAME
       - COVERALLS_REPO_TOKEN
     working_dir: /src
-    command: >
-      /bin/sh -c
-       "
-        pip install --user nose nose-cov coveralls .;
-        pip freeze | grep datajoint;
-        nosetests -vsw tests --with-coverage --cover-package=datajoint && coveralls;
-        # jupyter notebook;
-       "
+    command:
+      - sh
+      - -c
+      - |
+        set -e
+        pip install --user -r test_requirements.txt
+        pip install -e .
+        pip freeze | grep datajoint
+        nosetests -vsw tests --with-coverage --cover-package=datajoint
+        coveralls
+        # jupyter notebook
     # ports:
     #   - "8888:8888"
     user: ${UID}:${GID}
diff --git a/README.md b/README.md
index 78183c3c7..a5ee24e30 100644
--- a/README.md
+++ b/README.md
@@ -32,9 +32,10 @@ Some Python datatypes such as dicts were coerced into numpy recarrays and then f
 However, since some Python types were coerced into MATLAB types, old blobs and new blobs may now be fetched as different types of objects even if they were inserted the same way. 
 For example, new `dict` objects will be returned as `dict` while the same types of objects inserted with `datajoint 0.11` will be recarrays.
 
-Since this is a big change, we chose to disable full blob support by default as a temporary precaution, which will be removed in version 0.13.
+Since this is a big change, we chose to temporarily disable this feature by default in DataJoint for Python 0.12.x, allowing users to adjust their code if necessary. 
+From 13.x, the flag will default to True (on), and will ultimately be removed when corresponding decode support for the new format is added to datajoint-matlab (see: datajoint-matlab #222, datajoint-python #765).
 
-You may enable it by setting the `enable_python_native_blobs` flag in `dj.config`. 
+The flag is configured by setting the `enable_python_native_blobs` flag in `dj.config`. 
 
 ```python
 import datajoint as dj
@@ -68,7 +69,7 @@ as structured arrays, whereas new record inserted in DataJoint 0.12 with
 appropriate native python type (dict, etc).  
 Furthermore, DataJoint for MATLAB does not yet support unpacking native Python datatypes.
 
-With `dj.config["enable_python_native_blobs"]` set to `False` (default), 
+With `dj.config["enable_python_native_blobs"]` set to `False`, 
 any attempt to insert any datatype other than a numpy array will result in an exception.
 This is meant to get users to read this message in order to allow proper testing
 and migration of pre-0.12 pipelines to 0.12 in a safe manner.
diff --git a/datajoint.pub b/datajoint.pub
new file mode 100644
index 000000000..4aaa823d2
--- /dev/null
+++ b/datajoint.pub
@@ -0,0 +1,6 @@
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUMOo2U7YQ1uOrKU/IreM3AQP2
+AXJC3au+S9W+dilxHcJ3e98bRVqrFeOofcGeRPoNc38fiLmLDUiBskJeVrpm29Wo
+AkH6yhZWk1o8NvGMhK4DLsJYlsH6tZuOx9NITKzJuOOH6X1I5Ucs7NOSKnmu7g5g
+WTT5kCgF5QAe5JN8WQIDAQAB
+-----END PUBLIC KEY-----
diff --git a/datajoint/__init__.py b/datajoint/__init__.py
index 6d5031fde..d0303d2dd 100644
--- a/datajoint/__init__.py
+++ b/datajoint/__init__.py
@@ -15,7 +15,7 @@
 """
 
 __author__ = "DataJoint Contributors"
-__date__ = "February 7, 2019"
+__date__ = "November 7, 2020"
 __all__ = ['__author__', '__version__',
            'config', 'conn', 'Connection',
            'Schema', 'schema', 'VirtualModule', 'create_virtual_module',
@@ -24,7 +24,7 @@
            'Not', 'AndList', 'U', 'Diagram', 'Di', 'ERD',
            'set_password', 'kill',
            'MatCell', 'MatStruct', 'AttributeAdapter',
-           'errors', 'DataJointError', 'key']
+           'errors', 'DataJointError', 'key', 'key_hash']
 
 from .version import __version__
 from .settings import config
@@ -38,6 +38,7 @@
 from .admin import set_password, kill
 from .blob import MatCell, MatStruct
 from .fetch import key
+from .hash import key_hash
 from .attribute_adapter import AttributeAdapter
 from . import errors
 from .errors import DataJointError
diff --git a/datajoint/attribute_adapter.py b/datajoint/attribute_adapter.py
index b1bf74fb3..dc1c45706 100644
--- a/datajoint/attribute_adapter.py
+++ b/datajoint/attribute_adapter.py
@@ -1,5 +1,6 @@
 import re
 from .errors import DataJointError, _support_adapted_types
+from .plugin import type_plugins
 
 
 class AttributeAdapter:
@@ -38,10 +39,11 @@ def get_adapter(context, adapter_name):
         raise DataJointError('Support for Adapted Attribute types is disabled.')
     adapter_name = adapter_name.lstrip('<').rstrip('>')
     try:
-        adapter = context[adapter_name]
+        adapter = (context[adapter_name] if adapter_name in context
+                   else type_plugins[adapter_name]['object'].load())
     except KeyError:
         raise DataJointError(
-            "Attribute adapter '{adapter_name}' is not defined.".format(adapter_name=adapter_name)) from None
+            "Attribute adapter '{adapter_name}' is not defined.".format(adapter_name=adapter_name))
     if not isinstance(adapter, AttributeAdapter):
         raise DataJointError(
             "Attribute adapter '{adapter_name}' must be an instance of datajoint.AttributeAdapter".format(
diff --git a/datajoint/autopopulate.py b/datajoint/autopopulate.py
index d851fe9f0..9e9615144 100644
--- a/datajoint/autopopulate.py
+++ b/datajoint/autopopulate.py
@@ -7,7 +7,6 @@
 from tqdm import tqdm
 from .expression import QueryExpression, AndList
 from .errors import DataJointError, LostConnectionError
-from .table import FreeTable
 import signal
 
 # noinspection PyExceptionInherit,PyCallingNonCallable
@@ -41,7 +40,7 @@ def _rename_attributes(table, props):
             parents = self.target.parents(primary=True, as_objects=True, foreign_key_info=True)
             if not parents:
                 raise DataJointError(
-                    'A relation must have primary dependencies for auto-populate to work') from None
+                    'A relation must have primary dependencies for auto-populate to work')
             self._key_source = _rename_attributes(*parents[0])
             for q in parents[1:]:
                 self._key_source *= _rename_attributes(*q)
@@ -58,8 +57,8 @@ def make(self, key):
     @property
     def target(self):
         """
-        relation to be populated.
-        Typically, AutoPopulate are mixed into a Relation object and the target is self.
+        :return: table to be populated.
+        In the typical case, dj.AutoPopulate is mixed into a dj.Table class by inheritance and the target is self.
         """
         return self
 
@@ -67,6 +66,7 @@ def _job_key(self, key):
         """
         :param key:  they key returned for the job from the key source
         :return: the dict to use to generate the job reservation hash
+        This method allows subclasses to control the job reservation granularity.
         """
         return key
 
@@ -136,7 +136,7 @@ def handler(signum, frame):
 
         make = self._make_tuples if hasattr(self, '_make_tuples') else self.make
 
-        for key in (tqdm(keys) if display_progress else keys):
+        for key in (tqdm(keys, desc=self.__class__.__name__) if display_progress else keys):
             if max_calls is not None and call_count >= max_calls:
                 break
             if not reserve_jobs or jobs.reserve(self.target.table_name, self._job_key(key)):
diff --git a/datajoint/blob.py b/datajoint/blob.py
index 04750ba00..225c68c1f 100644
--- a/datajoint/blob.py
+++ b/datajoint/blob.py
@@ -11,11 +11,10 @@
 import uuid
 import numpy as np
 from .errors import DataJointError
-from .utils import OrderedDict
 from .settings import config
 
 
-mxClassID = OrderedDict((
+mxClassID = dict((
     # see http://www.mathworks.com/help/techdoc/apiref/mxclassid.html
     ('mxUNKNOWN_CLASS', None),
     ('mxCELL_CLASS', None),
@@ -346,8 +345,8 @@ def pack_set(self, t):
             len_u64(it) + it for it in (self.pack_blob(i) for i in t))
 
     def read_dict(self):
-        return OrderedDict((self.read_blob(self.read_value()), self.read_blob(self.read_value()))
-                           for _ in range(self.read_value()))
+        return dict((self.read_blob(self.read_value()), self.read_blob(self.read_value()))
+                    for _ in range(self.read_value()))
 
     def pack_dict(self, d):
         return b"\4" + len_u64(d) + b"".join(
diff --git a/datajoint/condition.py b/datajoint/condition.py
new file mode 100644
index 000000000..7d921be4f
--- /dev/null
+++ b/datajoint/condition.py
@@ -0,0 +1,210 @@
+""" methods for generating SQL WHERE clauses from datajoint restriction conditions """
+
+import inspect
+import collections
+import re
+import uuid
+import datetime
+import decimal
+import numpy
+import pandas
+from .errors import DataJointError
+
+
+class PromiscuousOperand:
+    """
+    A container for an operand to ignore join compatibility
+    """
+    def __init__(self, operand):
+        self.operand = operand
+
+
+class AndList(list):
+    """
+    A list of conditions to by applied to a query expression by logical conjunction: the conditions are AND-ed.
+    All other collections (lists, sets, other entity sets, etc) are applied by logical disjunction (OR).
+
+    Example:
+    expr2 = expr & dj.AndList((cond1, cond2, cond3))
+    is equivalent to
+    expr2 = expr & cond1 & cond2 & cond3
+    """
+    def append(self, restriction):
+        if isinstance(restriction, AndList):
+            # extend to reduce nesting
+            self.extend(restriction)
+        else:
+            super().append(restriction)
+
+
+class Not:
+    """ invert restriction """
+    def __init__(self, restriction):
+        self.restriction = restriction
+
+
+def assert_join_compatibility(expr1, expr2):
+    """
+    Determine if expressions expr1 and expr2 are join-compatible.  To be join-compatible,
+    the matching attributes in the two expressions must be in the primary key of one or the
+    other expression.
+    Raises an exception if not compatible.
+    :param expr1: A QueryExpression object
+    :param expr2: A QueryExpression object
+    """
+    from .expression import QueryExpression, U
+
+    for rel in (expr1, expr2):
+        if not isinstance(rel, (U, QueryExpression)):
+            raise DataJointError('Object %r is not a QueryExpression and cannot be joined.' % rel)
+    if not isinstance(expr1, U) and not isinstance(expr2, U):  # dj.U is always compatible
+        try:
+            raise DataJointError(
+                "Cannot join query expressions on dependent attribute `%s`" % next(
+                    r for r in set(expr1.heading.secondary_attributes).intersection(
+                        expr2.heading.secondary_attributes)))
+        except StopIteration:
+            pass  # all ok
+
+
+def make_condition(query_expression, condition, columns):
+    """
+    Translate the input condition into the equivalent SQL condition (a string)
+    :param query_expression: a dj.QueryExpression object to apply condition
+    :param condition: any valid restriction object.
+    :param columns: a set passed by reference to collect all column names used in the condition.
+    :return: an SQL condition string or a boolean value.
+    """
+    from .expression import QueryExpression, Aggregation, U
+
+    def prep_value(k, v):
+        """prepare value v for inclusion as a string in an SQL condition"""
+        if query_expression.heading[k].uuid:
+            if not isinstance(v, uuid.UUID):
+                try:
+                    v = uuid.UUID(v)
+                except (AttributeError, ValueError):
+                    raise DataJointError(
+                        'Badly formed UUID {v} in restriction by `{k}`'.format(k=k, v=v))
+            return "X'%s'" % v.bytes.hex()
+        if isinstance(v, (datetime.date, datetime.datetime, datetime.time, decimal.Decimal)):
+            return '"%s"' % v
+        if isinstance(v, str):
+            return '"%s"' % v.replace('%', '%%')
+        return '%r' % v
+
+    negate = False
+    while isinstance(condition, Not):
+        negate = not negate
+        condition = condition.restriction
+    template = "NOT (%s)" if negate else "%s"
+
+    # restrict by string
+    if isinstance(condition, str):
+        columns.update(extract_column_names(condition))
+        return template % condition.strip().replace("%", "%%")  # escape % in strings, see issue #376
+
+    # restrict by AndList
+    if isinstance(condition, AndList):
+        # omit all conditions that evaluate to True
+        items = [item for item in (make_condition(query_expression, cond, columns) for cond in condition)
+                 if item is not True]
+        if any(item is False for item in items):
+            return negate  # if any item is False, the whole thing is False
+        if not items:
+            return not negate  # and empty AndList is True
+        return template % ('(' + ') AND ('.join(items) + ')')
+
+    # restriction by dj.U evaluates to True
+    if isinstance(condition, U):
+        return not negate
+
+    # restrict by boolean
+    if isinstance(condition, bool):
+        return negate != condition
+
+    # restrict by a mapping such as a dict -- convert to an AndList of string equality conditions
+    if isinstance(condition, collections.abc.Mapping):
+        common_attributes = set(condition).intersection(query_expression.heading.names)
+        if not common_attributes:
+            return not negate  # no matching attributes -> evaluates to True
+        columns.update(common_attributes)
+        return template % ('(' + ') AND ('.join(
+            '`%s`=%s' % (k, prep_value(k, condition[k])) for k in common_attributes) + ')')
+
+    # restrict by a numpy record -- convert to an AndList of string equality conditions
+    if isinstance(condition, numpy.void):
+        common_attributes = set(condition.dtype.fields).intersection(query_expression.heading.names)
+        if not common_attributes:
+            return not negate   # no matching attributes -> evaluate to True
+        columns.update(common_attributes)
+        return template % ('(' + ') AND ('.join(
+            '`%s`=%s' % (k, prep_value(k, condition[k])) for k in common_attributes) + ')')
+
+    # restrict by a QueryExpression subclass -- trigger instantiation and move on
+    if inspect.isclass(condition) and issubclass(condition, QueryExpression):
+        condition = condition()
+
+    # restrict by another expression (aka semijoin and antijoin)
+    check_compatibility = True
+    if isinstance(condition, PromiscuousOperand):
+        condition = condition.operand
+        check_compatibility = False
+
+    if isinstance(condition, QueryExpression):
+        if check_compatibility:
+            assert_join_compatibility(query_expression, condition)
+        common_attributes = [q for q in condition.heading.names if q in query_expression.heading.names]
+        columns.update(common_attributes)
+        if isinstance(condition, Aggregation):
+            condition = condition.make_subquery()
+        return (
+            # without common attributes, any non-empty set matches everything
+            (not negate if condition else negate) if not common_attributes
+            else '({fields}) {not_}in ({subquery})'.format(
+                fields='`' + '`,`'.join(common_attributes) + '`',
+                not_="not " if negate else "",
+                subquery=condition.make_sql(common_attributes)))
+
+    # restrict by pandas.DataFrames
+    if isinstance(condition, pandas.DataFrame):
+        condition = condition.to_records()  # convert to numpy.recarray and move on
+
+    # if iterable (but not a string, a QueryExpression, or an AndList), treat as an OrList
+    try:
+        or_list = [make_condition(query_expression, q, columns) for q in condition]
+    except TypeError:
+        raise DataJointError('Invalid restriction type %r' % condition)
+    else:
+        or_list = [item for item in or_list if item is not False]  # ignore all False conditions
+        if any(item is True for item in or_list):  # if any item is True, the whole thing is True
+            return not negate
+        return template % ('(%s)' % ' OR '.join(or_list)) if or_list else negate  # an empty or list is False
+
+
+def extract_column_names(sql_expression):
+    """
+    extract all presumed column names from an sql expression such as the WHERE clause, for example.
+    :param sql_expression: a string containing an SQL expression
+    :return: set of extracted column names
+    This may be MySQL-specific for now.
+    """
+    assert isinstance(sql_expression, str)
+    result = set()
+    s = sql_expression  # for terseness
+    # remove escaped quotes
+    s = re.sub(r'(\\\")|(\\\')', '', s)
+    # remove quoted text
+    s = re.sub(r"'[^']*'", "", s)
+    s = re.sub(r'"[^"]*"', '', s)
+    # find all tokens in back quotes and remove them
+    result.update(re.findall(r"`([a-z][a-z_0-9]*)`", s))
+    s = re.sub(r"`[a-z][a-z_0-9]*`", '', s)
+    # remove space before parentheses
+    s = re.sub(r"\s*\(", "(", s)
+    # remove tokens followed by ( since they must be functions
+    s = re.sub(r"(\b[a-z][a-z_0-9]*)\(", "(", s)
+    remaining_tokens = set(re.findall(r"\b[a-z][a-z_0-9]*\b", s))
+    # update result removing reserved words
+    result.update(remaining_tokens - {"is", "in", "between", "like", "and", "or", "null", "not"})
+    return result
diff --git a/datajoint/connection.py b/datajoint/connection.py
index b212bb669..9db3dcb77 100644
--- a/datajoint/connection.py
+++ b/datajoint/connection.py
@@ -7,15 +7,44 @@
 import pymysql as client
 import logging
 from getpass import getpass
+import re
+import pathlib
 
 from .settings import config
 from . import errors
 from .dependencies import Dependencies
+from .blob import pack, unpack
+from .hash import uuid_from_buffer
+from .plugin import connection_plugins
 
 logger = logging.getLogger(__name__)
 query_log_max_length = 300
 
 
+def get_host_hook(host_input):
+    if '://' in host_input:
+        plugin_name = host_input.split('://')[0]
+        try:
+            return connection_plugins[plugin_name]['object'].load().get_host(host_input)
+        except KeyError:
+            raise errors.DataJointError(
+                "Connection plugin '{}' not found.".format(plugin_name))
+    else:
+        return host_input
+
+
+def connect_host_hook(connection_obj):
+    if '://' in connection_obj.conn_info['host_input']:
+        plugin_name = connection_obj.conn_info['host_input'].split('://')[0]
+        try:
+            connection_plugins[plugin_name]['object'].load().connect_host(connection_obj)
+        except KeyError:
+            raise errors.DataJointError(
+                "Connection plugin '{}' not found.".format(plugin_name))
+    else:
+        connection_obj.connect()
+
+
 def translate_query_error(client_error, query):
     """
     Take client error and original query and return the corresponding DataJoint exception.
@@ -40,6 +69,8 @@ def translate_query_error(client_error, query):
     # Integrity errors
     if err == 1062:
         return errors.DuplicateError(*args)
+    if err == 1451:
+        return errors.IntegrityError(*args)
     if err == 1452:
         return errors.IntegrityError(*args)
     # Syntax errors
@@ -76,7 +107,8 @@ def conn(host=None, user=None, password=None, *, init_fun=None, reset=False, use
                         #encrypted-connection-options).
     """
     if not hasattr(conn, 'connection') or reset:
-        host = host if host is not None else config['database.host']
+        host_input = host if host is not None else config['database.host']
+        host = get_host_hook(host_input)
         user = user if user is not None else config['database.user']
         password = password if password is not None else config['database.password']
         if user is None:  # pragma: no cover
@@ -85,10 +117,34 @@ def conn(host=None, user=None, password=None, *, init_fun=None, reset=False, use
             password = getpass(prompt="Please enter DataJoint password: ")
         init_fun = init_fun if init_fun is not None else config['connection.init_function']
         use_tls = use_tls if use_tls is not None else config['database.use_tls']
-        conn.connection = Connection(host, user, password, None, init_fun, use_tls)
+        conn.connection = Connection(host, user, password, None, init_fun, use_tls,
+                                     host_input=host_input)
     return conn.connection
 
 
+class EmulatedCursor:
+    """acts like a cursor"""
+    def __init__(self, data):
+        self._data = data
+        self._iter = iter(self._data)
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        return next(self._iter)
+
+    def fetchall(self):
+        return self._data
+
+    def fetchone(self):
+        return next(self._iter)
+
+    @property
+    def rowcount(self):
+        return len(self._data)
+
+
 class Connection:
     """
     A dj.Connection object manages a connection to a database server.
@@ -104,7 +160,8 @@ class Connection:
     :param use_tls: TLS encryption option
     """
 
-    def __init__(self, host, user, password, port=None, init_fun=None, use_tls=None):
+    def __init__(self, host, user, password, port=None, init_fun=None, use_tls=None,
+                 host_input=None):
         if ':' in host:
             # the port in the hostname overrides the port argument
             host, port = host.split(':')
@@ -115,15 +172,17 @@ def __init__(self, host, user, password, port=None, init_fun=None, use_tls=None)
         if use_tls is not False:
             self.conn_info['ssl'] = use_tls if isinstance(use_tls, dict) else {'ssl': {}}
         self.conn_info['ssl_input'] = use_tls
+        self.conn_info['host_input'] = host_input
         self.init_fun = init_fun
         print("Connecting {user}@{host}:{port}".format(**self.conn_info))
         self._conn = None
-        self.connect()
+        self._query_cache = None
+        connect_host_hook(self)
         if self.is_connected:
             logger.info("Connected {user}@{host}:{port}".format(**self.conn_info))
             self.connection_id = self.query('SELECT connection_id()').fetchone()[0]
         else:
-            raise errors.ConnectionError('Connection failed.')
+            raise errors.LostConnectionError('Connection failed.')
         self._in_transaction = False
         self.schemas = dict()
         self.dependencies = Dependencies(self)
@@ -137,9 +196,7 @@ def __repr__(self):
             connected=connected, **self.conn_info)
 
     def connect(self):
-        """
-        Connects to the database server.
-        """
+        """ Connect to the database server."""
         with warnings.catch_warnings():
             warnings.filterwarnings('ignore', '.*deprecated.*')
             try:
@@ -149,7 +206,7 @@ def connect(self):
                              "STRICT_ALL_TABLES,NO_ENGINE_SUBSTITUTION",
                     charset=config['connection.charset'],
                     **{k: v for k, v in self.conn_info.items()
-                       if k != 'ssl_input'})
+                       if k not in ['ssl_input', 'host_input']})
             except client.err.InternalError:
                 self._conn = client.connect(
                     init_command=self.init_fun,
@@ -157,10 +214,28 @@ def connect(self):
                              "STRICT_ALL_TABLES,NO_ENGINE_SUBSTITUTION",
                     charset=config['connection.charset'],
                     **{k: v for k, v in self.conn_info.items()
-                       if not(k == 'ssl_input' or
+                       if not(k in ['ssl_input', 'host_input'] or
                               k == 'ssl' and self.conn_info['ssl_input'] is None)})
         self._conn.autocommit(True)
 
+    def set_query_cache(self, query_cache=None):
+        """
+        When query_cache is not None, the connection switches into the query caching mode, which entails:
+        1. Only SELECT queries are allowed.
+        2. The results of queries are cached under the path indicated by dj.config['query_cache']
+        3. query_cache is a string that differentiates different cache states.
+        :param query_cache: a string to initialize the hash for query results
+        """
+        self._query_cache = query_cache
+
+    def purge_query_cache(self):
+        """ Purges all query cache. """
+        if 'query_cache' in config and isinstance(config['query_cache'], str) and \
+                pathlib.Path(config['query_cache']).is_dir():
+            path_iter = pathlib.Path(config['query_cache']).glob('**/*')
+            for path in path_iter:
+                path.unlink()
+
     def close(self):
         self._conn.close()
 
@@ -169,16 +244,12 @@ def register(self, schema):
         self.dependencies.clear()
 
     def ping(self):
-        """
-        Pings the connection. Raises an exception if the connection is closed.
-        """
+        """ Ping the connection or raises an exception if the connection is closed. """
         self._conn.ping(reconnect=False)
 
     @property
     def is_connected(self):
-        """
-        Returns true if the object is connected to the database server.
-        """
+        """ Return true if the object is connected to the database server. """
         try:
             self.ping()
         except:
@@ -186,7 +257,7 @@ def is_connected(self):
         return True
 
     @staticmethod
-    def _execute_query(cursor, query, args, cursor_class, suppress_warnings):
+    def _execute_query(cursor, query, args, suppress_warnings):
         try:
             with warnings.catch_warnings():
                 if suppress_warnings:
@@ -194,7 +265,7 @@ def _execute_query(cursor, query, args, cursor_class, suppress_warnings):
                     warnings.simplefilter("ignore")
                 cursor.execute(query, args)
         except client.err.Error as err:
-            raise translate_query_error(err, query) from None
+            raise translate_query_error(err, query)
 
     def query(self, query, args=(), *, as_dict=False, suppress_warnings=True, reconnect=None):
         """
@@ -206,24 +277,46 @@ def query(self, query, args=(), *, as_dict=False, suppress_warnings=True, reconn
         :param suppress_warnings: If True, suppress all warnings arising from underlying query library
         :param reconnect: when None, get from config, when True, attempt to reconnect if disconnected
         """
+        # check cache first:
+        use_query_cache = bool(self._query_cache)
+        if use_query_cache and not re.match(r"\s*(SELECT|SHOW)", query):
+            raise errors.DataJointError("Only SELECT query are allowed when query caching is on.")
+        if use_query_cache:
+            if not config['query_cache']:
+                raise errors.DataJointError("Provide filepath dj.config['query_cache'] when using query caching.")
+            hash_ = uuid_from_buffer((str(self._query_cache) + re.sub(r'`\$\w+`', '', query)).encode() + pack(args))
+            cache_path = pathlib.Path(config['query_cache']) / str(hash_)
+            try:
+                buffer = cache_path.read_bytes()
+            except FileNotFoundError:
+                pass   # proceed to query the database
+            else:
+                return EmulatedCursor(unpack(buffer))
+
         if reconnect is None:
             reconnect = config['database.reconnect']
         logger.debug("Executing SQL:" + query[:query_log_max_length])
         cursor_class = client.cursors.DictCursor if as_dict else client.cursors.Cursor
         cursor = self._conn.cursor(cursor=cursor_class)
         try:
-            self._execute_query(cursor, query, args, cursor_class, suppress_warnings)
+            self._execute_query(cursor, query, args, suppress_warnings)
         except errors.LostConnectionError:
             if not reconnect:
                 raise
             warnings.warn("MySQL server has gone away. Reconnecting to the server.")
-            self.connect()
+            connect_host_hook(self)
             if self._in_transaction:
                 self.cancel_transaction()
-                raise errors.LostConnectionError("Connection was lost during a transaction.") from None
+                raise errors.LostConnectionError("Connection was lost during a transaction.")
             logger.debug("Re-executing")
             cursor = self._conn.cursor(cursor=cursor_class)
-            self._execute_query(cursor, query, args, cursor_class, suppress_warnings)
+            self._execute_query(cursor, query, args, suppress_warnings)
+
+        if use_query_cache:
+            data = cursor.fetchall()
+            cache_path.write_bytes(pack(data))
+            return EmulatedCursor(data)
+
         return cursor
 
     def get_user(self):
diff --git a/datajoint/declare.py b/datajoint/declare.py
index df643dc8a..e94c1bfef 100644
--- a/datajoint/declare.py
+++ b/datajoint/declare.py
@@ -5,11 +5,10 @@
 import re
 import pyparsing as pp
 import logging
+import warnings
 from .errors import DataJointError, _support_filepath_types, FILEPATH_FEATURE_SWITCH
 from .attribute_adapter import get_adapter
 
-from .utils import OrderedDict
-
 UUID_DATA_TYPE = 'binary(16)'
 MAX_TABLE_NAME_LENGTH = 64
 CONSTANT_LITERALS = {'CURRENT_TIMESTAMP'}  # SQL literals to be used without quotes (case insensitive)
@@ -45,14 +44,14 @@ def match_type(attribute_type):
     try:
         return next(category for category, pattern in TYPE_PATTERN.items() if pattern.match(attribute_type))
     except StopIteration:
-        raise DataJointError("Unsupported attribute type {type}".format(type=attribute_type)) from None
+        raise DataJointError("Unsupported attribute type {type}".format(type=attribute_type))
 
 
 logger = logging.getLogger(__name__)
 
 
 def build_foreign_key_parser_old():
-    # old-style foreign key parser. Superceded by expression-based syntax. See issue #436
+    # old-style foreign key parser. Superseded by expression-based syntax. See issue #436
     # This will be deprecated in a future release.
     left = pp.Literal('(').suppress()
     right = pp.Literal(')').suppress()
@@ -126,7 +125,7 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig
     """
     # Parse and validate
     from .table import Table
-    from .expression import Projection
+    from .expression import QueryExpression
 
     obsolete = False   # See issue #436.  Old style to be deprecated in a future release
     try:
@@ -135,7 +134,7 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig
         try:
             result = foreign_key_parser_old.parseString(line)
         except pp.ParseBaseException as err:
-            raise DataJointError('Parsing error in line "%s". %s.' % (line, err)) from None
+            raise DataJointError('Parsing error in line "%s". %s.' % (line, err))
         else:
             obsolete = True
     try:
@@ -153,15 +152,18 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig
         raise DataJointError('Primary dependencies cannot be nullable in line "{line}"'.format(line=line))
 
     if obsolete:
+        warnings.warn(
+            'Line "{line}" uses obsolete syntax that will no longer be supported in datajoint 0.14. '
+            'For details, see issue #780 https://github.com/datajoint/datajoint-python/issues/780'.format(line=line))
         if not isinstance(ref, type) or not issubclass(ref, Table):
             raise DataJointError('Foreign key reference %r must be a valid query' % result.ref_table)
 
     if isinstance(ref, type) and issubclass(ref, Table):
         ref = ref()
 
-    # check that dependency is of supported type
-    if (not isinstance(ref, (Table, Projection)) or len(ref.restriction) or
-            (isinstance(ref, Projection) and (not isinstance(ref._arg, Table) or len(ref._arg.restriction)))):
+    # check that dependency is of a supported type
+    if (not isinstance(ref, QueryExpression) or len(ref.restriction) or
+            len(ref.support) != 1 or not isinstance(ref.support[0], str)):
         raise DataJointError('Dependency "%s" is not supported (yet). Use a base table or its projection.' %
                              result.ref_table)
 
@@ -202,21 +204,20 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig
             ref = ref.proj(**dict(zip(new_attrs, ref_attrs)))
 
     # declare new foreign key attributes
-    base = ref._arg if isinstance(ref, Projection) else ref   # base reference table
-    for attr, ref_attr in zip(ref.primary_key, base.primary_key):
+    for attr in ref.primary_key:
         if attr not in attributes:
             attributes.append(attr)
             if primary_key is not None:
                 primary_key.append(attr)
             attr_sql.append(
-                base.heading[ref_attr].sql.replace(ref_attr, attr, 1).replace('NOT NULL ', '', int(is_nullable)))
+                ref.heading[attr].sql.replace('NOT NULL ', '', int(is_nullable)))
 
     # declare the foreign key
     foreign_key_sql.append(
         'FOREIGN KEY (`{fk}`) REFERENCES {ref} (`{pk}`) ON UPDATE CASCADE ON DELETE RESTRICT'.format(
             fk='`,`'.join(ref.primary_key),
-            pk='`,`'.join(base.primary_key),
-            ref=base.full_table_name))
+            pk='`,`'.join(ref.heading[name].original_name for name in ref.primary_key),
+            ref=ref.support[0]))
 
     # declare unique index
     if is_unique:
@@ -300,7 +301,7 @@ def _make_attribute_alter(new, old, primary_key):
     name_regexp = re.compile(r"^`(?P<name>\w+)`")
     original_regexp = re.compile(r'COMMENT "{\s*(?P<name>\w+)\s*}')
     matched = ((name_regexp.match(d), original_regexp.search(d)) for d in new)
-    new_names = OrderedDict((d.group('name'), n and n.group('name')) for d, n in matched)
+    new_names = dict((d.group('name'), n and n.group('name')) for d, n in matched)
     old_names = [name_regexp.search(d).group('name') for d in old]
 
     # verify that original names are only used once
@@ -430,7 +431,7 @@ def compile_attribute(line, in_key, foreign_key_sql, context):
         match = attribute_parser.parseString(line + '#', parseAll=True)
     except pp.ParseException as err:
         raise DataJointError('Declaration error in position {pos} in line:\n  {line}\n{msg}'.format(
-            line=err.args[0], pos=err.args[1], msg=err.args[2])) from None
+            line=err.args[0], pos=err.args[1], msg=err.args[2]))
     match['comment'] = match['comment'].rstrip('#')
     if 'default' not in match:
         match['default'] = ''
diff --git a/datajoint/dependencies.py b/datajoint/dependencies.py
index a4e1afb0c..c1a331b7d 100644
--- a/datajoint/dependencies.py
+++ b/datajoint/dependencies.py
@@ -1,7 +1,7 @@
 import networkx as nx
 import itertools
 import re
-from collections import defaultdict, OrderedDict
+from collections import defaultdict
 from .errors import DataJointError
 
 
@@ -86,7 +86,7 @@ def load(self, force=True):
         WHERE referenced_table_name NOT LIKE "~%%" AND (referenced_table_schema in ('{schemas}') OR
             referenced_table_schema is not NULL AND table_schema in ('{schemas}'))
         """.format(schemas="','".join(self._conn.schemas)), as_dict=True))
-        fks = defaultdict(lambda: dict(attr_map=OrderedDict()))
+        fks = defaultdict(lambda: dict(attr_map=dict()))
         for key in keys:
             d = fks[(key['constraint_name'], key['referencing_table'], key['referenced_table'])]
             d['referencing_table'] = key['referencing_table']
diff --git a/datajoint/diagram.py b/datajoint/diagram.py
index 41ead0712..dd48e7b17 100644
--- a/datajoint/diagram.py
+++ b/datajoint/diagram.py
@@ -145,7 +145,7 @@ def is_part(part, master):
                 """
                 :param part:  `database`.`table_name`
                 :param master:   `database`.`table_name`
-                :return: True if part is part of master,
+                :return: True if part is part of master.
                 """
                 part = [s.strip('`') for s in part.split('.')]
                 master = [s.strip('`') for s in master.split('.')]
diff --git a/datajoint/errors.py b/datajoint/errors.py
index 93a4e01a9..fe0ffc539 100644
--- a/datajoint/errors.py
+++ b/datajoint/errors.py
@@ -5,11 +5,24 @@
 import os
 
 
+# --- Unverified Plugin Check ---
+class PluginWarning(Exception):
+    pass
+
+
 # --- Top Level ---
 class DataJointError(Exception):
     """
     Base class for errors specific to DataJoint internal operation.
     """
+    def __init__(self, *args):
+        from .plugin import connection_plugins, type_plugins
+        self.__cause__ = PluginWarning(
+            'Unverified DataJoint plugin detected.') if any([any(
+                [not plugins[k]['verified'] for k in plugins])
+                for plugins in [connection_plugins, type_plugins]
+                if plugins]) else None
+
     def suggest(self, *args):
         """
         regenerate the exception with additional arguments
diff --git a/datajoint/expression.py b/datajoint/expression.py
index 28ff4a6f4..6d07784f6 100644
--- a/datajoint/expression.py
+++ b/datajoint/expression.py
@@ -1,313 +1,130 @@
-import collections
 from itertools import count
 import logging
 import inspect
-import numpy as np
+import copy
 import re
-import datetime
-import decimal
-import pandas
-import uuid
-import binascii   # for Python 3.4 compatibility
 from .settings import config
 from .errors import DataJointError
 from .fetch import Fetch, Fetch1
+from .preview import preview, repr_html
+from .condition import AndList, Not, \
+    make_condition, assert_join_compatibility, extract_column_names, PromiscuousOperand
 
 logger = logging.getLogger(__name__)
 
 
-def assert_join_compatibility(rel1, rel2):
-    """
-    Determine if expressions rel1 and rel2 are join-compatible.  To be join-compatible, the matching attributes
-    in the two expressions must be in the primary key of one or the other expression.
-    Raises an exception if not compatible.
-    :param rel1: A QueryExpression object
-    :param rel2: A QueryExpression object
-    """
-    for rel in (rel1, rel2):
-        if not isinstance(rel, (U, QueryExpression)):
-            raise DataJointError('Object %r is not a QueryExpression and cannot be joined.' % rel)
-    if not isinstance(rel1, U) and not isinstance(rel2, U):  # dj.U is always compatible
-        try:
-            raise DataJointError("Cannot join query expressions on dependent attribute `%s`" % next(r for r in set(
-                rel1.heading.secondary_attributes).intersection(rel2.heading.secondary_attributes)))
-        except StopIteration:
-            pass
-
-
-class AndList(list):
-    """
-    A list of restrictions to by applied to a query expression.  The restrictions are AND-ed.
-    Each restriction can be a list or set or a query expression whose elements are OR-ed.
-    But the elements that are lists can contain other AndLists.
-
-    Example:
-    rel2 = rel & dj.AndList((cond1, cond2, cond3))
-    is equivalent to
-    rel2 = rel & cond1 & cond2 & cond3
+class QueryExpression:
     """
+    QueryExpression implements query operators to derive new entity set from its input.
+    A QueryExpression object generates a SELECT statement in SQL.
+    QueryExpression operators are restrict, join, proj, aggr, and union.
 
-    def append(self, restriction):
-        if isinstance(restriction, AndList):
-            # extend to reduce nesting
-            self.extend(restriction)
-        else:
-            super().append(restriction)
-
+    A QueryExpression object has a support, a restriction (an AndList), and heading.
+    Property `heading` (type dj.Heading) contains information about the attributes.
+    It is loaded from the database and updated by proj.
 
-def is_true(restriction):
-    return restriction is True or isinstance(restriction, AndList) and not len(restriction)
+    Property `support` is the list of table names or other QueryExpressions to be joined.
 
+    The restriction is applied first without having access to the attributes generated by the projection.
+    Then projection is applied by selecting modifying the heading attribute.
 
-class QueryExpression:
-    """
-    QueryExpression implements query operators to derive new entity sets from its inputs.
-    When fetching data from the database, the expression is compiled into an SQL expression.
-    QueryExpression operators are restrict, join, proj, aggr, and union.
+    Application of operators does not always lead to the creation of a subquery.
+    A subquery is generated when:
+        1. A restriction is applied on any computed or renamed attributes
+        2. A projection is applied remapping remapped attributes
+        3. Subclasses: Join, Aggregation, and Union have additional specific rules.
     """
+    _restriction = None
+    _restriction_attributes = None
+    _left = []  # True for left joins, False for inner joins
+    _original_heading = None  # heading before projections
 
-    def __init__(self, arg=None):
-        if arg is None:  # initialize
-            # initialize
-            self._restriction = AndList()
-            self._distinct = False
-            self._heading = None
-        else:  # copy
-            assert isinstance(arg, QueryExpression), 'Cannot make QueryExpression from %s' % arg.__class__.__name__
-            self._restriction = AndList(arg._restriction)
-            self._distinct = arg.distinct
-            self._heading = arg._heading
-
-    @classmethod
-    def create(cls):  # pragma: no cover
-        """abstract method for creating an instance"""
-        assert False, "Abstract method `create` must be overridden in subclass."
+    # subclasses or instantiators must provide values
+    _connection = None
+    _heading = None
+    _support = None
 
     @property
     def connection(self):
-        """
-        :return:  the dj.Connection object
-        """
+        """ a dj.Connection object """
+        assert self._connection is not None
         return self._connection
 
+    @property
+    def support(self):
+        """ A list of table names or subqueries to from the FROM clause """
+        assert self._support is not None
+        return self._support
+
     @property
     def heading(self):
-        """
-        :return: the dj.Heading object for the query expression
-        """
+        """ a dj.Heading object, reflects the effects of the projection operator .proj """
         return self._heading
 
     @property
-    def distinct(self):
-        """
-        :return: True if the DISTINCT modifier is required to make valid result
-        """
-        return self._distinct
+    def original_heading(self):
+        """ a dj.Heading object reflecting the attributes before projection """
+        return self._original_heading or self.heading
 
     @property
     def restriction(self):
-        """
-        :return:  The AndList of restrictions applied to input to produce the result.
-        """
-        assert isinstance(self._restriction, AndList)
+        """ a AndList object of restrictions applied to input to produce the result """
+        if self._restriction is None:
+            self._restriction = AndList()
         return self._restriction
 
+    @property
+    def restriction_attributes(self):
+        """ the set of attribute names invoked in the WHERE clause """
+        if self._restriction_attributes is None:
+            self._restriction_attributes = set()
+        return self._restriction_attributes
+
     @property
     def primary_key(self):
         return self.heading.primary_key
 
-    def _make_condition(self, arg):
-        """
-        Translate the input arg into the equivalent SQL condition (a string)
-        :param arg: any valid restriction object.
-        :return: an SQL condition string or a boolean value.
-        """
-        def prep_value(k, v):
-            """prepare value v for inclusion as a string in an SQL condition"""
-            if self.heading[k].uuid:
-                if not isinstance(v, uuid.UUID):
-                    try:
-                        v = uuid.UUID(v)
-                    except (AttributeError, ValueError):
-                        raise DataJointError('Badly formed UUID {v} in restriction by `{k}`'.format(k=k, v=v)) from None
-                return "X'%s'" % binascii.hexlify(v.bytes).decode()
-            if isinstance(v, (datetime.date, datetime.datetime, datetime.time, decimal.Decimal)):
-                return '"%s"' % v
-            return '%r' % v
-
-        negate = False
-        while isinstance(arg, Not):
-            negate = not negate
-            arg = arg.restriction
-        template = "NOT (%s)" if negate else "%s"
-
-        # restrict by string
-        if isinstance(arg, str):
-            return template % arg.strip().replace("%", "%%")  # escape % in strings, see issue #376
-
-        # restrict by AndList
-        if isinstance(arg, AndList):
-            # omit all conditions that evaluate to True
-            items = [item for item in (self._make_condition(i) for i in arg) if item is not True]
-            if any(item is False for item in items):
-                return negate  # if any item is False, the whole thing is False
-            if not items:
-                return not negate   # and empty AndList is True
-            return template % ('(' + ') AND ('.join(items) + ')')
-
-        # restriction by dj.U evaluates to True
-        if isinstance(arg, U):
-            return not negate
-
-        # restrict by boolean
-        if isinstance(arg, bool):
-            return negate != arg
-
-        # restrict by a mapping such as a dict -- convert to an AndList of string equality conditions
-        if isinstance(arg, collections.abc.Mapping):
-            return template % self._make_condition(
-                AndList('`%s`=%s' % (k, prep_value(k, v)) for k, v in arg.items() if k in self.heading))
-
-        # restrict by a numpy record -- convert to an AndList of string equality conditions
-        if isinstance(arg, np.void):
-            return template % self._make_condition(
-                AndList(('`%s`=%s' % (k, prep_value(k, arg[k])) for k in arg.dtype.fields if k in self.heading)))
-
-        # restrict by a QueryExpression subclass -- triggers instantiation
-        if inspect.isclass(arg) and issubclass(arg, QueryExpression):
-            arg = arg()
-
-        # restrict by another expression (aka semijoin and antijoin)
-        if isinstance(arg, QueryExpression):
-            assert_join_compatibility(self, arg)
-            common_attributes = [q for q in arg.heading.names if q in self.heading.names]
-            return (
-                # without common attributes, any non-empty set matches everything
-                (not negate if arg else negate) if not common_attributes
-                else '({fields}) {not_}in ({subquery})'.format(
-                    fields='`' + '`,`'.join(common_attributes) + '`',
-                    not_="not " if negate else "",
-                    subquery=arg.make_sql(common_attributes)))
-
-        # restrict by pandas.DataFrames
-        if isinstance(arg, pandas.DataFrame):
-            arg = arg.to_records()   # convert to np.recarray
-
-        # if iterable (but not a string, a QueryExpression, or an AndList), treat as an OrList
-        try:
-            or_list = [self._make_condition(q) for q in arg]
-        except TypeError:
-            raise DataJointError('Invalid restriction type %r' % arg)
-        else:
-            or_list = [item for item in or_list if item is not False]  # ignore all False conditions
-            if any(item is True for item in or_list):  # if any item is True, the whole thing is True
-                return not negate
-            return template % ('(%s)' % ' OR '.join(or_list)) if or_list else negate  # an empty or list is False
+    _subquery_alias_count = count()    # count for alias names used in from_clause
+
+    def from_clause(self):
+        support = ('(' + src.make_sql() + ') as `_s%x`' % next(
+            self._subquery_alias_count) if isinstance(src, QueryExpression) else src for src in self.support)
+        clause = next(support)
+        for s, left in zip(support, self._left):
+            clause += 'NATURAL{left} JOIN {clause}'.format(
+                left=" LEFT" if left else "",
+                clause=s)
+        return clause
 
-    @property
     def where_clause(self):
-        """
-        convert self.restriction to the SQL WHERE clause
-        """
-        cond = self._make_condition(self.restriction)
-        return '' if cond is True else ' WHERE %s' % cond
+        return '' if not self.restriction else ' WHERE(%s)' % ')AND('.join(
+            str(s) for s in self.restriction)
 
-    def get_select_fields(self, select_fields=None):
+    def make_sql(self, fields=None):
         """
-        :return: string specifying the attributes to return
+        Make the SQL SELECT statement.
+        :param fields: used to explicitly set the select attributes
         """
-        return self.heading.as_sql if select_fields is None else self.heading.project(select_fields).as_sql
+        distinct = self.heading.names == self.primary_key
+        return 'SELECT {distinct}{fields} FROM {from_}{where}'.format(
+            distinct="DISTINCT " if distinct else "",
+            fields=self.heading.as_sql(fields or self.heading.names),
+            from_=self.from_clause(), where=self.where_clause())
 
     # --------- query operators -----------
-
-    def __mul__(self, other):
-        """
-        natural join of query expressions `self` and `other`
-        """
-        return other * self if isinstance(other, U) else Join.create(self, other)
-
-    def __add__(self, other):
-        """
-        union of two entity sets `self` and `other`
-        """
-        return Union.create(self, other)
-
-    def proj(self, *attributes, **named_attributes):
-        """
-        Projection operator.
-        :param attributes:  attributes to be included in the result. (The primary key is already included).
-        :param named_attributes: new attributes computed or renamed from existing attributes.
-        :return: the projected expression.
-        Primary key attributes cannot be excluded but may be renamed.
-        Thus self.proj() leaves only the primary key attributes of self.
-        self.proj(a='id') renames the attribute 'id' into 'a' and includes 'a' in the projection.
-        self.proj(a='expr') adds a new field a with the value computed with an SQL expression.
-        self.proj(a='(id)') adds a new computed field named 'a' that has the same value as id
-        Each attribute can only be used once in attributes or named_attributes.
-        If the attribute list contains an Ellipsis ..., then all secondary attributes are included
-        If an entry of the attribute list starts with a dash, e.g. '-attr', then the secondary attribute
-        attr will be excluded, if already present but ignored if not found.
-        """
-        return Projection.create(self, attributes, named_attributes)
-
-    def aggr(self, group, *attributes, keep_all_rows=False, **named_attributes):
-        """
-        Aggregation/projection operator
-        :param group:  an entity set whose entities will be grouped per entity of `self`
-        :param attributes: attributes of self to include in the result
-        :param keep_all_rows: True = preserve the number of elements in the result (equivalent of LEFT JOIN in SQL)
-        :param named_attributes: renamings and computations on attributes of self and group
-        :return: an entity set representing the result of the aggregation/projection operator of entities from `group`
-        per entity of `self`
-        """
-        return GroupBy.create(self, group, keep_all_rows=keep_all_rows,
-                              attributes=attributes, named_attributes=named_attributes)
-
-    aggregate = aggr  # aliased name for aggr
-
-    def __iand__(self, restriction):
-        """
-        in-place restriction.
-        A subquery is created if the argument has renamed attributes.  Then the restriction is not in place.
-
-        See QueryExpression.restrict for more detail.
-        """
-        if is_true(restriction):
-            return self
-        return (Subquery.create(self) if self.heading.expressions else self).restrict(restriction)
-
-    def __and__(self, restriction):
-        """
-        Restriction operator
-        :return: a restricted copy of the input argument
-        See QueryExpression.restrict for more detail.
-        """
-        return (Subquery.create(self)  # the HAVING clause in GroupBy can handle renamed attributes but WHERE cannot
-                if not(is_true(restriction)) and self.heading.expressions and not isinstance(self, GroupBy)
-                else self.__class__(self)).restrict(restriction)
-
-    def __isub__(self, restriction):
-        """
-        in-place inverted restriction aka antijoin
-
-        See QueryExpression.restrict for more detail.
-        """
-        return self.restrict(Not(restriction))
-
-    def __sub__(self, restriction):
-        """
-        inverted restriction aka antijoin
-        :return: a restricted copy of the argument
-
-        See QueryExpression.restrict for more detail.
-        """
-        return self & Not(restriction)
+    def make_subquery(self):
+        """ create a new SELECT statement where self is the FROM clause """
+        result = QueryExpression()
+        result._connection = self.connection
+        result._support = [self]
+        result._heading = self.heading.make_subquery_heading()
+        return result
 
     def restrict(self, restriction):
         """
-        In-place restriction.  Restricts the result to a specified subset of the input.
-        rel.restrict(restriction)  is equivalent to  rel = rel & restriction  or  rel &= restriction
-        rel.restrict(Not(restriction))  is equivalent to  rel = rel - restriction  or  rel -= restriction
+        Produces a new expression with the new restriction applied.
+        rel.restrict(restriction)  is equivalent to  rel & restriction.
+        rel.restrict(Not(restriction))  is equivalent to  rel - restriction
         The primary key of the result is unaffected.
         Successive restrictions are combined as logical AND:   r & a & b  is equivalent to r & AndList((a, b))
         Any QueryExpression, collection, or sequence other than an AndList are treated as OrLists
@@ -348,11 +165,247 @@ def restrict(self, restriction):
         :param restriction: a sequence or an array (treated as OR list), another QueryExpression, an SQL condition
         string, or an AndList.
         """
-        assert is_true(restriction) or not self.heading.expressions or isinstance(self, GroupBy), \
-            "Cannot restrict a projection with renamed attributes in place."
-        self.restriction.append(restriction)
-        return self
+        attributes = set()
+        new_condition = make_condition(self, restriction, attributes)
+        if new_condition is True:
+            return self  # restriction has no effect, return the same object
+        # check that all attributes in condition are present in the query
+        try:
+            raise DataJointError("Attribute `%s` is not found in query." % next(
+                attr for attr in attributes if attr not in self.heading.names))
+        except StopIteration:
+            pass  # all ok
+        # If the new condition uses any new attributes, a subquery is required.
+        # However, Aggregation's HAVING statement works fine with aliased attributes.
+        need_subquery = isinstance(self, Union) or (
+                not isinstance(self, Aggregation) and self.heading.new_attributes)
+        if need_subquery:
+            result = self.make_subquery()
+        else:
+            result = copy.copy(self)
+            result._restriction = AndList(self.restriction)  # copy to preserve the original
+        result.restriction.append(new_condition)
+        result.restriction_attributes.update(attributes)
+        return result
+
+    def restrict_in_place(self, restriction):
+        self.__dict__.update(self.restrict(restriction).__dict__)
 
+    def __and__(self, restriction):
+        """
+        Restriction operator e.g. ``q1 & q2``.
+        :return: a restricted copy of the input argument
+        See QueryExpression.restrict for more detail.
+        """
+        return self.restrict(restriction)
+
+    def __xor__(self, restriction):
+        """
+        Permissive restriction operator ignoring compatibility check  e.g. ``q1 ^ q2``.
+        """
+        if inspect.isclass(restriction) and issubclass(restriction, QueryExpression):
+            restriction = restriction()
+        if isinstance(restriction, Not):
+            return self.restrict(Not(PromiscuousOperand(restriction.restriction)))
+        return self.restrict(PromiscuousOperand(restriction))
+
+    def __sub__(self, restriction):
+        """
+        Inverted restriction e.g. ``q1 - q2``.
+        :return: a restricted copy of the input argument
+        See QueryExpression.restrict for more detail.
+        """
+        return self.restrict(Not(restriction))
+
+    def __neg__(self):
+        """
+        Convert between restriction and inverted restriction e.g. ``-q1``.
+        :return: target restriction
+        See QueryExpression.restrict for more detail.
+        """
+        if isinstance(self, Not):
+            return self.restriction
+        return Not(self)
+
+    def __mul__(self, other):
+        """
+        join of query expressions `self` and `other` e.g. ``q1 * q2``.
+        """
+        return self.join(other)
+
+    def __matmul__(self, other):
+        """
+        Permissive join of query expressions `self` and `other` ignoring compatibility check
+            e.g. ``q1 @ q2``.
+        """
+        if inspect.isclass(other) and issubclass(other, QueryExpression):
+            other = other()  # instantiate
+        return self.join(other, semantic_check=False)
+
+    def join(self, other, semantic_check=True, left=False):
+        """
+        create the joined QueryExpression.
+        a * b  is short for A.join(B)
+        a @ b  is short for A.join(B, semantic_check=False)
+        Additionally, left=True will retain the rows of self, effectively performing a left join.
+        """
+        # trigger subqueries if joining on renamed attributes
+        if isinstance(other, U):
+            return other * self
+        if inspect.isclass(other) and issubclass(other, QueryExpression):
+            other = other()  # instantiate
+        if not isinstance(other, QueryExpression):
+            raise DataJointError("The argument of join must be a QueryExpression")
+        if semantic_check:
+            assert_join_compatibility(self, other)
+        join_attributes = set(n for n in self.heading.names if n in other.heading.names)
+        # needs subquery if FROM class has common attributes with the other's FROM clause
+        need_subquery1 = need_subquery2 = bool(
+            (set(self.original_heading.names) & set(other.original_heading.names))
+            - join_attributes)
+        # need subquery if any of the join attributes are derived
+        need_subquery1 = need_subquery1 or any(n in self.heading.new_attributes for n in join_attributes)
+        need_subquery2 = need_subquery2 or any(n in other.heading.new_attributes for n in join_attributes)
+        if need_subquery1:
+            self = self.make_subquery()
+        if need_subquery2:
+            other = other.make_subquery()
+        result = QueryExpression()
+        result._connection = self.connection
+        result._support = self.support + other.support
+        result._left = self._left + [left] + other._left
+        result._heading = self.heading.join(other.heading)
+        result._restriction = AndList(self.restriction)
+        result._restriction.append(other.restriction)
+        result._original_heading = self.original_heading.join(other.original_heading)
+        assert len(result.support) == len(result._left) + 1
+        return result
+
+    def __add__(self, other):
+        """union e.g. ``q1 + q2``."""
+        return Union.create(self, other)
+
+    def proj(self, *attributes, **named_attributes):
+        """
+        Projection operator.
+        :param attributes:  attributes to be included in the result. (The primary key is already included).
+        :param named_attributes: new attributes computed or renamed from existing attributes.
+        :return: the projected expression.
+        Primary key attributes cannot be excluded but may be renamed.
+        If the attribute list contains an Ellipsis ..., then all secondary attributes are included too
+        Prefixing an attribute name with a dash '-attr' removes the attribute from the list if present.
+        Keyword arguments can be used to rename attributes as in name='attr', duplicate them as in name='(attr)', or
+        self.proj(...) or self.proj(Ellipsis) -- include all attributes (return self)
+        self.proj() -- include only primary key
+        self.proj('attr1', 'attr2')  -- include primary key and attributes attr1 and attr2
+        self.proj(..., '-attr1', '-attr2')  -- include attributes except attr1 and attr2
+        self.proj(name1='attr1') -- include primary key and 'attr1' renamed as name1
+        self.proj('attr1', dup='(attr1)') -- include primary key and attribute attr1 twice, with the duplicate 'dup'
+        self.proj(k='abs(attr1)') adds the new attribute k with the value computed as an expression (SQL syntax)
+        from other attributes available before the projection.
+        Each attribute name can only be used once.
+        """
+        # new attributes in parentheses are included again with the new name without removing original
+        duplication_pattern = re.compile(r'\s*\(\s*(?P<name>[a-z][a-z_0-9]*)\s*\)\s*$')
+        # attributes without parentheses renamed
+        rename_pattern = re.compile(r'\s*(?P<name>[a-z][a-z_0-9]*)\s*$')
+        replicate_map = {k: m.group('name')
+                         for k, m in ((k, duplication_pattern.match(v)) for k, v in named_attributes.items()) if m}
+        rename_map = {k: m.group('name')
+                      for k, m in ((k, rename_pattern.match(v)) for k, v in named_attributes.items()) if m}
+        compute_map = {k: v for k, v in named_attributes.items()
+                       if not duplication_pattern.match(v) and not rename_pattern.match(v)}
+        attributes = set(attributes)
+        # include primary key
+        attributes.update((k for k in self.primary_key if k not in rename_map.values()))
+        # include all secondary attributes with Ellipsis
+        if Ellipsis in attributes:
+            attributes.discard(Ellipsis)
+            attributes.update((a for a in self.heading.secondary_attributes
+                               if a not in attributes and a not in rename_map.values()))
+        try:
+            raise DataJointError("%s is not a valid data type for an attribute in .proj" % next(
+                a for a in attributes if not isinstance(a, str)))
+        except StopIteration:
+            pass  # normal case
+        # remove excluded attributes, specified as `-attr'
+        excluded = set(a for a in attributes if a.strip().startswith('-'))
+        attributes.difference_update(excluded)
+        excluded = set(a.lstrip('-').strip() for a in excluded)
+        attributes.difference_update(excluded)
+        try:
+            raise DataJointError("Cannot exclude primary key attribute %s", next(
+                a for a in excluded if a in self.primary_key))
+        except StopIteration:
+            pass  # all ok
+        # check that all attributes exist in heading
+        try:
+            raise DataJointError(
+                'Attribute `%s` not found.' % next(a for a in attributes if a not in self.heading.names))
+        except StopIteration:
+            pass  # all ok
+
+        # check that all mentioned names are present in heading
+        mentions = attributes.union(replicate_map.values()).union(rename_map.values())
+        try:
+            raise DataJointError("Attribute '%s' not found." % next(a for a in mentions if not self.heading.names))
+        except StopIteration:
+            pass  # all ok
+
+        # check that newly created attributes do not clash with any other selected attributes
+        try:
+            raise DataJointError("Attribute `%s` already exists" % next(
+                a for a in rename_map if a in attributes.union(compute_map).union(replicate_map)))
+        except StopIteration:
+            pass  # all ok
+        try:
+            raise DataJointError("Attribute `%s` already exists" % next(
+                a for a in compute_map if a in attributes.union(rename_map).union(replicate_map)))
+        except StopIteration:
+            pass  # all ok
+        try:
+            raise DataJointError("Attribute `%s` already exists" % next(
+                a for a in replicate_map if a in attributes.union(rename_map).union(compute_map)))
+        except StopIteration:
+            pass  # all ok
+
+        # need a subquery if the projection remaps any remapped attributes
+        used = set(q for v in compute_map.values() for q in extract_column_names(v))
+        used.update(rename_map.values())
+        used.update(replicate_map.values())
+        used.intersection_update(self.heading.names)
+        need_subquery = isinstance(self, Union) or any(
+            self.heading[name].attribute_expression is not None for name in used)
+        if not need_subquery and self.restriction:
+            # need a subquery if the restriction applies to attributes that have been renamed
+            need_subquery = any(name in self.restriction_attributes for name in self.heading.new_attributes)
+
+        result = self.make_subquery() if need_subquery else copy.copy(self)
+        result._original_heading = result.original_heading
+        result._heading = result.heading.select(
+            attributes, rename_map=dict(**rename_map, **replicate_map), compute_map=compute_map)
+        return result
+
+    def aggr(self, group, *attributes, keep_all_rows=False, **named_attributes):
+        """
+        Aggregation of the type U('attr1','attr2').aggr(group, computation="QueryExpression")
+        has the primary key ('attr1','attr2') and performs aggregation computations for all matching elements of `group`.
+        :param group:  The query expression to be aggregated.
+        :param keep_all_rows: True=keep all the rows from self. False=keep only rows that match entries in group.
+        :param named_attributes: computations of the form new_attribute="sql expression on attributes of group"
+        :return: The derived query expression
+        """
+        if Ellipsis in attributes:
+            # expand ellipsis to include only attributes from the left table
+            attributes = set(attributes)
+            attributes.discard(Ellipsis)
+            attributes.update(self.heading.secondary_attributes)
+        return Aggregation.create(
+            self, group=group, keep_all_rows=keep_all_rows).proj(*attributes, **named_attributes)
+
+    aggregate = aggr  # alias for aggr
+
+    # ---------- Fetch operators --------------------
     @property
     def fetch1(self):
         return Fetch1(self)
@@ -381,172 +434,59 @@ def tail(self, limit=25, **fetch_kwargs):
         """
         return self.fetch(order_by="KEY DESC", limit=limit, **fetch_kwargs)[::-1]
 
-    def attributes_in_restriction(self):
-        """
-        :return: list of attributes that are probably used in the restriction.
-            The function errs on the side of false positives.
-            For example, if the restriction is "val='id'", then the attribute 'id' would be flagged.
-            This is used internally for optimizing SQL statements.
-        """
-        return set(name for name in self.heading.names
-                   if re.search(r'\b' + name + r'\b', self.where_clause))
-
-    def __repr__(self):
-        return super().__repr__() if config['loglevel'].lower() == 'debug' else self.preview()
-
-    def preview(self, limit=None, width=None):
-        """
-        returns a preview of the contents of the query.
-        """
-        heading = self.heading
-        rel = self.proj(*heading.non_blobs)
-        if limit is None:
-            limit = config['display.limit']
-        if width is None:
-            width = config['display.width']
-        tuples = rel.fetch(limit=limit+1, format="array")
-        has_more = len(tuples) > limit
-        tuples = tuples[:limit]
-        columns = heading.names
-        widths = {f: min(max([len(f)] +
-                  [len(str(e)) for e in tuples[f]] if f in tuples.dtype.names else
-                  [len('=BLOB=')]) + 4, width) for f in columns}
-        templates = {f: '%%-%d.%ds' % (widths[f], widths[f]) for f in columns}
-        return (
-            ' '.join([templates[f] % ('*' + f if f in rel.primary_key else f) for f in columns]) + '\n' +
-            ' '.join(['+' + '-' * (widths[column] - 2) + '+' for column in columns]) + '\n' +
-            '\n'.join(' '.join(templates[f] % (tup[f] if f in tup.dtype.names else '=BLOB=')
-                      for f in columns) for tup in tuples) +
-            ('\n   ...\n' if has_more else '\n') +
-            (' (Total: %d)\n' % len(rel) if config['display.show_tuple_count'] else ''))
-
-    def _repr_html_(self):
-        heading = self.heading
-        rel = self.proj(*heading.non_blobs)
-        info = heading.table_info
-        tuples = rel.fetch(limit=config['display.limit']+1, format='array')
-        has_more = len(tuples) > config['display.limit']
-        tuples = tuples[0:config['display.limit']]
-
-        css = """
-        <style type="text/css">
-            .Relation{
-                border-collapse:collapse;
-            }
-            .Relation th{
-                background: #A0A0A0; color: #ffffff; padding:4px; border:#f0e0e0 1px solid;
-                font-weight: normal; font-family: monospace; font-size: 100%;
-            }
-            .Relation td{
-                padding:4px; border:#f0e0e0 1px solid; font-size:100%;
-            }
-            .Relation tr:nth-child(odd){
-                background: #ffffff;
-            }
-            .Relation tr:nth-child(even){
-                background: #f3f1ff;
-            }
-            /* Tooltip container */
-            .djtooltip {
-            }
-            /* Tooltip text */
-            .djtooltip .djtooltiptext {
-                visibility: hidden;
-                width: 120px;
-                background-color: black;
-                color: #fff;
-                text-align: center;
-                padding: 5px 0;
-                border-radius: 6px;
-                /* Position the tooltip text - see examples below! */
-                position: absolute;
-                z-index: 1;
-            }
-            #primary {
-                font-weight: bold;
-                color: black;
-            }
-
-            #nonprimary {
-                font-weight: normal;
-                color: white;
-            }
-
-            /* Show the tooltip text when you mouse over the tooltip container */
-            .djtooltip:hover .djtooltiptext {
-                visibility: visible;
-            }
-        </style>
-        """
-        head_template = """<div class="djtooltip">
-                                <p id="{primary}">{column}</p>
-                                <span class="djtooltiptext">{comment}</span>
-                            </div>"""
-        return """
-        {css}
-        {title}
-            <div style="max-height:1000px;max-width:1500px;overflow:auto;">
-            <table border="1" class="Relation">
-                <thead> <tr style="text-align: right;"> <th> {head} </th> </tr> </thead>
-                <tbody> <tr> {body} </tr> </tbody>
-            </table>
-            {ellipsis}
-            {count}</div>
-            """.format(
-            css=css,
-            title="" if info is None else "<b>%s</b>" % info['comment'],
-            head='</th><th>'.join(
-                head_template.format(column=c, comment=heading.attributes[c].comment,
-                                     primary='primary' if c in self.primary_key else 'nonprimary') for c in
-                heading.names),
-            ellipsis='<p>...</p>' if has_more else '',
-            body='</tr><tr>'.join(
-                ['\n'.join(['<td>%s</td>' % (tup[name] if name in tup.dtype.names else '=BLOB=')
-                            for name in heading.names])
-                 for tup in tuples]),
-            count=('<p>Total: %d</p>' % len(rel)) if config['display.show_tuple_count'] else '')
-
-    def make_sql(self, select_fields=None):
-        return 'SELECT {fields} FROM {from_}{where}'.format(
-            fields=("DISTINCT " if self.distinct else "") + self.get_select_fields(select_fields),
-            from_=self.from_clause,
-            where=self.where_clause)
-
     def __len__(self):
-        """
-        number of elements in the result set.
-        """
+        """:return: number of elements in the result set e.g. ``len(q1)``."""
         return self.connection.query(
-            'SELECT count({count}) FROM {from_}{where}'.format(
-                count='DISTINCT `{pk}`'.format(pk='`,`'.join(self.primary_key)) if self.distinct and self.primary_key else '*',
-                from_=self.from_clause,
-                where=self.where_clause)).fetchone()[0]
+            'SELECT count(DISTINCT {fields}) FROM {from_}{where}'.format(
+                fields=self.heading.as_sql(self.primary_key, include_aliases=False),
+                from_=self.from_clause(),
+                where=self.where_clause())).fetchone()[0]
 
     def __bool__(self):
         """
-        :return:  True if the result is not empty. Equivalent to len(rel)>0 but may be more efficient.
+        :return: True if the result is not empty. Equivalent to len(self) > 0 but often
+            faster e.g. ``bool(q1)``.
         """
-        return len(self) > 0
+        return bool(self.connection.query(
+            'SELECT EXISTS(SELECT 1 FROM {from_}{where})'.format(
+                from_=self.from_clause(),
+                where=self.where_clause())).fetchone()[0])
 
     def __contains__(self, item):
         """
-        returns True if item is found in the .
+        returns True if the restriction in item matches any entries in self
+            e.g. ``restriction in q1``.
         :param item: any restriction
-        (item in query_expression) is equivalent to bool(query_expression & item) but may be executed more efficiently.
+        (item in query_expression) is equivalent to bool(query_expression & item) but may be
+        executed more efficiently.
         """
         return bool(self & item)  # May be optimized e.g. using an EXISTS query
 
     def __iter__(self):
+        """
+        returns an iterator-compatible QueryExpression object e.g. ``iter(q1)``.
+
+        :param self: iterator-compatible QueryExpression object
+        """
         self._iter_only_key = all(v.in_key for v in self.heading.attributes.values())
         self._iter_keys = self.fetch('KEY')
         return self
 
     def __next__(self):
+        """
+        returns the next record on an iterator-compatible QueryExpression object
+            e.g. ``next(q1)``.
+
+        :param self: A query expression
+        :type self: :class:`QueryExpression`
+        :rtype: dict
+        """
         try:
             key = self._iter_keys.pop(0)
         except AttributeError:
             # self._iter_keys is missing because __iter__ has not been called.
-            raise TypeError("'QueryExpression' object is not an iterator. Use iter(obj) to create an iterator.")
+            raise TypeError("A QueryExpression object is not an iterator. "
+                            "Use iter(obj) to create an iterator.")
         except IndexError:
             raise StopIteration
         else:
@@ -556,7 +496,8 @@ def __next__(self):
                 try:
                     return (self & key).fetch1()
                 except DataJointError:
-                    # The data may have been deleted since the moment the keys were fetched -- move on to next entry.
+                    # The data may have been deleted since the moment the keys were fetched
+                    # -- move on to next entry.
                     return next(self)
 
     def cursor(self, offset=0, limit=None, order_by=None, as_dict=False):
@@ -574,282 +515,140 @@ def cursor(self, offset=0, limit=None, order_by=None, as_dict=False):
         logger.debug(sql)
         return self.connection.query(sql, as_dict=as_dict)
 
+    def __repr__(self):
+        """
+        returns the string representation of a QueryExpression object e.g. ``str(q1)``.
+
+        :param self: A query expression
+        :type self: :class:`QueryExpression`
+        :rtype: str
+        """
+        return super().__repr__() if config['loglevel'].lower() == 'debug' else self.preview()
 
-class Not:
-    """
-    invert restriction
-    """
-    def __init__(self, restriction):
-        self.restriction = restriction
+    def preview(self, limit=None, width=None):
+        """ :return: a string of preview of the contents of the query. """
+        return preview(self, limit, width)
 
+    def _repr_html_(self):
+        """ :return: HTML to display table in Jupyter notebook. """
+        return repr_html(self)
 
-class Join(QueryExpression):
+
+class Aggregation(QueryExpression):
     """
-    Join operator.
-    Join is a private DataJoint class not exposed to users.  See QueryExpression.__mul__ for details.
+    Aggregation.create(arg, group, comp1='calc1', ..., compn='calcn')  yields an entity set
+    with primary key from arg.
+    The computed arguments comp1, ..., compn use aggregation calculations on the attributes of
+    group or simple projections and calculations on the attributes of arg.
+    Aggregation is used QueryExpression.aggr and U.aggr.
+    Aggregation is a private class in DataJoint, not exposed to users.
     """
-
-    def __init__(self, arg=None):
-        super().__init__(arg)
-        if arg is not None:
-            assert isinstance(arg, Join), "Join copy constructor requires a Join object"
-            self._connection = arg.connection
-            self._heading = arg.heading
-            self._arg1 = arg._arg1
-            self._arg2 = arg._arg2
-            self._left = arg._left
+    _left_restrict = None   # the pre-GROUP BY conditions for the WHERE clause
+    _subquery_alias_count = count()
 
     @classmethod
-    def create(cls, arg1, arg2, keep_all_rows=False):
-        obj = cls()
-        if inspect.isclass(arg2) and issubclass(arg2, QueryExpression):
-            arg2 = arg2()   # instantiate if joining with a class
-        assert_join_compatibility(arg1, arg2)
-        if arg1.connection != arg2.connection:
-            raise DataJointError("Cannot join query expressions from different connections.")
-        obj._connection = arg1.connection
-        obj._arg1 = cls.make_argument_subquery(arg1)
-        obj._arg2 = cls.make_argument_subquery(arg2)
-        obj._distinct = obj._arg1.distinct or obj._arg2.distinct
-        obj._left = keep_all_rows
-        obj._heading = obj._arg1.heading.join(obj._arg2.heading)
-        obj.restrict(obj._arg1.restriction)
-        obj.restrict(obj._arg2.restriction)
-        return obj
+    def create(cls, arg, group, keep_all_rows=False):
+        if inspect.isclass(group) and issubclass(group, QueryExpression):
+            group = group()   # instantiate if a class
+        assert isinstance(group, QueryExpression)
+        if keep_all_rows and len(group.support) > 1:
+            group = group.make_subquery()  # subquery if left joining a join
+        join = arg.join(group, left=keep_all_rows)  # reuse the join logic
+        result = cls()
+        result._connection = join.connection
+        result._heading = join.heading.set_primary_key(arg.primary_key)  # use left operand's primary key
+        result._support = join.support
+        result._left = join._left
+        result._left_restrict = join.restriction  # WHERE clause applied before GROUP BY
+        result._grouping_attributes = result.primary_key
+
+        return result
 
-    @staticmethod
-    def make_argument_subquery(arg):
-        """
-        Decide when a Join argument needs to be wrapped in a subquery
-        """
-        return Subquery.create(arg) if isinstance(arg, (GroupBy, Projection)) or arg.restriction else arg
+    def where_clause(self):
+        return '' if not self._left_restrict else ' WHERE (%s)' % ')AND('.join(
+            str(s) for s in self._left_restrict)
+
+    def make_sql(self, fields=None):
+        fields = self.heading.as_sql(fields or self.heading.names)
+        assert self._grouping_attributes or not self.restriction
+        distinct = set(self.heading.names) == set(self.primary_key)
+        return 'SELECT {distinct}{fields} FROM {from_}{where}{group_by}'.format(
+            distinct="DISTINCT " if distinct else "",
+            fields=fields,
+            from_=self.from_clause(),
+            where=self.where_clause(),
+            group_by="" if not self.primary_key else (
+                " GROUP BY `%s`" % '`,`'.join(self._grouping_attributes) +
+                ("" if not self.restriction else ' HAVING (%s)' % ')AND('.join(self.restriction))))
 
-    @property
-    def from_clause(self):
-        return '{from1} NATURAL{left} JOIN {from2}'.format(
-            from1=self._arg1.from_clause,
-            left=" LEFT" if self._left else "",
-            from2=self._arg2.from_clause)
+    def __len__(self):
+        return self.connection.query(
+            'SELECT count(1) FROM ({subquery}) `${alias:x}`'.format(
+                subquery=self.make_sql(),
+                alias=next(self._subquery_alias_count))).fetchone()[0]
+
+    def __bool__(self):
+        return bool(self.connection.query(
+            'SELECT EXISTS({sql})'.format(sql=self.make_sql())))
 
 
 class Union(QueryExpression):
     """
     Union is the private DataJoint class that implements the union operator.
     """
-
-    __count = count()
-
-    def __init__(self, arg=None):
-        super().__init__(arg)
-        if arg is not None:
-            assert isinstance(arg, Union), "Union copy constructore requires a Union object"
-            self._connection = arg.connection
-            self._heading = arg.heading
-            self._arg1 = arg._arg1
-            self._arg2 = arg._arg2
-
     @classmethod
     def create(cls, arg1, arg2):
-        obj = cls()
         if inspect.isclass(arg2) and issubclass(arg2, QueryExpression):
             arg2 = arg2()  # instantiate if a class
-        if not isinstance(arg1, QueryExpression) or not isinstance(arg2, QueryExpression):
-            raise DataJointError('an QueryExpression can only be unioned with another QueryExpression')
+        if not isinstance(arg2, QueryExpression):
+            raise DataJointError(
+                "A QueryExpression can only be unioned with another QueryExpression")
         if arg1.connection != arg2.connection:
-            raise DataJointError("Cannot operate on QueryExpressions originating from different connections.")
-        if set(arg1.heading.names) != set(arg2.heading.names):
-            raise DataJointError('Union requires the same attributes in both arguments')
-        if any(not v.in_key for v in arg1.heading.attributes.values()) or \
-                all(not v.in_key for v in arg2.heading.attributes.values()):
-            raise DataJointError('Union arguments must not have any secondary attributes.')
-        obj._connection = arg1.connection
-        obj._heading = arg1.heading
-        obj._arg1 = arg1
-        obj._arg2 = arg2
-        return obj
-
-    def make_sql(self, select_fields=None):
-        return "SELECT {_fields} FROM {_from}{_where}".format(
-            _fields=self.get_select_fields(select_fields),
-            _from=self.from_clause,
-            _where=self.where_clause)
+            raise DataJointError(
+                "Cannot operate on QueryExpressions originating from different connections.")
+        if set(arg1.primary_key) != set(arg2.primary_key):
+            raise DataJointError("The operands of a union must share the same primary key.")
+        if set(arg1.heading.secondary_attributes) & set(arg2.heading.secondary_attributes):
+            raise DataJointError(
+                "The operands of a union must not share any secondary attributes.")
+        result = cls()
+        result._connection = arg1.connection
+        result._heading = arg1.heading.join(arg2.heading)
+        result._support = [arg1, arg2]
+        return result
+
+    def make_sql(self):
+        arg1, arg2 = self._support
+        if not arg1.heading.secondary_attributes and not arg2.heading.secondary_attributes:
+            # no secondary attributes: use UNION DISTINCT
+            fields = arg1.primary_key
+            return "({sql1}) UNION ({sql2})".format(
+                sql1=arg1.make_sql(fields),
+                sql2=arg2.make_sql(fields))
+        # with secondary attributes, use union of left join with antijoin
+        fields = self.heading.names
+        sql1 = arg1.join(arg2, left=True).make_sql(fields)
+        sql2 = (arg2 - arg1).proj(
+            ..., **{k: 'NULL' for k in arg1.heading.secondary_attributes}).make_sql(fields)
+        return "({sql1})  UNION ({sql2})".format(sql1=sql1, sql2=sql2)
 
-    @property
     def from_clause(self):
-        return ("(SELECT {fields} FROM {from1}{where1} UNION SELECT {fields} FROM {from2}{where2}) as `_u%x`".format(
-            fields=self.get_select_fields(None), from1=self._arg1.from_clause,
-            where1=self._arg1.where_clause,
-            from2=self._arg2.from_clause,
-            where2=self._arg2.where_clause)) % next(self.__count)
-
-
-class Projection(QueryExpression):
-    """
-    Projection is a private DataJoint class that implements the projection operator.
-    See QueryExpression.proj() for user interface.
-    """
-
-    def __init__(self, arg=None):
-        super().__init__(arg)
-        if arg is not None:
-            assert isinstance(arg, Projection), "Projection copy constructor requires a Projection object."
-            self._connection = arg.connection
-            self._heading = arg.heading
-            self._arg = arg._arg
-
-    @staticmethod
-    def prepare_attribute_lists(arg, attributes, named_attributes):
-        # check that all attributes are strings
-        has_ellipsis = Ellipsis in attributes
-        attributes = [a for a in attributes if a is not Ellipsis]
-        try:
-            raise DataJointError("Attribute names must be strings or ..., got %s" % next(
-                type(a) for a in attributes if not isinstance(a, str)))
-        except StopIteration:
-            pass
-        named_attributes = {k: v.strip() for k, v in named_attributes.items()}  # clean up
-        excluded_attributes = set(a.lstrip('-').strip() for a in attributes if a.startswith('-'))
-        if has_ellipsis:
-            included_already = set(named_attributes.values())
-            attributes = [a for a in arg.heading.secondary_attributes if a not in included_already]
-        # process excluded attributes
-        attributes = [a for a in attributes if a not in excluded_attributes]
-        return attributes, named_attributes
-
-    @classmethod
-    def create(cls, arg, attributes, named_attributes, include_primary_key=True):
-        """
-        :param arg: The QueryExpression to be projected
-        :param attributes:  attributes to select
-        :param named_attributes:  new attributes to create by renaming or computing
-        :param include_primary_key:  True if the primary key must be included even if it's not in attributes.
-        :return: the resulting Projection object
-        """
-        obj = cls()
-        obj._connection = arg.connection
-
-        if inspect.isclass(arg) and issubclass(arg, QueryExpression):
-            arg = arg()  # instantiate if a class
-
-        attributes, named_attributes = Projection.prepare_attribute_lists(arg, attributes, named_attributes)
-        obj._distinct = arg.distinct
-
-        if include_primary_key:  # include primary key of the QueryExpression
-            attributes = (list(a for a in arg.primary_key if a not in named_attributes.values()) +
-                          list(a for a in attributes if a not in arg.primary_key))
-        else:
-            # make distinct if the primary key is not completely selected
-            obj._distinct = obj._distinct or not set(arg.primary_key).issubset(
-                set(attributes) | set(named_attributes.values()))
-        if obj._distinct or cls._need_subquery(arg, attributes, named_attributes):
-            obj._arg = Subquery.create(arg)
-            obj._heading = obj._arg.heading.project(attributes, named_attributes)
-            if not include_primary_key:
-                obj._heading = obj._heading.extend_primary_key(attributes)
-        else:
-            obj._arg = arg
-            obj._heading = obj._arg.heading.project(attributes, named_attributes)
-            obj &= arg.restriction  # copy restriction when no subquery
-        return obj
-
-    @staticmethod
-    def _need_subquery(arg, attributes, named_attributes):
-        """
-        Decide whether the projection argument needs to be wrapped in a subquery
-        """
-        if arg.heading.expressions or arg.distinct:  # argument has any renamed (computed) attributes
-            return True
-        restricting_attributes = arg.attributes_in_restriction()
-        return (not restricting_attributes.issubset(attributes) or  # if any restricting attribute is projected out or
-                any(v.strip() in restricting_attributes for v in named_attributes.values()))  # or renamed
+        """ The union does not use a FROM clause """
+        assert False
 
-    @property
-    def from_clause(self):
-        return self._arg.from_clause
-
-
-class GroupBy(QueryExpression):
-    """
-    GroupBy(rel, comp1='expr1', ..., compn='exprn')  yields an entity set with the primary key specified by rel.heading.
-    The computed arguments comp1, ..., compn use aggregation operators on the attributes of rel.
-    GroupBy is used QueryExpression.aggr and U.aggr.
-    GroupBy is a private class in DataJoint, not exposed to users.
-    """
-
-    def __init__(self, arg=None):
-        super().__init__(arg)
-        if arg is not None:
-            # copy constructor
-            assert isinstance(arg, GroupBy), "GroupBy copy constructor requires a GroupBy object"
-            self._connection = arg.connection
-            self._heading = arg.heading
-            self._arg = arg._arg
-            self._keep_all_rows = arg._keep_all_rows
-
-    @classmethod
-    def create(cls, arg, group, attributes, named_attributes, keep_all_rows=False):
-        if inspect.isclass(group) and issubclass(group, QueryExpression):
-            group = group()   # instantiate if a class
-        attributes, named_attributes = Projection.prepare_attribute_lists(arg, attributes, named_attributes)
-        assert_join_compatibility(arg, group)
-        obj = cls()
-        obj._keep_all_rows = keep_all_rows
-        obj._arg = (Join.make_argument_subquery(group) if isinstance(arg, U)
-                    else Join.create(arg, group, keep_all_rows=keep_all_rows))
-        obj._connection = obj._arg.connection
-        # always include primary key of arg
-        attributes = (list(a for a in arg.primary_key if a not in named_attributes.values()) +
-                      list(a for a in attributes if a not in arg.primary_key))
-        obj._heading = obj._arg.heading.project(
-            attributes, named_attributes, force_primary_key=arg.primary_key)
-        return obj
-
-    def make_sql(self, select_fields=None):
-        return 'SELECT {fields} FROM {from_}{where} GROUP  BY `{group_by}`{having}'.format(
-            fields=self.get_select_fields(select_fields),
-            from_=self._arg.from_clause,
-            where=self._arg.where_clause,
-            group_by='`,`'.join(self.primary_key),
-            having=re.sub(r'^ WHERE', ' HAVING', self.where_clause))
+    def where_clause(self):
+        """ The union does not use a WHERE clause """
+        assert False
 
     def __len__(self):
-        return len(Subquery.create(self))
-
-
-class Subquery(QueryExpression):
-    """
-    A Subquery encapsulates its argument in a SELECT statement, enabling its use as a subquery.
-    The attribute list and the WHERE clause are resolved.  Thus, a subquery no longer has any renamed attributes.
-    A subquery of a subquery is a just a copy of the subquery with no change in SQL.
-    """
-    __count = count()
-
-    def __init__(self, arg=None):
-        super().__init__(arg)
-        if arg is not None:
-            # copy constructor
-            assert isinstance(arg, Subquery)
-            self._connection = arg.connection
-            self._heading = arg.heading
-            self._arg = arg._arg
-
-    @classmethod
-    def create(cls, arg):
-        """
-        construct a subquery from arg
-        """
-        obj = cls()
-        obj._connection = arg.connection
-        obj._heading = arg.heading.make_subquery_heading()
-        obj._arg = arg
-        return obj
-
-    @property
-    def from_clause(self):
-        return '(' + self._arg.make_sql() + ') as `_s%x`' % next(self.__count)
+        return self.connection.query(
+            'SELECT count(1) FROM ({subquery}) `${alias:x}`'.format(
+                subquery=self.make_sql(),
+                alias=next(QueryExpression._subquery_alias_count))).fetchone()[0]
 
-    def get_select_fields(self, select_fields=None):
-        return '*' if select_fields is None else self.heading.project(select_fields).as_sql
+    def __bool__(self):
+        return bool(self.connection.query(
+            'SELECT EXISTS({sql})'.format(sql=self.make_sql())))
 
 
 class U:
@@ -910,28 +709,41 @@ def __init__(self, *primary_key):
     def primary_key(self):
         return self._primary_key
 
-    def __and__(self, query_expression):
-        if inspect.isclass(query_expression) and issubclass(query_expression, QueryExpression):
-            query_expression = query_expression()   # instantiate if a class
-        if not isinstance(query_expression, QueryExpression):
+    def __and__(self, other):
+        if inspect.isclass(other) and issubclass(other, QueryExpression):
+            other = other()   # instantiate if a class
+        if not isinstance(other, QueryExpression):
             raise DataJointError('Set U can only be restricted with a QueryExpression.')
-        return Projection.create(query_expression, attributes=self.primary_key,
-                                 named_attributes=dict(), include_primary_key=False)
+        result = copy.copy(other)
+        result._heading = result.heading.set_primary_key(self.primary_key)
+        result = result.proj()
+        return result
 
-    def __mul__(self, query_expression):
+    def join(self, other, left=False):
         """
         Joining U with a query expression has the effect of promoting the attributes of U to the primary key of
         the other query expression.
-        :param query_expression: a query expression to join with.
+        :param other: the other query expression to join with.
+        :param left: ignored. dj.U always acts as if left=False
         :return: a copy of the other query expression with the primary key extended.
         """
-        if inspect.isclass(query_expression) and issubclass(query_expression, QueryExpression):
-            query_expression = query_expression()   # instantiate if a class
-        if not isinstance(query_expression, QueryExpression):
+        if inspect.isclass(other) and issubclass(other, QueryExpression):
+            other = other()   # instantiate if a class
+        if not isinstance(other, QueryExpression):
             raise DataJointError('Set U can only be joined with a QueryExpression.')
-        copy = query_expression.__class__(query_expression)  # invoke copy constructor
-        copy._heading = copy.heading.extend_primary_key(self.primary_key)
-        return copy
+        try:
+            raise DataJointError(
+                'Attribute `%s` not found' % next(k for k in self.primary_key if k not in other.heading.names))
+        except StopIteration:
+            pass  # all ok
+        result = copy.copy(other)
+        result._heading = result.heading.set_primary_key(
+            other.primary_key + [k for k in self.primary_key if k not in other.primary_key])
+        return result
+
+    def __mul__(self, other):
+        """ shorthand for join """
+        return self.join(other)
 
     def aggr(self, group, **named_attributes):
         """
@@ -941,9 +753,9 @@ def aggr(self, group, **named_attributes):
         :param named_attributes: computations of the form new_attribute="sql expression on attributes of group"
         :return: The derived query expression
         """
-        if self.primary_key:
-            return GroupBy.create(
-                self, group=group, keep_all_rows=False, attributes=(), named_attributes=named_attributes)
-        return Projection.create(group, attributes=(), named_attributes=named_attributes, include_primary_key=False)
+        if named_attributes.get('keep_all_rows', False):
+            raise DataJointError(
+                'Cannot set keep_all_rows=True when aggregating on a universal set.')
+        return Aggregation.create(self, group=group,  keep_all_rows=False).proj(**named_attributes)
 
     aggregate = aggr  # alias for aggr
diff --git a/datajoint/external.py b/datajoint/external.py
index 5e4c7ff0c..c35a69b91 100644
--- a/datajoint/external.py
+++ b/datajoint/external.py
@@ -4,7 +4,8 @@
 from .settings import config
 from .errors import DataJointError, MissingExternalFile
 from .hash import uuid_from_buffer, uuid_from_file
-from .table import Table
+from .table import Table, FreeTable
+from .heading import Heading
 from .declare import EXTERNAL_TABLE_ROOT
 from . import s3
 from .utils import safe_write, safe_copy
@@ -25,24 +26,18 @@ class ExternalTable(Table):
     The table tracking externally stored objects.
     Declare as ExternalTable(connection, database)
     """
-    def __init__(self, connection, store=None, database=None):
-
-        # copy constructor -- all QueryExpressions must provide
-        if isinstance(connection, ExternalTable):
-            other = connection   # the first argument is interpreted as the other object
-            super().__init__(other)
-            self.store = other.store
-            self.spec = other.spec
-            self.database = other.database
-            self._connection = other._connection
-            return
-
-        # nominal constructor
-        super().__init__()
+    def __init__(self, connection, store, database):
         self.store = store
         self.spec = config.get_store_spec(store)
+        self._s3 = None
         self.database = database
         self._connection = connection
+        self._heading = Heading(table_info=dict(
+            conn=connection,
+            database=database,
+            table_name=self.table_name,
+            context=None))
+        self._support = [self.full_table_name]
         if not self.is_declared:
             self.declare()
         self._s3 = None
@@ -230,7 +225,7 @@ def upload_filepath(self, local_filepath):
             relative_filepath = str(local_filepath.relative_to(self.spec['stage']).as_posix())
         except ValueError:
             raise DataJointError('The path {path} is not in stage {stage}'.format(
-                path=local_filepath.parent, **self.spec)) from None
+                path=local_filepath.parent, **self.spec))
         uuid = uuid_from_buffer(init_string=relative_filepath)  # hash relative path, not contents
         contents_hash = uuid_from_file(local_filepath)
 
@@ -308,7 +303,7 @@ def unused(self):
         query expression for unused hashes
         :return: self restricted to elements that are not in use by any tables in the schema
         """
-        return self - ["hash IN (SELECT `{column_name}` FROM {referencing_table})".format(**ref)
+        return self - [FreeTable(self.connection, ref['referencing_table']).proj(hash=ref['column_name'])
                        for ref in self.references]
 
     def used(self):
@@ -316,7 +311,7 @@ def used(self):
         query expression for used hashes
         :return: self restricted to elements that in use by tables in the schema
         """
-        return self & ["hash IN (SELECT `{column_name}` FROM {referencing_table})".format(**ref)
+        return self & [FreeTable(self.connection, ref['referencing_table']).proj(hash=ref['column_name'])
                        for ref in self.references]
 
     def delete(self, *, delete_external_files=None, limit=None, display_progress=True):
diff --git a/datajoint/fetch.py b/datajoint/fetch.py
index cee7fa3fa..caf17d5ac 100644
--- a/datajoint/fetch.py
+++ b/datajoint/fetch.py
@@ -10,7 +10,7 @@
 from . import blob, hash
 from .errors import DataJointError
 from .settings import config
-from .utils import OrderedDict, safe_write
+from .utils import safe_write
 
 
 class key:
@@ -28,7 +28,7 @@ def is_key(attr):
 def to_dicts(recarray):
     """convert record array to a dictionaries"""
     for rec in recarray:
-        yield OrderedDict(zip(recarray.dtype.names, rec.tolist()))
+        yield dict(zip(recarray.dtype.names, rec.tolist()))
 
 
 def _get(connection, attr, data, squeeze, download_path):
@@ -149,49 +149,56 @@ def __call__(self, *attrs, offset=None, limit=None, order_by=None, format=None,
                 attrs = list(self._expression.primary_key) + [
                     a for a in attrs if a not in self._expression.primary_key]
         if as_dict is None:
-            as_dict = bool(attrs)  # default to True for "KEY" and False when fetching entire result
+            as_dict = bool(attrs)  # default to True for "KEY" and False otherwise
         # format should not be specified with attrs or is_dict=True
         if format is not None and (as_dict or attrs):
             raise DataJointError('Cannot specify output format when as_dict=True or '
                                  'when attributes are selected to be fetched separately.')
         if format not in {None, "array", "frame"}:
-            raise DataJointError('Fetch output format must be in {{"array", "frame"}} but "{}" was given'.format(format))
+            raise DataJointError(
+                'Fetch output format must be in '
+                '{{"array", "frame"}} but "{}" was given'.format(format))
 
         if not (attrs or as_dict) and format is None:
             format = config['fetch_format']  # default to array
             if format not in {"array", "frame"}:
-                raise DataJointError('Invalid entry "{}" in datajoint.config["fetch_format"]: use "array" or "frame"'.format(
-                    format))
+                raise DataJointError(
+                    'Invalid entry "{}" in datajoint.config["fetch_format"]: '
+                    'use "array" or "frame"'.format(format))
 
         if limit is None and offset is not None:
             warnings.warn('Offset set, but no limit. Setting limit to a large number. '
                           'Consider setting a limit explicitly.')
             limit = 8000000000  # just a very large number to effect no limit
 
-        get = partial(_get, self._expression.connection, squeeze=squeeze, download_path=download_path)
+        get = partial(_get, self._expression.connection,
+                      squeeze=squeeze, download_path=download_path)
         if attrs:  # a list of attributes provided
             attributes = [a for a in attrs if not is_key(a)]
-            ret = self._expression.proj(*attributes).fetch(
+            ret = self._expression.proj(*attributes)
+            ret = ret.fetch(
                 offset=offset, limit=limit, order_by=order_by,
                 as_dict=False, squeeze=squeeze, download_path=download_path,
-                format='array'
-            )
+                format='array')
             if attrs_as_dict:
                 ret = [{k: v for k, v in zip(ret.dtype.names, x) if k in attrs} for x in ret]
             else:
-                return_values = [
-                    list((to_dicts if as_dict else lambda x: x)(ret[self._expression.primary_key])) if is_key(attribute)
-                    else ret[attribute] for attribute in attrs]
+                return_values = [list(
+                    (to_dicts if as_dict else lambda x: x)(ret[self._expression.primary_key]))
+                                 if is_key(attribute) else ret[attribute]
+                                 for attribute in attrs]
                 ret = return_values[0] if len(attrs) == 1 else return_values
         else:  # fetch all attributes as a numpy.record_array or pandas.DataFrame
-            cur = self._expression.cursor(as_dict=as_dict, limit=limit, offset=offset, order_by=order_by)
+            cur = self._expression.cursor(
+                as_dict=as_dict, limit=limit, offset=offset, order_by=order_by)
             heading = self._expression.heading
             if as_dict:
-                ret = [OrderedDict((name, get(heading[name], d[name])) for name in heading.names) for d in cur]
+                ret = [dict((name, get(heading[name], d[name]))
+                            for name in heading.names) for d in cur]
             else:
                 ret = list(cur.fetchall())
                 record_type = (heading.as_dtype if not ret else np.dtype(
-                    [(name, type(value))   # use the first element to determine the type for blobs
+                    [(name, type(value))   # use the first element to determine blob type
                         if heading[name].is_blob and isinstance(value, numbers.Number)
                         else (name, heading.as_dtype[name])
                         for value, name in zip(ret[0], heading.as_dtype.names)]))
@@ -200,6 +207,7 @@ def __call__(self, *attrs, offset=None, limit=None, order_by=None, format=None,
                 except Exception as e:
                     raise e
                 for name in heading:
+                    # unpack blobs and externals
                     ret[name] = list(map(partial(get, heading[name]), ret[name]))
                 if format == "frame":
                     ret = pandas.DataFrame(ret).set_index(heading.primary_key)
@@ -208,15 +216,15 @@ def __call__(self, *attrs, offset=None, limit=None, order_by=None, format=None,
 
 class Fetch1:
     """
-    Fetch object for fetching exactly one row.
-    :param relation: relation the fetch object fetches data from
+    Fetch object for fetching the result of a query yielding one row.
+    :param expression: a query expression to fetch from.
     """
-    def __init__(self, relation):
-        self._expression = relation
+    def __init__(self, expression):
+        self._expression = expression
 
     def __call__(self, *attrs, squeeze=False, download_path='.'):
         """
-        Fetches the expression results from the database when the expression is known to yield only one entry.
+        Fetches the result of a query expression that yields one entry.
 
         If no attributes are specified, returns the result as a dict.
         If attributes are specified returns the corresponding results as a tuple.
@@ -225,7 +233,8 @@ def __call__(self, *attrs, squeeze=False, download_path='.'):
         d = rel.fetch1()   # as a dictionary
         a, b = rel.fetch1('a', 'b')   # as a tuple
 
-        :params *attrs: attributes to return when expanding into a tuple. If empty, the return result is a dict
+        :params *attrs: attributes to return when expanding into a tuple.
+                 If attrs is empty, the return result is a dict
         :param squeeze:  When true, remove extra dimensions from arrays in attributes
         :param download_path: for fetches that download data, e.g. attachments
         :return: the one tuple in the relation in the form of a dict
@@ -236,17 +245,20 @@ def __call__(self, *attrs, squeeze=False, download_path='.'):
             cur = self._expression.cursor(as_dict=True)
             ret = cur.fetchone()
             if not ret or cur.fetchone():
-                raise DataJointError('fetch1 should only be used for relations with exactly one tuple')
-            ret = OrderedDict((name, _get(self._expression.connection, heading[name], ret[name],
-                                          squeeze=squeeze, download_path=download_path))
-                              for name in heading.names)
+                raise DataJointError('fetch1 requires exactly one tuple in the input set.')
+            ret = dict((name, _get(self._expression.connection, heading[name], ret[name],
+                                   squeeze=squeeze, download_path=download_path))
+                       for name in heading.names)
         else:  # fetch some attributes, return as tuple
             attributes = [a for a in attrs if not is_key(a)]
-            result = self._expression.proj(*attributes).fetch(squeeze=squeeze, download_path=download_path, format="array")
+            result = self._expression.proj(*attributes).fetch(
+                squeeze=squeeze, download_path=download_path, format="array")
             if len(result) != 1:
-                raise DataJointError('fetch1 should only return one tuple. %d tuples were found' % len(result))
+                raise DataJointError(
+                    'fetch1 should only return one tuple. %d tuples found' % len(result))
             return_values = tuple(
-                next(to_dicts(result[self._expression.primary_key])) if is_key(attribute) else result[attribute][0]
+                next(to_dicts(result[self._expression.primary_key]))
+                if is_key(attribute) else result[attribute][0]
                 for attribute in attrs)
             ret = return_values[0] if len(attrs) == 1 else return_values
         return ret
diff --git a/datajoint/heading.py b/datajoint/heading.py
index ca637912f..076a2204e 100644
--- a/datajoint/heading.py
+++ b/datajoint/heading.py
@@ -5,7 +5,6 @@
 import logging
 from .errors import DataJointError, _support_filepath_types, FILEPATH_FEATURE_SWITCH
 from .declare import UUID_DATA_TYPE, SPECIAL_TYPES, TYPE_PATTERN, EXTERNAL_TYPES, NATIVE_TYPES
-from .utils import OrderedDict
 from .attribute_adapter import get_adapter, AttributeAdapter
 
 
@@ -15,7 +14,7 @@
     name=None, type='expression', in_key=False, nullable=False, default=None, comment='calculated attribute',
     autoincrement=False, numeric=None, string=None, uuid=False, is_blob=False, is_attachment=False, is_filepath=False,
     is_external=False, adapter=None,
-    store=None, unsupported=False, sql_expression=None, database=None, dtype=object)
+    store=None, unsupported=False, attribute_expression=None, database=None, dtype=object)
 
 
 class Attribute(namedtuple('_Attribute', default_attribute_properties)):
@@ -24,20 +23,16 @@ class Attribute(namedtuple('_Attribute', default_attribute_properties)):
     """
     def todict(self):
         """Convert namedtuple to dict."""
-        return OrderedDict((name, self[i]) for i, name in enumerate(self._fields))
+        return dict((name, self[i]) for i, name in enumerate(self._fields))
 
     @property
     def sql_type(self):
-        """
-        :return: datatype (as string) in database. In most cases, it is the same as self.type
-        """
+        """ :return: datatype (as string) in database. In most cases, it is the same as self.type """
         return UUID_DATA_TYPE if self.uuid else self.type
 
     @property
     def sql_comment(self):
-        """
-        :return: full comment for the SQL declaration. Includes custom type specification
-        """
+        """ :return: full comment for the SQL declaration. Includes custom type specification """
         return (':uuid:' if self.uuid else '') + self.comment
 
     @property
@@ -51,29 +46,48 @@ def sql(self):
         return '`{name}` {type} NOT NULL COMMENT "{comment}"'.format(
             name=self.name, type=self.sql_type, comment=self.sql_comment)
 
+    @property
+    def original_name(self):
+        if self.attribute_expression is None:
+            return self.name
+        assert self.attribute_expression.startswith('`')
+        return self.attribute_expression.strip('`')
+
 
 class Heading:
     """
     Local class for relations' headings.
-    Heading contains the property attributes, which is an OrderedDict in which the keys are
+    Heading contains the property attributes, which is an dict in which the keys are
     the attribute names and the values are Attributes.
     """
 
-    def __init__(self, arg=None):
+    def __init__(self, attribute_specs=None, table_info=None):
         """
-        :param arg: a list of dicts with the same keys as Attribute
+        :param attribute_specs: a list of dicts with the same keys as Attribute
+        :param table_info: a dict with information to load the heading from the database
         """
-        assert not isinstance(arg, Heading), 'Headings cannot be copied'
         self.indexes = None
-        self.table_info = None
-        self.attributes = None if arg is None else OrderedDict(
-            (q['name'], Attribute(**q)) for q in arg)
+        self.table_info = table_info
+        self._table_status = None
+        self._attributes = None if attribute_specs is None else dict(
+            (q['name'], Attribute(**q)) for q in attribute_specs)
 
     def __len__(self):
         return 0 if self.attributes is None else len(self.attributes)
 
-    def __bool__(self):
-        return self.attributes is not None
+    @property
+    def table_status(self):
+        if self.table_info is None:
+            return None
+        if self._table_status is None:
+            self._init_from_database()
+        return self._table_status
+
+    @property
+    def attributes(self):
+        if self._attributes is None:
+            self._init_from_database()   # lazy loading from database
+        return self._attributes
 
     @property
     def names(self):
@@ -96,8 +110,8 @@ def non_blobs(self):
         return [k for k, v in self.attributes.items() if not v.is_blob and not v.is_attachment and not v.is_filepath]
 
     @property
-    def expressions(self):
-        return [k for k, v in self.attributes.items() if v.sql_expression is not None]
+    def new_attributes(self):
+        return [k for k, v in self.attributes.items() if v.attribute_expression is not None]
 
     def __getitem__(self, name):
         """shortcut to the attribute"""
@@ -107,12 +121,10 @@ def __repr__(self):
         """
         :return:  heading representation in DataJoint declaration format but without foreign key expansion
         """
-        if self.attributes is None:
-            return 'heading not loaded'
         in_key = True
         ret = ''
-        if self.table_info:
-            ret += '# ' + self.table_info['comment'] + '\n'
+        if self._table_status is not None:
+            ret += '# ' + self.table_status['comment'] + '\n'
         for v in self.attributes.values():
             if in_key and not v.in_key:
                 ret += '---\n'
@@ -135,33 +147,31 @@ def as_dtype(self):
             names=self.names,
             formats=[v.dtype for v in self.attributes.values()]))
 
-    @property
-    def as_sql(self):
+    def as_sql(self, fields, include_aliases=True):
         """
-        represent heading as SQL field list
+        represent heading as the SQL SELECT clause.
         """
-        return ','.join('`%s`' % name if self.attributes[name].sql_expression is None
-                        else '%s as `%s`' % (self.attributes[name].sql_expression, name)
-                        for name in self.names)
+        return ','.join(
+            '`%s`' % name if self.attributes[name].attribute_expression is None
+            else self.attributes[name].attribute_expression + (' as `%s`' % name if include_aliases else '')
+            for name in fields)
 
     def __iter__(self):
         return iter(self.attributes)
 
-    def init_from_database(self, conn, database, table_name, context):
-        """
-        initialize heading from a database table.  The table must exist already.
-        """
+    def _init_from_database(self):
+        """ initialize heading from an existing database table. """
+        conn, database, table_name, context = (
+            self.table_info[k] for k in ('conn', 'database', 'table_name', 'context'))
         info = conn.query('SHOW TABLE STATUS FROM `{database}` WHERE name="{table_name}"'.format(
             table_name=table_name, database=database), as_dict=True).fetchone()
         if info is None:
             if table_name == '~log':
                 logger.warning('Could not create the ~log table')
                 return
-            else:
-                raise DataJointError('The table `{database}`.`{table_name}` is not defined.'.format(
-                    table_name=table_name, database=database))
-        self.table_info = {k.lower(): v for k, v in info.items()}
-
+            raise DataJointError('The table `{database}`.`{table_name}` is not defined.'.format(
+                table_name=table_name, database=database))
+        self._table_status = {k.lower(): v for k, v in info.items()}
         cur = conn.query(
             'SHOW FULL COLUMNS FROM `{table_name}` IN `{database}`'.format(
                 table_name=table_name, database=database), as_dict=True)
@@ -182,7 +192,6 @@ def init_from_database(self, conn, database, table_name, context):
         attributes = [{rename_map[k] if k in rename_map else k: v
                        for k, v in x.items() if k not in fields_to_drop}
                       for x in attributes]
-
         numeric_types = {
             ('float', False): np.float64,
             ('float', True): np.float64,
@@ -213,7 +222,7 @@ def init_from_database(self, conn, database, table_name, context):
                 string=any(TYPE_PATTERN[t].match(attr['type']) for t in ('ENUM', 'TEMPORAL', 'STRING')),
                 is_blob=bool(TYPE_PATTERN['INTERNAL_BLOB'].match(attr['type'])),
                 uuid=False, is_attachment=False, is_filepath=False, adapter=None,
-                store=None, is_external=False, sql_expression=None)
+                store=None, is_external=False, attribute_expression=None)
 
             if any(TYPE_PATTERN[t].match(attr['type']) for t in ('INTEGER', 'FLOAT')):
                 attr['type'] = re.sub(r'\(\d+\)', '', attr['type'], count=1)  # strip size off integers and floats
@@ -250,8 +259,8 @@ def init_from_database(self, conn, database, table_name, context):
                         url = "https://docs.datajoint.io/python/admin/5-blob-config.html" \
                               "#migration-between-datajoint-v0-11-and-v0-12"
                         raise DataJointError('Legacy datatype `{type}`. Migrate your external stores to '
-                                             'datajoint 0.12: {url}'.format(url=url, **attr)) from None
-                    raise DataJointError('Unknown attribute type `{type}`'.format(**attr)) from None
+                                             'datajoint 0.12: {url}'.format(url=url, **attr))
+                    raise DataJointError('Unknown attribute type `{type}`'.format(**attr))
                 if category == 'FILEPATH' and not _support_filepath_types():
                     raise DataJointError("""
                         The filepath data type is disabled until complete validation.
@@ -293,7 +302,7 @@ def init_from_database(self, conn, database, table_name, context):
                 # restore adapted type name
                 attr['type'] = adapter_name
 
-        self.attributes = OrderedDict(((q['name'], Attribute(**q)) for q in attributes))
+        self._attributes = dict(((q['name'], Attribute(**q)) for q in attributes))
 
         # Read and tabulate secondary indexes
         keys = defaultdict(dict)
@@ -309,32 +318,27 @@ def init_from_database(self, conn, database, table_name, context):
                      nullable=any(v['nullable'] for v in item.values()))
             for item in keys.values()}
 
-    def project(self, attribute_list, named_attributes=None, force_primary_key=None):
+    def select(self, select_list, rename_map=None, compute_map=None):
         """
         derive a new heading by selecting, renaming, or computing attributes.
         In relational algebra these operators are known as project, rename, and extend.
-        :param attribute_list:  the full list of existing attributes to include
-        :param force_primary_key:  attributes to force to be converted to primary
-        :param named_attributes:  dictionary of renamed attributes
+        :param select_list:  the full list of existing attributes to include
+        :param rename_map:  dictionary of renamed attributes: keys=new names, values=old names
+        :param compute_map: a direction of computed attributes
+        This low-level method performs no error checking.
         """
-        try:  # check for missing attributes
-            raise DataJointError('Attribute `%s` is not found' % next(a for a in attribute_list if a not in self.names))
-        except StopIteration:
-            if named_attributes is None:
-                named_attributes = {}
-            if force_primary_key is None:
-                force_primary_key = set()
-            rename_map = {v: k for k, v in named_attributes.items() if v in self.attributes}
-
-            # copied and renamed attributes
-            copy_attrs = (dict(self.attributes[k].todict(),
-                               in_key=self.attributes[k].in_key or k in force_primary_key,
-                               **({'name': rename_map[k], 'sql_expression': '`%s`' % k} if k in rename_map else {}))
-                          for k in self.attributes if k in rename_map or k in attribute_list)
-            compute_attrs = (dict(default_attribute_properties, name=new_name, sql_expression=expr)
-                             for new_name, expr in named_attributes.items() if expr not in rename_map)
-
-            return Heading(chain(copy_attrs, compute_attrs))
+        rename_map = rename_map or {}
+        compute_map = compute_map or {}
+        copy_attrs = list()
+        for name in self.attributes:
+            if name in select_list:
+                copy_attrs.append(self.attributes[name].todict())
+            copy_attrs.extend((
+                dict(self.attributes[old_name].todict(), name=new_name, attribute_expression='`%s`' % old_name)
+                for new_name, old_name in rename_map.items() if old_name == name))
+        compute_attrs = (dict(default_attribute_properties, name=new_name, attribute_expression=expr)
+                         for new_name, expr in compute_map.items())
+        return Heading(chain(copy_attrs, compute_attrs))
 
     def join(self, other):
         """
@@ -347,20 +351,18 @@ def join(self, other):
             [self.attributes[name].todict() for name in self.secondary_attributes if name not in other.primary_key] +
             [other.attributes[name].todict() for name in other.secondary_attributes if name not in self.primary_key])
 
-    def make_subquery_heading(self):
+    def set_primary_key(self, primary_key):
         """
-        Create a new heading with removed attribute sql_expressions.
-        Used by subqueries, which resolve the sql_expressions.
+        Create a new heading with the specified primary key.
+        This low-level method performs no error checking.
         """
-        return Heading(dict(v.todict(), sql_expression=None) for v in self.attributes.values())
+        return Heading(chain(
+            (dict(self.attributes[name].todict(), in_key=True) for name in primary_key),
+            (dict(self.attributes[name].todict(), in_key=False) for name in self.names if name not in primary_key)))
 
-    def extend_primary_key(self, new_attributes):
+    def make_subquery_heading(self):
         """
-        Create a new heading in which the primary key also includes new_attributes.
-        :param new_attributes: new attributes to be added to the primary key.
+        Create a new heading with removed attribute sql_expressions.
+        Used by subqueries, which resolve the sql_expressions.
         """
-        try:  # check for missing attributes
-            raise DataJointError('Attribute `%s` is not found' % next(a for a in new_attributes if a not in self.names))
-        except StopIteration:
-            return Heading(dict(v.todict(), in_key=v.in_key or v.name in new_attributes)
-                           for v in self.attributes.values())
+        return Heading(dict(v.todict(), attribute_expression=None) for v in self.attributes.values())
diff --git a/datajoint/jobs.py b/datajoint/jobs.py
index 2efed5afb..97433382d 100644
--- a/datajoint/jobs.py
+++ b/datajoint/jobs.py
@@ -4,6 +4,7 @@
 from .table import Table
 from .settings import config
 from .errors import DuplicateError
+from .heading import Heading
 
 ERROR_MESSAGE_LENGTH = 2047
 TRUNCATION_APPENDIX = '...truncated'
@@ -13,18 +14,17 @@ class JobTable(Table):
     """
     A base relation with no definition. Allows reserving jobs
     """
-    def __init__(self, arg, database=None):
-        if isinstance(arg, JobTable):
-            super().__init__(arg)
-            # copy constructor
-            self.database = arg.database
-            self._connection = arg._connection
-            self._definition = arg._definition
-            self._user = arg._user
-            return
-        super().__init__()
+    def __init__(self, conn, database):
         self.database = database
-        self._connection = arg
+        self._connection = conn
+        self._heading = Heading(table_info=dict(
+            conn=conn,
+            database=database,
+            table_name=self.table_name,
+            context=None
+        ))
+        self._support = [self.full_table_name]
+
         self._definition = """    # job reservation table for `{database}`
         table_name  :varchar(255)  # className of the table
         key_hash  :char(32)  # key hash
diff --git a/datajoint/plugin.py b/datajoint/plugin.py
new file mode 100644
index 000000000..c80388e09
--- /dev/null
+++ b/datajoint/plugin.py
@@ -0,0 +1,38 @@
+from .settings import config
+import pkg_resources
+from pathlib import Path
+from cryptography.exceptions import InvalidSignature
+from setuptools_certificate import hash_pkg, verify
+
+
+def _update_error_stack(plugin_name):
+    try:
+        base_name = 'datajoint'
+        base_meta = pkg_resources.get_distribution(base_name)
+        plugin_meta = pkg_resources.get_distribution(plugin_name)
+
+        data = hash_pkg(str(Path(plugin_meta.module_path, plugin_name)))
+        signature = plugin_meta.get_metadata('{}.sig'.format(plugin_name))
+        pubkey_path = str(Path(base_meta.egg_info, '{}.pub'.format(base_name)))
+        verify(pubkey_path, data, signature)
+        print('DataJoint verified plugin `{}` detected.'.format(plugin_name))
+        return True
+    except (FileNotFoundError, InvalidSignature):
+        print('Unverified plugin `{}` detected.'.format(plugin_name))
+        return False
+
+
+def _import_plugins(category):
+    return {
+            entry_point.name: dict(object=entry_point,
+                                   verified=_update_error_stack(
+                                        entry_point.module_name.split('.')[0]))
+            for entry_point
+            in pkg_resources.iter_entry_points('datajoint_plugins.{}'.format(category))
+            if 'plugin' not in config or category not in config['plugin'] or
+            entry_point.module_name.split('.')[0] in config['plugin'][category]
+        }
+
+
+connection_plugins = _import_plugins('connection')
+type_plugins = _import_plugins('datatype')
diff --git a/datajoint/preview.py b/datajoint/preview.py
new file mode 100644
index 000000000..f3daeebf5
--- /dev/null
+++ b/datajoint/preview.py
@@ -0,0 +1,113 @@
+""" methods for generating previews of query expression results in python command line and Jupyter """
+
+from .settings import config
+
+
+def preview(query_expression, limit, width):
+    heading = query_expression.heading
+    rel = query_expression.proj(*heading.non_blobs)
+    if limit is None:
+        limit = config['display.limit']
+    if width is None:
+        width = config['display.width']
+    tuples = rel.fetch(limit=limit + 1, format="array")
+    has_more = len(tuples) > limit
+    tuples = tuples[:limit]
+    columns = heading.names
+    widths = {f: min(max([len(f)] +
+                         [len(str(e)) for e in tuples[f]] if f in tuples.dtype.names else [len('=BLOB=')]) + 4, width) for f
+              in columns}
+    templates = {f: '%%-%d.%ds' % (widths[f], widths[f]) for f in columns}
+    return (
+            ' '.join([templates[f] % ('*' + f if f in rel.primary_key else f) for f in columns]) + '\n' +
+            ' '.join(['+' + '-' * (widths[column] - 2) + '+' for column in columns]) + '\n' +
+            '\n'.join(' '.join(templates[f] % (tup[f] if f in tup.dtype.names else '=BLOB=')
+                               for f in columns) for tup in tuples) +
+            ('\n   ...\n' if has_more else '\n') +
+            (' (Total: %d)\n' % len(rel) if config['display.show_tuple_count'] else ''))
+
+
+def repr_html(query_expression):
+    heading = query_expression.heading
+    rel = query_expression.proj(*heading.non_blobs)
+    info = heading.table_status
+    tuples = rel.fetch(limit=config['display.limit'] + 1, format='array')
+    has_more = len(tuples) > config['display.limit']
+    tuples = tuples[0:config['display.limit']]
+
+    css = """
+    <style type="text/css">
+        .Relation{
+            border-collapse:collapse;
+        }
+        .Relation th{
+            background: #A0A0A0; color: #ffffff; padding:4px; border:#f0e0e0 1px solid;
+            font-weight: normal; font-family: monospace; font-size: 100%;
+        }
+        .Relation td{
+            padding:4px; border:#f0e0e0 1px solid; font-size:100%;
+        }
+        .Relation tr:nth-child(odd){
+            background: #ffffff;
+        }
+        .Relation tr:nth-child(even){
+            background: #f3f1ff;
+        }
+        /* Tooltip container */
+        .djtooltip {
+        }
+        /* Tooltip text */
+        .djtooltip .djtooltiptext {
+            visibility: hidden;
+            width: 120px;
+            background-color: black;
+            color: #fff;
+            text-align: center;
+            padding: 5px 0;
+            border-radius: 6px;
+            /* Position the tooltip text - see examples below! */
+            position: absolute;
+            z-index: 1;
+        }
+        #primary {
+            font-weight: bold;
+            color: black;
+        }
+        #nonprimary {
+            font-weight: normal;
+            color: white;
+        }
+
+        /* Show the tooltip text when you mouse over the tooltip container */
+        .djtooltip:hover .djtooltiptext {
+            visibility: visible;
+        }
+    </style>
+    """
+    head_template = """<div class="djtooltip">
+                            <p id="{primary}">{column}</p>
+                            <span class="djtooltiptext">{comment}</span>
+                        </div>"""
+    return """
+    {css}
+    {title}
+        <div style="max-height:1000px;max-width:1500px;overflow:auto;">
+        <table border="1" class="Relation">
+            <thead> <tr style="text-align: right;"> <th> {head} </th> </tr> </thead>
+            <tbody> <tr> {body} </tr> </tbody>
+        </table>
+        {ellipsis}
+        {count}</div>
+        """.format(
+        css=css,
+        title="" if info is None else "<b>%s</b>" % info['comment'],
+        head='</th><th>'.join(
+            head_template.format(column=c, comment=heading.attributes[c].comment,
+                                 primary='primary' if c in query_expression.primary_key else 'nonprimary') for c in
+            heading.names),
+        ellipsis='<p>...</p>' if has_more else '',
+        body='</tr><tr>'.join(
+            ['\n'.join(['<td>%s</td>' % (tup[name] if name in tup.dtype.names else '=BLOB=')
+                        for name in heading.names])
+             for tup in tuples]),
+        count=('<p>Total: %d</p>' % len(rel)) if config['display.show_tuple_count'] else '')
diff --git a/datajoint/s3.py b/datajoint/s3.py
index 0e3efb2dd..f50e479db 100644
--- a/datajoint/s3.py
+++ b/datajoint/s3.py
@@ -21,7 +21,7 @@ def __init__(self, endpoint, bucket, access_key, secret_key, *, secure=False, **
                                   secure=secure)
         self.bucket = bucket
         if not self.client.bucket_exists(bucket):
-            raise errors.BucketInaccessible('Inaccessible s3 bucket %s' % bucket) from None
+            raise errors.BucketInaccessible('Inaccessible s3 bucket %s' % bucket)
 
     def put(self, name, buffer):
         logger.debug('put: {}:{}'.format(self.bucket, name))
@@ -39,7 +39,7 @@ def get(self, name):
             return self.client.get_object(self.bucket, str(name)).data
         except minio.error.S3Error as e:
             if e.code == 'NoSuchKey':
-                raise errors.MissingExternalFile('Missing s3 key %s' % name) from None
+                raise errors.MissingExternalFile('Missing s3 key %s' % name)
             else:
                 raise e
 
@@ -75,7 +75,7 @@ def get_size(self, name):
             return self.client.stat_object(self.bucket, str(name)).size
         except minio.error.S3Error as e:
             if e.code == 'NoSuchKey':
-                raise errors.MissingExternalFile from None
+                raise errors.MissingExternalFile
             else:
                 raise e
 
diff --git a/datajoint/schemas.py b/datajoint/schemas.py
index 046f20767..2c050a915 100644
--- a/datajoint/schemas.py
+++ b/datajoint/schemas.py
@@ -1,5 +1,4 @@
 import warnings
-import pymysql
 import logging
 import inspect
 import re
@@ -8,7 +7,7 @@
 from .connection import conn
 from .diagram import Diagram, _get_tier
 from .settings import config
-from .errors import DataJointError
+from .errors import DataJointError, AccessError
 from .jobs import JobTable
 from .external import ExternalMapping
 from .heading import Heading
@@ -42,48 +41,173 @@ class Schema:
     It also specifies the namespace `context` in which other UserTable classes are defined.
     """
 
-    def __init__(self, schema_name, context=None, *, connection=None, create_schema=True, create_tables=True):
+    def __init__(self, schema_name=None, context=None, *, connection=None, create_schema=True,
+                 create_tables=True, add_objects=None):
         """
-        Associate database schema `schema_name`. If the schema does not exist, attempt to create it on the server.
+        Associate database schema `schema_name`. If the schema does not exist, attempt to
+        create it on the server.
+
+        If the schema_name is omitted, then schema.activate(..) must be called later
+        to associate with the database.
 
         :param schema_name: the database schema to associate.
         :param context: dictionary for looking up foreign key references, leave None to use local context.
         :param connection: Connection object. Defaults to datajoint.conn().
         :param create_schema: When False, do not create the schema and raise an error if missing.
         :param create_tables: When False, do not create tables and raise errors when accessing missing tables.
+        :param add_objects: a mapping with additional objects to make available to the context in which table classes
+        are declared.
         """
-        if connection is None:
-            connection = conn()
         self._log = None
-
-        self.database = schema_name
         self.connection = connection
+        self.database = None
         self.context = context
+        self.create_schema = create_schema
         self.create_tables = create_tables
         self._jobs = None
         self.external = ExternalMapping(self)
+        self.add_objects = add_objects
+        self.declare_list = []
+        if schema_name:
+            self.activate(schema_name)
+
+    def is_activated(self):
+        return self.database is not None
 
+    def activate(self, schema_name=None, *, connection=None, create_schema=None,
+                 create_tables=None, add_objects=None):
+        """
+        Associate database schema `schema_name`. If the schema does not exist, attempt to
+        create it on the server.
+        :param schema_name: the database schema to associate.
+            schema_name=None is used to assert that the schema has already been activated.
+        :param connection: Connection object. Defaults to datajoint.conn().
+        :param create_schema: If False, do not create the schema and raise an error if missing.
+        :param create_tables: If False, do not create tables and raise errors when attempting
+            to access missing tables.
+        :param add_objects: a mapping with additional objects to make available to the context
+            in which table classes are declared.
+        """
+        if schema_name is None:
+            if self.exists:
+                return
+            raise DataJointError("Please provide a schema_name to activate the schema.")
+        if self.database is not None and self.exists:
+            if self.database == schema_name:  # already activated
+                return
+            raise DataJointError(
+                "The schema is already activated for schema {db}.".format(db=self.database))
+        if connection is not None:
+            self.connection = connection
+        if self.connection is None:
+            self.connection = conn()
+        self.database = schema_name
+        if create_schema is not None:
+            self.create_schema = create_schema
+        if create_tables is not None:
+            self.create_tables = create_tables
+        if add_objects:
+            self.add_objects = add_objects
         if not self.exists:
-            if not create_schema:
+            if not self.create_schema or not self.database:
                 raise DataJointError(
-                    "Database named `{name}` was not defined. "
+                    "Database `{name}` has not yet been declared. "
                     "Set argument create_schema=True to create it.".format(name=schema_name))
+            # create database
+            logger.info("Creating schema `{name}`.".format(name=schema_name))
+            try:
+                self.connection.query("CREATE DATABASE `{name}`".format(name=schema_name))
+            except AccessError:
+                raise DataJointError(
+                    "Schema `{name}` does not exist and could not be created. "
+                    "Check permissions.".format(name=schema_name))
             else:
-                # create database
-                logger.info("Creating schema `{name}`.".format(name=schema_name))
-                try:
-                    connection.query("CREATE DATABASE `{name}`".format(name=schema_name))
-                except pymysql.OperationalError:
-                    raise DataJointError(
-                        "Schema `{name}` does not exist and could not be created. "
-                        "Check permissions.".format(name=schema_name))
+                self.log('created')
+        self.connection.register(self)
+
+        # decorate all tables already decorated
+        for cls, context in self.declare_list:
+            if self.add_objects:
+                context = dict(context, **self.add_objects)
+            self._decorate_master(cls, context)
+
+    def _assert_exists(self, message=None):
+        if not self.exists:
+            raise DataJointError(
+                message or "Schema `{db}` has not been created.".format(db=self.database))
+
+    def __call__(self, cls, *, context=None):
+        """
+        Binds the supplied class to a schema. This is intended to be used as a decorator.
+        :param cls: class to decorate.
+        :param context: supplied when called from spawn_missing_classes
+        """
+        context = context or self.context or inspect.currentframe().f_back.f_locals
+        if issubclass(cls, Part):
+            raise DataJointError('The schema decorator should not be applied to Part relations')
+        if self.is_activated():
+            self._decorate_master(cls, context)
+        else:
+            self.declare_list.append((cls, context))
+        return cls
+
+    def _decorate_master(self, cls, context):
+        """
+        :param cls: the master class to process
+        :param context: the class' declaration context
+        """
+        self._decorate_table(cls, context=dict(context, self=cls, **{cls.__name__: cls}))
+        # Process part tables
+        for part in ordered_dir(cls):
+            if part[0].isupper():
+                part = getattr(cls, part)
+                if inspect.isclass(part) and issubclass(part, Part):
+                    part._master = cls
+                    # allow addressing master by name or keyword 'master'
+                    self._decorate_table(part, context=dict(
+                        context, master=cls, self=part, **{cls.__name__: cls}))
+
+    def _decorate_table(self, table_class, context, assert_declared=False):
+        """
+        assign schema properties to the table class and declare the table
+        """
+        table_class.database = self.database
+        table_class._connection = self.connection
+        table_class._heading = Heading(table_info=dict(
+            conn=self.connection,
+            database=self.database,
+            table_name=table_class.table_name,
+            context=context))
+        table_class._support = [table_class.full_table_name]
+        table_class.declaration_context = context
+
+        # instantiate the class, declare the table if not already
+        instance = table_class()
+        is_declared = instance.is_declared
+        if not is_declared:
+            if not self.create_tables or assert_declared:
+                raise DataJointError('Table `%s` not declared' % instance.table_name)
+            instance.declare(context)
+        is_declared = is_declared or instance.is_declared
+
+        # add table definition to the doc string
+        if isinstance(table_class.definition, str):
+            table_class.__doc__ = (table_class.__doc__ or "") + "\nTable definition:\n\n" + table_class.definition
+
+        # fill values in Lookup tables from their contents property
+        if isinstance(instance, Lookup) and hasattr(instance, 'contents') and is_declared:
+            contents = list(instance.contents)
+            if len(contents) > len(instance):
+                if instance.heading.has_autoincrement:
+                    warnings.warn(('Contents has changed but cannot be inserted because '
+                                  '{table} has autoincrement.').format(
+                        table=instance.__class__.__name__))
                 else:
-                    self.log('created')
-        self.log('connect')
-        connection.register(self)
+                    instance.insert(contents, skip_duplicates=True)
 
     @property
     def log(self):
+        self._assert_exists()
         if self._log is None:
             self._log = Log(self.connection, self.database)
         return self._log
@@ -96,6 +220,7 @@ def size_on_disk(self):
         """
         :return: size of the entire schema in bytes
         """
+        self._assert_exists()
         return int(self.connection.query(
             """
             SELECT SUM(data_length + index_length)
@@ -108,6 +233,7 @@ def spawn_missing_classes(self, context=None):
         in the context.
         :param context: alternative context to place the missing classes into, e.g. locals()
         """
+        self._assert_exists()
         if context is None:
             if self.context is not None:
                 context = self.context
@@ -143,7 +269,7 @@ def spawn_missing_classes(self, context=None):
                 raise DataJointError('The table %s does not follow DataJoint naming conventions' % table_name)
             part_class = type(class_name, (Part,), dict(definition=...))
             part_class._master = master_class
-            self.process_table_class(part_class, context=context, assert_declared=True)
+            self._decorate_table(part_class, context=context, assert_declared=True)
             setattr(master_class, class_name, part_class)
 
     def drop(self, force=False):
@@ -151,7 +277,8 @@ def drop(self, force=False):
         Drop the associated schema if it exists
         """
         if not self.exists:
-            logger.info("Schema named `{database}` does not exist. Doing nothing.".format(database=self.database))
+            logger.info("Schema named `{database}` does not exist. Doing nothing.".format(
+                database=self.database))
         elif (not config['safemode'] or
               force or
               user_choice("Proceed to delete entire schema `%s`?" % self.database, default='no') == 'yes'):
@@ -159,73 +286,22 @@ def drop(self, force=False):
             try:
                 self.connection.query("DROP DATABASE `{database}`".format(database=self.database))
                 logger.info("Schema `{database}` was dropped successfully.".format(database=self.database))
-            except pymysql.OperationalError:
-                raise DataJointError("An attempt to drop schema `{database}` "
-                                     "has failed. Check permissions.".format(database=self.database))
+            except AccessError:
+                raise AccessError(
+                    "An attempt to drop schema `{database}` "
+                    "has failed. Check permissions.".format(database=self.database))
 
     @property
     def exists(self):
         """
         :return: true if the associated schema exists on the server
         """
-        cur = self.connection.query("SHOW DATABASES LIKE '{database}'".format(database=self.database))
-        return cur.rowcount > 0
-
-    def process_table_class(self, table_class, context, assert_declared=False):
-        """
-        assign schema properties to the relation class and declare the table
-        """
-        table_class.database = self.database
-        table_class._connection = self.connection
-        table_class._heading = Heading()
-        table_class.declaration_context = context
-
-        # instantiate the class, declare the table if not already
-        instance = table_class()
-        is_declared = instance.is_declared
-        if not is_declared:
-            if not self.create_tables or assert_declared:
-                raise DataJointError('Table `%s` not declared' % instance.table_name)
-            else:
-                instance.declare(context)
-        is_declared = is_declared or instance.is_declared
-
-        # add table definition to the doc string
-        if isinstance(table_class.definition, str):
-            table_class.__doc__ = (table_class.__doc__ or "") + "\nTable definition:\n\n" + table_class.definition
-
-        # fill values in Lookup tables from their contents property
-        if isinstance(instance, Lookup) and hasattr(instance, 'contents') and is_declared:
-            contents = list(instance.contents)
-            if len(contents) > len(instance):
-                if instance.heading.has_autoincrement:
-                    warnings.warn(
-                        'Contents has changed but cannot be inserted because {table} has autoincrement.'.format(
-                            table=instance.__class__.__name__))
-                else:
-                    instance.insert(contents, skip_duplicates=True)
-
-    def __call__(self, cls, *, context=None):
-        """
-        Binds the supplied class to a schema. This is intended to be used as a decorator.
-        :param cls: class to decorate.
-        :param context: supplied when called from spawn_missing_classes
-        """
-        context = context or self.context or inspect.currentframe().f_back.f_locals
-        if issubclass(cls, Part):
-            raise DataJointError('The schema decorator should not be applied to Part relations')
-        self.process_table_class(cls, context=dict(context, self=cls, **{cls.__name__: cls}))
-
-        # Process part relations
-        for part in ordered_dir(cls):
-            if part[0].isupper():
-                part = getattr(cls, part)
-                if inspect.isclass(part) and issubclass(part, Part):
-                    part._master = cls
-                    # allow addressing master by name or keyword 'master'
-                    self.process_table_class(part, context=dict(
-                        context, master=cls, self=part, **{cls.__name__: cls}))
-        return cls
+        if self.database is None:
+            raise DataJointError("Schema must be activated first.")
+        return bool(self.connection.query(
+            "SELECT schema_name "
+            "FROM information_schema.schemata "
+            "WHERE schema_name = '{database}'".format(database=self.database)).rowcount)
 
     @property
     def jobs(self):
@@ -233,12 +309,14 @@ def jobs(self):
         schema.jobs provides a view of the job reservation table for the schema
         :return: jobs table
         """
+        self._assert_exists()
         if self._jobs is None:
             self._jobs = JobTable(self.connection, self.database)
         return self._jobs
 
     @property
     def code(self):
+        self._assert_exists()
         return self.save()
 
     def save(self, python_filename=None):
@@ -247,6 +325,7 @@ def save(self, python_filename=None):
         This method is in preparation for a future release and is not officially supported.
         :return: a string containing the body of a complete Python module defining this schema.
         """
+        self._assert_exists()
         module_count = itertools.count()
         # add virtual modules for referenced modules with names vmod0, vmod1, ...
         module_lookup = collections.defaultdict(lambda: 'vmod' + str(next(module_count)))
@@ -261,19 +340,21 @@ def make_class_definition(table):
                 indent += '    '
             class_name = to_camel_case(class_name)
 
-            def repl(s):
+            def replace(s):
                 d, tabs = s.group(1), s.group(2)
                 return ('' if d == db else (module_lookup[d]+'.')) + '.'.join(
                     to_camel_case(tab) for tab in tabs.lstrip('__').split('__'))
 
-            return ('' if tier == 'Part' else '\n@schema\n') + \
-                '{indent}class {class_name}(dj.{tier}):\n{indent}    definition = """\n{indent}    {defi}"""'.format(
+            return ('' if tier == 'Part' else '\n@schema\n') + (
+                '{indent}class {class_name}(dj.{tier}):\n'
+                '{indent}    definition = """\n'
+                '{indent}    {defi}"""').format(
                     class_name=class_name,
                     indent=indent,
                     tier=tier,
-                    defi=re.sub(
-                        r'`([^`]+)`.`([^`]+)`', repl,
-                        FreeTable(self.connection, table).describe(printout=False).replace('\n', '\n    ' + indent)))
+                    defi=re.sub(r'`([^`]+)`.`([^`]+)`', replace,
+                                FreeTable(self.connection, table).describe(printout=False)
+                                ).replace('\n', '\n    ' + indent))
 
         diagram = Diagram(self)
         body = '\n\n'.join(make_class_definition(table) for table in diagram.topological_sort())
@@ -284,25 +365,24 @@ def repl(s):
                       for k, v in module_lookup.items()), body))
         if python_filename is None:
             return python_code
-        else:
-            with open(python_filename, 'wt') as f:
-                f.write(python_code)
+        with open(python_filename, 'wt') as f:
+            f.write(python_code)
 
     def list_tables(self):
         """
         Return a list of all tables in the schema except tables with ~ in first character such
         as ~logs and ~job
-        :return: A list of table names in their raw datajoint naming convection form
+        :return: A list of table names from the database schema.
         """
-
         return [table_name for (table_name,) in self.connection.query("""
             SELECT table_name FROM information_schema.tables
-            WHERE table_schema = %s and table_name NOT LIKE '~%%'""", args=(self.database))]
+            WHERE table_schema = %s and table_name NOT LIKE '~%%'""", args=(self.database,))]
 
 
 class VirtualModule(types.ModuleType):
     """
-    A virtual module which will contain context for schema.
+    A virtual module imitates a Python module representing a DataJoint schema from table definitions in the database.
+    It declares the schema objects and a class for each table.
     """
     def __init__(self, module_name, schema_name, *, create_schema=False,
                  create_tables=False, connection=None, add_objects=None):
@@ -318,8 +398,8 @@ def __init__(self, module_name, schema_name, *, create_schema=False,
         :return: the python module containing classes from the schema object and the table classes
         """
         super(VirtualModule, self).__init__(name=module_name)
-        _schema = Schema(schema_name, create_schema=create_schema, create_tables=create_tables,
-                         connection=connection)
+        _schema = Schema(schema_name, create_schema=create_schema,
+                         create_tables=create_tables, connection=connection)
         if add_objects:
             self.__dict__.update(add_objects)
         self.__dict__['schema'] = _schema
@@ -331,4 +411,7 @@ def list_schemas(connection=None):
     :param connection: a dj.Connection object
     :return: list of all accessible schemas on the server
     """
-    return [r[0] for r in (connection or conn()).query('SHOW SCHEMAS') if r[0] not in {'information_schema'}]
+    return [r[0] for r in (connection or conn()).query(
+        'SELECT schema_name '
+        'FROM information_schema.schemata '
+        'WHERE schema_name <> "information_schema"')]
diff --git a/datajoint/settings.py b/datajoint/settings.py
index a58ab0f15..3ebcf3ed9 100644
--- a/datajoint/settings.py
+++ b/datajoint/settings.py
@@ -5,7 +5,6 @@
 import json
 import os
 import pprint
-from collections import OrderedDict
 import logging
 import collections
 from enum import Enum
@@ -30,7 +29,7 @@
 }
 prefix_to_role = dict(zip(role_to_prefix.values(), role_to_prefix))
 
-default = OrderedDict({
+default = dict({
     'database.host': 'localhost',
     'database.password': None,
     'database.user': None,
@@ -45,7 +44,7 @@
     'display.width': 14,
     'display.show_tuple_count': True,
     'database.use_tls': None,
-    'enable_python_native_blobs': False,  # python-native/dj0 encoding support
+    'enable_python_native_blobs': True,  # python-native/dj0 encoding support
 })
 
 logger = logging.getLogger(__name__)
@@ -133,7 +132,7 @@ def get_store_spec(self, store):
         try:
             spec = self['stores'][store]
         except KeyError:
-            raise DataJointError('Storage {store} is requested but not configured'.format(store=store)) from None
+            raise DataJointError('Storage {store} is requested but not configured'.format(store=store))
 
         spec['subfolding'] = spec.get('subfolding', DEFAULT_SUBFOLDING)
         spec_keys = {  # REQUIRED in uppercase and allowed in lowercase
@@ -144,7 +143,7 @@ def get_store_spec(self, store):
             spec_keys = spec_keys[spec.get('protocol', '').lower()]
         except KeyError:
             raise DataJointError(
-                'Missing or invalid protocol in dj.config["stores"]["{store}"]'.format(store=store)) from None
+                'Missing or invalid protocol in dj.config["stores"]["{store}"]'.format(store=store))
 
         # check that all required keys are present in spec
         try:
diff --git a/datajoint/table.py b/datajoint/table.py
index 301577433..d79c07a75 100644
--- a/datajoint/table.py
+++ b/datajoint/table.py
@@ -6,18 +6,38 @@
 import pandas
 import logging
 import uuid
+import re
+import warnings
 from pathlib import Path
 from .settings import config
 from .declare import declare, alter
+from .condition import make_condition
 from .expression import QueryExpression
 from . import blob
-from .utils import user_choice, OrderedDict
+from .utils import user_choice
 from .heading import Heading
-from .errors import DuplicateError, AccessError, DataJointError, UnknownAttributeError
+from .errors import (DuplicateError, AccessError, DataJointError, UnknownAttributeError,
+                     IntegrityError)
 from .version import __version__ as version
 
 logger = logging.getLogger(__name__)
 
+foreign_key_error_regexp = re.compile(
+    r"[\w\s:]*\((?P<child>`[^`]+`.`[^`]+`), "
+    r"CONSTRAINT (?P<name>`[^`]+`) "
+    r"(FOREIGN KEY \((?P<fk_attrs>[^)]+)\) "
+    r"REFERENCES (?P<parent>`[^`]+`(\.`[^`]+`)?) \((?P<pk_attrs>[^)]+)\)[\s\w]+\))?")
+
+constraint_info_query = ' '.join("""
+    SELECT
+        COLUMN_NAME as fk_attrs,
+        CONCAT('`', REFERENCED_TABLE_SCHEMA, '`.`', REFERENCED_TABLE_NAME, '`') as parent,
+        REFERENCED_COLUMN_NAME as pk_attrs
+    FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
+    WHERE
+        CONSTRAINT_NAME = %s AND TABLE_SCHEMA = %s AND TABLE_NAME = %s;
+    """.split())
+
 
 class _RenameMap(tuple):
     """ for internal use """
@@ -31,23 +51,21 @@ class Table(QueryExpression):
     table name, database, and definition.
     A Relation implements insert and delete methods in addition to inherited relational operators.
     """
-    _heading = None
+
+    _table_name = None  # must be defined in subclass
+    _log_ = None  # placeholder for the Log table object
+
+    # These properties must be set by the schema decorator (schemas.py) at class level or by FreeTable at instance level
     database = None
-    _log_ = None
     declaration_context = None
 
-    # -------------- required by QueryExpression ----------------- #
     @property
-    def heading(self):
-        """
-        :return: table heading. If the table is not declared, attempts to declare it first.
-        """
-        if self._heading is None:
-            self._heading = Heading()  # instance-level heading
-        if not self._heading and self.connection is not None:  # lazy loading of heading
-            self._heading.init_from_database(
-                self.connection, self.database, self.table_name, self.declaration_context)
-        return self._heading
+    def table_name(self):
+        return self._table_name
+
+    @property
+    def definition(self):
+        raise NotImplementedError('Subclasses of Table must implement the `definition` property')
 
     def declare(self, context=None):
         """
@@ -76,8 +94,8 @@ def alter(self, prompt=True, context=None):
         Alter the table definition from self.definition
         """
         if self.connection.in_transaction:
-            raise DataJointError('Cannot update table declaration inside a transaction, '
-                                 'e.g. from inside a populate/make call')
+            raise DataJointError(
+                'Cannot update table declaration inside a transaction, e.g. from inside a populate/make call')
         if context is None:
             frame = inspect.currentframe().f_back
             context = dict(frame.f_globals, **frame.f_locals)
@@ -99,11 +117,11 @@ def alter(self, prompt=True, context=None):
                     # skip if no create privilege
                     pass
                 else:
+                    self.__class__._heading = Heading(table_info=self.heading.table_info)  # reset heading
                     if prompt:
                         print('Table altered')
                     self._log('Altered ' + self.full_table_name)
 
-    @property
     def from_clause(self):
         """
         :return: the FROM clause of SQL SELECT statements.
@@ -205,6 +223,40 @@ def _log(self):
     def external(self):
         return self.connection.schemas[self.database].external
 
+    def update1(self, row):
+        """
+        update1 updates one existing entry in the table.
+        Caution: Updates are not part of the DataJoint data manipulation model. For strict data integrity,
+        use delete and insert.
+        :param row: a dict containing the primary key and the attributes to update.
+        Setting an attribute value to None will reset it to the default value (if any)
+        The primary key attributes must always be provided.
+        Examples:
+        >>> table.update1({'id': 1, 'value': 3})  # update value in record with id=1
+        >>> table.update1({'id': 1, 'value': None})  # reset value to default
+        """
+        # argument validations
+        if not isinstance(row, collections.abc.Mapping):
+            raise DataJointError('The argument of update1 must be dict-like.')
+        if not set(row).issuperset(self.primary_key):
+            raise DataJointError('The argument of update1 must supply all primary key values.')
+        try:
+            raise DataJointError('Attribute `%s` not found.' % next(k for k in row if k not in self.heading.names))
+        except StopIteration:
+            pass  # ok
+        if len(self.restriction):
+            raise DataJointError('Update cannot be applied to a restricted table.')
+        key = {k: row[k] for k in self.primary_key}
+        if len(self & key) != 1:
+            raise DataJointError('Update entry must exist.')
+        # UPDATE query
+        row = [self.__make_placeholder(k, v) for k, v in row.items() if k not in self.primary_key]
+        query = "UPDATE {table} SET {assignments} WHERE {where}".format(
+            table=self.full_table_name,
+            assignments=",".join('`%s`=%s' % r[:2] for r in row),
+            where=make_condition(self, key, set()))
+        self.connection.query(query, args=list(r[2] for r in row if r[2] is not None))
+
     def insert1(self, row, **kwargs):
         """
         Insert one data record or one Mapping (like a dict).
@@ -216,7 +268,6 @@ def insert1(self, row, **kwargs):
     def insert(self, rows, replace=False, skip_duplicates=False, ignore_extra_fields=False, allow_direct_insert=None):
         """
         Insert a collection of rows.
-
         :param rows: An iterable where an element is a numpy record, a dict-like object, a pandas.DataFrame, a sequence,
             or a query expression with the same heading as table self.
         :param replace: If True, replaces the existing tuple.
@@ -224,13 +275,11 @@ def insert(self, rows, replace=False, skip_duplicates=False, ignore_extra_fields
         :param ignore_extra_fields: If False, fields that are not in the heading raise error.
         :param allow_direct_insert: applies only in auto-populated tables.
                                     If False (default), insert are allowed only from inside the make callback.
-
         Example::
         >>> relation.insert([
         >>>     dict(subject_id=7, species="mouse", date_of_birth="2014-09-01"),
         >>>     dict(subject_id=8, species="mouse", date_of_birth="2014-09-02")])
         """
-
         if isinstance(rows, pandas.DataFrame):
             # drop 'extra' synthetic index for 1-field index case -
             # frames with more advanced indices should be prepared by user.
@@ -244,7 +293,6 @@ def insert(self, rows, replace=False, skip_duplicates=False, ignore_extra_fields
                 'Inserts into an auto-populated table can only done inside its make method during a populate call.'
                 ' To override, set keyword argument allow_direct_insert=True.')
 
-        heading = self.heading
         if inspect.isclass(rows) and issubclass(rows, QueryExpression):   # instantiate if a class
             rows = rows()
         if isinstance(rows, QueryExpression):
@@ -253,235 +301,144 @@ def insert(self, rows, replace=False, skip_duplicates=False, ignore_extra_fields
                 try:
                     raise DataJointError(
                         "Attribute %s not found. To ignore extra attributes in insert, set ignore_extra_fields=True." %
-                        next(name for name in rows.heading if name not in heading))
+                        next(name for name in rows.heading if name not in self.heading))
                 except StopIteration:
                     pass
-            fields = list(name for name in rows.heading if name in heading)
+            fields = list(name for name in rows.heading if name in self.heading)
             query = '{command} INTO {table} ({fields}) {select}{duplicate}'.format(
                 command='REPLACE' if replace else 'INSERT',
                 fields='`' + '`,`'.join(fields) + '`',
                 table=self.full_table_name,
-                select=rows.make_sql(select_fields=fields),
+                select=rows.make_sql(fields),
                 duplicate=(' ON DUPLICATE KEY UPDATE `{pk}`={table}.`{pk}`'.format(
                     table=self.full_table_name, pk=self.primary_key[0])
                            if skip_duplicates else ''))
             self.connection.query(query)
             return
 
-        if heading.attributes is None:
-            logger.warning('Could not access table {table}'.format(table=self.full_table_name))
-            return
-
-        field_list = None  # ensures that all rows have the same attributes in the same order as the first row.
-
-        def make_row_to_insert(row):
-            """
-            :param row:  A tuple to insert
-            :return: a dict with fields 'names', 'placeholders', 'values'
-            """
-            def make_placeholder(name, value):
-                """
-                For a given attribute `name` with `value`, return its processed value or value placeholder
-                as a string to be included in the query and the value, if any, to be submitted for
-                processing by mysql API.
-                :param name:  name of attribute to be inserted
-                :param value: value of attribute to be inserted
-                """
-                if ignore_extra_fields and name not in heading:
-                    return None
-                attr = heading[name]
-                if attr.adapter:
-                    value = attr.adapter.put(value)
-                if value is None or (attr.numeric and (value == '' or np.isnan(np.float(value)))):
-                    # set default value
-                    placeholder, value = 'DEFAULT', None
-                else:  # not NULL
-                    placeholder = '%s'
-                    if attr.uuid:
-                        if not isinstance(value, uuid.UUID):
-                            try:
-                                value = uuid.UUID(value)
-                            except (AttributeError, ValueError):
-                                raise DataJointError(
-                                    'badly formed UUID value {v} for attribute `{n}`'.format(v=value, n=name)) from None
-                        value = value.bytes
-                    elif attr.is_blob:
-                        value = blob.pack(value)
-                        value = self.external[attr.store].put(value).bytes if attr.is_external else value
-                    elif attr.is_attachment:
-                        attachment_path = Path(value)
-                        if attr.is_external:
-                            # value is hash of contents
-                            value = self.external[attr.store].upload_attachment(attachment_path).bytes
-                        else:
-                            # value is filename + contents
-                            value = str.encode(attachment_path.name) + b'\0' + attachment_path.read_bytes()
-                    elif attr.is_filepath:
-                        value = self.external[attr.store].upload_filepath(value).bytes
-                    elif attr.numeric:
-                        value = str(int(value) if isinstance(value, bool) else value)
-                return name, placeholder, value
-
-            def check_fields(fields):
-                """
-                Validates that all items in `fields` are valid attributes in the heading
-                :param fields: field names of a tuple
-                """
-                if field_list is None:
-                    if not ignore_extra_fields:
-                        for field in fields:
-                            if field not in heading:
-                                raise KeyError(u'`{0:s}` is not in the table heading'.format(field))
-                elif set(field_list) != set(fields).intersection(heading.names):
-                    raise DataJointError('Attempt to insert rows with different fields')
-
-            if isinstance(row, np.void):  # np.array
-                check_fields(row.dtype.fields)
-                attributes = [make_placeholder(name, row[name])
-                              for name in heading if name in row.dtype.fields]
-            elif isinstance(row, collections.abc.Mapping):  # dict-based
-                check_fields(row)
-                attributes = [make_placeholder(name, row[name]) for name in heading if name in row]
-            else:  # positional
-                try:
-                    if len(row) != len(heading):
-                        raise DataJointError(
-                            'Invalid insert argument. Incorrect number of attributes: '
-                            '{given} given; {expected} expected'.format(
-                                given=len(row), expected=len(heading)))
-                except TypeError:
-                    raise DataJointError('Datatype %s cannot be inserted' % type(row))
-                else:
-                    attributes = [make_placeholder(name, value) for name, value in zip(heading, row)]
-            if ignore_extra_fields:
-                attributes = [a for a in attributes if a is not None]
-
-            assert len(attributes), 'Empty tuple'
-            row_to_insert = dict(zip(('names', 'placeholders', 'values'), zip(*attributes)))
-            nonlocal field_list
-            if field_list is None:
-                # first row sets the composition of the field list
-                field_list = row_to_insert['names']
-            else:
-                #  reorder attributes in row_to_insert to match field_list
-                order = list(row_to_insert['names'].index(field) for field in field_list)
-                row_to_insert['names'] = list(row_to_insert['names'][i] for i in order)
-                row_to_insert['placeholders'] = list(row_to_insert['placeholders'][i] for i in order)
-                row_to_insert['values'] = list(row_to_insert['values'][i] for i in order)
-
-            return row_to_insert
-
-        rows = list(make_row_to_insert(row) for row in rows)
+        field_list = []  # collects the field list from first row (passed by reference)
+        rows = list(self.__make_row_to_insert(row, field_list, ignore_extra_fields) for row in rows)
         if rows:
             try:
                 query = "{command} INTO {destination}(`{fields}`) VALUES {placeholders}{duplicate}".format(
                     command='REPLACE' if replace else 'INSERT',
-                    destination=self.from_clause,
+                    destination=self.from_clause(),
                     fields='`,`'.join(field_list),
                     placeholders=','.join('(' + ','.join(row['placeholders']) + ')' for row in rows),
                     duplicate=(' ON DUPLICATE KEY UPDATE `{pk}`=`{pk}`'.format(pk=self.primary_key[0])
                                if skip_duplicates else ''))
                 self.connection.query(query, args=list(
-                    itertools.chain.from_iterable((v for v in r['values'] if v is not None) for r in rows)))
+                    itertools.chain.from_iterable(
+                        (v for v in r['values'] if v is not None) for r in rows)))
             except UnknownAttributeError as err:
-                raise err.suggest('To ignore extra fields in insert, set ignore_extra_fields=True') from None
+                raise err.suggest(
+                    'To ignore extra fields in insert, set ignore_extra_fields=True')
             except DuplicateError as err:
-                raise err.suggest('To ignore duplicate entries in insert, set skip_duplicates=True') from None
+                raise err.suggest(
+                    'To ignore duplicate entries in insert, set skip_duplicates=True')
 
     def delete_quick(self, get_count=False):
         """
         Deletes the table without cascading and without user prompt.
         If this table has populated dependent tables, this will fail.
         """
-        query = 'DELETE FROM ' + self.full_table_name + self.where_clause
+        query = 'DELETE FROM ' + self.full_table_name + self.where_clause()
         self.connection.query(query)
         count = self.connection.query("SELECT ROW_COUNT()").fetchone()[0] if get_count else None
         self._log(query[:255])
         return count
 
-    def delete(self, verbose=True):
+    def _delete_cascade(self):
+        """service function to perform cascading deletes recursively."""
+        max_attempts = 50
+        delete_count = 0
+        for _ in range(max_attempts):
+            try:
+                delete_count += self.delete_quick(get_count=True)
+            except IntegrityError as error:
+                match = foreign_key_error_regexp.match(error.args[0]).groupdict()
+                if "`.`" not in match['child']:  # if schema name missing, use self
+                    match['child'] = '{}.{}'.format(self.full_table_name.split(".")[0],
+                                                    match['child'])
+                if match['pk_attrs'] is not None:  # fully matched, adjusting the keys
+                    match['fk_attrs'] = [k.strip('`') for k in match['fk_attrs'].split(',')]
+                    match['pk_attrs'] = [k.strip('`') for k in match['pk_attrs'].split(',')]
+                else:  # only partially matched, querying with constraint to determine keys
+                    match['fk_attrs'], match['parent'], match['pk_attrs'] = list(map(
+                        list, zip(*self.connection.query(constraint_info_query, args=(
+                            match['name'].strip('`'),
+                            *[_.strip('`') for _ in match['child'].split('`.`')]
+                            )).fetchall())))
+                    match['parent'] = match['parent'][0]
+                # restrict child by self if
+                # 1. if self's restriction attributes are not in child's primary key
+                # 2. if child renames any attributes
+                # otherwise restrict child by self's restriction.
+                child = FreeTable(self.connection, match['child'])
+                if set(self.restriction_attributes) <= set(child.primary_key) and \
+                        match['fk_attrs'] == match['pk_attrs']:
+                    child._restriction = self._restriction
+                elif match['fk_attrs'] != match['pk_attrs']:
+                    child &= self.proj(**dict(zip(match['fk_attrs'],
+                                                  match['pk_attrs'])))
+                else:
+                    child &= self.proj()
+                delete_count += child._delete_cascade()
+            else:
+                print("Deleting {count} rows from {table}".format(
+                    count=delete_count, table=self.full_table_name))
+                break
+        else:
+            raise DataJointError('Exceeded maximum number of delete attempts.')
+        return delete_count
+
+    def delete(self, transaction=True, safemode=None):
         """
         Deletes the contents of the table and its dependent tables, recursively.
-        User is prompted for confirmation if config['safemode'] is set to True.
+
+        :param transaction: if True, use the entire delete becomes an atomic transaction.
+        :param safemode: If True, prohibit nested transactions and prompt to confirm. Default
+            is dj.config['safemode'].
         """
-        conn = self.connection
-        already_in_transaction = conn.in_transaction
-        safe = config['safemode']
-        if already_in_transaction and safe:
-            raise DataJointError('Cannot delete within a transaction in safemode. '
-                                 'Set dj.config["safemode"] = False or complete the ongoing transaction first.')
-        graph = conn.dependencies
-        graph.load()
-        delete_list = OrderedDict(
-            (name, _RenameMap(next(iter(graph.parents(name).items()))) if name.isdigit() else FreeTable(conn, name))
-            for name in graph.descendants(self.full_table_name))
-
-        # construct restrictions for each relation
-        restrict_by_me = set()
-        # restrictions: Or-Lists of restriction conditions for each table.
-        # Uncharacteristically of Or-Lists, an empty entry denotes "delete everything".
-        restrictions = collections.defaultdict(list)
-        # restrict by self
-        if self.restriction:
-            restrict_by_me.add(self.full_table_name)
-            restrictions[self.full_table_name].append(self.restriction)  # copy own restrictions
-        # restrict by renamed nodes
-        restrict_by_me.update(table for table in delete_list if table.isdigit())  # restrict by all renamed nodes
-        # restrict by secondary dependencies
-        for table in delete_list:
-            restrict_by_me.update(graph.children(table, primary=False))   # restrict by any non-primary dependents
-
-        # compile restriction lists
-        for name, table in delete_list.items():
-            for dep in graph.children(name):
-                # if restrict by me, then restrict by the entire relation otherwise copy restrictions
-                restrictions[dep].extend([table] if name in restrict_by_me else restrictions[name])
-
-        # apply restrictions
-        for name, table in delete_list.items():
-            if not name.isdigit() and restrictions[name]:  # do not restrict by an empty list
-                table.restrict([
-                    r.proj() if isinstance(r, FreeTable) else (
-                        delete_list[r[0]].proj(**{a: b for a, b in r[1]['attr_map'].items()})
-                        if isinstance(r, _RenameMap) else r)
-                    for r in restrictions[name]])
-        if safe:
-            print('About to delete:')
-
-        if not already_in_transaction:
-            conn.start_transaction()
-        total = 0
+        safemode = config['safemode'] if safemode is None else safemode
+
+        # Start transaction
+        if transaction:
+            if not self.connection.in_transaction:
+                self.connection.start_transaction()
+            else:
+                if not safemode:
+                    transaction = False
+                else:
+                    raise DataJointError(
+                        "Delete cannot use a transaction within an ongoing transaction. "
+                        "Set transaction=False or safemode=False).")
+
+        # Cascading delete
         try:
-            for name, table in reversed(list(delete_list.items())):
-                if not name.isdigit():
-                    count = table.delete_quick(get_count=True)
-                    total += count
-                    if (verbose or safe) and count:
-                        print('{table}: {count} items'.format(table=name, count=count))
+            delete_count = self._delete_cascade()
         except:
-            # Delete failed, perhaps due to insufficient privileges. Cancel transaction.
-            if not already_in_transaction:
-                conn.cancel_transaction()
+            if transaction:
+                self.connection.cancel_transaction()
             raise
+
+        # Confirm and commit
+        if delete_count == 0:
+            if safemode:
+                print('Nothing to delete.')
+            if transaction:
+                self.connection.cancel_transaction()
         else:
-            assert not (already_in_transaction and safe)
-            if not total:
-                print('Nothing to delete')
-                if not already_in_transaction:
-                    conn.cancel_transaction()
+            if not safemode or user_choice("Commit deletes?", default='no') == 'yes':
+                if transaction:
+                    self.connection.commit_transaction()
+                if safemode:
+                    print('Deletes committed.')
             else:
-                if already_in_transaction:
-                    if verbose:
-                        print('The delete is pending within the ongoing transaction.')
-                else:
-                    if not safe or user_choice("Proceed?", default='no') == 'yes':
-                        conn.commit_transaction()
-                        if verbose or safe:
-                            print('Committed.')
-                    else:
-                        conn.cancel_transaction()
-                        if verbose or safe:
-                            print('Cancelled deletes.')
+                if transaction:
+                    self.connection.cancel_transaction()
+                if safemode:
+                    print('Deletes cancelled')
 
     def drop_quick(self):
         """
@@ -542,8 +499,8 @@ def describe(self, context=None, printout=True):
             self.connection.dependencies.load()
         parents = self.parents(foreign_key_info=True)
         in_key = True
-        definition = ('# ' + self.heading.table_info['comment'] + '\n'
-                      if self.heading.table_info['comment'] else '')
+        definition = ('# ' + self.heading.table_status['comment'] + '\n'
+                      if self.heading.table_status['comment'] else '')
         attributes_thus_far = set()
         attributes_declared = set()
         indexes = self.heading.indexes.copy()
@@ -597,6 +554,8 @@ def describe(self, context=None, printout=True):
 
     def _update(self, attrname, value=None):
         """
+            This is a deprecated function to be removed in datajoint 0.14. Use .update1 instead.
+
             Updates a field in an existing tuple. This is not a datajoyous operation and should not be used
             routinely. Relational database maintain referential integrity on the level of a tuple. Therefore,
             the UPDATE operator can violate referential integrity. The datajoyous way to update information is
@@ -607,9 +566,12 @@ def _update(self, attrname, value=None):
                2. the update attribute must not be in primary key
 
             Example:
-            >>> (v2p.Mice() & key).update('mouse_dob', '2011-01-01')
-            >>> (v2p.Mice() & key).update( 'lens')   # set the value to NULL
+            >>> (v2p.Mice() & key)._update('mouse_dob', '2011-01-01')
+            >>> (v2p.Mice() & key)._update( 'lens')   # set the value to NULL
         """
+        warnings.warn(
+            '`_update` is a deprecated function to be removed in datajoint 0.14. '
+            'Use `.update1` instead.')
         if len(self) != 1:
             raise DataJointError('Update is only allowed on one tuple at a time')
         if attrname not in self.heading:
@@ -632,12 +594,113 @@ def _update(self, attrname, value=None):
         else:
             placeholder = '%s' if value is not None else 'NULL'
         command = "UPDATE {full_table_name} SET `{attrname}`={placeholder} {where_clause}".format(
-            full_table_name=self.from_clause,
+            full_table_name=self.from_clause(),
             attrname=attrname,
             placeholder=placeholder,
-            where_clause=self.where_clause)
+            where_clause=self.where_clause())
         self.connection.query(command, args=(value, ) if value is not None else ())
 
+    # --- private helper functions ----
+    def __make_placeholder(self, name, value, ignore_extra_fields=False):
+        """
+        For a given attribute `name` with `value`, return its processed value or value placeholder
+        as a string to be included in the query and the value, if any, to be submitted for
+        processing by mysql API.
+        :param name:  name of attribute to be inserted
+        :param value: value of attribute to be inserted
+        """
+        if ignore_extra_fields and name not in self.heading:
+            return None
+        attr = self.heading[name]
+        if attr.adapter:
+            value = attr.adapter.put(value)
+        if value is None or (attr.numeric and (value == '' or np.isnan(np.float(value)))):
+            # set default value
+            placeholder, value = 'DEFAULT', None
+        else:  # not NULL
+            placeholder = '%s'
+            if attr.uuid:
+                if not isinstance(value, uuid.UUID):
+                    try:
+                        value = uuid.UUID(value)
+                    except (AttributeError, ValueError):
+                        raise DataJointError(
+                            'badly formed UUID value {v} for attribute `{n}`'.format(v=value,
+                                                                                     n=name))
+                value = value.bytes
+            elif attr.is_blob:
+                value = blob.pack(value)
+                value = self.external[attr.store].put(value).bytes if attr.is_external else value
+            elif attr.is_attachment:
+                attachment_path = Path(value)
+                if attr.is_external:
+                    # value is hash of contents
+                    value = self.external[attr.store].upload_attachment(attachment_path).bytes
+                else:
+                    # value is filename + contents
+                    value = str.encode(attachment_path.name) + b'\0' + attachment_path.read_bytes()
+            elif attr.is_filepath:
+                value = self.external[attr.store].upload_filepath(value).bytes
+            elif attr.numeric:
+                value = str(int(value) if isinstance(value, bool) else value)
+        return name, placeholder, value
+
+    def __make_row_to_insert(self, row, field_list, ignore_extra_fields):
+        """
+        Helper function for insert and update
+        :param row:  A tuple to insert
+        :return: a dict with fields 'names', 'placeholders', 'values'
+        """
+
+        def check_fields(fields):
+            """
+            Validates that all items in `fields` are valid attributes in the heading
+            :param fields: field names of a tuple
+            """
+            if not field_list:
+                if not ignore_extra_fields:
+                    for field in fields:
+                        if field not in self.heading:
+                            raise KeyError(u'`{0:s}` is not in the table heading'.format(field))
+            elif set(field_list) != set(fields).intersection(self.heading.names):
+                raise DataJointError('Attempt to insert rows with different fields')
+
+        if isinstance(row, np.void):  # np.array
+            check_fields(row.dtype.fields)
+            attributes = [self.__make_placeholder(name, row[name], ignore_extra_fields)
+                          for name in self.heading if name in row.dtype.fields]
+        elif isinstance(row, collections.abc.Mapping):  # dict-based
+            check_fields(row)
+            attributes = [self.__make_placeholder(name, row[name], ignore_extra_fields)
+                          for name in self.heading if name in row]
+        else:  # positional
+            try:
+                if len(row) != len(self.heading):
+                    raise DataJointError(
+                        'Invalid insert argument. Incorrect number of attributes: '
+                        '{given} given; {expected} expected'.format(
+                            given=len(row), expected=len(self.heading)))
+            except TypeError:
+                raise DataJointError('Datatype %s cannot be inserted' % type(row))
+            else:
+                attributes = [self.__make_placeholder(name, value, ignore_extra_fields)
+                              for name, value in zip(self.heading, row)]
+        if ignore_extra_fields:
+            attributes = [a for a in attributes if a is not None]
+
+        assert len(attributes), 'Empty tuple'
+        row_to_insert = dict(zip(('names', 'placeholders', 'values'), zip(*attributes)))
+        if not field_list:
+            # first row sets the composition of the field list
+            field_list.extend(row_to_insert['names'])
+        else:
+            #  reorder attributes in row_to_insert to match field_list
+            order = list(row_to_insert['names'].index(field) for field in field_list)
+            row_to_insert['names'] = list(row_to_insert['names'][i] for i in order)
+            row_to_insert['placeholders'] = list(row_to_insert['placeholders'][i] for i in order)
+            row_to_insert['values'] = list(row_to_insert['values'][i] for i in order)
+        return row_to_insert
+
 
 def lookup_class_name(name, context, depth=3):
     """
@@ -679,29 +742,21 @@ class FreeTable(Table):
     """
     A base relation without a dedicated class. Each instance is associated with a table
     specified by full_table_name.
-    :param arg:  a dj.Connection or a dj.FreeTable
+    :param conn:  a dj.Connection object
+    :param full_table_name: in format `database`.`table_name`
     """
-
-    def __init__(self, arg, full_table_name=None):
-        super().__init__()
-        if isinstance(arg, FreeTable):
-            # copy constructor
-            self.database = arg.database
-            self._table_name = arg._table_name
-            self._connection = arg._connection
-        else:
-            self.database, self._table_name = (s.strip('`') for s in full_table_name.split('.'))
-            self._connection = arg
+    def __init__(self, conn, full_table_name):
+        self.database, self._table_name = (s.strip('`') for s in full_table_name.split('.'))
+        self._connection = conn
+        self._support = [full_table_name]
+        self._heading = Heading(table_info=dict(
+            conn=conn,
+            database=self.database,
+            table_name=self.table_name,
+            context=None))
 
     def __repr__(self):
-        return "FreeTable(`%s`.`%s`)" % (self.database, self._table_name)
-
-    @property
-    def table_name(self):
-        """
-        :return: the table name in the schema
-        """
-        return self._table_name
+        return "FreeTable(`%s`.`%s`)\n" % (self.database, self._table_name) + super().__repr__()
 
 
 class Log(Table):
@@ -711,21 +766,20 @@ class Log(Table):
     :param skip_logging: if True, then log entry is skipped by default. See __call__
     """
 
-    def __init__(self, arg, database=None, skip_logging=False):
-        super().__init__()
-
-        if isinstance(arg, Log):
-            # copy constructor
-            self.database = arg.database
-            self.skip_logging = arg.skip_logging
-            self._connection = arg._connection
-            self._definition = arg._definition
-            self._user = arg._user
-            return
+    _table_name = '~log'
 
+    def __init__(self, conn, database, skip_logging=False):
         self.database = database
         self.skip_logging = skip_logging
-        self._connection = arg
+        self._connection = conn
+        self._heading = Heading(table_info=dict(
+            conn=conn,
+            database=database,
+            table_name=self.table_name,
+            context=None
+        ))
+        self._support = [self.full_table_name]
+
         self._definition = """    # event logging table for `{database}`
         id       :int unsigned auto_increment     # event order id
         ---
@@ -736,6 +790,8 @@ def __init__(self, arg, database=None, skip_logging=False):
         event="" :varchar(255)                    # event message
         """.format(database=database)
 
+        super().__init__()
+
         if not self.is_declared:
             self.declare()
             self.connection.dependencies.clear()
@@ -745,10 +801,6 @@ def __init__(self, arg, database=None, skip_logging=False):
     def definition(self):
         return self._definition
 
-    @property
-    def table_name(self):
-        return '~log'
-
     def __call__(self, event, skip_logging=None):
         """
         :param event: string to write into the log table
diff --git a/datajoint/user_tables.py b/datajoint/user_tables.py
index cdf969902..b7bfd9db2 100644
--- a/datajoint/user_tables.py
+++ b/datajoint/user_tables.py
@@ -2,7 +2,6 @@
 Hosts the table tiers, user relations should be derived from.
 """
 
-import collections
 from .table import Table
 from .autopopulate import AutoPopulate
 from .utils import from_camel_case, ClassProperty
@@ -11,35 +10,20 @@
 _base_regexp = r'[a-z][a-z0-9]*(_[a-z][a-z0-9]*)*'
 
 # attributes that trigger instantiation of user classes
+
+
 supported_class_attrs = {
-    'key_source', 'describe', 'alter', 'heading', 'populate', 'progress', 'primary_key', 'proj', 'aggr',
-    'fetch', 'fetch1', 'head', 'tail',
-    'descendants', 'parts', 'ancestors', 'parents', 'children',
-    'insert', 'insert1', 'drop', 'drop_quick', 'delete', 'delete_quick'}
+    'key_source', 'describe', 'alter', 'heading', 'populate', 'progress', 'primary_key',
+    'proj', 'aggr', 'join', 'fetch', 'fetch1', 'head', 'tail',
+    'descendants', 'ancestors', 'parts', 'parents', 'children',
+    'insert', 'insert1', 'update1', 'drop', 'drop_quick', 'delete', 'delete_quick'}
 
 
-class OrderedClass(type):
+class TableMeta(type):
     """
-    Class whose members are ordered
-    See https://docs.python.org/3/reference/datamodel.html#metaclass-example
-
-    Note:  Since Python 3.6, _ordered_class_members will no longer be necessary (PEP 520)
-    https://www.python.org/dev/peps/pep-0520/
+    TableMeta subclasses allow applying some instance methods and properties directly
+    at class level. For example, this allows Table.fetch() instead of Table().fetch().
     """
-    @classmethod
-    def __prepare__(metacls, name, bases, **kwds):
-        return collections.OrderedDict()
-
-    def __new__(cls, name, bases, namespace, **kwds):
-        result = type.__new__(cls, name, bases, dict(namespace))
-        result._ordered_class_members = list(namespace)
-        return result
-
-    def __setattr__(cls, name, value):
-        if hasattr(cls, '_ordered_class_members'):
-            cls._ordered_class_members.append(name)
-        super().__setattr__(name, value)
-
     def __getattribute__(cls, name):
         # trigger instantiation for supported class attrs
         return (cls().__getattribute__(name) if name in supported_class_attrs
@@ -48,12 +32,21 @@ def __getattribute__(cls, name):
     def __and__(cls, arg):
         return cls() & arg
 
+    def __xor__(cls, arg):
+        return cls() ^ arg
+
     def __sub__(cls, arg):
         return cls() - arg
 
+    def __neg__(cls):
+        return -cls()
+
     def __mul__(cls, arg):
         return cls() * arg
 
+    def __matmul__(cls, arg):
+        return cls() @ arg
+
     def __add__(cls, arg):
         return cls() + arg
 
@@ -61,13 +54,17 @@ def __iter__(cls):
         return iter(cls())
 
 
-class UserTable(Table, metaclass=OrderedClass):
+class UserTable(Table, metaclass=TableMeta):
     """
     A subclass of UserTable is a dedicated class interfacing a base relation.
     UserTable is initialized by the decorator generated by schema().
     """
+    # set by @schema
     _connection = None
     _heading = None
+    _support = None
+
+    # set by subclass
     tier_regexp = None
     _prefix = None
 
@@ -76,7 +73,8 @@ def definition(self):
         """
         :return: a string containing the table definition using the DataJoint DDL.
         """
-        raise NotImplementedError('Subclasses of Table must implement the property "definition"')
+        raise NotImplementedError(
+            'Subclasses of Table must implement the property "definition"')
 
     @ClassProperty
     def connection(cls):
@@ -85,7 +83,7 @@ def connection(cls):
     @ClassProperty
     def table_name(cls):
         """
-        :returns: the table name of the table formatted for mysql.
+        :return: the table name of the table formatted for mysql.
         """
         if cls._prefix is None:
             raise AttributeError('Class prefix is not defined!')
@@ -94,8 +92,11 @@ def table_name(cls):
     @ClassProperty
     def full_table_name(cls):
         if cls not in {Manual, Imported, Lookup, Computed, Part, UserTable}:
+            # for derived classes only
             if cls.database is None:
-                raise DataJointError('Class %s is not properly declared (schema decorator not applied?)' % cls.__name__)
+                raise DataJointError(
+                    'Class %s is not properly declared (schema decorator not applied?)' %
+                    cls.__name__)
             return r"`{0:s}`.`{1:s}`".format(cls.database, cls.table_name)
 
 
@@ -144,7 +145,6 @@ class Part(UserTable):
     """
 
     _connection = None
-    _heading = None
     _master = None
 
     tier_regexp = r'(?P<master>' + '|'.join(
diff --git a/datajoint/utils.py b/datajoint/utils.py
index a20f26059..42b18222b 100644
--- a/datajoint/utils.py
+++ b/datajoint/utils.py
@@ -3,17 +3,9 @@
 import re
 from pathlib import Path
 import shutil
-import sys
 from .errors import DataJointError
 
 
-if sys.version_info[1] < 6:
-    from collections import OrderedDict
-else:
-    # use dict in Python 3.6+ -- They are already ordered and look nicer
-    OrderedDict = dict
-
-
 class ClassProperty:
     def __init__(self, f):
         self.f = f
diff --git a/datajoint/version.py b/datajoint/version.py
index bea7a65a6..a7571b6c4 100644
--- a/datajoint/version.py
+++ b/datajoint/version.py
@@ -1,3 +1,3 @@
-__version__ = "0.12.9"
+__version__ = "0.13.0"
 
 assert len(__version__) <= 10  # The log table limits version to the 10 characters
diff --git a/docs-parts/computation/01-autopopulate_lang1.rst b/docs-parts/computation/01-autopopulate_lang1.rst
index a1caecd6c..31cf621ab 100644
--- a/docs-parts/computation/01-autopopulate_lang1.rst
+++ b/docs-parts/computation/01-autopopulate_lang1.rst
@@ -11,7 +11,7 @@
         """
 
         def make(self, key):
-            img = (test.Image & key).fetch1['image']
+            img = (test.Image & key).fetch1('image')
             key['filtered_image'] = myfilter(img)
             self.insert(key)
 
diff --git a/docs-parts/computation/04-master-part_lang1.rst b/docs-parts/computation/04-master-part_lang1.rst
index d76404ff1..3bda5abb9 100644
--- a/docs-parts/computation/04-master-part_lang1.rst
+++ b/docs-parts/computation/04-master-part_lang1.rst
@@ -21,7 +21,7 @@ The part is subclassed from ``dj.Part`` and does not need the ``@schema`` decora
             """
 
         def make(self, key):
-            image = (Image & key).fetch1['image']
+            image = (Image & key).fetch1('image')
             self.insert1(key)
             count = itertools.count()
             Segmentation.ROI.insert(
diff --git a/docs-parts/computation/06-distributed-computing_jobs_by_key.rst b/docs-parts/computation/06-distributed-computing_jobs_by_key.rst
new file mode 100644
index 000000000..9862d06c7
--- /dev/null
+++ b/docs-parts/computation/06-distributed-computing_jobs_by_key.rst
@@ -0,0 +1,22 @@
+
+This can be done by using `dj.key_hash` to convert the key as follows:
+
+.. code-block:: python
+
+    In [4]: jk = {'table_name': JobResults.table_name, 'key_hash' : dj.key_hash({'id': 2})}
+    In [5]: schema.jobs & jk
+    Out[5]: 
+    *table_name    *key_hash      status     key        error_message  error_stac user           host      pid        connection_id  timestamp     
+    +------------+ +------------+ +--------+ +--------+ +------------+ +--------+ +------------+ +-------+ +--------+ +------------+ +------------+
+    __job_results  c81e728d9d4c2f error      =BLOB=     KeyboardInterr =BLOB=     datajoint@localhost  localhost     15571     59             2017-09-04 14:
+     (Total: 1)
+    
+    In [6]: (schema.jobs & jk).delete()     
+    
+    In [7]: schema.jobs & jk
+    Out[7]: 
+    *table_name    *key_hash    status     key        error_message  error_stac user     host     pid     connection_id  timestamp    
+    +------------+ +----------+ +--------+ +--------+ +------------+ +--------+ +------+ +------+ +-----+ +------------+ +-----------+
+    
+     (Total: 0)
+
diff --git a/docs-parts/computation/06-distributed-computing_kill_order_by.rst b/docs-parts/computation/06-distributed-computing_kill_order_by.rst
index 9830b70ed..585f309fc 100644
--- a/docs-parts/computation/06-distributed-computing_kill_order_by.rst
+++ b/docs-parts/computation/06-distributed-computing_kill_order_by.rst
@@ -1,7 +1,7 @@
 
 For example, to sort the output by hostname in descending order:
 
-.. code-block:: text
+.. code-block:: python
 
     In [3]: dj.kill(None, None, 'host desc')
     Out[3]:
diff --git a/docs-parts/computation/06-distributed-computing_lang2.rst b/docs-parts/computation/06-distributed-computing_lang2.rst
index d3a3338e1..2df361bf8 100644
--- a/docs-parts/computation/06-distributed-computing_lang2.rst
+++ b/docs-parts/computation/06-distributed-computing_lang2.rst
@@ -1,5 +1,5 @@
 
-.. code-block:: text
+.. code-block:: python
 
     In [1]: schema.jobs
     Out[1]:
diff --git a/docs-parts/computation/06-distributed-computing_lang3.rst b/docs-parts/computation/06-distributed-computing_lang3.rst
index f4b840866..2a26bc51c 100644
--- a/docs-parts/computation/06-distributed-computing_lang3.rst
+++ b/docs-parts/computation/06-distributed-computing_lang3.rst
@@ -1,7 +1,7 @@
 
 For example, if a Python process is interrupted via the keyboard, a KeyboardError will be logged to the database as follows:
 
-.. code-block:: text
+.. code-block:: python
 
     In [2]: schema.jobs
     Out[2]:
diff --git a/docs-parts/computation/06-distributed-computing_lang4.rst b/docs-parts/computation/06-distributed-computing_lang4.rst
index e8a98ae77..106fa51f1 100644
--- a/docs-parts/computation/06-distributed-computing_lang4.rst
+++ b/docs-parts/computation/06-distributed-computing_lang4.rst
@@ -1,7 +1,7 @@
 
 For example, given the above table, errors can be inspected as follows:
 
-.. code-block:: text
+.. code-block:: python
 
     In [3]: (schema.jobs & 'status="error"' ).fetch(as_dict=True)
     Out[3]:
diff --git a/docs-parts/computation/06-distributed-computing_lang5.rst b/docs-parts/computation/06-distributed-computing_lang5.rst
index 3f27bda08..31ffaf493 100644
--- a/docs-parts/computation/06-distributed-computing_lang5.rst
+++ b/docs-parts/computation/06-distributed-computing_lang5.rst
@@ -1,6 +1,6 @@
 
 For example:
 
-.. code-block:: text
+.. code-block:: python
 
    In [4]: (schema.jobs & 'status="error"' ).delete()
diff --git a/docs-parts/intro/Releases_lang1.rst b/docs-parts/intro/Releases_lang1.rst
index 1c3585110..3dc72f2ab 100644
--- a/docs-parts/intro/Releases_lang1.rst
+++ b/docs-parts/intro/Releases_lang1.rst
@@ -1,9 +1,27 @@
+0.13.0 -- Mar 24, 2021
+----------------------
+* Re-implement query transpilation into SQL, fixing issues (#386, #449, #450, #484). PR #754
+* Re-implement cascading deletes for better performance. PR #839.
+* Add table method `.update1` to update a row in the table with new values PR #763
+* Python datatypes are now enabled by default in blobs (#761). PR #785
+* Added permissive join and restriction operators `@` and `^` (#785) PR #754
+* Support DataJoint datatype and connection plugins (#715, #729) PR 730, #735
+* Add `dj.key_hash` alias to `dj.hash.key_hash`
+* Default enable_python_native_blobs to True
+* Bugfix - Regression error on joins with same attribute name (#857) PR #878
+* Bugfix - Error when `fetch1('KEY')` when `dj.config['fetch_format']='frame'` set (#876) PR #880, #878
+* Bugfix - Error when cascading deletes in tables with many, complex keys (#883, #886) PR #839
+* Add deprecation warning for `_update`. PR #889
+* Add `purge_query_cache` utility. PR #889
+* Add tests for query caching and permissive join and restriction. PR #889
+* Drop support for Python 3.5
+
 0.12.9 -- Mar 12, 2021
 ----------------------
 * Fix bug with fetch1 with `dj.config['fetch_format']="frame"`. Issue #876 (PR #880)
 
 0.12.8 -- Jan 12, 2021
----------------------
+----------------------
 * table.children, .parents, .descendents, and ancestors can return queryable objects. PR #833
 * Load dependencies before querying dependencies. (#179) PR #833
 * Fix display of part tables in `schema.save`. (#821) PR #833
diff --git a/docs-parts/manipulation/1-Update_lang1.rst b/docs-parts/manipulation/1-Update_lang1.rst
new file mode 100644
index 000000000..0960e2e22
--- /dev/null
+++ b/docs-parts/manipulation/1-Update_lang1.rst
@@ -0,0 +1,14 @@
+
+.. code-block:: python
+
+  # with record as a dict specifying the primary and
+  # secondary attribute values
+  table.update1(record)
+
+  # update value in record with id as primary key
+  table.update1({'id': 1, 'value': 3})
+
+  # reset value to default with id as primary key
+  table.update1({'id': 1, 'value': None})
+  ## OR
+  table.update1({'id': 1})
diff --git a/docs-parts/queries/12-Query-Caching_lang1.rst b/docs-parts/queries/12-Query-Caching_lang1.rst
new file mode 100644
index 000000000..673eef85b
--- /dev/null
+++ b/docs-parts/queries/12-Query-Caching_lang1.rst
@@ -0,0 +1,13 @@
+
+.. code-block:: python
+
+  # set the query cache path
+  dj.config['query_cache'] = os.path.expanduser('~/dj_query_cache')
+
+  # access the active connection object for the tables
+  conn = dj.conn() # if queries co-located with tables
+  conn = module.schema.connection # if schema co-located with tables
+  conn = module.table.connection # most flexible
+
+  # activate query caching for a namespace called 'main'
+  conn.set_query_cache(query_cache='main')
diff --git a/docs-parts/queries/12-Query-Caching_lang2.rst b/docs-parts/queries/12-Query-Caching_lang2.rst
new file mode 100644
index 000000000..54661c6b7
--- /dev/null
+++ b/docs-parts/queries/12-Query-Caching_lang2.rst
@@ -0,0 +1,7 @@
+
+.. code-block:: python
+
+  # deactivate query caching
+  conn.set_query_cache(query_cache=None)
+  ## OR
+  conn.set_query_cache()
diff --git a/docs-parts/queries/12-Query-Caching_lang3.rst b/docs-parts/queries/12-Query-Caching_lang3.rst
new file mode 100644
index 000000000..34e3784cd
--- /dev/null
+++ b/docs-parts/queries/12-Query-Caching_lang3.rst
@@ -0,0 +1,6 @@
+
+.. code-block:: python
+
+  # purged the cached queries
+  conn.purge_query_cache()
+
diff --git a/docs-parts/queries/13-Transpiler-Design_lang1.md b/docs-parts/queries/13-Transpiler-Design_lang1.md
new file mode 100644
index 000000000..e015646e1
--- /dev/null
+++ b/docs-parts/queries/13-Transpiler-Design_lang1.md
@@ -0,0 +1,113 @@
+MySQL appears to differ from standard SQL by the sequence of evaluating the clauses of the SELECT statement.
+
+```
+Standard SQL: FROM > WHERE > GROUP BY > HAVING > SELECT
+MySQL:		FROM > WHERE > SELECT > GROUP BY > HAVING
+```
+
+> TODO:  verify with latest SQL standards and postgres / CockroachDB implementations and whether this order can be configured
+
+Moving `SELECT` to an earlier phase allows the `GROUP BY` and `HAVING` clauses to use alias column names created by the `SELECT` clause.
+The current implementation targets the MySQL implementation where table column aliases can be used in `HAVING`.
+If postgres or CockroachDB cannot be coerced to work this way, restrictions of aggregations will have to be updated accordingly.
+
+## QueryExpression
+`QueryExpression` is the main object representing a distinct `SELECT` statement.
+It implements operators `&`, `*`, and `proj`  — restriction, join, and projection.
+
+Property `heading` describes all attributes.
+
+Operator `proj` creates a new heading.
+
+Property `restriction` contains the `AndList` of conditions. Operator `&` creates a new restriction appending the new condition to the input's restriction.
+
+Property `support` represents the `FROM` clause and contains a list of either `QueryExpression` objects or table names in the case of base queries.
+The joint operator `*` adds new elements to the `support` attribute.
+
+At least one element must be present in `support`. Multiple elements in `support` indicate a join.
+
+From the user's perspective `QueryExpression` objects are immutable: once created they cannot be modified. All operators derive new objects.
+
+### Alias attributes
+`proj` can create an alias attribute by renaming an existing attribute or calculating a new attribute.
+Alias attributes are the primary reason why subqueries are sometimes required.
+
+### Subqueries
+Projections, restrictions, and joins do not necessarily trigger new subqueries: the resulting `QueryExpression` object simply merges the properties of its inputs into self: `heading`, `restriction`, and `support`.
+
+The input object is treated as a subquery in the following cases:
+1. A restriction is applied that uses alias attributes in the heading
+1. A projection uses an alias attribute to create a new alias attribute.
+1. A join is performed on an alias attribute.
+1. An Aggregation is used a restriction. 
+
+An error arises if
+1. If a restriction or a projection attempts to use attributes not in the current heading.
+2. If attempting to join on attributes that are not join-compatible
+3. If attempting to restrict by a non-join-compatible expression
+
+A subquery is created by creating a new `QueryExpression` object (or a subclass object) with its `support` pointing to the input object.
+
+### Join compatibility
+The join is always natural (i.e. *equijoin* on the namesake attributes).
+
+**Before version 0.13:** As of version `0.12.*` and earlier, two query expressions were considered join-compatible if their namesake attributes were the primary key of at least one of the input expressions. This rule was easiest to implement but does not provide best semantics.
+
+**Version 0.13:** In version `0.13.*`, two query expressions are considered join-compatible if their namesake attributes are either in the primary key or in a foreign key in both input expressions.
+
+ **Future (potentially version 0.14+):**
+ This compatibility requirement will be further restricted to require that the namesake attributes ultimately derive from the same primary key attribute by being passed down through foreign keys.
+
+The same join compatibility rules apply when restricting one query expression with another.
+
+### Join mechanics
+Any restriction applied to the inputs of a join can be applied to its output.
+Therefore, those inputs that are not turned into queries donate their supports, restrictions, and projections to the join itself.
+
+## Table
+`Table` is a subclass of `QueryExpression` implementing table manipulation methods such as `insert`, `insert1`, `delete`, `update1`, and `drop`.
+
+The restriction operator `&` applied to a `Table` preserves its class identity so that the result remains of type `Table`.
+However, `proj` converts the result into a `QueryExpression` object. This may produce a base query that is not an instance of Table.
+
+## Aggregation
+`Aggregation` is a subclass of `QueryExpression`.
+Its main input is the *aggregating* query expression and it takes an additional second input — the *aggregated* query expression.
+
+The SQL equivalent of aggregation is
+1. the NATURAL LEFT JOIN of the two inputs.
+1. followed by a GROUP BY on the primary key arguments of the first input
+1. followed by a projection.
+
+The projection works the same as `.proj` with respect to the first input.
+With respect to the second input, the projection part of aggregation allows only calculated attributes that use aggregating functions (*eg* `SUM`, `AVG`, `COUNT`)  applied to the attributes of the aggregated (second) input and non-aggregating functions on the attribute of the aggregating (first) input.
+
+`Aggregation` supports all the same operators as `QueryExpression` except:
+1. `restriction` turns into a `HAVING` clause instead of a `WHERE` clause. This allows applying any valid restriction without making a subquery (at least for MySQL). Therefore, restricting an `Aggregation` object never results in a subquery.
+2. In joins, aggregation always turns into a subquery.
+
+All other rules for subqueries remain the same as for `QueryExpression`
+
+## Union
+`Union` is a subclass of `QueryExpression`.
+A `Union` object results from the `+` operator on two `QueryExpression` objects.
+Its `support` property contains the list of expressions (at least two) to unify.
+Thus the `+` operator on unions simply merges their supports, making a bigger union.
+
+The `Union` operator performs an OUTER JOIN of its inputs provided that the inputs have the same primary key and no secondary attributes in common.  
+
+Union treats all its inputs as subqueries except for unrestricted Union objects.
+
+## Universal Sets `dj.U`
+`dj.U` is a special operand in query expressions that allows performing special operations.  By itself, it can never form a query and is not a subclass of `QueryExpression`. Other query expressions are modified through participation in operations with `dj.U`.
+
+### Aggegating by `dj.U`
+
+### Resttricting a `dj.U` object with a `QueryExpression` object
+
+### Joining a `dj.U` object
+
+## Query "Backprojection"
+Once a QueryExpression is used in a `fetch` operation or becomes a subquery in another query, it can project out all unnecessary attributes from its own inputs, recursively.
+This is implemented by the `finalize` method.
+This simplification produces much leaner queries resulting in improved query performance in version 0.13, especially on complex queries with blob data, compensating for MySQL's deficiencies in query optimization.
diff --git a/docs-parts/version_common.json b/docs-parts/version_common.json
index c39fa12d1..2296f52e6 100644
--- a/docs-parts/version_common.json
+++ b/docs-parts/version_common.json
@@ -1,3 +1,3 @@
 {
-    "comm_version": "v0.1"
+    "comm_version": "v0.2"
 }
\ No newline at end of file
diff --git a/local-docker-compose.yml b/local-docker-compose.yml
index 28522b619..4b12e74e0 100644
--- a/local-docker-compose.yml
+++ b/local-docker-compose.yml
@@ -34,7 +34,7 @@ services:
       interval: 1s
   fakeservices.datajoint.io:
     <<: *net
-    image: raphaelguzman/nginx:v0.0.13
+    image: datajoint/nginx:v0.0.16
     environment:
     - ADD_db_TYPE=DATABASE
     - ADD_db_ENDPOINT=db:3306
@@ -77,11 +77,14 @@ services:
       - JUPYTER_PASSWORD=datajoint
       - DISPLAY
     working_dir: /src
-    command: >
-      /bin/sh -c
-       "
-        pip install --user nose nose-cov coveralls flake8 ptvsd .;
-        pip freeze | grep datajoint;
+    command:
+      - sh
+      - -c
+      - |
+        set -e
+        pip install --user nose nose-cov coveralls flake8 ptvsd
+        pip install -e .
+        pip freeze | grep datajoint
         ## You may run the below tests once sh'ed into container i.e. docker exec -it datajoint-python_app_1 sh
         # nosetests -vsw tests; #run all tests
         # nosetests -vs --tests=tests.test_external_class:test_insert_and_fetch; #run specific basic test
@@ -91,11 +94,11 @@ services:
         ## Interactive Jupyter Notebook environment
         jupyter notebook &
         ## Remote debugger
-        while true;
-        do python -m ptvsd --host 0.0.0.0 --port 5678 --wait .;
-        sleep 2;
-        done;
-       "
+        while true
+        do
+          python -m ptvsd --host 0.0.0.0 --port 5678 --wait .
+          sleep 2
+        done
     ports:
       - "8888:8888"
       - "5678:5678"
diff --git a/requirements.txt b/requirements.txt
index fa3882448..d5a2656c0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,3 +8,5 @@ networkx
 pydot
 minio>=7.0.0
 matplotlib
+cryptography
+setuptools_certificate
diff --git a/setup.py b/setup.py
index 339bd502e..5569607ad 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@
 from os import path
 import sys
 
-min_py_version = (3, 5)
+min_py_version = (3, 6)
 
 if sys.version_info <  min_py_version:
     sys.exit('DataJoint is only supported for Python {}.{} or higher'.format(*min_py_version))
@@ -31,5 +31,7 @@
     keywords='database organization',
     packages=find_packages(exclude=['contrib', 'docs', 'tests*']),
     install_requires=requirements,
-    python_requires='~={}.{}'.format(*min_py_version)
+    python_requires='~={}.{}'.format(*min_py_version),
+    setup_requires=['setuptools_certificate'], # maybe remove due to conflicts?
+    pubkey_path='./datajoint.pub'
 )


Test patch:
diff --git a/test_requirements.txt b/test_requirements.txt
index 0b6e15ef4..6f13c7c6d 100644
--- a/test_requirements.txt
+++ b/test_requirements.txt
@@ -1,3 +1,4 @@
-matplotlib
-pydotplus
-moto
+nose
+nose-cov
+coveralls
+Faker
diff --git a/tests/__init__.py b/tests/__init__.py
index 6d578f95d..1a48a3a91 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -49,44 +49,6 @@
 PREFIX = environ.get('DJ_TEST_DB_PREFIX', 'djtest')
 conn_root = dj.conn(**CONN_INFO_ROOT)
 
-if LooseVersion(conn_root.query(
-        "select @@version;").fetchone()[0]) >= LooseVersion('8.0.0'):
-    # create user if necessary on mysql8
-    conn_root.query("""
-            CREATE USER IF NOT EXISTS 'datajoint'@'%%'
-            IDENTIFIED BY 'datajoint';
-            """)
-    conn_root.query("""
-            CREATE USER IF NOT EXISTS 'djview'@'%%'
-            IDENTIFIED BY 'djview';
-            """)
-    conn_root.query("""
-            CREATE USER IF NOT EXISTS 'djssl'@'%%'
-            IDENTIFIED BY 'djssl'
-            REQUIRE SSL;
-            """)
-    conn_root.query(
-        "GRANT ALL PRIVILEGES ON `djtest%%`.* TO 'datajoint'@'%%';")
-    conn_root.query(
-        "GRANT SELECT ON `djtest%%`.* TO 'djview'@'%%';")
-    conn_root.query(
-        "GRANT SELECT ON `djtest%%`.* TO 'djssl'@'%%';")
-else:
-    # grant permissions. For mysql5.6/5.7 this also automatically creates user
-    # if not exists
-    conn_root.query("""
-        GRANT ALL PRIVILEGES ON `djtest%%`.* TO 'datajoint'@'%%'
-        IDENTIFIED BY 'datajoint';
-        """)
-    conn_root.query(
-        "GRANT SELECT ON `djtest%%`.* TO 'djview'@'%%' IDENTIFIED BY 'djview';"
-        )
-    conn_root.query("""
-        GRANT SELECT ON `djtest%%`.* TO 'djssl'@'%%'
-        IDENTIFIED BY 'djssl'
-        REQUIRE SSL;
-        """)
-
 # Initialize httpClient with relevant timeout.
 httpClient = urllib3.PoolManager(
             timeout=30,
@@ -115,6 +77,45 @@ def setup_package():
     """
     dj.config['safemode'] = False
 
+    # Create MySQL users
+    if LooseVersion(conn_root.query(
+            "select @@version;").fetchone()[0]) >= LooseVersion('8.0.0'):
+        # create user if necessary on mysql8
+        conn_root.query("""
+                CREATE USER IF NOT EXISTS 'datajoint'@'%%'
+                IDENTIFIED BY 'datajoint';
+                """)
+        conn_root.query("""
+                CREATE USER IF NOT EXISTS 'djview'@'%%'
+                IDENTIFIED BY 'djview';
+                """)
+        conn_root.query("""
+                CREATE USER IF NOT EXISTS 'djssl'@'%%'
+                IDENTIFIED BY 'djssl'
+                REQUIRE SSL;
+                """)
+        conn_root.query(
+            "GRANT ALL PRIVILEGES ON `djtest%%`.* TO 'datajoint'@'%%';")
+        conn_root.query(
+            "GRANT SELECT ON `djtest%%`.* TO 'djview'@'%%';")
+        conn_root.query(
+            "GRANT SELECT ON `djtest%%`.* TO 'djssl'@'%%';")
+    else:
+        # grant permissions. For mysql5.6/5.7 this also automatically creates user
+        # if not exists
+        conn_root.query("""
+            GRANT ALL PRIVILEGES ON `djtest%%`.* TO 'datajoint'@'%%'
+            IDENTIFIED BY 'datajoint';
+            """)
+        conn_root.query(
+            "GRANT SELECT ON `djtest%%`.* TO 'djview'@'%%' IDENTIFIED BY 'djview';"
+            )
+        conn_root.query("""
+            GRANT SELECT ON `djtest%%`.* TO 'djssl'@'%%'
+            IDENTIFIED BY 'djssl'
+            REQUIRE SSL;
+            """)
+
     # Add old MySQL
     source = Path(
         Path(__file__).resolve().parent,
@@ -171,15 +172,19 @@ def teardown_package():
     To deal with possible foreign key constraints, it will unset
     and then later reset FOREIGN_KEY_CHECKS flag
     """
-    conn = dj.conn(**CONN_INFO)
-    conn.query('SET FOREIGN_KEY_CHECKS=0')
-    cur = conn.query('SHOW DATABASES LIKE "{}\_%%"'.format(PREFIX))
+    conn_root.query('SET FOREIGN_KEY_CHECKS=0')
+    cur = conn_root.query('SHOW DATABASES LIKE "{}\_%%"'.format(PREFIX))
     for db in cur.fetchall():
-        conn.query('DROP DATABASE `{}`'.format(db[0]))
-    conn.query('SET FOREIGN_KEY_CHECKS=1')
+        conn_root.query('DROP DATABASE `{}`'.format(db[0]))
+    conn_root.query('SET FOREIGN_KEY_CHECKS=1')
     if os.path.exists("dj_local_conf.json"):
         remove("dj_local_conf.json")
 
+    # Remove created users
+    conn_root.query('DROP USER `datajoint`')
+    conn_root.query('DROP USER `djview`')
+    conn_root.query('DROP USER `djssl`')
+
     # Remove old S3
     objs = list(minioClient.list_objects(
             S3_MIGRATE_BUCKET, recursive=True))
diff --git a/tests/data/Course.csv b/tests/data/Course.csv
new file mode 100644
index 000000000..a308d8d6a
--- /dev/null
+++ b/tests/data/Course.csv
@@ -0,0 +1,46 @@
+dept,course,course_name,credits
+BIOL,1006,World of Dinosaurs,3.0
+BIOL,1010,Biology in the 21st Century,3.0
+BIOL,1030,Human Biology,3.0
+BIOL,1210,Principles of Biology,4.0
+BIOL,2010,Evolution & Diversity of Life,3.0
+BIOL,2020,Principles of Cell Biology,3.0
+BIOL,2021,Principles of Cell Science,4.0
+BIOL,2030,Principles of Genetics,3.0
+BIOL,2210,Human Genetics,3.0
+BIOL,2325,Human Anatomy,4.0
+BIOL,2330,Plants & Society,3.0
+BIOL,2355,Field Botany,2.0
+BIOL,2420,Human Physiology,4.0
+CS,1030,Foundations of Computer Science,3.0
+CS,1410,Introduction to Object-Oriented Programming,4.0
+CS,2100,Discrete Structures,3.0
+CS,2420,Introduction to Algorithms & Data Structures,4.0
+CS,3100,Models of Computation,3.0
+CS,3200,Introduction to Scientific Computing,3.0
+CS,3500,Software Practice,4.0
+CS,3505,Software Practice II,3.0
+CS,3810,Computer Organization,4.0
+CS,4000,Senior Capstone Project - Design Phase,3.0
+CS,4150,Algorithms,3.0
+CS,4400,Computer Systems,4.0
+CS,4500,Senior Capstone Project,3.0
+CS,4940,Undergraduate Research,3.0
+CS,4970,Computer Science Bachelors Thesis,3.0
+MATH,1210,Calculus I,4.0
+MATH,1220,Calculus II,4.0
+MATH,1250,Calculus for AP Students I,4.0
+MATH,1260,Calculus for AP Students II,4.0
+MATH,2210,Calculus III,3.0
+MATH,2270,Linear Algebra,4.0
+MATH,2280,Introduction to Differential Equations,4.0
+MATH,3210,Foundations of Analysis I,4.0
+MATH,3220,Foundations of Analysis II,4.0
+PHYS,2040,Classical Theoretical Physics II,4.0
+PHYS,2060,Quantum Mechanics,3.0
+PHYS,2100,General Relativity and Cosmology,3.0
+PHYS,2140,Statistical Mechanics,4.0
+PHYS,2210,Physics for Scientists and Engineers I,4.0
+PHYS,2220,Physics for Scientists and Engineers II,4.0
+PHYS,3210,Physics for Scientists I (Honors),4.0
+PHYS,3220,Physics for Scientists II (Honors),4.0
diff --git a/tests/data/CurrentTerm.csv b/tests/data/CurrentTerm.csv
new file mode 100644
index 000000000..037d9b344
--- /dev/null
+++ b/tests/data/CurrentTerm.csv
@@ -0,0 +1,2 @@
+omega,term_year,term
+1,2020,Fall
diff --git a/tests/data/Department.csv b/tests/data/Department.csv
new file mode 100644
index 000000000..5a7857eef
--- /dev/null
+++ b/tests/data/Department.csv
@@ -0,0 +1,9 @@
+dept,dept_name,dept_address,dept_phone
+BIOL,Life Sciences,"931 Eric Trail Suite 331
+Lake Scott, CT 53527",(238)497-9162x0223
+CS,Computer Science,"0104 Santos Hill Apt. 497
+Michelleland, MT 94473",3828723244
+MATH,Mathematics,"8358 Bryan Ports
+Lake Matthew, SC 36983",+1-461-767-9298x842
+PHYS,Physics,"7744 Haley Meadows Suite 661
+Lake Eddie, CT 51544",4097052774
diff --git a/tests/data/Enroll.csv b/tests/data/Enroll.csv
new file mode 100644
index 000000000..fc9a6b2a0
--- /dev/null
+++ b/tests/data/Enroll.csv
@@ -0,0 +1,3365 @@
+student_id,dept,course,term_year,term,section
+394,BIOL,1006,2015,Spring,b
+138,BIOL,1006,2015,Summer,a
+182,BIOL,1006,2015,Summer,a
+246,BIOL,1006,2015,Summer,a
+249,BIOL,1006,2015,Summer,b
+290,BIOL,1006,2015,Summer,b
+115,BIOL,1006,2016,Spring,a
+160,BIOL,1006,2016,Spring,a
+176,BIOL,1006,2016,Spring,a
+276,BIOL,1006,2016,Spring,a
+285,BIOL,1006,2016,Spring,a
+123,BIOL,1006,2016,Spring,b
+312,BIOL,1006,2016,Summer,a
+179,BIOL,1006,2016,Summer,b
+214,BIOL,1006,2016,Summer,d
+389,BIOL,1006,2016,Summer,d
+124,BIOL,1006,2017,Fall,a
+128,BIOL,1006,2017,Fall,a
+199,BIOL,1006,2017,Fall,a
+262,BIOL,1006,2017,Fall,a
+288,BIOL,1006,2017,Fall,a
+321,BIOL,1006,2017,Fall,a
+326,BIOL,1006,2017,Fall,a
+345,BIOL,1006,2017,Fall,a
+392,BIOL,1006,2017,Fall,a
+165,BIOL,1006,2017,Fall,b
+229,BIOL,1006,2017,Fall,b
+318,BIOL,1006,2017,Fall,b
+107,BIOL,1006,2018,Spring,a
+117,BIOL,1006,2018,Spring,a
+164,BIOL,1006,2018,Spring,a
+362,BIOL,1006,2018,Spring,a
+366,BIOL,1006,2018,Spring,a
+397,BIOL,1006,2018,Spring,a
+227,BIOL,1006,2018,Spring,b
+261,BIOL,1006,2018,Spring,b
+270,BIOL,1006,2018,Spring,b
+292,BIOL,1006,2018,Spring,b
+294,BIOL,1006,2018,Spring,b
+348,BIOL,1006,2018,Spring,b
+373,BIOL,1006,2018,Spring,b
+375,BIOL,1006,2018,Spring,b
+102,BIOL,1006,2018,Fall,a
+113,BIOL,1006,2018,Fall,a
+131,BIOL,1006,2018,Fall,a
+296,BIOL,1006,2018,Fall,a
+391,BIOL,1006,2018,Fall,a
+127,BIOL,1006,2019,Spring,a
+139,BIOL,1006,2019,Summer,a
+143,BIOL,1006,2019,Summer,a
+178,BIOL,1006,2019,Summer,a
+234,BIOL,1006,2019,Summer,a
+247,BIOL,1006,2019,Summer,a
+259,BIOL,1006,2019,Summer,a
+303,BIOL,1006,2019,Summer,a
+329,BIOL,1006,2019,Summer,a
+356,BIOL,1006,2019,Summer,a
+109,BIOL,1006,2019,Fall,a
+173,BIOL,1006,2019,Fall,a
+187,BIOL,1006,2019,Fall,a
+364,BIOL,1006,2019,Fall,a
+169,BIOL,1006,2019,Fall,b
+332,BIOL,1006,2019,Fall,b
+398,BIOL,1006,2019,Fall,b
+142,BIOL,1006,2020,Spring,a
+194,BIOL,1006,2020,Spring,a
+267,BIOL,1006,2020,Spring,a
+330,BIOL,1006,2020,Spring,a
+340,BIOL,1006,2020,Spring,a
+365,BIOL,1006,2020,Spring,a
+129,BIOL,1006,2020,Fall,a
+222,BIOL,1006,2020,Fall,a
+241,BIOL,1006,2020,Fall,a
+297,BIOL,1006,2020,Fall,a
+313,BIOL,1006,2020,Fall,a
+333,BIOL,1006,2020,Fall,a
+376,BIOL,1006,2020,Fall,a
+379,BIOL,1006,2020,Fall,a
+390,BIOL,1006,2020,Fall,a
+220,BIOL,1006,2020,Fall,b
+255,BIOL,1006,2020,Fall,b
+272,BIOL,1006,2020,Fall,b
+277,BIOL,1006,2020,Fall,b
+313,BIOL,1006,2020,Fall,b
+371,BIOL,1006,2020,Fall,b
+378,BIOL,1006,2020,Fall,b
+118,BIOL,1006,2020,Fall,c
+235,BIOL,1006,2020,Fall,c
+271,BIOL,1006,2020,Fall,c
+289,BIOL,1006,2020,Fall,c
+313,BIOL,1006,2020,Fall,c
+378,BIOL,1006,2020,Fall,c
+182,BIOL,1010,2015,Summer,a
+276,BIOL,1010,2015,Summer,a
+277,BIOL,1010,2015,Summer,a
+382,BIOL,1010,2015,Summer,a
+123,BIOL,1010,2015,Summer,b
+177,BIOL,1010,2015,Summer,b
+382,BIOL,1010,2015,Summer,b
+277,BIOL,1010,2015,Summer,c
+301,BIOL,1010,2015,Summer,c
+163,BIOL,1010,2015,Summer,d
+179,BIOL,1010,2015,Fall,a
+210,BIOL,1010,2015,Fall,a
+211,BIOL,1010,2015,Fall,b
+290,BIOL,1010,2015,Fall,b
+211,BIOL,1010,2015,Fall,c
+176,BIOL,1010,2016,Summer,a
+192,BIOL,1010,2016,Summer,a
+195,BIOL,1010,2016,Summer,a
+282,BIOL,1010,2016,Summer,a
+317,BIOL,1010,2016,Summer,a
+249,BIOL,1010,2017,Spring,a
+278,BIOL,1010,2017,Spring,a
+312,BIOL,1010,2017,Spring,a
+373,BIOL,1010,2017,Spring,a
+391,BIOL,1010,2017,Spring,a
+397,BIOL,1010,2017,Spring,a
+151,BIOL,1010,2017,Summer,a
+321,BIOL,1010,2017,Summer,a
+353,BIOL,1010,2017,Summer,a
+102,BIOL,1010,2018,Summer,a
+105,BIOL,1010,2018,Summer,a
+214,BIOL,1010,2018,Summer,a
+260,BIOL,1010,2018,Summer,a
+294,BIOL,1010,2018,Summer,a
+318,BIOL,1010,2018,Summer,a
+368,BIOL,1010,2018,Summer,a
+392,BIOL,1010,2018,Summer,a
+399,BIOL,1010,2018,Summer,a
+133,BIOL,1010,2018,Summer,b
+173,BIOL,1010,2018,Summer,b
+197,BIOL,1010,2018,Summer,b
+238,BIOL,1010,2018,Summer,b
+275,BIOL,1010,2018,Summer,b
+285,BIOL,1010,2018,Summer,b
+292,BIOL,1010,2018,Summer,b
+311,BIOL,1010,2018,Summer,b
+313,BIOL,1010,2018,Summer,b
+366,BIOL,1010,2018,Summer,b
+378,BIOL,1010,2018,Summer,b
+259,BIOL,1010,2018,Summer,c
+262,BIOL,1010,2018,Summer,c
+309,BIOL,1010,2018,Summer,c
+313,BIOL,1010,2018,Summer,c
+329,BIOL,1010,2018,Summer,c
+342,BIOL,1010,2018,Summer,c
+374,BIOL,1010,2018,Summer,c
+169,BIOL,1010,2018,Fall,a
+239,BIOL,1010,2018,Fall,a
+252,BIOL,1010,2018,Fall,a
+258,BIOL,1010,2018,Fall,a
+345,BIOL,1010,2018,Fall,a
+362,BIOL,1010,2018,Fall,a
+164,BIOL,1010,2018,Fall,b
+298,BIOL,1010,2018,Fall,b
+139,BIOL,1010,2019,Spring,a
+372,BIOL,1010,2019,Spring,a
+375,BIOL,1010,2019,Spring,a
+109,BIOL,1010,2019,Spring,b
+165,BIOL,1010,2019,Spring,b
+217,BIOL,1010,2019,Spring,b
+228,BIOL,1010,2019,Spring,b
+231,BIOL,1010,2019,Spring,b
+240,BIOL,1010,2019,Spring,c
+332,BIOL,1010,2019,Spring,c
+247,BIOL,1010,2019,Spring,d
+314,BIOL,1010,2019,Spring,d
+379,BIOL,1010,2019,Spring,d
+113,BIOL,1010,2020,Summer,a
+122,BIOL,1010,2020,Summer,a
+148,BIOL,1010,2020,Summer,a
+153,BIOL,1010,2020,Summer,a
+178,BIOL,1010,2020,Summer,a
+200,BIOL,1010,2020,Summer,a
+256,BIOL,1010,2020,Summer,a
+270,BIOL,1010,2020,Summer,a
+340,BIOL,1010,2020,Summer,a
+108,BIOL,1010,2020,Summer,b
+118,BIOL,1010,2020,Summer,b
+122,BIOL,1010,2020,Summer,b
+175,BIOL,1010,2020,Summer,b
+244,BIOL,1010,2020,Summer,b
+257,BIOL,1010,2020,Summer,b
+270,BIOL,1010,2020,Summer,b
+306,BIOL,1010,2020,Summer,b
+348,BIOL,1010,2020,Summer,b
+384,BIOL,1010,2020,Summer,b
+112,BIOL,1010,2020,Summer,c
+131,BIOL,1010,2020,Summer,c
+146,BIOL,1010,2020,Summer,c
+185,BIOL,1010,2020,Summer,c
+270,BIOL,1010,2020,Summer,c
+348,BIOL,1010,2020,Summer,c
+371,BIOL,1010,2020,Summer,c
+390,BIOL,1010,2020,Summer,c
+398,BIOL,1010,2020,Summer,c
+100,BIOL,1010,2020,Summer,d
+121,BIOL,1010,2020,Summer,d
+244,BIOL,1010,2020,Summer,d
+254,BIOL,1010,2020,Summer,d
+263,BIOL,1010,2020,Summer,d
+270,BIOL,1010,2020,Summer,d
+300,BIOL,1010,2020,Summer,d
+323,BIOL,1010,2020,Summer,d
+340,BIOL,1010,2020,Summer,d
+371,BIOL,1010,2020,Summer,d
+211,BIOL,1030,2015,Spring,c
+379,BIOL,1030,2015,Spring,d
+204,BIOL,1030,2015,Summer,a
+246,BIOL,1030,2015,Summer,a
+321,BIOL,1030,2015,Summer,a
+117,BIOL,1030,2016,Spring,a
+273,BIOL,1030,2016,Spring,a
+282,BIOL,1030,2016,Spring,a
+392,BIOL,1030,2016,Spring,a
+160,BIOL,1030,2016,Summer,a
+195,BIOL,1030,2016,Summer,a
+270,BIOL,1030,2016,Summer,a
+277,BIOL,1030,2016,Summer,a
+290,BIOL,1030,2016,Summer,a
+329,BIOL,1030,2016,Summer,a
+395,BIOL,1030,2016,Summer,a
+120,BIOL,1030,2016,Fall,a
+176,BIOL,1030,2016,Fall,a
+213,BIOL,1030,2016,Fall,a
+276,BIOL,1030,2016,Fall,a
+115,BIOL,1030,2017,Spring,a
+257,BIOL,1030,2017,Spring,a
+299,BIOL,1030,2017,Spring,a
+313,BIOL,1030,2017,Spring,a
+214,BIOL,1030,2017,Spring,b
+243,BIOL,1030,2017,Spring,b
+374,BIOL,1030,2017,Spring,b
+151,BIOL,1030,2017,Spring,c
+215,BIOL,1030,2017,Spring,c
+257,BIOL,1030,2017,Spring,c
+335,BIOL,1030,2017,Spring,c
+348,BIOL,1030,2017,Spring,c
+388,BIOL,1030,2017,Spring,c
+132,BIOL,1030,2018,Summer,a
+197,BIOL,1030,2018,Summer,a
+285,BIOL,1030,2018,Summer,a
+372,BIOL,1030,2018,Summer,a
+378,BIOL,1030,2018,Summer,a
+102,BIOL,1030,2018,Fall,a
+183,BIOL,1030,2018,Fall,a
+199,BIOL,1030,2018,Fall,a
+230,BIOL,1030,2018,Fall,a
+253,BIOL,1030,2018,Fall,a
+259,BIOL,1030,2018,Fall,a
+275,BIOL,1030,2018,Fall,a
+387,BIOL,1030,2018,Fall,a
+391,BIOL,1030,2018,Fall,a
+179,BIOL,1030,2019,Spring,a
+333,BIOL,1030,2019,Spring,a
+139,BIOL,1030,2019,Spring,b
+217,BIOL,1030,2019,Spring,b
+258,BIOL,1030,2019,Spring,b
+143,BIOL,1030,2019,Spring,c
+177,BIOL,1030,2019,Spring,c
+248,BIOL,1030,2019,Spring,c
+256,BIOL,1030,2019,Spring,c
+258,BIOL,1030,2019,Spring,c
+298,BIOL,1030,2019,Spring,c
+307,BIOL,1030,2019,Spring,c
+318,BIOL,1030,2019,Spring,c
+375,BIOL,1030,2019,Spring,c
+397,BIOL,1030,2019,Spring,c
+231,BIOL,1030,2019,Spring,d
+384,BIOL,1030,2019,Spring,d
+128,BIOL,1030,2019,Summer,a
+167,BIOL,1030,2019,Summer,a
+260,BIOL,1030,2019,Summer,a
+314,BIOL,1030,2019,Summer,a
+347,BIOL,1030,2019,Summer,a
+380,BIOL,1030,2019,Summer,a
+100,BIOL,1030,2020,Spring,a
+135,BIOL,1030,2020,Spring,a
+153,BIOL,1030,2020,Spring,a
+254,BIOL,1030,2020,Spring,a
+292,BIOL,1030,2020,Spring,a
+325,BIOL,1030,2020,Spring,a
+341,BIOL,1030,2020,Spring,a
+109,BIOL,1030,2020,Summer,a
+113,BIOL,1030,2020,Summer,a
+123,BIOL,1030,2020,Summer,a
+131,BIOL,1030,2020,Summer,a
+164,BIOL,1030,2020,Summer,a
+170,BIOL,1030,2020,Summer,a
+185,BIOL,1030,2020,Summer,a
+332,BIOL,1030,2020,Summer,a
+340,BIOL,1030,2020,Summer,a
+360,BIOL,1030,2020,Summer,a
+371,BIOL,1030,2020,Summer,a
+386,BIOL,1030,2020,Summer,a
+144,BIOL,1210,2016,Spring,a
+182,BIOL,1210,2016,Spring,a
+270,BIOL,1210,2016,Spring,a
+301,BIOL,1210,2016,Spring,a
+115,BIOL,1210,2017,Spring,a
+117,BIOL,1210,2017,Spring,a
+210,BIOL,1210,2017,Spring,a
+278,BIOL,1210,2017,Spring,a
+299,BIOL,1210,2017,Spring,a
+372,BIOL,1210,2017,Spring,a
+377,BIOL,1210,2017,Spring,a
+275,BIOL,1210,2017,Summer,a
+282,BIOL,1210,2017,Summer,a
+120,BIOL,1210,2018,Spring,a
+131,BIOL,1210,2018,Spring,a
+134,BIOL,1210,2018,Spring,a
+177,BIOL,1210,2018,Spring,a
+332,BIOL,1210,2018,Spring,a
+220,BIOL,1210,2018,Fall,a
+255,BIOL,1210,2018,Fall,a
+151,BIOL,1210,2018,Fall,b
+179,BIOL,1210,2018,Fall,b
+366,BIOL,1210,2018,Fall,b
+173,BIOL,1210,2019,Spring,a
+230,BIOL,1210,2019,Spring,a
+256,BIOL,1210,2019,Spring,a
+305,BIOL,1210,2019,Spring,a
+307,BIOL,1210,2019,Spring,a
+342,BIOL,1210,2019,Spring,a
+356,BIOL,1210,2019,Spring,a
+193,BIOL,2010,2015,Spring,a
+182,BIOL,2010,2015,Summer,a
+195,BIOL,2010,2015,Summer,a
+377,BIOL,2010,2015,Summer,a
+336,BIOL,2010,2015,Fall,a
+123,BIOL,2010,2017,Summer,a
+127,BIOL,2010,2017,Summer,a
+173,BIOL,2010,2017,Summer,a
+259,BIOL,2010,2017,Summer,a
+277,BIOL,2010,2017,Summer,a
+120,BIOL,2010,2017,Fall,a
+208,BIOL,2010,2017,Fall,a
+262,BIOL,2010,2017,Fall,a
+304,BIOL,2010,2017,Fall,a
+355,BIOL,2010,2017,Fall,a
+372,BIOL,2010,2017,Fall,a
+391,BIOL,2010,2017,Fall,a
+134,BIOL,2010,2018,Spring,a
+197,BIOL,2010,2018,Spring,a
+210,BIOL,2010,2018,Spring,a
+214,BIOL,2010,2018,Spring,a
+255,BIOL,2010,2018,Spring,a
+270,BIOL,2010,2018,Spring,a
+285,BIOL,2010,2018,Spring,a
+348,BIOL,2010,2018,Spring,a
+373,BIOL,2010,2018,Spring,a
+385,BIOL,2010,2018,Spring,a
+309,BIOL,2010,2019,Fall,a
+312,BIOL,2010,2019,Fall,a
+313,BIOL,2010,2019,Fall,a
+316,BIOL,2010,2019,Fall,a
+109,BIOL,2010,2020,Spring,a
+113,BIOL,2010,2020,Spring,a
+135,BIOL,2010,2020,Spring,a
+169,BIOL,2010,2020,Spring,a
+223,BIOL,2010,2020,Spring,a
+231,BIOL,2010,2020,Spring,a
+384,BIOL,2010,2020,Spring,a
+386,BIOL,2010,2020,Spring,a
+108,BIOL,2010,2020,Spring,b
+164,BIOL,2010,2020,Spring,b
+178,BIOL,2010,2020,Spring,b
+179,BIOL,2010,2020,Spring,b
+292,BIOL,2010,2020,Spring,b
+146,BIOL,2010,2020,Summer,a
+166,BIOL,2010,2020,Summer,a
+167,BIOL,2010,2020,Summer,a
+170,BIOL,2010,2020,Summer,a
+175,BIOL,2010,2020,Summer,a
+221,BIOL,2010,2020,Summer,a
+228,BIOL,2010,2020,Summer,a
+242,BIOL,2010,2020,Summer,a
+248,BIOL,2010,2020,Summer,a
+250,BIOL,2010,2020,Summer,a
+251,BIOL,2010,2020,Summer,a
+256,BIOL,2010,2020,Summer,a
+311,BIOL,2010,2020,Summer,a
+333,BIOL,2010,2020,Summer,a
+364,BIOL,2010,2020,Summer,a
+375,BIOL,2010,2020,Summer,a
+378,BIOL,2010,2020,Summer,a
+128,BIOL,2010,2020,Summer,b
+177,BIOL,2010,2020,Summer,b
+228,BIOL,2010,2020,Summer,b
+235,BIOL,2010,2020,Summer,b
+293,BIOL,2010,2020,Summer,b
+296,BIOL,2010,2020,Summer,b
+306,BIOL,2010,2020,Summer,b
+363,BIOL,2010,2020,Summer,b
+390,BIOL,2010,2020,Summer,b
+120,BIOL,2020,2015,Summer,a
+144,BIOL,2020,2015,Summer,a
+210,BIOL,2020,2015,Summer,a
+126,BIOL,2020,2015,Fall,a
+140,BIOL,2020,2015,Fall,a
+374,BIOL,2020,2015,Fall,b
+392,BIOL,2020,2015,Fall,b
+176,BIOL,2020,2015,Fall,c
+182,BIOL,2020,2015,Fall,c
+295,BIOL,2020,2015,Fall,c
+377,BIOL,2020,2015,Fall,c
+192,BIOL,2020,2015,Fall,d
+115,BIOL,2020,2016,Spring,a
+117,BIOL,2020,2016,Spring,a
+212,BIOL,2020,2016,Spring,a
+214,BIOL,2020,2016,Spring,a
+313,BIOL,2020,2016,Spring,a
+357,BIOL,2020,2016,Spring,a
+123,BIOL,2020,2018,Spring,a
+129,BIOL,2020,2018,Spring,a
+139,BIOL,2020,2018,Spring,a
+285,BIOL,2020,2018,Spring,a
+292,BIOL,2020,2018,Spring,a
+321,BIOL,2020,2018,Spring,a
+332,BIOL,2020,2018,Spring,a
+152,BIOL,2020,2018,Fall,a
+158,BIOL,2020,2018,Fall,a
+163,BIOL,2020,2018,Fall,a
+165,BIOL,2020,2018,Fall,a
+177,BIOL,2020,2018,Fall,a
+183,BIOL,2020,2018,Fall,a
+199,BIOL,2020,2018,Fall,a
+255,BIOL,2020,2018,Fall,a
+257,BIOL,2020,2018,Fall,a
+261,BIOL,2020,2018,Fall,a
+270,BIOL,2020,2018,Fall,a
+274,BIOL,2020,2018,Fall,a
+276,BIOL,2020,2018,Fall,a
+399,BIOL,2020,2018,Fall,a
+100,BIOL,2020,2018,Fall,b
+113,BIOL,2020,2018,Fall,b
+260,BIOL,2020,2018,Fall,b
+262,BIOL,2020,2018,Fall,b
+267,BIOL,2020,2018,Fall,b
+344,BIOL,2020,2018,Fall,b
+345,BIOL,2020,2018,Fall,b
+373,BIOL,2020,2018,Fall,b
+378,BIOL,2020,2018,Fall,b
+362,BIOL,2020,2018,Fall,c
+387,BIOL,2020,2018,Fall,c
+101,BIOL,2020,2018,Fall,d
+231,BIOL,2020,2018,Fall,d
+288,BIOL,2020,2018,Fall,d
+325,BIOL,2020,2018,Fall,d
+342,BIOL,2020,2018,Fall,d
+379,BIOL,2020,2018,Fall,d
+102,BIOL,2020,2019,Summer,a
+119,BIOL,2020,2019,Summer,a
+289,BIOL,2020,2019,Summer,a
+293,BIOL,2020,2019,Summer,a
+307,BIOL,2020,2019,Summer,a
+282,BIOL,2021,2015,Spring,a
+377,BIOL,2021,2015,Spring,a
+394,BIOL,2021,2015,Spring,a
+249,BIOL,2021,2015,Summer,b
+290,BIOL,2021,2015,Summer,c
+179,BIOL,2021,2016,Fall,a
+243,BIOL,2021,2016,Fall,a
+268,BIOL,2021,2016,Fall,a
+270,BIOL,2021,2016,Fall,a
+379,BIOL,2021,2016,Fall,a
+115,BIOL,2021,2017,Summer,a
+182,BIOL,2021,2017,Summer,a
+348,BIOL,2021,2017,Summer,a
+388,BIOL,2021,2017,Summer,a
+207,BIOL,2021,2017,Fall,a
+264,BIOL,2021,2017,Fall,a
+292,BIOL,2021,2017,Fall,a
+345,BIOL,2021,2017,Fall,a
+102,BIOL,2021,2018,Spring,a
+177,BIOL,2021,2018,Spring,a
+311,BIOL,2021,2018,Spring,a
+361,BIOL,2021,2018,Spring,a
+373,BIOL,2021,2018,Spring,a
+117,BIOL,2021,2018,Summer,a
+169,BIOL,2021,2018,Summer,a
+257,BIOL,2021,2018,Summer,a
+312,BIOL,2021,2018,Summer,a
+318,BIOL,2021,2018,Summer,a
+344,BIOL,2021,2018,Summer,a
+356,BIOL,2021,2018,Summer,a
+366,BIOL,2021,2018,Summer,a
+378,BIOL,2021,2018,Summer,a
+127,BIOL,2021,2018,Fall,a
+152,BIOL,2021,2018,Fall,a
+199,BIOL,2021,2018,Fall,a
+239,BIOL,2021,2018,Fall,a
+256,BIOL,2021,2018,Fall,a
+152,BIOL,2021,2018,Fall,b
+309,BIOL,2021,2018,Fall,b
+397,BIOL,2021,2018,Fall,b
+248,BIOL,2021,2018,Fall,c
+296,BIOL,2021,2018,Fall,c
+342,BIOL,2021,2018,Fall,c
+384,BIOL,2021,2018,Fall,c
+133,BIOL,2021,2018,Fall,d
+296,BIOL,2021,2018,Fall,d
+196,BIOL,2021,2019,Spring,a
+399,BIOL,2021,2019,Spring,a
+139,BIOL,2021,2019,Spring,b
+178,BIOL,2021,2019,Spring,b
+238,BIOL,2021,2019,Spring,b
+313,BIOL,2021,2019,Spring,b
+107,BIOL,2021,2019,Fall,a
+164,BIOL,2021,2019,Fall,a
+300,BIOL,2021,2019,Fall,a
+303,BIOL,2021,2019,Fall,a
+340,BIOL,2021,2019,Fall,a
+364,BIOL,2021,2019,Fall,a
+140,BIOL,2030,2015,Fall,a
+212,BIOL,2030,2015,Fall,a
+215,BIOL,2030,2015,Fall,a
+249,BIOL,2030,2015,Fall,a
+379,BIOL,2030,2015,Fall,a
+119,BIOL,2030,2016,Summer,a
+163,BIOL,2030,2016,Summer,b
+207,BIOL,2030,2016,Summer,b
+392,BIOL,2030,2016,Summer,b
+151,BIOL,2030,2016,Fall,a
+213,BIOL,2030,2016,Fall,a
+277,BIOL,2030,2016,Fall,a
+314,BIOL,2030,2016,Fall,a
+397,BIOL,2030,2016,Fall,a
+123,BIOL,2030,2017,Spring,a
+179,BIOL,2030,2017,Spring,a
+182,BIOL,2030,2017,Spring,a
+257,BIOL,2030,2017,Spring,a
+313,BIOL,2030,2017,Spring,a
+374,BIOL,2030,2017,Spring,a
+377,BIOL,2030,2017,Spring,a
+243,BIOL,2030,2017,Spring,b
+246,BIOL,2030,2017,Spring,b
+285,BIOL,2030,2017,Spring,b
+348,BIOL,2030,2017,Spring,b
+372,BIOL,2030,2017,Spring,b
+378,BIOL,2030,2017,Spring,c
+120,BIOL,2030,2017,Spring,d
+285,BIOL,2030,2017,Spring,d
+355,BIOL,2030,2017,Spring,d
+393,BIOL,2030,2017,Spring,d
+230,BIOL,2030,2018,Summer,a
+342,BIOL,2030,2018,Summer,a
+373,BIOL,2030,2018,Summer,a
+101,BIOL,2030,2018,Summer,b
+132,BIOL,2030,2018,Summer,b
+214,BIOL,2030,2018,Summer,b
+276,BIOL,2030,2018,Summer,b
+371,BIOL,2030,2018,Summer,b
+312,BIOL,2030,2019,Summer,a
+318,BIOL,2030,2019,Summer,a
+100,BIOL,2030,2019,Summer,b
+113,BIOL,2030,2019,Summer,b
+173,BIOL,2030,2019,Summer,b
+228,BIOL,2030,2019,Summer,b
+270,BIOL,2030,2019,Summer,b
+309,BIOL,2030,2019,Summer,b
+362,BIOL,2030,2019,Summer,b
+396,BIOL,2030,2019,Summer,b
+109,BIOL,2030,2019,Summer,c
+135,BIOL,2030,2019,Summer,c
+188,BIOL,2030,2019,Summer,c
+247,BIOL,2030,2019,Summer,c
+270,BIOL,2030,2019,Summer,c
+296,BIOL,2030,2019,Summer,c
+320,BIOL,2030,2019,Summer,c
+399,BIOL,2030,2019,Summer,c
+131,BIOL,2030,2019,Summer,d
+143,BIOL,2030,2019,Summer,d
+241,BIOL,2030,2019,Summer,d
+300,BIOL,2030,2019,Summer,d
+345,BIOL,2030,2019,Summer,d
+164,BIOL,2030,2020,Spring,a
+171,BIOL,2030,2020,Spring,a
+366,BIOL,2030,2020,Spring,a
+102,BIOL,2030,2020,Spring,b
+199,BIOL,2030,2020,Spring,b
+311,BIOL,2030,2020,Spring,b
+347,BIOL,2030,2020,Spring,b
+375,BIOL,2030,2020,Spring,b
+243,BIOL,2210,2016,Summer,a
+278,BIOL,2210,2016,Summer,a
+312,BIOL,2210,2016,Summer,a
+356,BIOL,2210,2016,Summer,a
+392,BIOL,2210,2016,Summer,a
+115,BIOL,2210,2017,Spring,a
+231,BIOL,2210,2017,Spring,a
+182,BIOL,2210,2017,Spring,b
+215,BIOL,2210,2017,Spring,b
+255,BIOL,2210,2017,Spring,b
+309,BIOL,2210,2017,Spring,b
+348,BIOL,2210,2017,Spring,b
+107,BIOL,2210,2017,Spring,c
+177,BIOL,2210,2017,Spring,c
+215,BIOL,2210,2017,Spring,c
+277,BIOL,2210,2017,Spring,c
+393,BIOL,2210,2017,Spring,c
+397,BIOL,2210,2017,Spring,c
+151,BIOL,2210,2017,Summer,a
+187,BIOL,2210,2017,Summer,a
+214,BIOL,2210,2017,Summer,a
+257,BIOL,2210,2017,Summer,a
+120,BIOL,2210,2017,Summer,b
+164,BIOL,2210,2017,Summer,b
+259,BIOL,2210,2017,Summer,b
+270,BIOL,2210,2017,Summer,b
+342,BIOL,2210,2017,Summer,b
+378,BIOL,2210,2017,Summer,b
+387,BIOL,2210,2017,Summer,b
+285,BIOL,2210,2017,Summer,c
+374,BIOL,2210,2017,Summer,c
+375,BIOL,2210,2017,Summer,c
+128,BIOL,2210,2018,Spring,a
+275,BIOL,2210,2018,Spring,a
+276,BIOL,2210,2018,Spring,a
+391,BIOL,2210,2018,Spring,a
+131,BIOL,2210,2018,Summer,a
+143,BIOL,2210,2018,Summer,a
+169,BIOL,2210,2018,Summer,a
+174,BIOL,2210,2018,Summer,a
+239,BIOL,2210,2018,Summer,a
+260,BIOL,2210,2018,Summer,a
+298,BIOL,2210,2018,Summer,a
+369,BIOL,2210,2018,Summer,a
+227,BIOL,2210,2018,Summer,b
+230,BIOL,2210,2018,Summer,b
+311,BIOL,2210,2018,Summer,b
+313,BIOL,2210,2018,Summer,b
+173,BIOL,2210,2018,Summer,c
+210,BIOL,2210,2018,Summer,c
+258,BIOL,2210,2018,Summer,c
+102,BIOL,2210,2019,Summer,a
+179,BIOL,2210,2019,Summer,a
+314,BIOL,2210,2019,Summer,a
+329,BIOL,2210,2019,Summer,a
+368,BIOL,2210,2019,Summer,a
+377,BIOL,2210,2019,Summer,a
+119,BIOL,2210,2019,Summer,b
+228,BIOL,2210,2019,Summer,b
+318,BIOL,2210,2019,Summer,b
+386,BIOL,2210,2019,Summer,b
+293,BIOL,2210,2019,Fall,a
+380,BIOL,2210,2019,Fall,a
+289,BIOL,2210,2019,Fall,b
+293,BIOL,2210,2019,Fall,b
+121,BIOL,2210,2020,Fall,a
+185,BIOL,2210,2020,Fall,a
+219,BIOL,2210,2020,Fall,a
+220,BIOL,2210,2020,Fall,a
+240,BIOL,2210,2020,Fall,a
+271,BIOL,2210,2020,Fall,a
+297,BIOL,2210,2020,Fall,a
+347,BIOL,2210,2020,Fall,a
+360,BIOL,2210,2020,Fall,a
+366,BIOL,2210,2020,Fall,a
+371,BIOL,2210,2020,Fall,a
+373,BIOL,2210,2020,Fall,a
+321,BIOL,2325,2015,Spring,a
+182,BIOL,2325,2015,Fall,a
+277,BIOL,2325,2015,Fall,b
+290,BIOL,2325,2015,Fall,b
+379,BIOL,2325,2015,Fall,b
+149,BIOL,2325,2015,Fall,c
+163,BIOL,2325,2015,Fall,c
+192,BIOL,2325,2015,Fall,c
+204,BIOL,2325,2015,Fall,c
+312,BIOL,2325,2015,Fall,c
+138,BIOL,2325,2016,Summer,a
+357,BIOL,2325,2016,Summer,a
+369,BIOL,2325,2016,Summer,a
+394,BIOL,2325,2016,Summer,a
+127,BIOL,2325,2017,Fall,a
+385,BIOL,2325,2017,Fall,a
+102,BIOL,2325,2017,Fall,b
+123,BIOL,2325,2017,Fall,b
+260,BIOL,2325,2017,Fall,b
+296,BIOL,2325,2017,Fall,b
+387,BIOL,2325,2017,Fall,b
+100,BIOL,2325,2018,Spring,a
+105,BIOL,2325,2018,Spring,a
+119,BIOL,2325,2018,Spring,a
+214,BIOL,2325,2018,Spring,a
+332,BIOL,2325,2018,Spring,a
+373,BIOL,2325,2018,Spring,a
+374,BIOL,2325,2018,Spring,a
+132,BIOL,2325,2018,Summer,a
+151,BIOL,2325,2018,Summer,a
+255,BIOL,2325,2018,Summer,a
+262,BIOL,2325,2018,Summer,a
+275,BIOL,2325,2018,Summer,a
+318,BIOL,2325,2018,Summer,a
+386,BIOL,2325,2018,Summer,a
+393,BIOL,2325,2018,Summer,a
+397,BIOL,2325,2018,Summer,a
+124,BIOL,2325,2018,Fall,a
+133,BIOL,2325,2018,Fall,a
+164,BIOL,2325,2018,Fall,a
+220,BIOL,2325,2018,Fall,a
+247,BIOL,2325,2018,Fall,a
+309,BIOL,2325,2018,Fall,a
+129,BIOL,2325,2018,Fall,b
+131,BIOL,2325,2018,Fall,b
+167,BIOL,2325,2018,Fall,b
+129,BIOL,2325,2018,Fall,c
+217,BIOL,2325,2018,Fall,c
+239,BIOL,2325,2018,Fall,c
+274,BIOL,2325,2018,Fall,c
+356,BIOL,2325,2018,Fall,c
+399,BIOL,2325,2018,Fall,c
+152,BIOL,2325,2019,Spring,a
+292,BIOL,2325,2019,Spring,a
+329,BIOL,2325,2019,Spring,a
+333,BIOL,2325,2019,Spring,a
+342,BIOL,2325,2019,Spring,a
+377,BIOL,2325,2019,Spring,a
+391,BIOL,2325,2019,Spring,a
+270,BIOL,2325,2019,Spring,b
+313,BIOL,2325,2019,Spring,b
+314,BIOL,2325,2019,Spring,b
+342,BIOL,2325,2019,Spring,b
+120,BIOL,2325,2019,Summer,a
+135,BIOL,2325,2019,Summer,a
+139,BIOL,2325,2019,Summer,a
+179,BIOL,2325,2019,Summer,a
+276,BIOL,2325,2019,Summer,a
+285,BIOL,2325,2019,Summer,a
+325,BIOL,2325,2019,Summer,a
+290,BIOL,2330,2015,Fall,a
+138,BIOL,2330,2015,Fall,b
+204,BIOL,2330,2015,Fall,d
+312,BIOL,2330,2015,Fall,d
+120,BIOL,2330,2016,Spring,a
+123,BIOL,2330,2016,Spring,a
+195,BIOL,2330,2016,Spring,a
+282,BIOL,2330,2016,Spring,a
+357,BIOL,2330,2016,Spring,a
+377,BIOL,2330,2016,Spring,a
+177,BIOL,2330,2016,Fall,a
+270,BIOL,2330,2016,Fall,a
+291,BIOL,2330,2016,Fall,a
+335,BIOL,2330,2016,Fall,a
+369,BIOL,2330,2016,Fall,a
+393,BIOL,2330,2016,Fall,a
+214,BIOL,2330,2017,Summer,a
+229,BIOL,2330,2017,Summer,a
+277,BIOL,2330,2017,Summer,a
+309,BIOL,2330,2017,Summer,a
+155,BIOL,2330,2017,Fall,a
+165,BIOL,2330,2017,Fall,a
+208,BIOL,2330,2017,Fall,a
+342,BIOL,2330,2017,Fall,a
+355,BIOL,2330,2017,Fall,a
+387,BIOL,2330,2017,Fall,a
+391,BIOL,2330,2017,Fall,a
+187,BIOL,2330,2017,Fall,b
+199,BIOL,2330,2017,Fall,b
+266,BIOL,2330,2017,Fall,b
+288,BIOL,2330,2017,Fall,b
+392,BIOL,2330,2017,Fall,b
+106,BIOL,2330,2019,Fall,a
+125,BIOL,2330,2019,Fall,a
+227,BIOL,2330,2019,Fall,a
+240,BIOL,2330,2019,Fall,a
+307,BIOL,2330,2019,Fall,a
+378,BIOL,2330,2019,Fall,a
+380,BIOL,2330,2019,Fall,a
+183,BIOL,2330,2020,Spring,a
+210,BIOL,2330,2020,Spring,a
+300,BIOL,2330,2020,Spring,a
+340,BIOL,2330,2020,Spring,a
+348,BIOL,2330,2020,Spring,a
+211,BIOL,2355,2015,Spring,a
+192,BIOL,2355,2015,Summer,a
+246,BIOL,2355,2015,Summer,a
+377,BIOL,2355,2015,Summer,a
+144,BIOL,2355,2016,Spring,a
+395,BIOL,2355,2016,Spring,a
+215,BIOL,2355,2016,Spring,b
+321,BIOL,2355,2016,Spring,b
+392,BIOL,2355,2016,Spring,b
+395,BIOL,2355,2016,Spring,b
+105,BIOL,2355,2017,Spring,a
+145,BIOL,2355,2017,Spring,a
+278,BIOL,2355,2017,Spring,a
+290,BIOL,2355,2017,Spring,a
+312,BIOL,2355,2017,Spring,a
+105,BIOL,2355,2017,Spring,b
+270,BIOL,2355,2017,Spring,b
+329,BIOL,2355,2017,Spring,b
+282,BIOL,2355,2017,Spring,c
+299,BIOL,2355,2017,Spring,c
+369,BIOL,2355,2017,Spring,c
+397,BIOL,2355,2017,Spring,c
+102,BIOL,2355,2017,Spring,d
+163,BIOL,2355,2017,Spring,d
+179,BIOL,2355,2017,Spring,d
+243,BIOL,2355,2017,Spring,d
+285,BIOL,2355,2017,Spring,d
+329,BIOL,2355,2017,Spring,d
+374,BIOL,2355,2017,Spring,d
+378,BIOL,2355,2017,Spring,d
+123,BIOL,2355,2017,Summer,a
+318,BIOL,2355,2017,Summer,a
+375,BIOL,2355,2017,Summer,a
+237,BIOL,2355,2017,Fall,a
+335,BIOL,2355,2017,Fall,a
+366,BIOL,2355,2017,Fall,a
+155,BIOL,2355,2017,Fall,b
+182,BIOL,2355,2017,Fall,b
+256,BIOL,2355,2017,Fall,b
+264,BIOL,2355,2017,Fall,b
+373,BIOL,2355,2017,Fall,b
+169,BIOL,2355,2018,Spring,a
+214,BIOL,2355,2018,Spring,a
+230,BIOL,2355,2018,Spring,a
+277,BIOL,2355,2018,Spring,a
+393,BIOL,2355,2018,Spring,a
+119,BIOL,2355,2018,Summer,a
+128,BIOL,2355,2018,Summer,a
+131,BIOL,2355,2018,Summer,a
+185,BIOL,2355,2018,Summer,a
+227,BIOL,2355,2018,Summer,a
+262,BIOL,2355,2018,Summer,a
+332,BIOL,2355,2018,Summer,a
+342,BIOL,2355,2018,Summer,a
+187,BIOL,2355,2018,Summer,b
+276,BIOL,2355,2018,Summer,b
+311,BIOL,2355,2018,Summer,b
+348,BIOL,2355,2018,Summer,b
+379,BIOL,2355,2018,Summer,b
+391,BIOL,2355,2018,Summer,b
+398,BIOL,2355,2018,Summer,b
+113,BIOL,2355,2018,Summer,c
+129,BIOL,2355,2018,Summer,c
+274,BIOL,2355,2018,Summer,c
+275,BIOL,2355,2018,Summer,c
+332,BIOL,2355,2018,Summer,c
+119,BIOL,2355,2018,Summer,d
+207,BIOL,2355,2018,Summer,d
+276,BIOL,2355,2018,Summer,d
+347,BIOL,2355,2018,Summer,d
+379,BIOL,2355,2018,Summer,d
+387,BIOL,2355,2018,Summer,d
+127,BIOL,2355,2018,Fall,a
+292,BIOL,2355,2018,Fall,a
+313,BIOL,2355,2018,Fall,a
+314,BIOL,2355,2018,Fall,a
+359,BIOL,2355,2018,Fall,a
+380,BIOL,2355,2018,Fall,a
+178,BIOL,2355,2019,Spring,a
+247,BIOL,2355,2019,Spring,a
+356,BIOL,2355,2019,Spring,a
+151,BIOL,2355,2019,Spring,b
+372,BIOL,2355,2019,Spring,b
+146,BIOL,2355,2019,Spring,c
+248,BIOL,2355,2019,Spring,c
+255,BIOL,2355,2019,Spring,c
+345,BIOL,2355,2019,Spring,c
+109,BIOL,2355,2019,Spring,d
+107,BIOL,2355,2020,Spring,a
+118,BIOL,2355,2020,Spring,a
+309,BIOL,2355,2020,Spring,a
+362,BIOL,2355,2020,Spring,a
+106,BIOL,2355,2020,Summer,a
+122,BIOL,2355,2020,Summer,a
+221,BIOL,2355,2020,Summer,a
+258,BIOL,2355,2020,Summer,a
+323,BIOL,2355,2020,Summer,a
+333,BIOL,2355,2020,Summer,a
+106,BIOL,2355,2020,Summer,b
+137,BIOL,2355,2020,Summer,b
+177,BIOL,2355,2020,Summer,b
+244,BIOL,2355,2020,Summer,b
+307,BIOL,2355,2020,Summer,b
+325,BIOL,2355,2020,Summer,b
+363,BIOL,2355,2020,Summer,b
+120,BIOL,2355,2020,Fall,a
+124,BIOL,2355,2020,Fall,a
+135,BIOL,2355,2020,Fall,a
+142,BIOL,2355,2020,Fall,a
+167,BIOL,2355,2020,Fall,a
+175,BIOL,2355,2020,Fall,a
+181,BIOL,2355,2020,Fall,a
+186,BIOL,2355,2020,Fall,a
+220,BIOL,2355,2020,Fall,a
+233,BIOL,2355,2020,Fall,a
+271,BIOL,2355,2020,Fall,a
+390,BIOL,2355,2020,Fall,a
+177,BIOL,2420,2015,Spring,a
+246,BIOL,2420,2015,Spring,b
+140,BIOL,2420,2015,Spring,c
+192,BIOL,2420,2015,Spring,d
+374,BIOL,2420,2015,Summer,a
+290,BIOL,2420,2015,Fall,a
+119,BIOL,2420,2016,Spring,a
+162,BIOL,2420,2016,Spring,a
+115,BIOL,2420,2017,Summer,a
+117,BIOL,2420,2017,Summer,a
+132,BIOL,2420,2017,Summer,a
+164,BIOL,2420,2017,Summer,a
+182,BIOL,2420,2017,Summer,a
+229,BIOL,2420,2017,Summer,a
+264,BIOL,2420,2017,Summer,a
+107,BIOL,2420,2017,Summer,b
+123,BIOL,2420,2017,Summer,b
+207,BIOL,2420,2017,Summer,b
+309,BIOL,2420,2017,Summer,b
+348,BIOL,2420,2017,Summer,b
+169,BIOL,2420,2018,Spring,a
+185,BIOL,2420,2018,Spring,a
+270,BIOL,2420,2018,Spring,a
+375,BIOL,2420,2018,Spring,a
+120,BIOL,2420,2020,Spring,a
+210,BIOL,2420,2020,Spring,a
+235,BIOL,2420,2020,Spring,a
+242,BIOL,2420,2020,Spring,a
+248,BIOL,2420,2020,Spring,a
+285,BIOL,2420,2020,Spring,a
+373,BIOL,2420,2020,Spring,a
+397,BIOL,2420,2020,Spring,a
+121,BIOL,2420,2020,Spring,b
+183,BIOL,2420,2020,Spring,b
+230,BIOL,2420,2020,Spring,b
+241,BIOL,2420,2020,Spring,b
+248,BIOL,2420,2020,Spring,b
+365,BIOL,2420,2020,Spring,b
+124,BIOL,2420,2020,Summer,a
+128,BIOL,2420,2020,Summer,a
+131,BIOL,2420,2020,Summer,a
+151,BIOL,2420,2020,Summer,a
+189,BIOL,2420,2020,Summer,a
+200,BIOL,2420,2020,Summer,a
+292,BIOL,2420,2020,Summer,a
+311,BIOL,2420,2020,Summer,a
+313,BIOL,2420,2020,Summer,a
+323,BIOL,2420,2020,Summer,a
+333,BIOL,2420,2020,Summer,a
+347,BIOL,2420,2020,Summer,a
+363,BIOL,2420,2020,Summer,a
+368,BIOL,2420,2020,Summer,a
+122,BIOL,2420,2020,Fall,a
+146,BIOL,2420,2020,Fall,a
+175,BIOL,2420,2020,Fall,a
+224,BIOL,2420,2020,Fall,a
+255,BIOL,2420,2020,Fall,a
+272,BIOL,2420,2020,Fall,a
+321,BIOL,2420,2020,Fall,a
+329,BIOL,2420,2020,Fall,a
+342,BIOL,2420,2020,Fall,a
+391,BIOL,2420,2020,Fall,a
+138,CS,1030,2016,Spring,a
+149,CS,1030,2016,Spring,a
+162,CS,1030,2016,Spring,a
+290,CS,1030,2016,Spring,a
+291,CS,1030,2016,Spring,a
+312,CS,1030,2016,Spring,a
+348,CS,1030,2016,Spring,a
+395,CS,1030,2016,Spring,a
+123,CS,1030,2016,Summer,a
+214,CS,1030,2016,Summer,a
+245,CS,1030,2016,Summer,a
+277,CS,1030,2016,Summer,a
+385,CS,1030,2016,Summer,a
+393,CS,1030,2016,Summer,a
+102,CS,1030,2016,Fall,a
+116,CS,1030,2016,Fall,a
+243,CS,1030,2016,Fall,a
+262,CS,1030,2016,Fall,a
+321,CS,1030,2016,Fall,a
+128,CS,1030,2018,Fall,a
+238,CS,1030,2018,Fall,a
+256,CS,1030,2018,Fall,a
+305,CS,1030,2018,Fall,a
+344,CS,1030,2018,Fall,a
+366,CS,1030,2018,Fall,a
+387,CS,1030,2018,Fall,a
+143,CS,1030,2019,Fall,a
+260,CS,1030,2019,Fall,a
+285,CS,1030,2019,Fall,a
+398,CS,1030,2019,Fall,a
+173,CS,1030,2019,Fall,b
+185,CS,1030,2019,Fall,b
+210,CS,1030,2019,Fall,b
+247,CS,1030,2019,Fall,b
+303,CS,1030,2019,Fall,b
+329,CS,1030,2019,Fall,b
+359,CS,1030,2019,Fall,b
+100,CS,1030,2020,Spring,a
+122,CS,1030,2020,Spring,a
+175,CS,1030,2020,Spring,a
+221,CS,1030,2020,Spring,a
+307,CS,1030,2020,Spring,a
+170,CS,1030,2020,Spring,b
+332,CS,1030,2020,Spring,b
+391,CS,1030,2020,Spring,b
+118,CS,1030,2020,Spring,c
+120,CS,1030,2020,Spring,c
+124,CS,1030,2020,Spring,c
+135,CS,1030,2020,Spring,c
+309,CS,1030,2020,Spring,c
+119,CS,1030,2020,Fall,a
+131,CS,1030,2020,Fall,a
+167,CS,1030,2020,Fall,a
+181,CS,1030,2020,Fall,a
+202,CS,1030,2020,Fall,a
+227,CS,1030,2020,Fall,a
+255,CS,1030,2020,Fall,a
+271,CS,1030,2020,Fall,a
+342,CS,1030,2020,Fall,a
+347,CS,1030,2020,Fall,a
+215,CS,1410,2015,Summer,b
+276,CS,1410,2015,Summer,b
+182,CS,1410,2015,Summer,c
+172,CS,1410,2015,Summer,d
+270,CS,1410,2015,Summer,d
+301,CS,1410,2015,Summer,d
+382,CS,1410,2015,Summer,d
+216,CS,1410,2016,Spring,a
+335,CS,1410,2016,Spring,a
+355,CS,1410,2016,Spring,a
+216,CS,1410,2016,Spring,b
+273,CS,1410,2016,Spring,b
+291,CS,1410,2016,Spring,b
+335,CS,1410,2016,Spring,b
+207,CS,1410,2016,Summer,a
+389,CS,1410,2016,Summer,a
+394,CS,1410,2016,Summer,a
+290,CS,1410,2017,Spring,a
+391,CS,1410,2017,Spring,a
+120,CS,1410,2018,Spring,a
+231,CS,1410,2018,Spring,a
+348,CS,1410,2018,Spring,a
+100,CS,1410,2018,Spring,b
+107,CS,1410,2018,Spring,b
+109,CS,1410,2018,Spring,b
+120,CS,1410,2018,Spring,b
+164,CS,1410,2018,Spring,b
+199,CS,1410,2018,Spring,b
+203,CS,1410,2018,Spring,b
+229,CS,1410,2018,Spring,b
+109,CS,1410,2018,Spring,c
+388,CS,1410,2018,Spring,c
+199,CS,1410,2018,Spring,d
+275,CS,1410,2018,Spring,d
+307,CS,1410,2018,Spring,d
+366,CS,1410,2018,Spring,d
+392,CS,1410,2018,Spring,d
+121,CS,1410,2020,Spring,a
+122,CS,1410,2020,Spring,a
+267,CS,1410,2020,Spring,a
+312,CS,1410,2020,Spring,a
+200,CS,1410,2020,Spring,b
+277,CS,1410,2020,Spring,b
+329,CS,1410,2020,Spring,b
+375,CS,1410,2020,Spring,b
+277,CS,2100,2015,Summer,a
+313,CS,2100,2015,Summer,a
+214,CS,2100,2016,Spring,a
+276,CS,2100,2016,Spring,a
+295,CS,2100,2016,Spring,a
+123,CS,2100,2016,Summer,a
+179,CS,2100,2016,Summer,a
+160,CS,2100,2016,Summer,b
+179,CS,2100,2016,Summer,b
+262,CS,2100,2016,Summer,b
+335,CS,2100,2016,Summer,b
+374,CS,2100,2016,Summer,b
+388,CS,2100,2016,Summer,b
+134,CS,2100,2016,Summer,c
+278,CS,2100,2016,Summer,c
+256,CS,2100,2017,Spring,a
+377,CS,2100,2017,Spring,a
+378,CS,2100,2017,Spring,a
+143,CS,2100,2017,Fall,a
+163,CS,2100,2017,Fall,a
+215,CS,2100,2017,Fall,a
+311,CS,2100,2017,Fall,a
+348,CS,2100,2017,Fall,a
+356,CS,2100,2017,Fall,a
+366,CS,2100,2017,Fall,a
+101,CS,2100,2018,Spring,a
+185,CS,2100,2018,Spring,a
+255,CS,2100,2018,Spring,a
+361,CS,2100,2018,Spring,a
+387,CS,2100,2018,Spring,a
+258,CS,2100,2018,Summer,a
+261,CS,2100,2018,Summer,a
+270,CS,2100,2018,Summer,a
+369,CS,2100,2018,Summer,a
+133,CS,2100,2018,Summer,b
+182,CS,2100,2018,Summer,b
+285,CS,2100,2018,Summer,b
+329,CS,2100,2018,Summer,b
+139,CS,2100,2018,Summer,c
+258,CS,2100,2018,Summer,c
+298,CS,2100,2018,Summer,c
+329,CS,2100,2018,Summer,c
+332,CS,2100,2018,Summer,c
+345,CS,2100,2018,Summer,c
+371,CS,2100,2018,Summer,c
+381,CS,2100,2018,Summer,c
+392,CS,2100,2018,Summer,c
+393,CS,2100,2018,Summer,c
+158,CS,2100,2018,Fall,a
+230,CS,2100,2018,Fall,a
+292,CS,2100,2018,Fall,a
+373,CS,2100,2018,Fall,a
+257,CS,2100,2018,Fall,b
+309,CS,2100,2018,Fall,b
+344,CS,2100,2018,Fall,b
+384,CS,2100,2018,Fall,b
+124,CS,2100,2018,Fall,c
+196,CS,2100,2018,Fall,c
+217,CS,2100,2018,Fall,c
+231,CS,2100,2018,Fall,c
+252,CS,2100,2018,Fall,c
+257,CS,2100,2018,Fall,c
+164,CS,2100,2018,Fall,d
+199,CS,2100,2018,Fall,d
+253,CS,2100,2018,Fall,d
+259,CS,2100,2018,Fall,d
+391,CS,2100,2018,Fall,d
+399,CS,2100,2018,Fall,d
+107,CS,2100,2019,Spring,a
+240,CS,2100,2019,Spring,a
+307,CS,2100,2019,Spring,a
+379,CS,2100,2019,Spring,a
+156,CS,2100,2019,Spring,b
+312,CS,2100,2019,Spring,b
+241,CS,2100,2019,Summer,a
+293,CS,2100,2019,Summer,a
+296,CS,2100,2019,Summer,a
+314,CS,2100,2019,Summer,a
+347,CS,2100,2019,Summer,a
+390,CS,2100,2019,Summer,a
+106,CS,2100,2019,Summer,b
+131,CS,2100,2019,Summer,b
+169,CS,2100,2019,Summer,b
+194,CS,2100,2019,Summer,b
+238,CS,2100,2019,Summer,b
+359,CS,2100,2019,Summer,b
+368,CS,2100,2019,Summer,b
+118,CS,2100,2019,Fall,a
+181,CS,2100,2019,Fall,a
+223,CS,2100,2019,Fall,a
+386,CS,2100,2019,Fall,a
+118,CS,2100,2019,Fall,b
+178,CS,2100,2019,Fall,b
+235,CS,2100,2019,Fall,b
+321,CS,2100,2019,Fall,b
+397,CS,2100,2019,Fall,b
+118,CS,2100,2019,Fall,c
+146,CS,2100,2019,Fall,c
+220,CS,2100,2019,Fall,c
+260,CS,2100,2019,Fall,c
+318,CS,2100,2019,Fall,c
+397,CS,2100,2019,Fall,c
+120,CS,2100,2019,Fall,d
+146,CS,2100,2019,Fall,d
+181,CS,2100,2019,Fall,d
+183,CS,2100,2019,Fall,d
+316,CS,2100,2019,Fall,d
+152,CS,2100,2020,Spring,a
+167,CS,2100,2020,Spring,a
+228,CS,2100,2020,Spring,a
+122,CS,2100,2020,Fall,a
+171,CS,2100,2020,Fall,a
+177,CS,2100,2020,Fall,a
+191,CS,2100,2020,Fall,a
+219,CS,2100,2020,Fall,a
+247,CS,2100,2020,Fall,a
+289,CS,2100,2020,Fall,a
+333,CS,2100,2020,Fall,a
+138,CS,2420,2015,Spring,a
+277,CS,2420,2015,Spring,a
+377,CS,2420,2015,Spring,a
+160,CS,2420,2015,Summer,a
+204,CS,2420,2015,Summer,a
+140,CS,2420,2015,Summer,c
+302,CS,2420,2015,Summer,c
+276,CS,2420,2015,Fall,a
+115,CS,2420,2016,Spring,a
+312,CS,2420,2016,Spring,a
+348,CS,2420,2016,Spring,a
+385,CS,2420,2016,Spring,a
+389,CS,2420,2016,Spring,a
+172,CS,2420,2016,Summer,a
+195,CS,2420,2016,Summer,a
+314,CS,2420,2016,Summer,a
+321,CS,2420,2016,Summer,a
+163,CS,2420,2016,Fall,a
+177,CS,2420,2016,Fall,a
+229,CS,2420,2016,Fall,a
+245,CS,2420,2016,Fall,a
+282,CS,2420,2016,Fall,a
+313,CS,2420,2016,Fall,a
+369,CS,2420,2016,Fall,a
+392,CS,2420,2016,Fall,a
+105,CS,2420,2016,Fall,b
+117,CS,2420,2016,Fall,b
+151,CS,2420,2016,Fall,b
+215,CS,2420,2016,Fall,b
+262,CS,2420,2016,Fall,b
+268,CS,2420,2016,Fall,b
+295,CS,2420,2016,Fall,b
+329,CS,2420,2016,Fall,b
+243,CS,2420,2016,Fall,c
+270,CS,2420,2016,Fall,c
+397,CS,2420,2016,Fall,c
+119,CS,2420,2017,Summer,a
+353,CS,2420,2017,Summer,a
+361,CS,2420,2017,Summer,a
+132,CS,2420,2017,Summer,b
+285,CS,2420,2017,Summer,b
+299,CS,2420,2017,Summer,b
+309,CS,2420,2017,Summer,b
+179,CS,2420,2017,Summer,c
+208,CS,2420,2017,Summer,c
+261,CS,2420,2017,Summer,c
+288,CS,2420,2017,Summer,c
+311,CS,2420,2017,Summer,c
+372,CS,2420,2017,Summer,c
+120,CS,2420,2017,Fall,a
+123,CS,2420,2017,Fall,a
+128,CS,2420,2017,Fall,a
+326,CS,2420,2017,Fall,a
+387,CS,2420,2017,Fall,a
+107,CS,2420,2018,Spring,a
+296,CS,2420,2018,Spring,a
+124,CS,2420,2019,Summer,a
+131,CS,2420,2019,Summer,a
+199,CS,2420,2019,Summer,a
+356,CS,2420,2019,Summer,a
+390,CS,2420,2019,Summer,a
+133,CS,2420,2020,Summer,a
+153,CS,2420,2020,Summer,a
+167,CS,2420,2020,Summer,a
+219,CS,2420,2020,Summer,a
+220,CS,2420,2020,Summer,a
+231,CS,2420,2020,Summer,a
+233,CS,2420,2020,Summer,a
+263,CS,2420,2020,Summer,a
+365,CS,2420,2020,Summer,a
+368,CS,2420,2020,Summer,a
+168,CS,2420,2020,Fall,a
+222,CS,2420,2020,Fall,a
+225,CS,2420,2020,Fall,a
+230,CS,2420,2020,Fall,a
+345,CS,2420,2020,Fall,a
+163,CS,3100,2015,Summer,a
+172,CS,3100,2015,Summer,a
+276,CS,3100,2015,Summer,a
+302,CS,3100,2015,Summer,a
+215,CS,3100,2015,Summer,b
+214,CS,3100,2016,Spring,a
+243,CS,3100,2016,Spring,a
+120,CS,3100,2016,Spring,b
+138,CS,3100,2016,Spring,b
+285,CS,3100,2016,Spring,b
+374,CS,3100,2016,Spring,b
+134,CS,3100,2016,Spring,d
+138,CS,3100,2016,Spring,d
+192,CS,3100,2016,Spring,d
+195,CS,3100,2016,Spring,d
+207,CS,3100,2016,Summer,a
+182,CS,3100,2016,Fall,a
+213,CS,3100,2016,Fall,a
+277,CS,3100,2016,Fall,a
+314,CS,3100,2016,Fall,a
+378,CS,3100,2016,Fall,a
+392,CS,3100,2016,Fall,a
+210,CS,3100,2017,Spring,a
+261,CS,3100,2017,Spring,a
+210,CS,3100,2017,Spring,b
+255,CS,3100,2017,Spring,b
+355,CS,3100,2017,Spring,b
+385,CS,3100,2017,Spring,b
+393,CS,3100,2017,Summer,a
+123,CS,3100,2017,Fall,a
+124,CS,3100,2017,Fall,a
+139,CS,3100,2017,Fall,a
+237,CS,3100,2017,Fall,a
+260,CS,3100,2017,Fall,a
+264,CS,3100,2017,Fall,a
+296,CS,3100,2017,Fall,a
+391,CS,3100,2017,Fall,a
+397,CS,3100,2017,Fall,a
+196,CS,3100,2019,Spring,a
+129,CS,3100,2019,Spring,b
+288,CS,3100,2019,Spring,b
+348,CS,3100,2019,Spring,b
+366,CS,3100,2019,Spring,b
+399,CS,3100,2019,Spring,b
+211,CS,3200,2015,Spring,b
+138,CS,3200,2015,Fall,a
+249,CS,3200,2015,Fall,a
+134,CS,3200,2015,Fall,b
+179,CS,3200,2015,Fall,b
+312,CS,3200,2015,Fall,c
+336,CS,3200,2015,Fall,c
+282,CS,3200,2015,Fall,d
+295,CS,3200,2015,Fall,d
+182,CS,3200,2016,Summer,a
+246,CS,3200,2016,Summer,a
+270,CS,3200,2016,Summer,a
+290,CS,3200,2016,Summer,a
+357,CS,3200,2016,Summer,a
+373,CS,3200,2016,Summer,a
+379,CS,3200,2016,Summer,a
+176,CS,3200,2016,Summer,b
+207,CS,3200,2016,Summer,b
+246,CS,3200,2016,Summer,b
+120,CS,3200,2016,Fall,a
+268,CS,3200,2016,Fall,a
+102,CS,3200,2016,Fall,b
+313,CS,3200,2016,Fall,b
+348,CS,3200,2016,Fall,b
+123,CS,3200,2016,Fall,c
+229,CS,3200,2016,Fall,c
+291,CS,3200,2016,Fall,c
+105,CS,3200,2016,Fall,d
+107,CS,3200,2016,Fall,d
+151,CS,3200,2016,Fall,d
+369,CS,3200,2016,Fall,d
+385,CS,3200,2016,Fall,d
+116,CS,3200,2017,Spring,a
+264,CS,3200,2017,Spring,a
+377,CS,3200,2017,Spring,a
+397,CS,3200,2017,Spring,a
+133,CS,3200,2018,Spring,a
+165,CS,3200,2018,Spring,a
+197,CS,3200,2018,Spring,a
+257,CS,3200,2018,Spring,a
+274,CS,3200,2018,Spring,a
+255,CS,3200,2018,Spring,b
+276,CS,3200,2018,Spring,b
+391,CS,3200,2018,Spring,b
+109,CS,3200,2018,Spring,c
+285,CS,3200,2018,Spring,c
+388,CS,3200,2018,Spring,c
+139,CS,3200,2019,Spring,a
+164,CS,3200,2019,Spring,a
+277,CS,3200,2019,Spring,a
+372,CS,3200,2019,Spring,a
+131,CS,3200,2020,Spring,a
+194,CS,3200,2020,Spring,a
+228,CS,3200,2020,Spring,a
+303,CS,3200,2020,Spring,a
+342,CS,3200,2020,Spring,a
+187,CS,3200,2020,Spring,b
+108,CS,3200,2020,Spring,c
+248,CS,3200,2020,Spring,c
+325,CS,3200,2020,Spring,c
+332,CS,3200,2020,Spring,c
+378,CS,3200,2020,Spring,c
+398,CS,3200,2020,Spring,c
+112,CS,3200,2020,Summer,a
+113,CS,3200,2020,Summer,a
+177,CS,3200,2020,Summer,a
+185,CS,3200,2020,Summer,a
+231,CS,3200,2020,Summer,a
+242,CS,3200,2020,Summer,a
+254,CS,3200,2020,Summer,a
+260,CS,3200,2020,Summer,a
+292,CS,3200,2020,Summer,a
+306,CS,3200,2020,Summer,a
+311,CS,3200,2020,Summer,a
+375,CS,3200,2020,Summer,a
+124,CS,3200,2020,Fall,a
+135,CS,3200,2020,Fall,a
+161,CS,3200,2020,Fall,a
+178,CS,3200,2020,Fall,a
+230,CS,3200,2020,Fall,a
+345,CS,3200,2020,Fall,a
+376,CS,3200,2020,Fall,a
+149,CS,3500,2015,Fall,b
+246,CS,3500,2015,Fall,b
+313,CS,3500,2015,Fall,b
+123,CS,3500,2016,Spring,a
+229,CS,3500,2016,Spring,a
+277,CS,3500,2016,Spring,a
+374,CS,3500,2016,Spring,a
+395,CS,3500,2016,Spring,a
+107,CS,3500,2016,Summer,a
+282,CS,3500,2016,Summer,a
+288,CS,3500,2016,Summer,a
+379,CS,3500,2016,Summer,a
+292,CS,3500,2017,Summer,a
+311,CS,3500,2017,Summer,a
+182,CS,3500,2017,Fall,a
+314,CS,3500,2017,Fall,a
+335,CS,3500,2017,Fall,a
+391,CS,3500,2017,Fall,a
+109,CS,3500,2017,Fall,b
+131,CS,3500,2017,Fall,b
+355,CS,3500,2017,Fall,b
+203,CS,3500,2017,Fall,c
+275,CS,3500,2017,Fall,c
+294,CS,3500,2017,Fall,c
+309,CS,3500,2017,Fall,c
+385,CS,3500,2017,Fall,c
+392,CS,3500,2017,Fall,c
+118,CS,3500,2019,Summer,a
+152,CS,3500,2019,Summer,a
+179,CS,3500,2019,Summer,a
+228,CS,3500,2019,Summer,a
+258,CS,3500,2019,Summer,a
+276,CS,3500,2019,Summer,a
+396,CS,3500,2019,Summer,a
+180,CS,3500,2019,Fall,a
+255,CS,3500,2019,Fall,a
+332,CS,3500,2019,Fall,a
+377,CS,3500,2019,Fall,a
+380,CS,3500,2019,Fall,a
+397,CS,3500,2019,Fall,a
+108,CS,3500,2019,Fall,b
+133,CS,3500,2019,Fall,b
+171,CS,3500,2019,Fall,b
+199,CS,3500,2019,Fall,b
+223,CS,3500,2019,Fall,b
+270,CS,3500,2019,Fall,b
+321,CS,3500,2019,Fall,b
+375,CS,3500,2019,Fall,b
+143,CS,3500,2019,Fall,c
+363,CS,3500,2019,Fall,c
+112,CS,3500,2020,Summer,a
+124,CS,3500,2020,Summer,a
+127,CS,3500,2020,Summer,a
+142,CS,3500,2020,Summer,a
+164,CS,3500,2020,Summer,a
+166,CS,3500,2020,Summer,a
+247,CS,3500,2020,Summer,a
+260,CS,3500,2020,Summer,a
+281,CS,3500,2020,Summer,a
+312,CS,3500,2020,Summer,a
+325,CS,3500,2020,Summer,a
+329,CS,3500,2020,Summer,a
+331,CS,3500,2020,Summer,a
+333,CS,3500,2020,Summer,a
+347,CS,3500,2020,Summer,a
+348,CS,3500,2020,Summer,a
+364,CS,3500,2020,Summer,a
+365,CS,3500,2020,Summer,a
+373,CS,3500,2020,Summer,a
+386,CS,3500,2020,Summer,a
+192,CS,3505,2015,Spring,a
+282,CS,3505,2015,Spring,a
+211,CS,3505,2015,Fall,a
+313,CS,3505,2015,Fall,a
+182,CS,3505,2015,Fall,b
+335,CS,3505,2015,Fall,b
+392,CS,3505,2015,Fall,b
+126,CS,3505,2015,Fall,c
+162,CS,3505,2015,Fall,c
+348,CS,3505,2015,Fall,d
+107,CS,3505,2016,Summer,a
+163,CS,3505,2016,Summer,a
+290,CS,3505,2016,Summer,a
+378,CS,3505,2016,Summer,a
+393,CS,3505,2016,Summer,a
+123,CS,3505,2016,Fall,a
+379,CS,3505,2016,Fall,a
+116,CS,3505,2016,Fall,b
+249,CS,3505,2016,Fall,b
+329,CS,3505,2016,Fall,b
+151,CS,3505,2017,Summer,a
+260,CS,3505,2017,Summer,a
+312,CS,3505,2017,Summer,a
+124,CS,3505,2017,Fall,a
+128,CS,3505,2017,Fall,a
+199,CS,3505,2017,Fall,a
+214,CS,3505,2017,Fall,a
+355,CS,3505,2017,Fall,a
+397,CS,3505,2017,Fall,a
+102,CS,3505,2017,Fall,b
+131,CS,3505,2017,Fall,b
+177,CS,3505,2017,Fall,b
+199,CS,3505,2017,Fall,b
+208,CS,3505,2017,Fall,b
+294,CS,3505,2017,Fall,b
+321,CS,3505,2017,Fall,b
+385,CS,3505,2017,Fall,b
+100,CS,3505,2018,Summer,a
+101,CS,3505,2018,Summer,a
+197,CS,3505,2018,Summer,a
+247,CS,3505,2018,Summer,a
+255,CS,3505,2018,Summer,a
+368,CS,3505,2018,Summer,a
+374,CS,3505,2018,Summer,a
+377,CS,3505,2018,Summer,a
+386,CS,3505,2018,Summer,a
+127,CS,3505,2018,Summer,b
+143,CS,3505,2018,Summer,b
+173,CS,3505,2018,Summer,b
+185,CS,3505,2018,Summer,b
+247,CS,3505,2018,Summer,b
+259,CS,3505,2018,Summer,b
+262,CS,3505,2018,Summer,b
+288,CS,3505,2018,Summer,b
+156,CS,3505,2018,Fall,a
+179,CS,3505,2018,Fall,a
+240,CS,3505,2018,Fall,a
+256,CS,3505,2018,Fall,a
+258,CS,3505,2018,Fall,a
+305,CS,3505,2018,Fall,a
+345,CS,3505,2018,Fall,a
+371,CS,3505,2018,Fall,a
+252,CS,3505,2018,Fall,b
+285,CS,3505,2018,Fall,c
+371,CS,3505,2018,Fall,c
+396,CS,3505,2018,Fall,c
+152,CS,3505,2019,Spring,a
+228,CS,3505,2019,Spring,a
+241,CS,3505,2019,Spring,a
+276,CS,3505,2019,Spring,a
+320,CS,3505,2019,Spring,a
+187,CS,3505,2019,Spring,b
+230,CS,3505,2019,Spring,b
+314,CS,3505,2019,Spring,b
+358,CS,3505,2019,Spring,b
+119,CS,3505,2019,Summer,a
+169,CS,3505,2019,Summer,a
+220,CS,3505,2019,Summer,a
+296,CS,3505,2019,Summer,a
+307,CS,3505,2019,Summer,a
+129,CS,3505,2019,Summer,b
+223,CS,3505,2019,Summer,b
+238,CS,3505,2019,Summer,b
+296,CS,3505,2019,Summer,b
+298,CS,3505,2019,Summer,b
+300,CS,3505,2019,Summer,b
+340,CS,3505,2019,Summer,b
+372,CS,3505,2019,Summer,b
+373,CS,3505,2019,Summer,b
+380,CS,3505,2019,Summer,b
+129,CS,3505,2019,Summer,c
+300,CS,3505,2019,Summer,c
+384,CS,3505,2019,Summer,c
+113,CS,3505,2019,Summer,d
+133,CS,3505,2019,Summer,d
+270,CS,3505,2019,Summer,d
+292,CS,3505,2019,Summer,d
+318,CS,3505,2019,Summer,d
+356,CS,3505,2019,Summer,d
+362,CS,3505,2019,Summer,d
+178,CS,3505,2019,Fall,a
+284,CS,3505,2019,Fall,a
+391,CS,3505,2019,Fall,a
+118,CS,3505,2019,Fall,b
+289,CS,3505,2019,Fall,b
+309,CS,3505,2019,Fall,b
+399,CS,3505,2019,Fall,b
+194,CS,3505,2019,Fall,c
+235,CS,3505,2019,Fall,c
+248,CS,3505,2019,Fall,c
+311,CS,3505,2019,Fall,c
+391,CS,3505,2019,Fall,c
+146,CS,3505,2020,Spring,a
+164,CS,3505,2020,Spring,a
+277,CS,3505,2020,Spring,a
+332,CS,3505,2020,Spring,a
+137,CS,3505,2020,Summer,a
+200,CS,3505,2020,Summer,a
+219,CS,3505,2020,Summer,a
+257,CS,3505,2020,Summer,a
+267,CS,3505,2020,Summer,a
+306,CS,3505,2020,Summer,a
+365,CS,3505,2020,Summer,a
+142,CS,3505,2020,Fall,a
+339,CS,3505,2020,Fall,a
+398,CS,3505,2020,Fall,a
+106,CS,3505,2020,Fall,b
+110,CS,3505,2020,Fall,b
+121,CS,3505,2020,Fall,b
+333,CS,3505,2020,Fall,b
+109,CS,3505,2020,Fall,c
+120,CS,3505,2020,Fall,c
+171,CS,3505,2020,Fall,c
+250,CS,3505,2020,Fall,c
+293,CS,3505,2020,Fall,c
+390,CS,3505,2020,Fall,c
+140,CS,3810,2015,Spring,a
+276,CS,3810,2015,Spring,a
+123,CS,3810,2016,Summer,a
+160,CS,3810,2016,Summer,a
+314,CS,3810,2016,Summer,a
+393,CS,3810,2016,Summer,a
+107,CS,3810,2016,Fall,a
+195,CS,3810,2016,Fall,a
+213,CS,3810,2016,Fall,a
+282,CS,3810,2016,Fall,a
+285,CS,3810,2016,Fall,a
+348,CS,3810,2016,Fall,a
+105,CS,3810,2016,Fall,b
+116,CS,3810,2016,Fall,b
+245,CS,3810,2016,Fall,b
+264,CS,3810,2016,Fall,b
+329,CS,3810,2016,Fall,b
+335,CS,3810,2016,Fall,b
+173,CS,3810,2018,Spring,a
+179,CS,3810,2018,Spring,a
+230,CS,3810,2018,Spring,a
+237,CS,3810,2018,Spring,a
+255,CS,3810,2018,Spring,a
+305,CS,3810,2018,Spring,a
+313,CS,3810,2018,Spring,a
+372,CS,3810,2018,Spring,a
+388,CS,3810,2018,Spring,a
+129,CS,3810,2018,Summer,a
+177,CS,3810,2018,Summer,a
+260,CS,3810,2018,Summer,a
+374,CS,3810,2018,Summer,a
+386,CS,3810,2018,Summer,a
+177,CS,3810,2018,Summer,b
+214,CS,3810,2018,Summer,b
+231,CS,3810,2018,Summer,b
+270,CS,3810,2018,Summer,b
+288,CS,3810,2018,Summer,b
+344,CS,3810,2018,Summer,b
+377,CS,3810,2018,Summer,b
+399,CS,3810,2018,Summer,b
+128,CS,3810,2018,Summer,c
+129,CS,3810,2018,Summer,c
+133,CS,3810,2018,Summer,c
+151,CS,3810,2018,Summer,c
+240,CS,3810,2018,Summer,c
+257,CS,3810,2018,Summer,c
+311,CS,3810,2018,Summer,c
+182,CS,3810,2018,Summer,d
+210,CS,3810,2018,Summer,d
+252,CS,3810,2018,Summer,d
+270,CS,3810,2018,Summer,d
+312,CS,3810,2018,Summer,d
+356,CS,3810,2018,Summer,d
+379,CS,3810,2018,Summer,d
+127,CS,3810,2019,Fall,a
+131,CS,3810,2019,Fall,a
+241,CS,3810,2019,Fall,a
+258,CS,3810,2019,Fall,a
+333,CS,3810,2019,Fall,a
+102,CS,3810,2019,Fall,b
+359,CS,3810,2019,Fall,b
+113,CS,3810,2020,Fall,a
+124,CS,3810,2020,Fall,a
+171,CS,3810,2020,Fall,a
+187,CS,3810,2020,Fall,a
+220,CS,3810,2020,Fall,a
+225,CS,3810,2020,Fall,a
+233,CS,3810,2020,Fall,a
+340,CS,3810,2020,Fall,a
+347,CS,3810,2020,Fall,a
+193,CS,4000,2015,Spring,a
+160,CS,4000,2015,Summer,a
+282,CS,4000,2015,Fall,a
+307,CS,4000,2015,Fall,a
+138,CS,4000,2016,Fall,a
+276,CS,4000,2016,Fall,a
+321,CS,4000,2016,Fall,a
+378,CS,4000,2016,Fall,a
+393,CS,4000,2016,Fall,a
+151,CS,4000,2017,Spring,a
+187,CS,4000,2017,Spring,a
+207,CS,4000,2017,Spring,a
+255,CS,4000,2017,Spring,a
+134,CS,4000,2017,Summer,a
+139,CS,4000,2017,Summer,a
+179,CS,4000,2017,Summer,a
+259,CS,4000,2017,Summer,a
+318,CS,4000,2017,Summer,a
+373,CS,4000,2017,Summer,a
+107,CS,4000,2017,Fall,a
+163,CS,4000,2017,Fall,a
+252,CS,4000,2017,Fall,a
+262,CS,4000,2017,Fall,a
+291,CS,4000,2017,Fall,a
+342,CS,4000,2017,Fall,a
+361,CS,4000,2017,Fall,a
+163,CS,4000,2017,Fall,b
+329,CS,4000,2017,Fall,b
+345,CS,4000,2017,Fall,b
+361,CS,4000,2017,Fall,b
+164,CS,4000,2018,Spring,a
+173,CS,4000,2018,Spring,a
+203,CS,4000,2018,Spring,a
+275,CS,4000,2018,Spring,a
+313,CS,4000,2018,Spring,a
+385,CS,4000,2018,Spring,a
+127,CS,4000,2019,Spring,a
+256,CS,4000,2019,Spring,a
+169,CS,4000,2020,Spring,a
+181,CS,4000,2020,Spring,a
+254,CS,4000,2020,Spring,a
+257,CS,4000,2020,Spring,a
+285,CS,4000,2020,Spring,a
+312,CS,4000,2020,Spring,a
+364,CS,4000,2020,Spring,a
+375,CS,4000,2020,Spring,a
+386,CS,4000,2020,Spring,a
+123,CS,4000,2020,Spring,b
+152,CS,4000,2020,Spring,b
+181,CS,4000,2020,Spring,b
+257,CS,4000,2020,Spring,b
+309,CS,4000,2020,Spring,b
+311,CS,4000,2020,Spring,b
+371,CS,4000,2020,Spring,b
+109,CS,4000,2020,Fall,a
+110,CS,4000,2020,Fall,a
+118,CS,4000,2020,Fall,a
+120,CS,4000,2020,Fall,a
+131,CS,4000,2020,Fall,a
+161,CS,4000,2020,Fall,a
+185,CS,4000,2020,Fall,a
+277,CS,4000,2020,Fall,a
+292,CS,4000,2020,Fall,a
+341,CS,4000,2020,Fall,a
+348,CS,4000,2020,Fall,a
+366,CS,4000,2020,Fall,a
+368,CS,4000,2020,Fall,a
+376,CS,4000,2020,Fall,a
+397,CS,4000,2020,Fall,a
+162,CS,4150,2015,Summer,a
+176,CS,4150,2015,Summer,a
+192,CS,4150,2015,Summer,a
+204,CS,4150,2015,Summer,a
+348,CS,4150,2015,Summer,b
+163,CS,4150,2016,Summer,a
+245,CS,4150,2016,Summer,a
+249,CS,4150,2016,Summer,a
+378,CS,4150,2016,Summer,a
+249,CS,4150,2016,Summer,b
+264,CS,4150,2016,Summer,b
+285,CS,4150,2016,Summer,b
+288,CS,4150,2016,Summer,b
+131,CS,4150,2018,Fall,a
+240,CS,4150,2018,Fall,a
+270,CS,4150,2018,Fall,a
+292,CS,4150,2018,Fall,a
+362,CS,4150,2018,Fall,a
+391,CS,4150,2018,Fall,a
+255,CS,4150,2018,Fall,b
+371,CS,4150,2018,Fall,b
+102,CS,4150,2019,Spring,a
+210,CS,4150,2019,Spring,a
+260,CS,4150,2019,Spring,a
+106,CS,4150,2020,Spring,a
+120,CS,4150,2020,Spring,a
+123,CS,4150,2020,Spring,a
+125,CS,4150,2020,Spring,a
+179,CS,4150,2020,Spring,a
+277,CS,4150,2020,Spring,a
+314,CS,4150,2020,Spring,a
+396,CS,4150,2020,Spring,a
+397,CS,4150,2020,Spring,a
+135,CS,4150,2020,Fall,a
+148,CS,4150,2020,Fall,a
+235,CS,4150,2020,Fall,a
+309,CS,4150,2020,Fall,a
+329,CS,4150,2020,Fall,a
+339,CS,4150,2020,Fall,a
+347,CS,4150,2020,Fall,a
+386,CS,4150,2020,Fall,a
+120,CS,4400,2015,Summer,a
+140,CS,4400,2015,Summer,a
+215,CS,4400,2015,Summer,a
+277,CS,4400,2015,Summer,a
+290,CS,4400,2015,Summer,a
+392,CS,4400,2015,Fall,b
+282,CS,4400,2015,Fall,c
+373,CS,4400,2015,Fall,c
+149,CS,4400,2016,Spring,a
+307,CS,4400,2016,Spring,a
+179,CS,4400,2016,Summer,a
+262,CS,4400,2016,Summer,a
+138,CS,4400,2016,Fall,a
+102,CS,4400,2017,Spring,a
+246,CS,4400,2017,Spring,a
+249,CS,4400,2017,Spring,a
+329,CS,4400,2017,Spring,a
+369,CS,4400,2017,Spring,a
+231,CS,4400,2017,Spring,b
+255,CS,4400,2017,Spring,b
+309,CS,4400,2017,Spring,b
+276,CS,4400,2017,Spring,c
+313,CS,4400,2017,Spring,c
+388,CS,4400,2017,Spring,c
+321,CS,4400,2019,Spring,a
+333,CS,4400,2019,Spring,a
+379,CS,4400,2019,Spring,a
+109,CS,4400,2019,Spring,b
+128,CS,4400,2019,Spring,b
+151,CS,4400,2019,Spring,b
+275,CS,4400,2019,Spring,b
+169,CS,4400,2019,Spring,c
+187,CS,4400,2019,Spring,c
+248,CS,4400,2019,Spring,c
+257,CS,4400,2019,Spring,d
+312,CS,4400,2019,Spring,d
+345,CS,4400,2019,Spring,d
+146,CS,4400,2019,Summer,a
+167,CS,4400,2019,Summer,a
+173,CS,4400,2019,Summer,a
+234,CS,4400,2019,Summer,a
+285,CS,4400,2019,Summer,a
+287,CS,4400,2019,Summer,a
+294,CS,4400,2019,Summer,a
+325,CS,4400,2019,Summer,a
+397,CS,4400,2019,Summer,a
+398,CS,4400,2019,Summer,a
+135,CS,4400,2019,Summer,b
+143,CS,4400,2019,Summer,b
+177,CS,4400,2019,Summer,b
+267,CS,4400,2019,Summer,b
+285,CS,4400,2019,Summer,b
+298,CS,4400,2019,Summer,b
+332,CS,4400,2019,Summer,b
+368,CS,4400,2019,Summer,b
+391,CS,4400,2019,Summer,b
+183,CS,4400,2019,Fall,a
+241,CS,4400,2019,Fall,a
+124,CS,4400,2019,Fall,b
+259,CS,4400,2019,Fall,b
+364,CS,4400,2019,Fall,b
+377,CS,4400,2019,Fall,b
+113,CS,4400,2020,Spring,a
+170,CS,4400,2020,Spring,a
+199,CS,4400,2020,Spring,a
+228,CS,4400,2020,Spring,a
+348,CS,4400,2020,Spring,a
+390,CS,4400,2020,Spring,a
+119,CS,4400,2020,Fall,a
+123,CS,4400,2020,Fall,a
+131,CS,4400,2020,Fall,a
+152,CS,4400,2020,Fall,a
+230,CS,4400,2020,Fall,a
+258,CS,4400,2020,Fall,a
+272,CS,4400,2020,Fall,a
+378,CS,4400,2020,Fall,a
+106,CS,4400,2020,Fall,b
+127,CS,4400,2020,Fall,b
+185,CS,4400,2020,Fall,b
+202,CS,4400,2020,Fall,b
+235,CS,4400,2020,Fall,b
+292,CS,4400,2020,Fall,b
+340,CS,4400,2020,Fall,b
+276,CS,4500,2015,Summer,a
+290,CS,4500,2015,Summer,b
+215,CS,4500,2016,Spring,a
+317,CS,4500,2016,Spring,a
+119,CS,4500,2016,Spring,b
+138,CS,4500,2016,Spring,b
+149,CS,4500,2016,Spring,b
+162,CS,4500,2016,Spring,b
+179,CS,4500,2016,Spring,b
+215,CS,4500,2016,Spring,b
+285,CS,4500,2016,Spring,b
+301,CS,4500,2016,Spring,b
+307,CS,4500,2016,Spring,b
+321,CS,4500,2016,Spring,b
+357,CS,4500,2016,Spring,b
+117,CS,4500,2016,Fall,a
+176,CS,4500,2016,Fall,a
+177,CS,4500,2016,Fall,a
+309,CS,4500,2016,Fall,a
+139,CS,4500,2017,Summer,a
+207,CS,4500,2017,Summer,a
+335,CS,4500,2017,Summer,a
+348,CS,4500,2017,Summer,a
+378,CS,4500,2017,Summer,a
+101,CS,4500,2018,Spring,a
+128,CS,4500,2018,Spring,a
+132,CS,4500,2018,Spring,a
+182,CS,4500,2018,Spring,a
+203,CS,4500,2018,Spring,a
+231,CS,4500,2018,Spring,a
+294,CS,4500,2018,Spring,a
+329,CS,4500,2018,Spring,a
+361,CS,4500,2018,Spring,a
+132,CS,4500,2018,Spring,b
+270,CS,4500,2018,Spring,b
+305,CS,4500,2018,Spring,b
+318,CS,4500,2018,Spring,b
+379,CS,4500,2018,Spring,b
+133,CS,4500,2018,Spring,c
+164,CS,4500,2018,Spring,c
+312,CS,4500,2018,Spring,c
+369,CS,4500,2018,Spring,c
+128,CS,4500,2018,Spring,d
+313,CS,4500,2018,Spring,d
+345,CS,4500,2018,Spring,d
+366,CS,4500,2018,Spring,d
+391,CS,4500,2018,Spring,d
+107,CS,4500,2019,Summer,a
+123,CS,4500,2019,Summer,a
+185,CS,4500,2019,Summer,a
+248,CS,4500,2019,Summer,a
+333,CS,4500,2019,Summer,a
+340,CS,4500,2019,Summer,a
+371,CS,4500,2019,Summer,a
+386,CS,4500,2019,Summer,a
+256,CS,4500,2019,Fall,a
+260,CS,4500,2019,Fall,a
+293,CS,4500,2019,Fall,a
+303,CS,4500,2019,Fall,a
+131,CS,4500,2019,Fall,b
+173,CS,4500,2019,Fall,b
+250,CS,4500,2019,Fall,b
+255,CS,4500,2019,Fall,b
+300,CS,4500,2019,Fall,b
+398,CS,4500,2019,Fall,b
+131,CS,4500,2019,Fall,c
+143,CS,4500,2019,Fall,c
+256,CS,4500,2019,Fall,c
+274,CS,4500,2019,Fall,c
+316,CS,4500,2019,Fall,c
+109,CS,4500,2019,Fall,d
+194,CS,4500,2019,Fall,d
+220,CS,4500,2019,Fall,d
+254,CS,4500,2019,Fall,d
+255,CS,4500,2019,Fall,d
+296,CS,4500,2019,Fall,d
+341,CS,4500,2019,Fall,d
+365,CS,4500,2019,Fall,d
+108,CS,4500,2020,Spring,a
+142,CS,4500,2020,Spring,a
+169,CS,4500,2020,Spring,a
+200,CS,4500,2020,Spring,a
+364,CS,4500,2020,Spring,a
+373,CS,4500,2020,Spring,a
+127,CS,4500,2020,Summer,a
+152,CS,4500,2020,Summer,a
+167,CS,4500,2020,Summer,a
+240,CS,4500,2020,Summer,a
+368,CS,4500,2020,Summer,a
+397,CS,4500,2020,Summer,a
+138,CS,4940,2015,Summer,a
+117,CS,4940,2017,Fall,a
+143,CS,4940,2017,Fall,a
+260,CS,4940,2017,Fall,a
+294,CS,4940,2017,Fall,a
+311,CS,4940,2017,Fall,a
+326,CS,4940,2017,Fall,a
+119,CS,4940,2017,Fall,b
+379,CS,4940,2017,Fall,b
+167,CS,4940,2019,Fall,a
+220,CS,4940,2019,Fall,a
+255,CS,4940,2019,Fall,a
+256,CS,4940,2019,Fall,a
+285,CS,4940,2019,Fall,a
+314,CS,4940,2019,Fall,a
+398,CS,4940,2019,Fall,a
+100,CS,4940,2020,Summer,a
+170,CS,4940,2020,Summer,a
+200,CS,4940,2020,Summer,a
+228,CS,4940,2020,Summer,a
+251,CS,4940,2020,Summer,a
+258,CS,4940,2020,Summer,a
+277,CS,4940,2020,Summer,a
+292,CS,4940,2020,Summer,a
+313,CS,4940,2020,Summer,a
+331,CS,4940,2020,Summer,a
+362,CS,4940,2020,Summer,a
+378,CS,4940,2020,Summer,a
+386,CS,4940,2020,Summer,a
+391,CS,4940,2020,Summer,a
+397,CS,4940,2020,Summer,a
+100,CS,4940,2020,Summer,b
+123,CS,4940,2020,Summer,b
+127,CS,4940,2020,Summer,b
+171,CS,4940,2020,Summer,b
+177,CS,4940,2020,Summer,b
+194,CS,4940,2020,Summer,b
+231,CS,4940,2020,Summer,b
+233,CS,4940,2020,Summer,b
+247,CS,4940,2020,Summer,b
+250,CS,4940,2020,Summer,b
+251,CS,4940,2020,Summer,b
+258,CS,4940,2020,Summer,b
+271,CS,4940,2020,Summer,b
+277,CS,4940,2020,Summer,b
+300,CS,4940,2020,Summer,b
+312,CS,4940,2020,Summer,b
+321,CS,4940,2020,Summer,b
+339,CS,4940,2020,Summer,b
+345,CS,4940,2020,Summer,b
+391,CS,4940,2020,Summer,b
+397,CS,4940,2020,Summer,b
+107,CS,4970,2016,Fall,a
+123,CS,4970,2016,Fall,a
+145,CS,4970,2016,Fall,a
+268,CS,4970,2016,Fall,a
+276,CS,4970,2016,Fall,a
+285,CS,4970,2016,Fall,a
+335,CS,4970,2016,Fall,a
+394,CS,4970,2016,Fall,a
+177,CS,4970,2016,Fall,b
+179,CS,4970,2016,Fall,b
+249,CS,4970,2016,Fall,b
+276,CS,4970,2016,Fall,b
+285,CS,4970,2016,Fall,b
+291,CS,4970,2016,Fall,b
+312,CS,4970,2016,Fall,b
+313,CS,4970,2016,Fall,b
+397,CS,4970,2016,Fall,b
+116,CS,4970,2017,Spring,a
+120,CS,4970,2017,Spring,a
+282,CS,4970,2017,Spring,a
+295,CS,4970,2017,Spring,a
+314,CS,4970,2017,Spring,a
+393,CS,4970,2017,Spring,a
+117,CS,4970,2017,Summer,a
+261,CS,4970,2017,Summer,a
+288,CS,4970,2017,Summer,a
+231,CS,4970,2018,Summer,a
+270,CS,4970,2018,Summer,a
+277,CS,4970,2018,Summer,a
+344,CS,4970,2018,Summer,a
+398,CS,4970,2018,Summer,a
+100,CS,4970,2018,Summer,b
+105,CS,4970,2018,Summer,b
+132,CS,4970,2018,Summer,b
+227,CS,4970,2018,Summer,b
+277,CS,4970,2018,Summer,b
+348,CS,4970,2018,Summer,b
+133,CS,4970,2018,Summer,c
+163,CS,4970,2018,Summer,c
+185,CS,4970,2018,Summer,c
+214,CS,4970,2018,Summer,c
+220,CS,4970,2018,Summer,c
+372,CS,4970,2018,Summer,c
+387,CS,4970,2018,Summer,c
+392,CS,4970,2018,Summer,c
+274,CS,4970,2018,Fall,a
+128,CS,4970,2018,Fall,b
+247,CS,4970,2018,Fall,b
+262,CS,4970,2018,Fall,b
+267,CS,4970,2018,Fall,b
+386,CS,4970,2018,Fall,b
+121,CS,4970,2018,Fall,c
+143,CS,4970,2018,Fall,c
+196,CS,4970,2018,Fall,c
+102,CS,4970,2018,Fall,d
+121,CS,4970,2018,Fall,d
+178,CS,4970,2018,Fall,d
+255,CS,4970,2018,Fall,d
+267,CS,4970,2018,Fall,d
+342,CS,4970,2018,Fall,d
+356,CS,4970,2018,Fall,d
+165,CS,4970,2019,Spring,a
+275,CS,4970,2019,Spring,a
+351,CS,4970,2019,Spring,a
+366,CS,4970,2019,Spring,a
+311,CS,4970,2019,Spring,b
+345,CS,4970,2019,Spring,b
+364,CS,4970,2019,Spring,b
+124,CS,4970,2019,Summer,a
+199,CS,4970,2019,Summer,a
+289,CS,4970,2019,Summer,a
+300,CS,4970,2019,Summer,a
+368,CS,4970,2019,Summer,a
+378,CS,4970,2019,Summer,a
+113,CS,4970,2019,Summer,b
+164,CS,4970,2019,Summer,b
+298,CS,4970,2019,Summer,b
+325,CS,4970,2019,Summer,b
+359,CS,4970,2019,Summer,b
+378,CS,4970,2019,Summer,b
+391,CS,4970,2019,Summer,b
+173,CS,4970,2019,Summer,c
+333,CS,4970,2019,Summer,c
+363,CS,4970,2019,Summer,c
+119,CS,4970,2019,Summer,d
+135,CS,4970,2019,Summer,d
+164,CS,4970,2019,Summer,d
+294,CS,4970,2019,Summer,d
+303,CS,4970,2019,Summer,d
+329,CS,4970,2019,Summer,d
+362,CS,4970,2019,Summer,d
+399,CS,4970,2019,Summer,d
+194,CS,4970,2019,Fall,a
+235,CS,4970,2019,Fall,a
+250,CS,4970,2019,Fall,a
+127,CS,4970,2019,Fall,b
+131,CS,4970,2019,Fall,b
+293,CS,4970,2019,Fall,b
+321,CS,4970,2019,Fall,b
+152,CS,4970,2019,Fall,c
+200,CS,4970,2019,Fall,c
+259,CS,4970,2019,Fall,c
+318,CS,4970,2019,Fall,d
+340,CS,4970,2019,Fall,d
+347,CS,4970,2019,Fall,d
+112,CS,4970,2020,Summer,a
+221,CS,4970,2020,Summer,a
+242,CS,4970,2020,Summer,a
+251,CS,4970,2020,Summer,a
+257,CS,4970,2020,Summer,a
+118,CS,4970,2020,Summer,b
+151,CS,4970,2020,Summer,b
+187,CS,4970,2020,Summer,b
+219,CS,4970,2020,Summer,b
+221,CS,4970,2020,Summer,b
+222,CS,4970,2020,Summer,b
+309,CS,4970,2020,Summer,b
+373,CS,4970,2020,Summer,b
+379,CS,4970,2020,Summer,b
+146,CS,4970,2020,Summer,c
+233,CS,4970,2020,Summer,c
+257,CS,4970,2020,Summer,c
+260,CS,4970,2020,Summer,c
+292,CS,4970,2020,Summer,c
+339,CS,4970,2020,Summer,c
+379,CS,4970,2020,Summer,c
+384,CS,4970,2020,Summer,c
+109,CS,4970,2020,Summer,d
+146,CS,4970,2020,Summer,d
+151,CS,4970,2020,Summer,d
+171,CS,4970,2020,Summer,d
+228,CS,4970,2020,Summer,d
+254,CS,4970,2020,Summer,d
+307,CS,4970,2020,Summer,d
+309,CS,4970,2020,Summer,d
+379,CS,4970,2020,Summer,d
+390,CS,4970,2020,Summer,d
+122,CS,4970,2020,Fall,a
+191,CS,4970,2020,Fall,a
+136,CS,4970,2020,Fall,b
+283,CS,4970,2020,Fall,b
+130,CS,4970,2020,Fall,c
+148,CS,4970,2020,Fall,c
+281,CS,4970,2020,Fall,c
+186,CS,4970,2020,Fall,d
+202,CS,4970,2020,Fall,d
+323,CS,4970,2020,Fall,d
+341,CS,4970,2020,Fall,d
+120,MATH,1210,2015,Summer,a
+138,MATH,1210,2015,Summer,a
+117,MATH,1210,2016,Spring,a
+119,MATH,1210,2016,Spring,a
+144,MATH,1210,2016,Spring,a
+270,MATH,1210,2016,Spring,a
+276,MATH,1210,2016,Spring,a
+229,MATH,1210,2016,Spring,b
+295,MATH,1210,2016,Spring,b
+335,MATH,1210,2016,Spring,b
+182,MATH,1210,2016,Spring,c
+277,MATH,1210,2016,Spring,c
+179,MATH,1210,2016,Spring,d
+273,MATH,1210,2016,Spring,d
+277,MATH,1210,2016,Spring,d
+295,MATH,1210,2016,Spring,d
+214,MATH,1210,2016,Fall,a
+249,MATH,1210,2016,Fall,a
+397,MATH,1210,2016,Fall,a
+215,MATH,1210,2016,Fall,b
+278,MATH,1210,2016,Fall,b
+357,MATH,1210,2016,Fall,b
+378,MATH,1210,2016,Fall,b
+107,MATH,1210,2016,Fall,c
+195,MATH,1210,2016,Fall,c
+285,MATH,1210,2016,Fall,c
+369,MATH,1210,2016,Fall,c
+379,MATH,1210,2016,Fall,c
+195,MATH,1210,2016,Fall,d
+385,MATH,1210,2016,Fall,d
+356,MATH,1210,2017,Spring,a
+394,MATH,1210,2017,Spring,a
+345,MATH,1210,2017,Summer,a
+230,MATH,1210,2017,Summer,b
+210,MATH,1210,2017,Summer,c
+342,MATH,1210,2017,Summer,c
+387,MATH,1210,2017,Summer,c
+392,MATH,1210,2017,Summer,c
+102,MATH,1210,2018,Spring,a
+199,MATH,1210,2018,Spring,a
+372,MATH,1210,2018,Spring,a
+257,MATH,1210,2018,Summer,a
+279,MATH,1210,2018,Summer,a
+288,MATH,1210,2018,Summer,a
+368,MATH,1210,2018,Summer,a
+371,MATH,1210,2018,Summer,a
+398,MATH,1210,2018,Summer,a
+167,MATH,1210,2018,Fall,a
+177,MATH,1210,2018,Fall,a
+185,MATH,1210,2018,Fall,a
+231,MATH,1210,2018,Fall,a
+311,MATH,1210,2018,Fall,a
+312,MATH,1210,2018,Fall,a
+384,MATH,1210,2018,Fall,a
+104,MATH,1210,2018,Fall,b
+128,MATH,1210,2018,Fall,b
+163,MATH,1210,2018,Fall,b
+178,MATH,1210,2018,Fall,b
+133,MATH,1210,2019,Spring,a
+294,MATH,1210,2019,Spring,a
+307,MATH,1210,2019,Spring,a
+332,MATH,1210,2019,Spring,a
+333,MATH,1210,2019,Spring,a
+348,MATH,1210,2019,Spring,a
+351,MATH,1210,2019,Spring,a
+275,MATH,1210,2019,Spring,b
+123,MATH,1210,2019,Summer,a
+124,MATH,1210,2019,Summer,a
+228,MATH,1210,2019,Summer,a
+255,MATH,1210,2019,Summer,a
+313,MATH,1210,2019,Summer,a
+135,MATH,1210,2020,Spring,a
+220,MATH,1210,2020,Spring,a
+310,MATH,1210,2020,Spring,a
+373,MATH,1210,2020,Spring,a
+390,MATH,1210,2020,Spring,a
+106,MATH,1210,2020,Spring,b
+108,MATH,1210,2020,Spring,b
+260,MATH,1210,2020,Spring,b
+386,MATH,1210,2020,Spring,b
+192,MATH,1220,2015,Summer,a
+211,MATH,1220,2015,Summer,a
+162,MATH,1220,2015,Summer,b
+270,MATH,1220,2015,Summer,b
+280,MATH,1220,2015,Summer,b
+195,MATH,1220,2015,Summer,c
+245,MATH,1220,2015,Summer,c
+282,MATH,1220,2015,Summer,c
+377,MATH,1220,2015,Summer,c
+210,MATH,1220,2016,Spring,a
+307,MATH,1220,2016,Spring,a
+313,MATH,1220,2016,Spring,a
+357,MATH,1220,2016,Spring,a
+389,MATH,1220,2016,Spring,a
+116,MATH,1220,2017,Spring,a
+187,MATH,1220,2017,Spring,a
+256,MATH,1220,2017,Spring,a
+299,MATH,1220,2017,Spring,a
+117,MATH,1220,2017,Spring,b
+163,MATH,1220,2017,Spring,b
+179,MATH,1220,2017,Spring,b
+182,MATH,1220,2017,Spring,b
+259,MATH,1220,2017,Spring,b
+260,MATH,1220,2017,Spring,b
+285,MATH,1220,2017,Spring,b
+314,MATH,1220,2017,Spring,b
+388,MATH,1220,2017,Spring,b
+393,MATH,1220,2017,Spring,b
+117,MATH,1220,2017,Spring,c
+145,MATH,1220,2017,Spring,c
+277,MATH,1220,2017,Spring,c
+355,MATH,1220,2017,Spring,c
+385,MATH,1220,2017,Spring,c
+105,MATH,1220,2017,Spring,d
+260,MATH,1220,2017,Spring,d
+378,MATH,1220,2017,Spring,d
+215,MATH,1220,2017,Summer,a
+165,MATH,1220,2018,Spring,a
+173,MATH,1220,2018,Spring,a
+276,MATH,1220,2018,Spring,a
+312,MATH,1220,2018,Spring,a
+332,MATH,1220,2018,Spring,a
+375,MATH,1220,2018,Spring,a
+131,MATH,1220,2018,Spring,b
+169,MATH,1220,2018,Spring,b
+309,MATH,1220,2018,Spring,b
+362,MATH,1220,2018,Spring,b
+139,MATH,1220,2018,Summer,a
+185,MATH,1220,2018,Summer,a
+348,MATH,1220,2018,Summer,a
+127,MATH,1220,2019,Fall,a
+133,MATH,1220,2019,Fall,a
+181,MATH,1220,2019,Fall,a
+231,MATH,1220,2019,Fall,a
+234,MATH,1220,2019,Fall,a
+248,MATH,1220,2019,Fall,a
+254,MATH,1220,2019,Fall,a
+323,MATH,1220,2019,Fall,a
+341,MATH,1220,2019,Fall,a
+102,MATH,1220,2019,Fall,b
+120,MATH,1220,2019,Fall,b
+123,MATH,1220,2019,Fall,b
+152,MATH,1220,2019,Fall,b
+180,MATH,1220,2019,Fall,b
+274,MATH,1220,2019,Fall,b
+321,MATH,1220,2019,Fall,b
+366,MATH,1220,2019,Fall,b
+135,MATH,1220,2019,Fall,c
+247,MATH,1220,2019,Fall,c
+358,MATH,1220,2019,Fall,c
+390,MATH,1220,2019,Fall,c
+396,MATH,1220,2019,Fall,c
+100,MATH,1220,2020,Spring,a
+151,MATH,1220,2020,Spring,a
+178,MATH,1220,2020,Spring,a
+228,MATH,1220,2020,Spring,a
+118,MATH,1220,2020,Summer,a
+164,MATH,1220,2020,Summer,a
+281,MATH,1220,2020,Summer,a
+293,MATH,1220,2020,Summer,a
+329,MATH,1220,2020,Summer,a
+397,MATH,1220,2020,Summer,a
+211,MATH,1250,2015,Spring,c
+276,MATH,1250,2015,Spring,c
+149,MATH,1250,2015,Fall,a
+172,MATH,1250,2015,Fall,a
+335,MATH,1250,2015,Fall,a
+214,MATH,1250,2016,Spring,a
+290,MATH,1250,2016,Spring,a
+377,MATH,1250,2016,Spring,a
+270,MATH,1250,2016,Summer,a
+285,MATH,1250,2016,Summer,a
+373,MATH,1250,2016,Summer,a
+215,MATH,1250,2016,Fall,a
+138,MATH,1250,2016,Fall,b
+182,MATH,1250,2016,Fall,b
+120,MATH,1250,2016,Fall,c
+374,MATH,1250,2016,Fall,c
+127,MATH,1250,2017,Summer,a
+173,MATH,1250,2017,Summer,a
+292,MATH,1250,2017,Summer,a
+355,MATH,1250,2017,Summer,a
+127,MATH,1250,2017,Summer,b
+210,MATH,1250,2017,Summer,b
+311,MATH,1250,2017,Summer,b
+230,MATH,1250,2017,Summer,c
+257,MATH,1250,2017,Summer,c
+117,MATH,1250,2017,Summer,d
+208,MATH,1250,2017,Summer,d
+109,MATH,1250,2018,Spring,a
+123,MATH,1250,2018,Spring,a
+260,MATH,1250,2018,Spring,a
+274,MATH,1250,2018,Spring,a
+345,MATH,1250,2018,Spring,a
+361,MATH,1250,2018,Spring,a
+379,MATH,1250,2018,Spring,a
+385,MATH,1250,2018,Spring,a
+392,MATH,1250,2018,Spring,a
+102,MATH,1250,2018,Summer,a
+247,MATH,1250,2018,Summer,a
+255,MATH,1250,2018,Summer,a
+312,MATH,1250,2018,Summer,a
+332,MATH,1250,2018,Summer,a
+356,MATH,1250,2018,Summer,a
+372,MATH,1250,2018,Summer,a
+101,MATH,1250,2018,Summer,b
+119,MATH,1250,2018,Summer,b
+239,MATH,1250,2018,Summer,b
+313,MATH,1250,2018,Summer,b
+321,MATH,1250,2018,Summer,b
+368,MATH,1250,2018,Summer,b
+100,MATH,1250,2018,Summer,c
+139,MATH,1250,2018,Summer,c
+158,MATH,1250,2018,Summer,c
+197,MATH,1250,2018,Summer,c
+207,MATH,1250,2018,Summer,c
+261,MATH,1250,2018,Summer,c
+277,MATH,1250,2018,Summer,c
+288,MATH,1250,2018,Summer,c
+321,MATH,1250,2018,Summer,c
+362,MATH,1250,2018,Summer,c
+106,MATH,1250,2020,Summer,a
+108,MATH,1250,2020,Summer,a
+133,MATH,1250,2020,Summer,a
+135,MATH,1250,2020,Summer,a
+151,MATH,1250,2020,Summer,a
+167,MATH,1250,2020,Summer,a
+185,MATH,1250,2020,Summer,a
+231,MATH,1250,2020,Summer,a
+281,MATH,1250,2020,Summer,a
+289,MATH,1250,2020,Summer,a
+309,MATH,1250,2020,Summer,a
+342,MATH,1250,2020,Summer,a
+378,MATH,1250,2020,Summer,a
+384,MATH,1250,2020,Summer,a
+386,MATH,1250,2020,Summer,a
+391,MATH,1250,2020,Summer,a
+177,MATH,1260,2015,Spring,c
+144,MATH,1260,2015,Summer,a
+162,MATH,1260,2015,Summer,a
+211,MATH,1260,2015,Summer,a
+229,MATH,1260,2016,Fall,a
+278,MATH,1260,2016,Fall,a
+304,MATH,1260,2017,Summer,a
+353,MATH,1260,2017,Summer,a
+361,MATH,1260,2017,Summer,a
+252,MATH,1260,2017,Fall,a
+260,MATH,1260,2017,Fall,a
+291,MATH,1260,2017,Fall,a
+133,MATH,1260,2019,Spring,a
+256,MATH,1260,2019,Spring,a
+347,MATH,1260,2019,Spring,a
+152,MATH,1260,2019,Spring,b
+169,MATH,1260,2019,Spring,b
+179,MATH,1260,2019,Spring,b
+187,MATH,1260,2019,Spring,b
+247,MATH,1260,2019,Spring,b
+277,MATH,1260,2019,Spring,b
+285,MATH,1260,2019,Spring,b
+313,MATH,1260,2019,Spring,b
+356,MATH,1260,2019,Spring,b
+102,MATH,1260,2019,Spring,c
+165,MATH,1260,2019,Spring,c
+293,MATH,1260,2019,Spring,c
+321,MATH,1260,2019,Spring,c
+113,MATH,1260,2019,Summer,a
+118,MATH,1260,2019,Summer,a
+124,MATH,1260,2019,Summer,a
+131,MATH,1260,2019,Summer,a
+185,MATH,1260,2019,Summer,a
+257,MATH,1260,2019,Summer,a
+276,MATH,1260,2019,Summer,a
+318,MATH,1260,2019,Summer,a
+391,MATH,1260,2019,Summer,a
+397,MATH,1260,2019,Summer,a
+120,MATH,1260,2019,Summer,b
+123,MATH,1260,2019,Summer,b
+194,MATH,1260,2019,Summer,b
+276,MATH,1260,2019,Summer,b
+303,MATH,1260,2019,Summer,b
+314,MATH,1260,2019,Summer,b
+377,MATH,1260,2019,Summer,b
+100,MATH,1260,2019,Fall,a
+108,MATH,1260,2019,Fall,a
+258,MATH,1260,2019,Fall,a
+309,MATH,1260,2019,Fall,a
+364,MATH,1260,2019,Fall,a
+375,MATH,1260,2019,Fall,a
+164,MATH,1260,2020,Spring,a
+173,MATH,1260,2020,Spring,a
+231,MATH,1260,2020,Spring,a
+235,MATH,1260,2020,Spring,a
+242,MATH,1260,2020,Spring,a
+276,MATH,2210,2015,Spring,b
+120,MATH,2210,2015,Summer,c
+212,MATH,2210,2015,Summer,c
+348,MATH,2210,2015,Summer,c
+172,MATH,2210,2015,Fall,a
+182,MATH,2210,2015,Fall,a
+373,MATH,2210,2015,Fall,a
+176,MATH,2210,2017,Spring,a
+208,MATH,2210,2017,Spring,a
+215,MATH,2210,2017,Spring,a
+249,MATH,2210,2017,Spring,a
+261,MATH,2210,2017,Spring,a
+270,MATH,2210,2017,Spring,a
+314,MATH,2210,2017,Spring,a
+128,MATH,2210,2017,Summer,a
+277,MATH,2210,2017,Summer,a
+361,MATH,2210,2017,Summer,a
+387,MATH,2210,2017,Summer,a
+392,MATH,2210,2017,Summer,a
+117,MATH,2210,2018,Spring,a
+123,MATH,2210,2018,Spring,a
+262,MATH,2210,2018,Spring,a
+391,MATH,2210,2018,Spring,a
+131,MATH,2210,2018,Spring,b
+185,MATH,2210,2018,Spring,b
+197,MATH,2210,2018,Spring,b
+199,MATH,2210,2018,Spring,b
+229,MATH,2210,2018,Spring,b
+230,MATH,2210,2018,Spring,b
+231,MATH,2210,2018,Spring,b
+239,MATH,2210,2018,Spring,b
+256,MATH,2210,2018,Spring,b
+275,MATH,2210,2018,Spring,b
+309,MATH,2210,2018,Spring,b
+369,MATH,2210,2018,Spring,b
+102,MATH,2210,2019,Spring,a
+169,MATH,2210,2019,Spring,a
+285,MATH,2210,2019,Spring,a
+119,MATH,2210,2019,Spring,b
+173,MATH,2210,2019,Spring,b
+228,MATH,2210,2019,Spring,b
+285,MATH,2210,2019,Spring,b
+296,MATH,2210,2019,Spring,b
+305,MATH,2210,2019,Spring,b
+342,MATH,2210,2019,Spring,b
+375,MATH,2210,2019,Spring,b
+113,MATH,2210,2020,Spring,a
+255,MATH,2210,2020,Spring,a
+274,MATH,2210,2020,Spring,a
+347,MATH,2210,2020,Spring,a
+124,MATH,2210,2020,Spring,b
+170,MATH,2210,2020,Spring,b
+200,MATH,2210,2020,Spring,b
+241,MATH,2210,2020,Spring,c
+251,MATH,2210,2020,Spring,c
+274,MATH,2210,2020,Spring,c
+122,MATH,2210,2020,Fall,a
+136,MATH,2210,2020,Fall,a
+167,MATH,2210,2020,Fall,a
+175,MATH,2210,2020,Fall,a
+179,MATH,2210,2020,Fall,a
+225,MATH,2210,2020,Fall,a
+272,MATH,2210,2020,Fall,a
+281,MATH,2210,2020,Fall,a
+329,MATH,2210,2020,Fall,a
+345,MATH,2210,2020,Fall,a
+378,MATH,2210,2020,Fall,a
+384,MATH,2210,2020,Fall,a
+397,MATH,2210,2020,Fall,a
+179,MATH,2270,2015,Fall,a
+212,MATH,2270,2015,Fall,a
+210,MATH,2270,2015,Fall,b
+313,MATH,2270,2015,Fall,b
+132,MATH,2270,2017,Summer,a
+143,MATH,2270,2017,Summer,a
+277,MATH,2270,2017,Summer,a
+304,MATH,2270,2017,Summer,a
+318,MATH,2270,2017,Summer,a
+107,MATH,2270,2017,Fall,a
+109,MATH,2270,2017,Fall,a
+292,MATH,2270,2017,Fall,a
+329,MATH,2270,2017,Fall,a
+246,MATH,2270,2017,Fall,b
+259,MATH,2270,2017,Fall,b
+342,MATH,2270,2017,Fall,b
+356,MATH,2270,2017,Fall,b
+120,MATH,2270,2017,Fall,c
+131,MATH,2270,2017,Fall,c
+182,MATH,2270,2017,Fall,c
+394,MATH,2270,2017,Fall,c
+102,MATH,2270,2017,Fall,d
+107,MATH,2270,2017,Fall,d
+123,MATH,2270,2017,Fall,d
+124,MATH,2270,2017,Fall,d
+128,MATH,2270,2017,Fall,d
+182,MATH,2270,2017,Fall,d
+276,MATH,2270,2017,Fall,d
+291,MATH,2270,2017,Fall,d
+312,MATH,2270,2017,Fall,d
+314,MATH,2270,2017,Fall,d
+397,MATH,2270,2017,Fall,d
+255,MATH,2270,2019,Spring,a
+285,MATH,2270,2019,Spring,a
+366,MATH,2270,2019,Spring,a
+379,MATH,2270,2019,Spring,a
+139,MATH,2270,2019,Summer,a
+146,MATH,2270,2019,Summer,a
+173,MATH,2270,2019,Summer,a
+248,MATH,2270,2019,Summer,a
+377,MATH,2270,2019,Summer,a
+194,MATH,2270,2019,Summer,b
+303,MATH,2270,2019,Summer,b
+325,MATH,2270,2019,Summer,b
+378,MATH,2270,2019,Summer,b
+183,MATH,2270,2019,Summer,c
+345,MATH,2270,2019,Summer,c
+396,MATH,2270,2019,Summer,c
+399,MATH,2270,2019,Summer,c
+254,MATH,2270,2019,Fall,a
+333,MATH,2270,2019,Fall,a
+175,MATH,2270,2020,Spring,a
+178,MATH,2270,2020,Spring,a
+223,MATH,2270,2020,Spring,a
+258,MATH,2270,2020,Spring,a
+270,MATH,2270,2020,Spring,a
+309,MATH,2270,2020,Spring,a
+130,MATH,2270,2020,Fall,a
+152,MATH,2270,2020,Fall,a
+177,MATH,2270,2020,Fall,a
+181,MATH,2270,2020,Fall,a
+230,MATH,2270,2020,Fall,a
+240,MATH,2270,2020,Fall,a
+331,MATH,2270,2020,Fall,a
+348,MATH,2270,2020,Fall,a
+360,MATH,2270,2020,Fall,a
+373,MATH,2270,2020,Fall,a
+391,MATH,2270,2020,Fall,a
+398,MATH,2270,2020,Fall,a
+119,MATH,2270,2020,Fall,b
+127,MATH,2270,2020,Fall,b
+129,MATH,2270,2020,Fall,b
+135,MATH,2270,2020,Fall,b
+167,MATH,2270,2020,Fall,b
+186,MATH,2270,2020,Fall,b
+260,MATH,2270,2020,Fall,b
+321,MATH,2270,2020,Fall,b
+331,MATH,2270,2020,Fall,b
+348,MATH,2270,2020,Fall,b
+371,MATH,2270,2020,Fall,b
+391,MATH,2270,2020,Fall,b
+204,MATH,2280,2015,Summer,a
+249,MATH,2280,2015,Summer,a
+123,MATH,2280,2015,Fall,a
+276,MATH,2280,2015,Fall,a
+393,MATH,2280,2016,Fall,a
+182,MATH,2280,2018,Spring,a
+230,MATH,2280,2018,Spring,a
+238,MATH,2280,2018,Spring,a
+256,MATH,2280,2018,Spring,a
+262,MATH,2280,2018,Spring,a
+307,MATH,2280,2018,Spring,a
+387,MATH,2280,2018,Spring,a
+173,MATH,2280,2018,Fall,a
+220,MATH,2280,2018,Fall,a
+259,MATH,2280,2018,Fall,a
+342,MATH,2280,2018,Fall,a
+104,MATH,2280,2018,Fall,b
+119,MATH,2280,2018,Fall,b
+165,MATH,2280,2018,Fall,b
+227,MATH,2280,2018,Fall,b
+359,MATH,2280,2018,Fall,b
+119,MATH,2280,2018,Fall,c
+120,MATH,2280,2018,Fall,c
+178,MATH,2280,2018,Fall,c
+196,MATH,2280,2018,Fall,c
+309,MATH,2280,2018,Fall,c
+345,MATH,2280,2018,Fall,c
+100,MATH,2280,2019,Fall,a
+102,MATH,2280,2019,Fall,a
+270,MATH,2280,2019,Fall,a
+314,MATH,2280,2019,Fall,a
+133,MATH,2280,2019,Fall,b
+247,MATH,2280,2019,Fall,b
+267,MATH,2280,2019,Fall,b
+318,MATH,2280,2019,Fall,b
+379,MATH,2280,2019,Fall,b
+390,MATH,2280,2019,Fall,b
+146,MATH,2280,2019,Fall,c
+223,MATH,2280,2019,Fall,c
+234,MATH,2280,2019,Fall,c
+248,MATH,2280,2019,Fall,c
+270,MATH,2280,2019,Fall,c
+292,MATH,2280,2019,Fall,c
+107,MATH,2280,2020,Spring,a
+183,MATH,2280,2020,Spring,a
+210,MATH,2280,2020,Spring,a
+255,MATH,2280,2020,Spring,a
+285,MATH,2280,2020,Spring,a
+313,MATH,2280,2020,Spring,a
+106,MATH,2280,2020,Spring,b
+169,MATH,2280,2020,Spring,b
+285,MATH,2280,2020,Spring,b
+398,MATH,2280,2020,Spring,b
+177,MATH,3210,2015,Spring,b
+282,MATH,3210,2015,Spring,b
+394,MATH,3210,2015,Spring,b
+144,MATH,3210,2015,Summer,a
+210,MATH,3210,2015,Summer,a
+215,MATH,3210,2015,Summer,a
+301,MATH,3210,2015,Summer,a
+126,MATH,3210,2015,Fall,a
+172,MATH,3210,2015,Fall,a
+246,MATH,3210,2015,Fall,a
+307,MATH,3210,2015,Fall,a
+313,MATH,3210,2015,Fall,a
+374,MATH,3210,2015,Fall,a
+138,MATH,3210,2015,Fall,b
+192,MATH,3210,2015,Fall,c
+172,MATH,3210,2015,Fall,d
+335,MATH,3210,2015,Fall,d
+149,MATH,3210,2016,Spring,a
+229,MATH,3210,2016,Spring,a
+276,MATH,3210,2016,Spring,a
+102,MATH,3210,2016,Fall,a
+134,MATH,3210,2016,Fall,a
+195,MATH,3210,2016,Fall,a
+277,MATH,3210,2016,Fall,a
+120,MATH,3210,2017,Spring,a
+207,MATH,3210,2017,Spring,a
+304,MATH,3210,2017,Spring,a
+107,MATH,3210,2017,Summer,a
+292,MATH,3210,2017,Summer,a
+309,MATH,3210,2017,Summer,a
+372,MATH,3210,2017,Summer,a
+270,MATH,3210,2019,Spring,a
+348,MATH,3210,2019,Spring,a
+364,MATH,3210,2019,Spring,a
+378,MATH,3210,2019,Spring,a
+399,MATH,3210,2019,Spring,a
+259,MATH,3210,2019,Spring,b
+314,MATH,3210,2019,Spring,b
+321,MATH,3210,2019,Spring,b
+124,MATH,3210,2019,Fall,a
+223,MATH,3210,2019,Fall,a
+230,MATH,3210,2019,Fall,a
+248,MATH,3210,2019,Fall,a
+284,MATH,3210,2019,Fall,a
+285,MATH,3210,2019,Fall,a
+358,MATH,3210,2019,Fall,a
+123,MATH,3210,2020,Spring,a
+146,MATH,3210,2020,Spring,a
+181,MATH,3210,2020,Spring,a
+251,MATH,3210,2020,Spring,a
+113,MATH,3210,2020,Summer,a
+135,MATH,3210,2020,Summer,a
+166,MATH,3210,2020,Summer,a
+171,MATH,3210,2020,Summer,a
+187,MATH,3210,2020,Summer,a
+260,MATH,3210,2020,Summer,a
+312,MATH,3210,2020,Summer,a
+368,MATH,3210,2020,Summer,a
+391,MATH,3210,2020,Summer,a
+109,MATH,3210,2020,Fall,a
+200,MATH,3210,2020,Fall,a
+227,MATH,3210,2020,Fall,a
+255,MATH,3210,2020,Fall,a
+256,MATH,3210,2020,Fall,a
+289,MATH,3210,2020,Fall,a
+329,MATH,3210,2020,Fall,a
+365,MATH,3210,2020,Fall,a
+386,MATH,3210,2020,Fall,a
+397,MATH,3210,2020,Fall,a
+210,MATH,3220,2016,Spring,a
+285,MATH,3220,2016,Spring,a
+373,MATH,3220,2016,Spring,a
+195,MATH,3220,2016,Spring,b
+301,MATH,3220,2016,Spring,b
+392,MATH,3220,2016,Spring,b
+119,MATH,3220,2016,Spring,c
+216,MATH,3220,2016,Spring,c
+374,MATH,3220,2016,Spring,c
+192,MATH,3220,2016,Spring,d
+210,MATH,3220,2016,Spring,d
+290,MATH,3220,2016,Spring,d
+394,MATH,3220,2016,Spring,d
+163,MATH,3220,2016,Summer,a
+214,MATH,3220,2016,Summer,a
+270,MATH,3220,2016,Summer,a
+276,MATH,3220,2016,Summer,a
+278,MATH,3220,2016,Summer,a
+246,MATH,3220,2016,Fall,a
+277,MATH,3220,2016,Fall,a
+385,MATH,3220,2016,Fall,a
+134,MATH,3220,2016,Fall,b
+245,MATH,3220,2016,Fall,b
+264,MATH,3220,2016,Fall,b
+329,MATH,3220,2016,Fall,b
+123,MATH,3220,2017,Spring,a
+176,MATH,3220,2017,Spring,a
+391,MATH,3220,2017,Spring,a
+102,MATH,3220,2017,Fall,a
+107,MATH,3220,2017,Fall,a
+207,MATH,3220,2017,Fall,a
+266,MATH,3220,2017,Fall,a
+311,MATH,3220,2017,Fall,a
+377,MATH,3220,2017,Fall,a
+139,MATH,3220,2017,Fall,b
+261,MATH,3220,2017,Fall,b
+326,MATH,3220,2017,Fall,b
+366,MATH,3220,2017,Fall,b
+237,MATH,3220,2018,Spring,a
+292,MATH,3220,2018,Spring,a
+296,MATH,3220,2018,Spring,a
+345,MATH,3220,2018,Spring,a
+362,MATH,3220,2018,Spring,a
+379,MATH,3220,2018,Spring,a
+101,MATH,3220,2018,Spring,b
+132,MATH,3220,2018,Spring,b
+312,MATH,3220,2018,Spring,b
+387,MATH,3220,2018,Spring,b
+127,MATH,3220,2018,Spring,c
+131,MATH,3220,2018,Spring,c
+165,MATH,3220,2018,Spring,c
+229,MATH,3220,2018,Spring,c
+305,MATH,3220,2018,Spring,c
+309,MATH,3220,2018,Spring,c
+312,MATH,3220,2018,Spring,c
+129,MATH,3220,2018,Spring,d
+179,MATH,3220,2018,Spring,d
+203,MATH,3220,2018,Spring,d
+238,MATH,3220,2018,Spring,d
+177,PHYS,2040,2015,Spring,a
+192,PHYS,2040,2015,Spring,a
+245,PHYS,2040,2015,Fall,a
+149,PHYS,2040,2015,Fall,b
+295,PHYS,2040,2015,Fall,b
+312,PHYS,2040,2015,Fall,b
+373,PHYS,2040,2015,Fall,b
+374,PHYS,2040,2015,Fall,b
+210,PHYS,2040,2015,Fall,c
+212,PHYS,2040,2015,Fall,c
+307,PHYS,2040,2015,Fall,c
+387,PHYS,2040,2015,Fall,c
+321,PHYS,2040,2016,Spring,a
+389,PHYS,2040,2016,Spring,a
+292,PHYS,2040,2017,Summer,a
+203,PHYS,2040,2017,Fall,a
+237,PHYS,2040,2017,Fall,a
+259,PHYS,2040,2017,Fall,a
+314,PHYS,2040,2017,Fall,a
+379,PHYS,2040,2017,Fall,a
+119,PHYS,2040,2017,Fall,b
+256,PHYS,2040,2017,Fall,b
+285,PHYS,2040,2017,Fall,b
+132,PHYS,2040,2017,Fall,c
+187,PHYS,2040,2017,Fall,c
+214,PHYS,2040,2017,Fall,c
+230,PHYS,2040,2017,Fall,c
+266,PHYS,2040,2017,Fall,c
+270,PHYS,2040,2017,Fall,c
+314,PHYS,2040,2017,Fall,c
+348,PHYS,2040,2017,Fall,c
+101,PHYS,2040,2018,Spring,a
+105,PHYS,2040,2018,Spring,a
+123,PHYS,2040,2018,Spring,a
+169,PHYS,2040,2018,Spring,a
+227,PHYS,2040,2018,Spring,a
+342,PHYS,2040,2018,Spring,a
+178,PHYS,2040,2019,Spring,a
+275,PHYS,2040,2019,Spring,a
+296,PHYS,2040,2019,Spring,a
+372,PHYS,2040,2019,Spring,a
+391,PHYS,2040,2019,Spring,a
+399,PHYS,2040,2019,Spring,a
+152,PHYS,2040,2019,Spring,b
+305,PHYS,2040,2019,Spring,b
+120,PHYS,2040,2020,Spring,a
+125,PHYS,2040,2020,Spring,a
+128,PHYS,2040,2020,Spring,a
+131,PHYS,2040,2020,Spring,a
+194,PHYS,2040,2020,Spring,a
+267,PHYS,2040,2020,Spring,a
+313,PHYS,2040,2020,Spring,a
+377,PHYS,2060,2015,Spring,a
+115,PHYS,2060,2016,Spring,a
+195,PHYS,2060,2016,Spring,a
+229,PHYS,2060,2016,Spring,a
+355,PHYS,2060,2016,Spring,a
+379,PHYS,2060,2016,Spring,a
+392,PHYS,2060,2016,Spring,a
+163,PHYS,2060,2016,Spring,b
+290,PHYS,2060,2016,Spring,b
+262,PHYS,2060,2016,Summer,a
+264,PHYS,2060,2016,Summer,a
+278,PHYS,2060,2016,Summer,a
+373,PHYS,2060,2016,Summer,a
+393,PHYS,2060,2016,Summer,a
+276,PHYS,2060,2016,Summer,b
+282,PHYS,2060,2016,Summer,b
+285,PHYS,2060,2016,Summer,b
+348,PHYS,2060,2016,Summer,b
+374,PHYS,2060,2016,Summer,b
+102,PHYS,2060,2018,Summer,a
+131,PHYS,2060,2018,Summer,a
+120,PHYS,2060,2018,Fall,a
+156,PHYS,2060,2018,Fall,a
+239,PHYS,2060,2018,Fall,a
+298,PHYS,2060,2018,Fall,a
+399,PHYS,2060,2018,Fall,a
+127,PHYS,2060,2018,Fall,b
+158,PHYS,2060,2018,Fall,b
+247,PHYS,2060,2018,Fall,b
+248,PHYS,2060,2018,Fall,b
+257,PHYS,2060,2018,Fall,b
+261,PHYS,2060,2018,Fall,b
+270,PHYS,2060,2018,Fall,b
+275,PHYS,2060,2018,Fall,b
+311,PHYS,2060,2018,Fall,b
+329,PHYS,2060,2018,Fall,b
+127,PHYS,2060,2018,Fall,c
+165,PHYS,2060,2018,Fall,c
+217,PHYS,2060,2018,Fall,c
+275,PHYS,2060,2018,Fall,c
+311,PHYS,2060,2018,Fall,c
+318,PHYS,2060,2018,Fall,c
+329,PHYS,2060,2018,Fall,c
+231,PHYS,2060,2018,Fall,d
+252,PHYS,2060,2018,Fall,d
+259,PHYS,2060,2018,Fall,d
+288,PHYS,2060,2018,Fall,d
+311,PHYS,2060,2018,Fall,d
+230,PHYS,2060,2019,Summer,a
+238,PHYS,2060,2019,Summer,a
+277,PHYS,2060,2019,Summer,a
+307,PHYS,2060,2019,Summer,a
+312,PHYS,2060,2019,Summer,a
+398,PHYS,2060,2019,Summer,a
+106,PHYS,2060,2019,Summer,b
+121,PHYS,2060,2019,Summer,b
+179,PHYS,2060,2019,Summer,b
+194,PHYS,2060,2019,Summer,b
+294,PHYS,2060,2019,Summer,b
+313,PHYS,2060,2019,Summer,b
+366,PHYS,2060,2019,Summer,b
+384,PHYS,2060,2019,Summer,b
+397,PHYS,2060,2019,Summer,b
+108,PHYS,2060,2019,Fall,a
+185,PHYS,2060,2019,Fall,a
+210,PHYS,2060,2019,Fall,a
+359,PHYS,2060,2019,Fall,a
+380,PHYS,2060,2019,Fall,a
+171,PHYS,2060,2019,Fall,b
+241,PHYS,2060,2019,Fall,b
+274,PHYS,2060,2019,Fall,b
+341,PHYS,2060,2019,Fall,b
+368,PHYS,2060,2019,Fall,b
+100,PHYS,2060,2019,Fall,c
+123,PHYS,2060,2019,Fall,c
+151,PHYS,2060,2019,Fall,c
+177,PHYS,2060,2019,Fall,c
+375,PHYS,2060,2019,Fall,c
+122,PHYS,2060,2020,Spring,a
+167,PHYS,2060,2020,Spring,a
+223,PHYS,2060,2020,Spring,a
+255,PHYS,2060,2020,Spring,a
+310,PHYS,2060,2020,Spring,a
+321,PHYS,2060,2020,Spring,a
+153,PHYS,2060,2020,Spring,b
+221,PHYS,2060,2020,Spring,b
+240,PHYS,2060,2020,Spring,b
+269,PHYS,2060,2020,Spring,b
+292,PHYS,2060,2020,Spring,b
+293,PHYS,2060,2020,Spring,b
+321,PHYS,2060,2020,Spring,b
+391,PHYS,2060,2020,Spring,b
+112,PHYS,2060,2020,Fall,a
+142,PHYS,2060,2020,Fall,a
+178,PHYS,2060,2020,Fall,a
+181,PHYS,2060,2020,Fall,a
+187,PHYS,2060,2020,Fall,a
+250,PHYS,2060,2020,Fall,a
+371,PHYS,2060,2020,Fall,a
+376,PHYS,2060,2020,Fall,a
+390,PHYS,2060,2020,Fall,a
+193,PHYS,2100,2015,Spring,a
+277,PHYS,2100,2015,Spring,b
+321,PHYS,2100,2015,Spring,b
+120,PHYS,2100,2016,Fall,a
+312,PHYS,2100,2016,Fall,a
+314,PHYS,2100,2016,Fall,a
+392,PHYS,2100,2016,Fall,a
+176,PHYS,2100,2016,Fall,b
+179,PHYS,2100,2016,Fall,b
+278,PHYS,2100,2016,Fall,b
+177,PHYS,2100,2017,Summer,a
+262,PHYS,2100,2017,Summer,a
+276,PHYS,2100,2017,Summer,a
+375,PHYS,2100,2017,Summer,a
+117,PHYS,2100,2017,Summer,b
+177,PHYS,2100,2017,Summer,b
+215,PHYS,2100,2017,Summer,b
+307,PHYS,2100,2017,Summer,b
+377,PHYS,2100,2017,Summer,b
+378,PHYS,2100,2017,Summer,b
+151,PHYS,2100,2017,Summer,c
+173,PHYS,2100,2017,Summer,c
+215,PHYS,2100,2017,Summer,c
+264,PHYS,2100,2017,Summer,c
+353,PHYS,2100,2017,Summer,c
+355,PHYS,2100,2017,Summer,c
+246,PHYS,2100,2017,Fall,a
+374,PHYS,2100,2017,Fall,a
+387,PHYS,2100,2017,Fall,a
+128,PHYS,2100,2018,Fall,a
+158,PHYS,2100,2018,Fall,a
+185,PHYS,2100,2018,Fall,a
+285,PHYS,2100,2018,Fall,a
+288,PHYS,2100,2018,Fall,a
+366,PHYS,2100,2019,Summer,a
+386,PHYS,2100,2019,Summer,a
+399,PHYS,2100,2019,Summer,a
+282,PHYS,2140,2015,Spring,a
+192,PHYS,2140,2015,Spring,b
+394,PHYS,2140,2015,Spring,b
+140,PHYS,2140,2015,Summer,a
+172,PHYS,2140,2015,Summer,b
+176,PHYS,2140,2015,Summer,b
+270,PHYS,2140,2015,Summer,b
+138,PHYS,2140,2015,Summer,c
+246,PHYS,2140,2015,Summer,c
+373,PHYS,2140,2015,Summer,c
+120,PHYS,2140,2015,Fall,a
+276,PHYS,2140,2015,Fall,a
+123,PHYS,2140,2016,Spring,a
+117,PHYS,2140,2016,Spring,b
+313,PHYS,2140,2016,Spring,b
+134,PHYS,2140,2016,Spring,c
+215,PHYS,2140,2016,Spring,c
+307,PHYS,2140,2016,Spring,c
+312,PHYS,2140,2016,Summer,a
+317,PHYS,2140,2016,Summer,a
+277,PHYS,2140,2016,Summer,b
+392,PHYS,2140,2016,Summer,b
+116,PHYS,2140,2016,Fall,a
+335,PHYS,2140,2016,Fall,a
+387,PHYS,2140,2016,Fall,a
+177,PHYS,2140,2017,Summer,a
+255,PHYS,2140,2017,Summer,a
+285,PHYS,2140,2017,Summer,a
+314,PHYS,2140,2017,Summer,a
+187,PHYS,2140,2017,Fall,a
+259,PHYS,2140,2017,Fall,a
+361,PHYS,2140,2017,Fall,b
+379,PHYS,2140,2017,Fall,b
+101,PHYS,2140,2018,Summer,a
+105,PHYS,2140,2018,Summer,a
+113,PHYS,2140,2018,Summer,a
+128,PHYS,2140,2018,Summer,a
+143,PHYS,2140,2018,Summer,a
+151,PHYS,2140,2018,Summer,a
+231,PHYS,2140,2018,Summer,a
+298,PHYS,2140,2018,Summer,a
+199,PHYS,2140,2018,Summer,b
+305,PHYS,2140,2018,Summer,b
+369,PHYS,2140,2018,Summer,b
+163,PHYS,2140,2018,Fall,a
+253,PHYS,2140,2018,Fall,a
+386,PHYS,2140,2018,Fall,a
+129,PHYS,2140,2019,Fall,a
+167,PHYS,2140,2019,Fall,a
+227,PHYS,2140,2019,Fall,a
+329,PHYS,2140,2019,Fall,a
+366,PHYS,2140,2019,Fall,a
+371,PHYS,2140,2019,Fall,a
+289,PHYS,2140,2019,Fall,b
+318,PHYS,2140,2019,Fall,b
+362,PHYS,2140,2019,Fall,b
+377,PHYS,2140,2019,Fall,b
+119,PHYS,2140,2020,Fall,a
+131,PHYS,2140,2020,Fall,a
+136,PHYS,2140,2020,Fall,a
+146,PHYS,2140,2020,Fall,a
+175,PHYS,2140,2020,Fall,a
+185,PHYS,2140,2020,Fall,a
+222,PHYS,2140,2020,Fall,a
+235,PHYS,2140,2020,Fall,a
+267,PHYS,2140,2020,Fall,a
+292,PHYS,2140,2020,Fall,a
+297,PHYS,2140,2020,Fall,a
+309,PHYS,2140,2020,Fall,a
+345,PHYS,2140,2020,Fall,a
+391,PHYS,2140,2020,Fall,a
+246,PHYS,2210,2015,Fall,a
+374,PHYS,2210,2015,Fall,b
+392,PHYS,2210,2015,Fall,b
+379,PHYS,2210,2015,Fall,c
+177,PHYS,2210,2017,Summer,a
+230,PHYS,2210,2017,Summer,a
+231,PHYS,2210,2017,Summer,a
+373,PHYS,2210,2017,Summer,a
+179,PHYS,2210,2017,Summer,b
+285,PHYS,2210,2017,Summer,b
+326,PHYS,2210,2017,Summer,b
+127,PHYS,2210,2017,Summer,c
+342,PHYS,2210,2017,Summer,c
+208,PHYS,2210,2017,Summer,d
+261,PHYS,2210,2017,Summer,d
+304,PHYS,2210,2017,Summer,d
+373,PHYS,2210,2017,Summer,d
+101,PHYS,2210,2018,Fall,a
+113,PHYS,2210,2018,Fall,a
+183,PHYS,2210,2018,Fall,a
+296,PHYS,2210,2018,Fall,a
+329,PHYS,2210,2018,Fall,a
+113,PHYS,2210,2018,Fall,b
+120,PHYS,2210,2018,Fall,b
+133,PHYS,2210,2018,Fall,b
+151,PHYS,2210,2018,Fall,b
+270,PHYS,2210,2018,Fall,b
+274,PHYS,2210,2018,Fall,b
+288,PHYS,2210,2018,Fall,b
+378,PHYS,2210,2018,Fall,b
+120,PHYS,2210,2018,Fall,c
+124,PHYS,2210,2018,Fall,c
+332,PHYS,2210,2018,Fall,c
+362,PHYS,2210,2018,Fall,c
+119,PHYS,2210,2019,Spring,a
+238,PHYS,2210,2019,Spring,a
+255,PHYS,2210,2019,Spring,a
+305,PHYS,2210,2019,Spring,a
+311,PHYS,2210,2019,Spring,a
+157,PHYS,2210,2019,Spring,b
+199,PHYS,2210,2019,Spring,b
+238,PHYS,2210,2019,Spring,b
+102,PHYS,2210,2019,Spring,c
+165,PHYS,2210,2019,Spring,c
+253,PHYS,2210,2019,Spring,c
+292,PHYS,2210,2019,Spring,c
+368,PHYS,2210,2019,Spring,c
+391,PHYS,2210,2019,Spring,c
+187,PHYS,2210,2019,Spring,d
+255,PHYS,2210,2019,Spring,d
+257,PHYS,2210,2019,Spring,d
+391,PHYS,2210,2019,Spring,d
+128,PHYS,2210,2019,Summer,a
+256,PHYS,2210,2019,Summer,a
+289,PHYS,2210,2019,Summer,a
+359,PHYS,2210,2019,Summer,a
+397,PHYS,2210,2019,Summer,a
+123,PHYS,2210,2019,Fall,a
+135,PHYS,2210,2019,Fall,a
+143,PHYS,2210,2019,Fall,a
+241,PHYS,2210,2019,Fall,a
+340,PHYS,2210,2019,Fall,a
+108,PHYS,2210,2019,Fall,b
+171,PHYS,2210,2019,Fall,b
+200,PHYS,2210,2019,Fall,b
+309,PHYS,2210,2019,Fall,b
+312,PHYS,2210,2019,Fall,b
+333,PHYS,2210,2019,Fall,b
+345,PHYS,2210,2019,Fall,b
+363,PHYS,2210,2019,Fall,b
+366,PHYS,2210,2019,Fall,b
+396,PHYS,2210,2019,Fall,b
+123,PHYS,2210,2019,Fall,c
+221,PHYS,2210,2019,Fall,c
+276,PHYS,2210,2019,Fall,c
+347,PHYS,2210,2019,Fall,c
+371,PHYS,2210,2019,Fall,c
+390,PHYS,2210,2019,Fall,c
+303,PHYS,2210,2019,Fall,d
+374,PHYS,2220,2015,Spring,a
+179,PHYS,2220,2015,Fall,a
+276,PHYS,2220,2015,Fall,a
+321,PHYS,2220,2015,Fall,a
+282,PHYS,2220,2015,Fall,b
+172,PHYS,2220,2016,Summer,a
+317,PHYS,2220,2016,Summer,a
+378,PHYS,2220,2016,Summer,a
+391,PHYS,2220,2016,Summer,a
+245,PHYS,2220,2016,Fall,a
+295,PHYS,2220,2016,Fall,a
+356,PHYS,2220,2016,Fall,a
+385,PHYS,2220,2016,Fall,a
+119,PHYS,2220,2017,Spring,a
+176,PHYS,2220,2017,Spring,a
+187,PHYS,2220,2017,Spring,a
+256,PHYS,2220,2017,Spring,a
+313,PHYS,2220,2017,Spring,a
+372,PHYS,2220,2017,Spring,a
+120,PHYS,2220,2017,Spring,b
+312,PHYS,2220,2017,Spring,b
+355,PHYS,2220,2017,Spring,b
+151,PHYS,2220,2017,Spring,c
+187,PHYS,2220,2017,Spring,c
+270,PHYS,2220,2017,Spring,c
+277,PHYS,2220,2017,Spring,c
+119,PHYS,2220,2017,Spring,d
+163,PHYS,2220,2017,Spring,d
+249,PHYS,2220,2017,Spring,d
+288,PHYS,2220,2017,Spring,d
+312,PHYS,2220,2017,Spring,d
+102,PHYS,2220,2018,Spring,a
+105,PHYS,2220,2018,Spring,a
+107,PHYS,2220,2018,Spring,a
+128,PHYS,2220,2018,Spring,a
+132,PHYS,2220,2018,Spring,a
+134,PHYS,2220,2018,Spring,a
+210,PHYS,2220,2018,Spring,a
+214,PHYS,2220,2018,Spring,a
+227,PHYS,2220,2018,Spring,a
+237,PHYS,2220,2018,Spring,a
+239,PHYS,2220,2018,Spring,a
+305,PHYS,2220,2018,Spring,a
+231,PHYS,2220,2018,Summer,a
+255,PHYS,2220,2018,Summer,a
+257,PHYS,2220,2018,Summer,a
+342,PHYS,2220,2018,Summer,a
+344,PHYS,2220,2018,Summer,a
+373,PHYS,2220,2018,Summer,a
+393,PHYS,2220,2018,Summer,a
+123,PHYS,2220,2018,Fall,a
+133,PHYS,2220,2018,Fall,a
+177,PHYS,2220,2018,Fall,a
+178,PHYS,2220,2018,Fall,a
+196,PHYS,2220,2018,Fall,a
+267,PHYS,2220,2018,Fall,a
+285,PHYS,2220,2018,Fall,a
+292,PHYS,2220,2018,Fall,a
+332,PHYS,2220,2018,Fall,a
+241,PHYS,2220,2019,Spring,a
+113,PHYS,2220,2020,Spring,a
+124,PHYS,2220,2020,Spring,a
+175,PHYS,2220,2020,Spring,a
+235,PHYS,2220,2020,Spring,a
+106,PHYS,2220,2020,Summer,a
+118,PHYS,2220,2020,Summer,a
+121,PHYS,2220,2020,Summer,a
+127,PHYS,2220,2020,Summer,a
+194,PHYS,2220,2020,Summer,a
+247,PHYS,2220,2020,Summer,a
+293,PHYS,2220,2020,Summer,a
+296,PHYS,2220,2020,Summer,a
+309,PHYS,2220,2020,Summer,a
+311,PHYS,2220,2020,Summer,a
+339,PHYS,2220,2020,Summer,a
+345,PHYS,2220,2020,Summer,a
+164,PHYS,2220,2020,Summer,b
+242,PHYS,2220,2020,Summer,b
+289,PHYS,2220,2020,Summer,b
+300,PHYS,2220,2020,Summer,b
+323,PHYS,2220,2020,Summer,b
+390,PHYS,2220,2020,Summer,b
+109,PHYS,2220,2020,Fall,a
+228,PHYS,2220,2020,Fall,a
+386,PHYS,2220,2020,Fall,a
+107,PHYS,3210,2016,Summer,a
+249,PHYS,3210,2016,Summer,a
+134,PHYS,3210,2016,Summer,b
+172,PHYS,3210,2016,Summer,b
+249,PHYS,3210,2016,Summer,b
+314,PHYS,3210,2016,Summer,b
+123,PHYS,3210,2016,Fall,a
+260,PHYS,3210,2016,Fall,a
+321,PHYS,3210,2016,Fall,a
+139,PHYS,3210,2017,Summer,a
+179,PHYS,3210,2017,Summer,a
+230,PHYS,3210,2017,Summer,a
+246,PHYS,3210,2017,Summer,a
+373,PHYS,3210,2017,Summer,a
+378,PHYS,3210,2017,Summer,a
+391,PHYS,3210,2017,Summer,a
+393,PHYS,3210,2017,Summer,a
+208,PHYS,3210,2017,Summer,b
+264,PHYS,3210,2017,Summer,b
+379,PHYS,3210,2017,Summer,b
+155,PHYS,3210,2017,Fall,a
+262,PHYS,3210,2017,Fall,a
+270,PHYS,3210,2017,Fall,a
+335,PHYS,3210,2017,Fall,a
+377,PHYS,3210,2017,Fall,a
+397,PHYS,3210,2017,Fall,a
+119,PHYS,3210,2018,Spring,a
+229,PHYS,3210,2018,Spring,a
+277,PHYS,3210,2018,Spring,a
+294,PHYS,3210,2018,Spring,a
+385,PHYS,3210,2018,Spring,a
+274,PHYS,3210,2018,Spring,b
+372,PHYS,3210,2018,Spring,b
+102,PHYS,3210,2018,Spring,c
+105,PHYS,3210,2018,Spring,c
+197,PHYS,3210,2018,Spring,c
+209,PHYS,3210,2018,Spring,c
+374,PHYS,3210,2018,Spring,c
+381,PHYS,3210,2018,Spring,c
+101,PHYS,3210,2018,Fall,a
+109,PHYS,3210,2018,Fall,a
+227,PHYS,3210,2018,Fall,a
+276,PHYS,3210,2018,Fall,a
+285,PHYS,3210,2018,Fall,a
+113,PHYS,3210,2019,Spring,a
+258,PHYS,3210,2019,Spring,a
+329,PHYS,3210,2019,Spring,a
+351,PHYS,3210,2019,Spring,a
+356,PHYS,3210,2019,Spring,a
+384,PHYS,3210,2019,Spring,a
+217,PHYS,3210,2019,Spring,b
+312,PHYS,3210,2019,Spring,b
+351,PHYS,3210,2019,Spring,b
+231,PHYS,3210,2019,Spring,c
+258,PHYS,3210,2019,Spring,c
+292,PHYS,3210,2019,Spring,c
+329,PHYS,3210,2019,Spring,c
+375,PHYS,3210,2019,Spring,c
+156,PHYS,3210,2019,Spring,d
+173,PHYS,3210,2019,Spring,d
+128,PHYS,3210,2019,Summer,a
+133,PHYS,3210,2019,Summer,a
+146,PHYS,3210,2019,Summer,a
+177,PHYS,3210,2019,Summer,a
+199,PHYS,3210,2019,Summer,a
+133,PHYS,3210,2019,Summer,b
+152,PHYS,3210,2019,Summer,b
+255,PHYS,3210,2019,Summer,b
+287,PHYS,3210,2019,Summer,b
+313,PHYS,3210,2019,Summer,b
+362,PHYS,3210,2019,Summer,b
+366,PHYS,3210,2019,Summer,b
+106,PHYS,3210,2019,Summer,c
+152,PHYS,3210,2019,Summer,c
+167,PHYS,3210,2019,Summer,c
+188,PHYS,3210,2019,Summer,c
+307,PHYS,3210,2019,Summer,c
+309,PHYS,3210,2019,Summer,c
+333,PHYS,3210,2019,Summer,c
+345,PHYS,3210,2019,Summer,c
+100,PHYS,3210,2019,Fall,a
+178,PHYS,3210,2019,Fall,a
+125,PHYS,3210,2020,Spring,a
+131,PHYS,3210,2020,Spring,a
+183,PHYS,3210,2020,Spring,a
+185,PHYS,3210,2020,Spring,a
+254,PHYS,3210,2020,Spring,a
+310,PHYS,3210,2020,Spring,a
+348,PHYS,3210,2020,Spring,a
+390,PHYS,3210,2020,Spring,a
+175,PHYS,3210,2020,Summer,a
+187,PHYS,3210,2020,Summer,a
+240,PHYS,3210,2020,Summer,a
+300,PHYS,3210,2020,Summer,a
+136,PHYS,3210,2020,Fall,a
+153,PHYS,3210,2020,Fall,a
+228,PHYS,3210,2020,Fall,a
+289,PHYS,3210,2020,Fall,a
+293,PHYS,3210,2020,Fall,a
+297,PHYS,3210,2020,Fall,a
+306,PHYS,3210,2020,Fall,a
+339,PHYS,3210,2020,Fall,a
+342,PHYS,3210,2020,Fall,a
+121,PHYS,3210,2020,Fall,b
+129,PHYS,3210,2020,Fall,b
+200,PHYS,3210,2020,Fall,b
+228,PHYS,3210,2020,Fall,b
+256,PHYS,3210,2020,Fall,b
+130,PHYS,3210,2020,Fall,c
+331,PHYS,3210,2020,Fall,c
+115,PHYS,3220,2016,Summer,a
+195,PHYS,3220,2016,Summer,a
+285,PHYS,3220,2016,Summer,a
+312,PHYS,3220,2016,Summer,a
+107,PHYS,3220,2016,Summer,b
+123,PHYS,3220,2016,Summer,b
+277,PHYS,3220,2016,Summer,b
+119,PHYS,3220,2017,Summer,a
+139,PHYS,3220,2017,Summer,a
+215,PHYS,3220,2017,Summer,a
+329,PHYS,3220,2017,Summer,a
+392,PHYS,3220,2017,Summer,a
+120,PHYS,3220,2017,Fall,a
+131,PHYS,3220,2017,Fall,a
+155,PHYS,3220,2017,Fall,a
+214,PHYS,3220,2017,Fall,a
+237,PHYS,3220,2017,Fall,a
+109,PHYS,3220,2017,Fall,b
+203,PHYS,3220,2017,Fall,b
+345,PHYS,3220,2017,Fall,b
+213,PHYS,3220,2017,Fall,c
+230,PHYS,3220,2017,Fall,c
+307,PHYS,3220,2017,Fall,c
+127,PHYS,3220,2017,Fall,d
+187,PHYS,3220,2017,Fall,d
+252,PHYS,3220,2017,Fall,d
+270,PHYS,3220,2017,Fall,d
+276,PHYS,3220,2017,Fall,d
+288,PHYS,3220,2017,Fall,d
+128,PHYS,3220,2018,Summer,a
+143,PHYS,3220,2018,Summer,a
+260,PHYS,3220,2018,Summer,a
+377,PHYS,3220,2018,Summer,a
+379,PHYS,3220,2018,Summer,a
+398,PHYS,3220,2018,Summer,a
+102,PHYS,3220,2020,Spring,a
+133,PHYS,3220,2020,Spring,a
+170,PHYS,3220,2020,Spring,a
+267,PHYS,3220,2020,Spring,a
+310,PHYS,3220,2020,Spring,a
+227,PHYS,3220,2020,Spring,b
+241,PHYS,3220,2020,Spring,b
+251,PHYS,3220,2020,Spring,b
+255,PHYS,3220,2020,Spring,b
+269,PHYS,3220,2020,Spring,b
+321,PHYS,3220,2020,Spring,b
+348,PHYS,3220,2020,Spring,b
+106,PHYS,3220,2020,Spring,c
+152,PHYS,3220,2020,Spring,c
+185,PHYS,3220,2020,Spring,c
+194,PHYS,3220,2020,Spring,c
+200,PHYS,3220,2020,Spring,c
+241,PHYS,3220,2020,Spring,c
+251,PHYS,3220,2020,Spring,c
+271,PHYS,3220,2020,Spring,c
+296,PHYS,3220,2020,Spring,c
+325,PHYS,3220,2020,Spring,c
+365,PHYS,3220,2020,Spring,c
+124,PHYS,3220,2020,Spring,d
+167,PHYS,3220,2020,Spring,d
+185,PHYS,3220,2020,Spring,d
+227,PHYS,3220,2020,Spring,d
+303,PHYS,3220,2020,Spring,d
+341,PHYS,3220,2020,Spring,d
+342,PHYS,3220,2020,Spring,d
+373,PHYS,3220,2020,Spring,d
diff --git a/tests/data/Grade.csv b/tests/data/Grade.csv
new file mode 100644
index 000000000..8ba592194
--- /dev/null
+++ b/tests/data/Grade.csv
@@ -0,0 +1,3028 @@
+student_id,dept,course,term_year,term,section,grade
+100,CS,1030,2020,Spring,a,A
+101,PHYS,2040,2018,Spring,a,A
+102,BIOL,1006,2018,Fall,a,A
+104,MATH,2280,2018,Fall,b,A
+105,PHYS,3210,2018,Spring,c,A
+107,MATH,3210,2017,Summer,a,A
+107,PHYS,2220,2018,Spring,a,A
+109,BIOL,2355,2019,Spring,d,A
+113,CS,3200,2020,Summer,a,A
+113,CS,3505,2019,Summer,d,A
+115,BIOL,1030,2017,Spring,a,A
+118,CS,2100,2019,Fall,b,A
+119,BIOL,2355,2018,Summer,d,A
+119,CS,3505,2019,Summer,a,A
+119,CS,4940,2017,Fall,b,A
+119,MATH,2280,2018,Fall,c,A
+119,PHYS,3210,2018,Spring,a,A
+120,PHYS,2060,2018,Fall,a,A
+122,CS,4970,2020,Fall,a,A
+123,BIOL,2030,2017,Spring,a,A
+123,BIOL,2325,2017,Fall,b,A
+123,BIOL,2355,2017,Summer,a,A
+123,CS,4940,2020,Summer,b,A
+123,MATH,3220,2017,Spring,a,A
+124,CS,2100,2018,Fall,c,A
+124,CS,2420,2019,Summer,a,A
+124,MATH,3210,2019,Fall,a,A
+125,BIOL,2330,2019,Fall,a,A
+127,BIOL,2355,2018,Fall,a,A
+127,PHYS,2060,2018,Fall,c,A
+127,PHYS,2220,2020,Summer,a,A
+128,BIOL,1006,2017,Fall,a,A
+128,BIOL,2010,2020,Summer,b,A
+128,CS,3505,2017,Fall,a,A
+128,CS,4500,2018,Spring,a,A
+132,BIOL,1030,2018,Summer,a,A
+132,CS,4500,2018,Spring,b,A
+132,CS,4970,2018,Summer,b,A
+135,CS,4400,2019,Summer,b,A
+139,BIOL,1006,2019,Summer,a,A
+139,CS,4000,2017,Summer,a,A
+140,CS,3810,2015,Spring,a,A
+140,CS,4400,2015,Summer,a,A
+143,CS,2100,2017,Fall,a,A
+145,MATH,1220,2017,Spring,c,A
+146,CS,4970,2020,Summer,c,A
+146,PHYS,2140,2020,Fall,a,A
+149,BIOL,2325,2015,Fall,c,A
+149,PHYS,2040,2015,Fall,b,A
+151,BIOL,2355,2019,Spring,b,A
+151,CS,4970,2020,Summer,b,A
+151,MATH,1220,2020,Spring,a,A
+152,BIOL,2021,2018,Fall,b,A
+155,PHYS,3210,2017,Fall,a,A
+155,PHYS,3220,2017,Fall,a,A
+165,BIOL,2330,2017,Fall,a,A
+165,MATH,1260,2019,Spring,c,A
+166,CS,3500,2020,Summer,a,A
+167,BIOL,2355,2020,Fall,a,A
+167,PHYS,3220,2020,Spring,d,A
+168,CS,2420,2020,Fall,a,A
+169,CS,2100,2019,Summer,b,A
+169,MATH,2280,2020,Spring,b,A
+169,PHYS,2040,2018,Spring,a,A
+170,CS,4940,2020,Summer,a,A
+173,BIOL,1006,2019,Fall,a,A
+173,MATH,2210,2019,Spring,b,A
+175,PHYS,3210,2020,Summer,a,A
+176,BIOL,1006,2016,Spring,a,A
+176,PHYS,2140,2015,Summer,b,A
+177,BIOL,2330,2016,Fall,a,A
+177,BIOL,2420,2015,Spring,a,A
+177,CS,3810,2018,Summer,b,A
+177,MATH,1260,2015,Spring,c,A
+179,CS,2100,2016,Summer,a,A
+179,PHYS,2060,2019,Summer,b,A
+185,MATH,1250,2020,Summer,a,A
+185,MATH,1260,2019,Summer,a,A
+186,MATH,2270,2020,Fall,b,A
+187,CS,4970,2020,Summer,b,A
+187,PHYS,3210,2020,Summer,a,A
+191,CS,4970,2020,Fall,a,A
+192,BIOL,2020,2015,Fall,d,A
+200,PHYS,3220,2020,Spring,c,A
+203,PHYS,3220,2017,Fall,b,A
+207,BIOL,2355,2018,Summer,d,A
+207,CS,1410,2016,Summer,a,A
+207,MATH,1250,2018,Summer,c,A
+210,MATH,3220,2016,Spring,d,A
+214,MATH,3220,2016,Summer,a,A
+215,CS,4500,2016,Spring,b,A
+215,PHYS,2140,2016,Spring,c,A
+216,CS,1410,2016,Spring,b,A
+217,BIOL,1010,2019,Spring,b,A
+217,PHYS,2060,2018,Fall,c,A
+223,PHYS,2060,2020,Spring,a,A
+224,BIOL,2420,2020,Fall,a,A
+227,BIOL,2330,2019,Fall,a,A
+228,CS,4970,2020,Summer,d,A
+229,CS,2420,2016,Fall,a,A
+230,CS,3505,2019,Spring,b,A
+230,MATH,1250,2017,Summer,c,A
+230,PHYS,2210,2017,Summer,a,A
+231,BIOL,2210,2017,Spring,a,A
+231,CS,2100,2018,Fall,c,A
+231,MATH,1220,2019,Fall,a,A
+234,CS,4400,2019,Summer,a,A
+237,CS,3810,2018,Spring,a,A
+238,BIOL,2021,2019,Spring,b,A
+240,MATH,2270,2020,Fall,a,A
+241,CS,2100,2019,Summer,a,A
+242,CS,4970,2020,Summer,a,A
+246,BIOL,2420,2015,Spring,b,A
+247,CS,3505,2018,Summer,a,A
+249,BIOL,1006,2015,Summer,b,A
+249,CS,4150,2016,Summer,a,A
+249,CS,4150,2016,Summer,b,A
+249,PHYS,3210,2016,Summer,b,A
+252,CS,3810,2018,Summer,d,A
+255,CS,2100,2018,Spring,a,A
+255,CS,4400,2017,Spring,b,A
+255,CS,4500,2019,Fall,d,A
+256,CS,4500,2019,Fall,a,A
+257,BIOL,1030,2017,Spring,c,A
+257,CS,3505,2020,Summer,a,A
+257,MATH,1250,2017,Summer,c,A
+260,CS,4150,2019,Spring,a,A
+262,CS,2420,2016,Fall,b,A
+262,CS,4400,2016,Summer,a,A
+262,CS,4970,2018,Fall,b,A
+264,BIOL,2420,2017,Summer,a,A
+264,PHYS,3210,2017,Summer,b,A
+267,PHYS,2040,2020,Spring,a,A
+269,PHYS,2060,2020,Spring,b,A
+270,PHYS,2060,2018,Fall,b,A
+271,CS,1030,2020,Fall,a,A
+273,BIOL,1030,2016,Spring,a,A
+274,PHYS,2060,2019,Fall,b,A
+275,BIOL,1210,2017,Summer,a,A
+275,BIOL,2210,2018,Spring,a,A
+275,MATH,2210,2018,Spring,b,A
+276,CS,3200,2018,Spring,b,A
+276,CS,4970,2016,Fall,b,A
+277,BIOL,2330,2017,Summer,a,A
+277,CS,4000,2020,Fall,a,A
+277,CS,4970,2018,Summer,a,A
+277,PHYS,2100,2015,Spring,b,A
+277,PHYS,2140,2016,Summer,b,A
+282,CS,4970,2017,Spring,a,A
+283,CS,4970,2020,Fall,b,A
+285,BIOL,1010,2018,Summer,b,A
+285,BIOL,2020,2018,Spring,a,A
+285,BIOL,2030,2017,Spring,d,A
+285,BIOL,2420,2020,Spring,a,A
+285,CS,4400,2019,Summer,a,A
+285,MATH,2280,2020,Spring,a,A
+285,PHYS,2220,2018,Fall,a,A
+288,MATH,1250,2018,Summer,c,A
+289,PHYS,2140,2019,Fall,b,A
+290,BIOL,1030,2016,Summer,a,A
+292,BIOL,2010,2020,Spring,b,A
+292,BIOL,2021,2017,Fall,a,A
+292,CS,3200,2020,Summer,a,A
+292,MATH,1250,2017,Summer,a,A
+292,PHYS,2140,2020,Fall,a,A
+293,BIOL,2210,2019,Fall,a,A
+293,CS,2100,2019,Summer,a,A
+293,PHYS,3210,2020,Fall,a,A
+295,MATH,1210,2016,Spring,b,A
+299,CS,2420,2017,Summer,b,A
+300,CS,3505,2019,Summer,c,A
+302,CS,2420,2015,Summer,c,A
+307,CS,4400,2016,Spring,a,A
+307,MATH,2280,2018,Spring,a,A
+307,PHYS,2060,2019,Summer,a,A
+310,PHYS,3220,2020,Spring,a,A
+311,BIOL,2030,2020,Spring,b,A
+311,BIOL,2420,2020,Summer,a,A
+311,CS,3810,2018,Summer,c,A
+312,BIOL,2330,2015,Fall,d,A
+312,PHYS,2060,2019,Summer,a,A
+313,BIOL,2420,2020,Summer,a,A
+313,PHYS,2220,2017,Spring,a,A
+314,BIOL,2030,2016,Fall,a,A
+314,CS,3810,2016,Summer,a,A
+314,MATH,1260,2019,Summer,b,A
+314,MATH,2210,2017,Spring,a,A
+318,BIOL,2355,2017,Summer,a,A
+321,CS,3500,2019,Fall,b,A
+321,CS,4400,2019,Spring,a,A
+321,MATH,1220,2019,Fall,b,A
+321,MATH,3210,2019,Spring,b,A
+323,PHYS,2220,2020,Summer,b,A
+329,BIOL,1006,2019,Summer,a,A
+329,CS,4400,2017,Spring,a,A
+331,PHYS,3210,2020,Fall,c,A
+333,CS,3500,2020,Summer,a,A
+333,CS,3810,2019,Fall,a,A
+335,PHYS,2140,2016,Fall,a,A
+336,BIOL,2010,2015,Fall,a,A
+340,BIOL,1010,2020,Summer,d,A
+340,BIOL,2021,2019,Fall,a,A
+342,BIOL,2030,2018,Summer,a,A
+342,PHYS,3220,2020,Spring,d,A
+345,CS,4400,2019,Spring,d,A
+345,PHYS,2210,2019,Fall,b,A
+347,BIOL,2210,2020,Fall,a,A
+347,BIOL,2420,2020,Summer,a,A
+348,BIOL,2355,2018,Summer,b,A
+348,CS,3200,2016,Fall,b,A
+348,MATH,1220,2018,Summer,a,A
+351,CS,4970,2019,Spring,a,A
+353,BIOL,1010,2017,Summer,a,A
+353,MATH,1260,2017,Summer,a,A
+356,MATH,1210,2017,Spring,a,A
+357,BIOL,2325,2016,Summer,a,A
+359,MATH,2280,2018,Fall,b,A
+362,BIOL,1006,2018,Spring,a,A
+362,BIOL,2030,2019,Summer,b,A
+362,PHYS,2140,2019,Fall,b,A
+364,MATH,3210,2019,Spring,a,A
+366,BIOL,2355,2017,Fall,a,A
+366,CS,1410,2018,Spring,d,A
+366,MATH,3220,2017,Fall,b,A
+366,PHYS,3210,2019,Summer,b,A
+368,CS,4500,2020,Summer,a,A
+369,CS,2420,2016,Fall,a,A
+369,CS,4400,2017,Spring,a,A
+371,CS,3505,2018,Fall,c,A
+372,MATH,1210,2018,Spring,a,A
+373,BIOL,2355,2017,Fall,b,A
+373,PHYS,2220,2018,Summer,a,A
+374,PHYS,2100,2017,Fall,a,A
+375,BIOL,2355,2017,Summer,a,A
+377,BIOL,1210,2017,Spring,a,A
+377,BIOL,2030,2017,Spring,a,A
+378,PHYS,2210,2018,Fall,b,A
+379,BIOL,2355,2018,Summer,b,A
+379,CS,4970,2020,Summer,b,A
+380,PHYS,2060,2019,Fall,a,A
+384,CS,4970,2020,Summer,c,A
+384,PHYS,3210,2019,Spring,a,A
+386,BIOL,2325,2018,Summer,a,A
+386,MATH,1250,2020,Summer,a,A
+387,BIOL,2020,2018,Fall,c,A
+387,MATH,2280,2018,Spring,a,A
+387,PHYS,2100,2017,Fall,a,A
+391,CS,4940,2020,Summer,a,A
+391,CS,4940,2020,Summer,b,A
+391,PHYS,2040,2019,Spring,a,A
+391,PHYS,2140,2020,Fall,a,A
+391,PHYS,2210,2019,Spring,d,A
+392,BIOL,1006,2017,Fall,a,A
+393,CS,3100,2017,Summer,a,A
+394,MATH,2270,2017,Fall,c,A
+394,PHYS,2140,2015,Spring,b,A
+396,CS,3500,2019,Summer,a,A
+397,BIOL,1010,2017,Spring,a,A
+397,CS,3500,2019,Fall,a,A
+397,CS,4940,2020,Summer,a,A
+397,PHYS,3210,2017,Fall,a,A
+399,PHYS,2060,2018,Fall,a,A
+399,PHYS,2100,2019,Summer,a,A
+100,MATH,1220,2020,Spring,a,A-
+102,BIOL,1030,2018,Fall,a,A-
+102,BIOL,2020,2019,Summer,a,A-
+102,BIOL,2021,2018,Spring,a,A-
+102,BIOL,2210,2019,Summer,a,A-
+102,CS,4150,2019,Spring,a,A-
+102,MATH,1250,2018,Summer,a,A-
+107,BIOL,2021,2019,Fall,a,A-
+107,CS,3505,2016,Summer,a,A-
+107,PHYS,3220,2016,Summer,b,A-
+108,BIOL,1010,2020,Summer,b,A-
+109,BIOL,1030,2020,Summer,a,A-
+109,CS,4970,2020,Summer,d,A-
+110,CS,3505,2020,Fall,b,A-
+113,BIOL,2030,2019,Summer,b,A-
+113,MATH,2210,2020,Spring,a,A-
+113,PHYS,2210,2018,Fall,a,A-
+113,PHYS,2210,2018,Fall,b,A-
+118,CS,4970,2020,Summer,b,A-
+120,CS,4970,2017,Spring,a,A-
+120,PHYS,2210,2018,Fall,c,A-
+120,PHYS,3220,2017,Fall,a,A-
+123,BIOL,1010,2015,Summer,b,A-
+123,CS,2100,2016,Summer,a,A-
+123,MATH,1250,2018,Spring,a,A-
+123,MATH,1260,2019,Summer,b,A-
+123,MATH,2270,2017,Fall,d,A-
+123,MATH,3210,2020,Spring,a,A-
+123,PHYS,2040,2018,Spring,a,A-
+123,PHYS,3220,2016,Summer,b,A-
+124,BIOL,2420,2020,Summer,a,A-
+124,MATH,1260,2019,Summer,a,A-
+126,BIOL,2020,2015,Fall,a,A-
+126,MATH,3210,2015,Fall,a,A-
+127,BIOL,2021,2018,Fall,a,A-
+127,PHYS,3220,2017,Fall,d,A-
+128,CS,1030,2018,Fall,a,A-
+128,CS,2420,2017,Fall,a,A-
+129,BIOL,2020,2018,Spring,a,A-
+130,CS,4970,2020,Fall,c,A-
+131,BIOL,1210,2018,Spring,a,A-
+131,MATH,2210,2018,Spring,b,A-
+133,MATH,1250,2020,Summer,a,A-
+138,CS,4940,2015,Summer,a,A-
+138,MATH,3210,2015,Fall,b,A-
+142,BIOL,1006,2020,Spring,a,A-
+142,CS,3500,2020,Summer,a,A-
+143,CS,3500,2019,Fall,c,A-
+143,CS,3505,2018,Summer,b,A-
+143,PHYS,2140,2018,Summer,a,A-
+144,BIOL,2020,2015,Summer,a,A-
+151,BIOL,1010,2017,Summer,a,A-
+151,CS,2420,2016,Fall,b,A-
+160,CS,2420,2015,Summer,a,A-
+162,MATH,1220,2015,Summer,b,A-
+169,CS,3505,2019,Summer,a,A-
+170,CS,4400,2020,Spring,a,A-
+171,CS,4940,2020,Summer,b,A-
+172,CS,2420,2016,Summer,a,A-
+173,BIOL,1210,2019,Spring,a,A-
+173,BIOL,2010,2017,Summer,a,A-
+173,CS,4500,2019,Fall,b,A-
+175,BIOL,2420,2020,Fall,a,A-
+178,BIOL,2010,2020,Spring,b,A-
+179,BIOL,1030,2019,Spring,a,A-
+179,CS,4500,2016,Spring,b,A-
+181,CS,4000,2020,Spring,b,A-
+181,MATH,3210,2020,Spring,a,A-
+182,MATH,1250,2016,Fall,b,A-
+183,CS,2100,2019,Fall,d,A-
+185,CS,1030,2019,Fall,b,A-
+185,PHYS,2100,2018,Fall,a,A-
+187,BIOL,2210,2017,Summer,a,A-
+187,CS,3810,2020,Fall,a,A-
+187,CS,4000,2017,Spring,a,A-
+187,PHYS,2220,2017,Spring,c,A-
+192,CS,3505,2015,Spring,a,A-
+193,BIOL,2010,2015,Spring,a,A-
+193,PHYS,2100,2015,Spring,a,A-
+194,CS,4970,2019,Fall,a,A-
+194,PHYS,2220,2020,Summer,a,A-
+195,CS,3100,2016,Spring,d,A-
+196,CS,3100,2019,Spring,a,A-
+197,MATH,1250,2018,Summer,c,A-
+199,BIOL,2020,2018,Fall,a,A-
+199,CS,2100,2018,Fall,d,A-
+202,CS,4400,2020,Fall,b,A-
+203,CS,4500,2018,Spring,a,A-
+204,CS,2420,2015,Summer,a,A-
+208,BIOL,2010,2017,Fall,a,A-
+208,MATH,2210,2017,Spring,a,A-
+210,PHYS,2220,2018,Spring,a,A-
+212,BIOL,2030,2015,Fall,a,A-
+212,PHYS,2040,2015,Fall,c,A-
+214,CS,4970,2018,Summer,c,A-
+215,CS,4400,2015,Summer,a,A-
+215,MATH,1250,2016,Fall,a,A-
+215,MATH,2210,2017,Spring,a,A-
+221,PHYS,2210,2019,Fall,c,A-
+228,BIOL,2210,2019,Summer,b,A-
+228,MATH,2210,2019,Spring,b,A-
+229,MATH,3220,2018,Spring,c,A-
+230,CS,4400,2020,Fall,a,A-
+231,BIOL,1010,2019,Spring,b,A-
+233,CS,4940,2020,Summer,b,A-
+235,CS,3505,2019,Fall,c,A-
+237,BIOL,2355,2017,Fall,a,A-
+237,PHYS,2220,2018,Spring,a,A-
+240,CS,3810,2018,Summer,c,A-
+240,CS,4150,2018,Fall,a,A-
+241,CS,3505,2019,Spring,a,A-
+243,BIOL,2030,2017,Spring,b,A-
+243,BIOL,2210,2016,Summer,a,A-
+243,BIOL,2355,2017,Spring,d,A-
+245,CS,3810,2016,Fall,b,A-
+246,MATH,3220,2016,Fall,a,A-
+247,BIOL,1006,2019,Summer,a,A-
+247,BIOL,2355,2019,Spring,a,A-
+248,CS,3505,2019,Fall,c,A-
+248,MATH,3210,2019,Fall,a,A-
+250,CS,4940,2020,Summer,b,A-
+252,CS,3505,2018,Fall,b,A-
+254,PHYS,3210,2020,Spring,a,A-
+255,CS,4150,2018,Fall,b,A-
+255,PHYS,2220,2018,Summer,a,A-
+257,CS,3200,2018,Spring,a,A-
+258,CS,4400,2020,Fall,a,A-
+260,CS,3100,2017,Fall,a,A-
+260,MATH,3210,2020,Summer,a,A-
+261,CS,2100,2018,Summer,a,A-
+261,MATH,3220,2017,Fall,b,A-
+262,BIOL,2010,2017,Fall,a,A-
+262,CS,3505,2018,Summer,b,A-
+262,PHYS,2060,2016,Summer,a,A-
+270,BIOL,2010,2018,Spring,a,A-
+270,BIOL,2021,2016,Fall,a,A-
+270,CS,3500,2019,Fall,b,A-
+271,CS,4940,2020,Summer,b,A-
+272,MATH,2210,2020,Fall,a,A-
+275,BIOL,2325,2018,Summer,a,A-
+276,BIOL,2020,2018,Fall,a,A-
+276,CS,4000,2016,Fall,a,A-
+276,PHYS,2060,2016,Summer,b,A-
+276,PHYS,2100,2017,Summer,a,A-
+277,BIOL,2355,2018,Spring,a,A-
+277,CS,1030,2016,Summer,a,A-
+277,CS,1410,2020,Spring,b,A-
+277,CS,2420,2015,Spring,a,A-
+277,CS,4150,2020,Spring,a,A-
+277,MATH,3210,2016,Fall,a,A-
+278,CS,2100,2016,Summer,c,A-
+279,MATH,1210,2018,Summer,a,A-
+282,BIOL,1030,2016,Spring,a,A-
+282,CS,2420,2016,Fall,a,A-
+282,PHYS,2060,2016,Summer,b,A-
+285,MATH,1250,2016,Summer,a,A-
+285,MATH,1260,2019,Spring,b,A-
+285,PHYS,3210,2018,Fall,a,A-
+287,PHYS,3210,2019,Summer,b,A-
+288,BIOL,2020,2018,Fall,d,A-
+288,CS,3100,2019,Spring,b,A-
+288,PHYS,3220,2017,Fall,d,A-
+289,PHYS,2220,2020,Summer,b,A-
+289,PHYS,3210,2020,Fall,a,A-
+290,BIOL,2330,2015,Fall,a,A-
+290,CS,3200,2016,Summer,a,A-
+292,CS,4400,2020,Fall,b,A-
+292,CS,4970,2020,Summer,c,A-
+292,PHYS,2220,2018,Fall,a,A-
+293,CS,4970,2019,Fall,b,A-
+293,PHYS,2060,2020,Spring,b,A-
+294,CS,4500,2018,Spring,a,A-
+295,CS,3200,2015,Fall,d,A-
+296,BIOL,2021,2018,Fall,c,A-
+296,MATH,2210,2019,Spring,b,A-
+296,PHYS,2220,2020,Summer,a,A-
+298,CS,4970,2019,Summer,b,A-
+300,BIOL,2330,2020,Spring,a,A-
+300,CS,4500,2019,Fall,b,A-
+300,CS,4940,2020,Summer,b,A-
+300,PHYS,3210,2020,Summer,a,A-
+301,MATH,3220,2016,Spring,b,A-
+305,BIOL,1210,2019,Spring,a,A-
+305,MATH,3220,2018,Spring,c,A-
+305,PHYS,2220,2018,Spring,a,A-
+307,PHYS,2140,2016,Spring,c,A-
+307,PHYS,3210,2019,Summer,c,A-
+311,CS,4000,2020,Spring,b,A-
+311,MATH,1250,2017,Summer,b,A-
+311,MATH,3220,2017,Fall,a,A-
+311,PHYS,2220,2020,Summer,a,A-
+312,BIOL,2021,2018,Summer,a,A-
+313,BIOL,1006,2020,Fall,b,A-
+313,CS,3505,2015,Fall,a,A-
+313,MATH,1250,2018,Summer,b,A-
+314,MATH,1220,2017,Spring,b,A-
+317,BIOL,1010,2016,Summer,a,A-
+317,PHYS,2220,2016,Summer,a,A-
+318,CS,4970,2019,Fall,d,A-
+321,MATH,1250,2018,Summer,b,A-
+321,MATH,1250,2018,Summer,c,A-
+325,CS,3200,2020,Spring,c,A-
+329,MATH,1220,2020,Summer,a,A-
+329,MATH,3220,2016,Fall,b,A-
+330,BIOL,1006,2020,Spring,a,A-
+332,BIOL,2355,2018,Summer,c,A-
+333,PHYS,2210,2019,Fall,b,A-
+335,BIOL,1030,2017,Spring,c,A-
+335,MATH,3210,2015,Fall,d,A-
+339,CS,3505,2020,Fall,a,A-
+340,BIOL,2330,2020,Spring,a,A-
+342,BIOL,2325,2019,Spring,b,A-
+342,BIOL,2355,2018,Summer,a,A-
+342,PHYS,3210,2020,Fall,a,A-
+344,CS,1030,2018,Fall,a,A-
+345,CS,2100,2018,Summer,c,A-
+345,CS,2420,2020,Fall,a,A-
+345,PHYS,2220,2020,Summer,a,A-
+347,CS,4150,2020,Fall,a,A-
+348,CS,1410,2018,Spring,a,A-
+348,CS,3500,2020,Summer,a,A-
+357,CS,4500,2016,Spring,b,A-
+359,CS,3810,2019,Fall,b,A-
+359,PHYS,2060,2019,Fall,a,A-
+361,CS,4500,2018,Spring,a,A-
+361,MATH,2210,2017,Summer,a,A-
+362,PHYS,3210,2019,Summer,b,A-
+363,CS,4970,2019,Summer,c,A-
+363,PHYS,2210,2019,Fall,b,A-
+366,CS,3100,2019,Spring,b,A-
+368,CS,2100,2019,Summer,b,A-
+369,BIOL,2325,2016,Summer,a,A-
+369,MATH,2210,2018,Spring,b,A-
+371,CS,2100,2018,Summer,c,A-
+372,BIOL,2355,2019,Spring,b,A-
+373,BIOL,2420,2020,Spring,a,A-
+373,CS,3200,2016,Summer,a,A-
+373,CS,4400,2015,Fall,c,A-
+373,PHYS,2060,2016,Summer,a,A-
+374,BIOL,2325,2018,Spring,a,A-
+374,CS,3100,2016,Spring,b,A-
+374,MATH,3220,2016,Spring,c,A-
+374,PHYS,2040,2015,Fall,b,A-
+377,CS,3810,2018,Summer,b,A-
+377,MATH,1260,2019,Summer,b,A-
+378,BIOL,2030,2017,Spring,c,A-
+378,PHYS,2220,2016,Summer,a,A-
+379,BIOL,2021,2016,Fall,a,A-
+379,CS,4940,2017,Fall,b,A-
+379,CS,4970,2020,Summer,d,A-
+379,PHYS,3220,2018,Summer,a,A-
+380,BIOL,2330,2019,Fall,a,A-
+384,MATH,1250,2020,Summer,a,A-
+385,PHYS,3210,2018,Spring,a,A-
+386,CS,3810,2018,Summer,a,A-
+386,CS,4500,2019,Summer,a,A-
+388,CS,3810,2018,Spring,a,A-
+391,BIOL,2420,2020,Fall,a,A-
+391,CS,3505,2019,Fall,a,A-
+392,CS,4970,2018,Summer,c,A-
+392,MATH,1210,2017,Summer,c,A-
+392,PHYS,2060,2016,Spring,a,A-
+393,BIOL,2355,2018,Spring,a,A-
+393,CS,3505,2016,Summer,a,A-
+395,CS,3500,2016,Spring,a,A-
+396,MATH,2270,2019,Summer,c,A-
+397,BIOL,1006,2018,Spring,a,A-
+397,BIOL,2030,2016,Fall,a,A-
+397,CS,3200,2017,Spring,a,A-
+398,BIOL,1006,2019,Fall,b,A-
+398,CS,4940,2019,Fall,a,A-
+398,MATH,1210,2018,Summer,a,A-
+399,CS,3810,2018,Summer,b,A-
+100,CS,4970,2018,Summer,b,B
+100,PHYS,3210,2019,Fall,a,B
+102,BIOL,1010,2018,Summer,a,B
+102,BIOL,2325,2017,Fall,b,B
+105,BIOL,2355,2017,Spring,b,B
+105,MATH,1220,2017,Spring,d,B
+105,PHYS,2140,2018,Summer,a,B
+106,MATH,1210,2020,Spring,b,B
+106,MATH,1250,2020,Summer,a,B
+106,PHYS,3220,2020,Spring,c,B
+107,CS,3500,2016,Summer,a,B
+107,PHYS,3210,2016,Summer,a,B
+108,CS,3200,2020,Spring,c,B
+108,MATH,1260,2019,Fall,a,B
+109,BIOL,1006,2019,Fall,a,B
+112,CS,3200,2020,Summer,a,B
+113,CS,3810,2020,Fall,a,B
+115,BIOL,2020,2016,Spring,a,B
+117,BIOL,1006,2018,Spring,a,B
+117,BIOL,2021,2018,Summer,a,B
+118,CS,2100,2019,Fall,a,B
+119,MATH,1210,2016,Spring,a,B
+119,MATH,3220,2016,Spring,c,B
+120,CS,1410,2018,Spring,b,B
+121,PHYS,2060,2019,Summer,b,B
+122,BIOL,1010,2020,Summer,b,B
+122,CS,2100,2020,Fall,a,B
+123,MATH,1210,2019,Summer,a,B
+123,MATH,2210,2018,Spring,a,B
+124,BIOL,2355,2020,Fall,a,B
+124,CS,4970,2019,Summer,a,B
+124,MATH,2270,2017,Fall,d,B
+127,CS,4970,2019,Fall,b,B
+127,MATH,1250,2017,Summer,a,B
+127,MATH,3220,2018,Spring,c,B
+128,BIOL,2210,2018,Spring,a,B
+128,BIOL,2420,2020,Summer,a,B
+129,CS,3505,2019,Summer,b,B
+131,MATH,3220,2018,Spring,c,B
+132,CS,4500,2018,Spring,a,B
+133,BIOL,2021,2018,Fall,d,B
+133,CS,3810,2018,Summer,c,B
+134,CS,4000,2017,Summer,a,B
+135,CS,3200,2020,Fall,a,B
+135,MATH,1220,2019,Fall,c,B
+139,MATH,1220,2018,Summer,a,B
+143,CS,4970,2018,Fall,c,B
+144,MATH,3210,2015,Summer,a,B
+146,CS,2100,2019,Fall,c,B
+149,CS,3500,2015,Fall,b,B
+151,BIOL,2325,2018,Summer,a,B
+151,BIOL,2420,2020,Summer,a,B
+151,CS,4400,2019,Spring,b,B
+151,MATH,1250,2020,Summer,a,B
+152,MATH,1260,2019,Spring,b,B
+153,BIOL,1010,2020,Summer,a,B
+158,MATH,1250,2018,Summer,c,B
+162,CS,4150,2015,Summer,a,B
+163,MATH,1210,2018,Fall,b,B
+164,BIOL,2030,2020,Spring,a,B
+164,CS,3500,2020,Summer,a,B
+164,CS,3505,2020,Spring,a,B
+167,CS,4500,2020,Summer,a,B
+169,CS,4000,2020,Spring,a,B
+169,CS,4500,2020,Spring,a,B
+170,MATH,2210,2020,Spring,b,B
+170,PHYS,3220,2020,Spring,a,B
+171,CS,3500,2019,Fall,b,B
+171,CS,3810,2020,Fall,a,B
+173,BIOL,1010,2018,Summer,b,B
+173,CS,3505,2018,Summer,b,B
+173,MATH,1250,2017,Summer,a,B
+176,BIOL,1010,2016,Summer,a,B
+176,BIOL,1030,2016,Fall,a,B
+177,BIOL,1010,2015,Summer,b,B
+177,CS,3810,2018,Summer,a,B
+178,PHYS,2040,2019,Spring,a,B
+179,CS,3500,2019,Summer,a,B
+179,CS,3810,2018,Spring,a,B
+179,MATH,1210,2016,Spring,d,B
+180,MATH,1220,2019,Fall,b,B
+181,CS,2100,2019,Fall,a,B
+181,CS,2100,2019,Fall,d,B
+181,CS,4000,2020,Spring,a,B
+182,BIOL,1010,2015,Summer,a,B
+185,BIOL,1010,2020,Summer,c,B
+185,BIOL,2210,2020,Fall,a,B
+187,PHYS,2040,2017,Fall,c,B
+192,CS,3100,2016,Spring,d,B
+199,BIOL,1006,2017,Fall,a,B
+199,BIOL,2330,2017,Fall,b,B
+199,CS,1410,2018,Spring,b,B
+199,CS,3500,2019,Fall,b,B
+200,BIOL,1010,2020,Summer,a,B
+200,CS,3505,2020,Summer,a,B
+204,BIOL,2325,2015,Fall,c,B
+207,BIOL,2030,2016,Summer,b,B
+207,CS,3200,2016,Summer,b,B
+207,MATH,3220,2017,Fall,a,B
+210,MATH,1220,2016,Spring,a,B
+210,MATH,1250,2017,Summer,b,B
+210,MATH,3220,2016,Spring,a,B
+211,MATH,1260,2015,Summer,a,B
+212,MATH,2210,2015,Summer,c,B
+214,BIOL,2355,2018,Spring,a,B
+214,MATH,1210,2016,Fall,a,B
+215,CS,4500,2016,Spring,a,B
+215,MATH,1210,2016,Fall,b,B
+215,PHYS,2100,2017,Summer,b,B
+216,CS,1410,2016,Spring,a,B
+221,PHYS,2060,2020,Spring,b,B
+227,BIOL,2210,2018,Summer,b,B
+229,CS,1410,2018,Spring,b,B
+229,CS,3500,2016,Spring,a,B
+230,MATH,2270,2020,Fall,a,B
+231,MATH,2210,2018,Spring,b,B
+231,PHYS,2210,2017,Summer,a,B
+234,BIOL,1006,2019,Summer,a,B
+235,CS,4150,2020,Fall,a,B
+238,MATH,2280,2018,Spring,a,B
+240,BIOL,1010,2019,Spring,c,B
+240,CS,3505,2018,Fall,a,B
+241,BIOL,2420,2020,Spring,b,B
+241,CS,3810,2019,Fall,a,B
+241,MATH,2210,2020,Spring,c,B
+246,CS,3200,2016,Summer,a,B
+246,MATH,3210,2015,Fall,a,B
+247,CS,4970,2018,Fall,b,B
+247,MATH,1250,2018,Summer,a,B
+248,BIOL,2021,2018,Fall,c,B
+248,MATH,1220,2019,Fall,a,B
+248,MATH,2270,2019,Summer,a,B
+249,BIOL,1010,2017,Spring,a,B
+249,BIOL,2030,2015,Fall,a,B
+251,CS,4970,2020,Summer,a,B
+251,MATH,2210,2020,Spring,c,B
+255,CS,3810,2018,Spring,a,B
+255,CS,4000,2017,Spring,a,B
+255,MATH,2270,2019,Spring,a,B
+255,PHYS,3210,2019,Summer,b,B
+257,BIOL,1030,2017,Spring,a,B
+258,BIOL,2355,2020,Summer,a,B
+258,CS,3505,2018,Fall,a,B
+258,CS,3810,2019,Fall,a,B
+258,PHYS,3210,2019,Spring,a,B
+260,BIOL,2210,2018,Summer,a,B
+260,CS,2100,2019,Fall,c,B
+264,PHYS,2060,2016,Summer,a,B
+264,PHYS,2100,2017,Summer,c,B
+267,CS,4400,2019,Summer,b,B
+267,PHYS,2140,2020,Fall,a,B
+267,PHYS,2220,2018,Fall,a,B
+268,CS,2420,2016,Fall,b,B
+270,BIOL,1210,2016,Spring,a,B
+270,CS,3200,2016,Summer,a,B
+270,CS,3810,2018,Summer,b,B
+270,MATH,2270,2020,Spring,a,B
+270,PHYS,2220,2017,Spring,c,B
+274,BIOL,2355,2018,Summer,c,B
+274,CS,3200,2018,Spring,a,B
+276,BIOL,2325,2019,Summer,a,B
+276,CS,1410,2015,Summer,b,B
+276,CS,2100,2016,Spring,a,B
+276,CS,2420,2015,Fall,a,B
+276,CS,4500,2015,Summer,a,B
+276,MATH,3220,2016,Summer,a,B
+277,MATH,1220,2017,Spring,c,B
+277,MATH,3220,2016,Fall,a,B
+277,PHYS,2220,2017,Spring,c,B
+277,PHYS,3210,2018,Spring,a,B
+278,MATH,1210,2016,Fall,b,B
+282,BIOL,2355,2017,Spring,c,B
+285,BIOL,2030,2017,Spring,b,B
+285,PHYS,2040,2017,Fall,b,B
+288,CS,3500,2016,Summer,a,B
+289,BIOL,1006,2020,Fall,c,B
+289,MATH,1250,2020,Summer,a,B
+290,BIOL,2021,2015,Summer,c,B
+290,CS,1410,2017,Spring,a,B
+292,CS,4150,2018,Fall,a,B
+292,PHYS,2060,2020,Spring,b,B
+292,PHYS,3210,2019,Spring,c,B
+293,CS,4500,2019,Fall,a,B
+294,CS,4970,2019,Summer,d,B
+296,BIOL,2021,2018,Fall,d,B
+296,CS,2100,2019,Summer,a,B
+296,CS,3505,2019,Summer,b,B
+297,BIOL,2210,2020,Fall,a,B
+305,CS,3810,2018,Spring,a,B
+306,PHYS,3210,2020,Fall,a,B
+307,BIOL,1210,2019,Spring,a,B
+307,MATH,1220,2016,Spring,a,B
+309,BIOL,2330,2017,Summer,a,B
+309,CS,4970,2020,Summer,d,B
+309,MATH,2270,2020,Spring,a,B
+309,MATH,3220,2018,Spring,c,B
+309,PHYS,2210,2019,Fall,b,B
+311,CS,3505,2019,Fall,c,B
+312,BIOL,1010,2017,Spring,a,B
+312,PHYS,2140,2016,Summer,a,B
+312,PHYS,2220,2017,Spring,b,B
+312,PHYS,2220,2017,Spring,d,B
+312,PHYS,3210,2019,Spring,b,B
+313,BIOL,1010,2018,Summer,b,B
+314,BIOL,2355,2018,Fall,a,B
+314,CS,2100,2019,Summer,a,B
+314,MATH,3210,2019,Spring,b,B
+314,PHYS,2140,2017,Summer,a,B
+316,CS,2100,2019,Fall,d,B
+318,BIOL,1030,2019,Spring,c,B
+318,BIOL,2325,2018,Summer,a,B
+318,CS,4500,2018,Spring,b,B
+321,BIOL,1030,2015,Summer,a,B
+321,CS,1030,2016,Fall,a,B
+321,CS,4000,2016,Fall,a,B
+321,CS,4500,2016,Spring,b,B
+321,CS,4970,2019,Fall,b,B
+321,PHYS,2040,2016,Spring,a,B
+321,PHYS,3220,2020,Spring,b,B
+323,BIOL,2355,2020,Summer,a,B
+326,MATH,3220,2017,Fall,b,B
+329,BIOL,2355,2017,Spring,b,B
+329,CS,2100,2018,Summer,b,B
+329,CS,3810,2016,Fall,b,B
+329,PHYS,2060,2018,Fall,b,B
+332,BIOL,2325,2018,Spring,a,B
+332,MATH,1210,2019,Spring,a,B
+333,BIOL,2355,2020,Summer,a,B
+333,CS,2100,2020,Fall,a,B
+333,MATH,2270,2019,Fall,a,B
+335,CS,1410,2016,Spring,b,B
+335,MATH,1250,2015,Fall,a,B
+341,CS,4000,2020,Fall,a,B
+342,MATH,1250,2020,Summer,a,B
+344,CS,4970,2018,Summer,a,B
+345,BIOL,2021,2017,Fall,a,B
+345,BIOL,2030,2019,Summer,d,B
+345,CS,4970,2019,Spring,b,B
+348,BIOL,1010,2020,Summer,b,B
+348,BIOL,2030,2017,Spring,b,B
+348,CS,2100,2017,Fall,a,B
+348,MATH,3210,2019,Spring,a,B
+351,MATH,1210,2019,Spring,a,B
+356,BIOL,2355,2019,Spring,a,B
+357,BIOL,2020,2016,Spring,a,B
+358,MATH,3210,2019,Fall,a,B
+360,MATH,2270,2020,Fall,a,B
+363,BIOL,2010,2020,Summer,b,B
+364,CS,3500,2020,Summer,a,B
+365,BIOL,2420,2020,Spring,b,B
+366,BIOL,2021,2018,Summer,a,B
+366,MATH,1220,2019,Fall,b,B
+368,BIOL,1010,2018,Summer,a,B
+368,CS,4000,2020,Fall,a,B
+368,PHYS,2210,2019,Spring,c,B
+369,BIOL,2210,2018,Summer,a,B
+371,BIOL,1010,2020,Summer,d,B
+372,CS,3810,2018,Spring,a,B
+372,CS,4970,2018,Summer,c,B
+373,PHYS,2040,2015,Fall,b,B
+373,PHYS,2210,2017,Summer,d,B
+375,BIOL,2210,2017,Summer,c,B
+378,BIOL,1030,2018,Summer,a,B
+378,BIOL,2330,2019,Fall,a,B
+378,MATH,1250,2020,Summer,a,B
+378,MATH,3210,2019,Spring,a,B
+379,CS,4500,2018,Spring,b,B
+379,MATH,2270,2019,Spring,a,B
+380,CS,3500,2019,Fall,a,B
+382,CS,1410,2015,Summer,d,B
+384,CS,2100,2018,Fall,b,B
+384,MATH,1210,2018,Fall,a,B
+385,CS,4000,2018,Spring,a,B
+386,CS,3500,2020,Summer,a,B
+387,CS,1030,2018,Fall,a,B
+390,CS,2100,2019,Summer,a,B
+390,CS,2420,2019,Summer,a,B
+390,CS,3505,2020,Fall,c,B
+390,MATH,1220,2019,Fall,c,B
+390,PHYS,2060,2020,Fall,a,B
+390,PHYS,2210,2019,Fall,c,B
+390,PHYS,2220,2020,Summer,b,B
+391,CS,2100,2018,Fall,d,B
+392,CS,4400,2015,Fall,b,B
+392,MATH,2210,2017,Summer,a,B
+397,MATH,1260,2019,Summer,a,B
+398,PHYS,2060,2019,Summer,a,B
+100,BIOL,2020,2018,Fall,b,B+
+100,MATH,1260,2019,Fall,a,B+
+101,PHYS,2140,2018,Summer,a,B+
+102,MATH,2270,2017,Fall,d,B+
+102,PHYS,2220,2018,Spring,a,B+
+105,CS,3200,2016,Fall,d,B+
+106,CS,3505,2020,Fall,b,B+
+107,BIOL,2355,2020,Spring,a,B+
+107,MATH,3220,2017,Fall,a,B+
+109,BIOL,2010,2020,Spring,a,B+
+110,CS,4000,2020,Fall,a,B+
+115,BIOL,1006,2016,Spring,a,B+
+115,BIOL,1210,2017,Spring,a,B+
+116,CS,3810,2016,Fall,b,B+
+117,MATH,1220,2017,Spring,c,B+
+117,MATH,2210,2018,Spring,a,B+
+118,CS,1030,2020,Spring,c,B+
+120,BIOL,2210,2017,Summer,b,B+
+120,CS,4400,2015,Summer,a,B+
+120,PHYS,2100,2016,Fall,a,B+
+120,PHYS,2140,2015,Fall,a,B+
+122,BIOL,1010,2020,Summer,a,B+
+123,BIOL,2420,2017,Summer,b,B+
+123,MATH,2280,2015,Fall,a,B+
+123,PHYS,2060,2019,Fall,c,B+
+124,CS,4400,2019,Fall,b,B+
+124,PHYS,2210,2018,Fall,c,B+
+127,CS,4000,2019,Spring,a,B+
+128,MATH,2210,2017,Summer,a,B+
+129,CS,3100,2019,Spring,b,B+
+129,CS,3505,2019,Summer,c,B+
+129,CS,3810,2018,Summer,c,B+
+131,CS,3200,2020,Spring,a,B+
+131,CS,3810,2019,Fall,a,B+
+131,CS,4500,2019,Fall,b,B+
+132,CS,2420,2017,Summer,b,B+
+134,CS,2100,2016,Summer,c,B+
+134,MATH,3220,2016,Fall,b,B+
+135,CS,4150,2020,Fall,a,B+
+135,MATH,3210,2020,Summer,a,B+
+140,BIOL,2030,2015,Fall,a,B+
+143,CS,4500,2019,Fall,c,B+
+143,CS,4940,2017,Fall,a,B+
+148,CS,4150,2020,Fall,a,B+
+151,BIOL,1210,2018,Fall,b,B+
+151,PHYS,2140,2018,Summer,a,B+
+152,CS,4970,2019,Fall,c,B+
+152,PHYS,3210,2019,Summer,b,B+
+153,PHYS,3210,2020,Fall,a,B+
+158,CS,2100,2018,Fall,a,B+
+160,BIOL,1030,2016,Summer,a,B+
+160,CS,3810,2016,Summer,a,B+
+163,BIOL,2325,2015,Fall,c,B+
+163,CS,4150,2016,Summer,a,B+
+163,MATH,3220,2016,Summer,a,B+
+166,BIOL,2010,2020,Summer,a,B+
+166,MATH,3210,2020,Summer,a,B+
+174,BIOL,2210,2018,Summer,a,B+
+176,CS,4150,2015,Summer,a,B+
+176,CS,4500,2016,Fall,a,B+
+177,BIOL,2021,2018,Spring,a,B+
+177,BIOL,2355,2020,Summer,b,B+
+179,CS,2420,2017,Summer,c,B+
+179,CS,4400,2016,Summer,a,B+
+179,MATH,3220,2018,Spring,d,B+
+179,PHYS,2100,2016,Fall,b,B+
+180,CS,3500,2019,Fall,a,B+
+181,MATH,1220,2019,Fall,a,B+
+182,BIOL,2020,2015,Fall,c,B+
+182,MATH,2270,2017,Fall,c,B+
+183,PHYS,2210,2018,Fall,a,B+
+185,PHYS,2060,2019,Fall,a,B+
+186,BIOL,2355,2020,Fall,a,B+
+187,BIOL,1006,2019,Fall,a,B+
+192,BIOL,2325,2015,Fall,c,B+
+192,CS,4150,2015,Summer,a,B+
+196,MATH,2280,2018,Fall,c,B+
+196,PHYS,2220,2018,Fall,a,B+
+197,CS,3200,2018,Spring,a,B+
+197,PHYS,3210,2018,Spring,c,B+
+200,MATH,3210,2020,Fall,a,B+
+207,CS,4500,2017,Summer,a,B+
+208,BIOL,2330,2017,Fall,a,B+
+210,MATH,2270,2015,Fall,b,B+
+210,MATH,2280,2020,Spring,a,B+
+210,PHYS,2040,2015,Fall,c,B+
+214,BIOL,1010,2018,Summer,a,B+
+214,BIOL,2020,2016,Spring,a,B+
+214,CS,1030,2016,Summer,a,B+
+214,MATH,1250,2016,Spring,a,B+
+215,BIOL,2210,2017,Spring,b,B+
+215,BIOL,2210,2017,Spring,c,B+
+217,BIOL,2325,2018,Fall,c,B+
+219,CS,2100,2020,Fall,a,B+
+220,CS,3810,2020,Fall,a,B+
+222,BIOL,1006,2020,Fall,a,B+
+222,CS,4970,2020,Summer,b,B+
+225,MATH,2210,2020,Fall,a,B+
+227,PHYS,2220,2018,Spring,a,B+
+227,PHYS,3220,2020,Spring,b,B+
+228,CS,4400,2020,Spring,a,B+
+228,MATH,1210,2019,Summer,a,B+
+228,PHYS,3210,2020,Fall,a,B+
+229,BIOL,2330,2017,Summer,a,B+
+229,PHYS,2060,2016,Spring,a,B+
+230,BIOL,2355,2018,Spring,a,B+
+231,BIOL,2020,2018,Fall,d,B+
+234,MATH,2280,2019,Fall,c,B+
+240,PHYS,3210,2020,Summer,a,B+
+243,CS,1030,2016,Fall,a,B+
+245,PHYS,2040,2015,Fall,a,B+
+246,BIOL,2030,2017,Spring,b,B+
+246,CS,4400,2017,Spring,a,B+
+246,PHYS,3210,2017,Summer,a,B+
+247,BIOL,1010,2019,Spring,d,B+
+247,CS,2100,2020,Fall,a,B+
+248,PHYS,2060,2018,Fall,b,B+
+249,CS,4400,2017,Spring,a,B+
+249,MATH,2210,2017,Spring,a,B+
+249,PHYS,3210,2016,Summer,a,B+
+254,BIOL,1010,2020,Summer,d,B+
+254,CS,3200,2020,Summer,a,B+
+255,CS,3200,2018,Spring,b,B+
+256,BIOL,1010,2020,Summer,a,B+
+256,CS,4000,2019,Spring,a,B+
+257,BIOL,1010,2020,Summer,b,B+
+257,CS,4000,2020,Spring,b,B+
+258,MATH,1260,2019,Fall,a,B+
+259,BIOL,1006,2019,Summer,a,B+
+259,MATH,3210,2019,Spring,b,B+
+259,PHYS,2040,2017,Fall,a,B+
+260,MATH,1210,2020,Spring,b,B+
+260,MATH,1250,2018,Spring,a,B+
+262,BIOL,2325,2018,Summer,a,B+
+262,MATH,2280,2018,Spring,a,B+
+263,CS,2420,2020,Summer,a,B+
+264,BIOL,2355,2017,Fall,b,B+
+264,CS,3100,2017,Fall,a,B+
+267,BIOL,1006,2020,Spring,a,B+
+269,PHYS,3220,2020,Spring,b,B+
+270,BIOL,1006,2018,Spring,b,B+
+270,BIOL,1010,2020,Summer,c,B+
+270,BIOL,1030,2016,Summer,a,B+
+270,BIOL,2020,2018,Fall,a,B+
+270,BIOL,2330,2016,Fall,a,B+
+270,BIOL,2420,2018,Spring,a,B+
+270,MATH,1220,2015,Summer,b,B+
+270,PHYS,2040,2017,Fall,c,B+
+270,PHYS,3210,2017,Fall,a,B+
+270,PHYS,3220,2017,Fall,d,B+
+271,BIOL,1006,2020,Fall,c,B+
+274,MATH,1220,2019,Fall,b,B+
+274,MATH,2210,2020,Spring,a,B+
+276,MATH,1210,2016,Spring,a,B+
+276,MATH,1220,2018,Spring,a,B+
+276,MATH,1260,2019,Summer,b,B+
+276,MATH,2210,2015,Spring,b,B+
+277,BIOL,1030,2016,Summer,a,B+
+277,BIOL,2010,2017,Summer,a,B+
+277,CS,4940,2020,Summer,a,B+
+278,BIOL,1210,2017,Spring,a,B+
+278,BIOL,2355,2017,Spring,a,B+
+281,MATH,2210,2020,Fall,a,B+
+282,BIOL,1210,2017,Summer,a,B+
+284,MATH,3210,2019,Fall,a,B+
+285,BIOL,2010,2018,Spring,a,B+
+285,CS,4150,2016,Summer,b,B+
+285,PHYS,2140,2017,Summer,a,B+
+288,PHYS,2210,2018,Fall,b,B+
+290,PHYS,2060,2016,Spring,b,B+
+292,MATH,3220,2018,Spring,a,B+
+293,BIOL,2020,2019,Summer,a,B+
+293,BIOL,2210,2019,Fall,b,B+
+293,MATH,1220,2020,Summer,a,B+
+294,PHYS,2060,2019,Summer,b,B+
+296,BIOL,1006,2018,Fall,a,B+
+296,BIOL,2010,2020,Summer,b,B+
+296,PHYS,3220,2020,Spring,c,B+
+300,BIOL,1010,2020,Summer,d,B+
+301,CS,4500,2016,Spring,b,B+
+301,MATH,3210,2015,Summer,a,B+
+303,MATH,1260,2019,Summer,b,B+
+304,MATH,2270,2017,Summer,a,B+
+306,CS,3200,2020,Summer,a,B+
+307,BIOL,2020,2019,Summer,a,B+
+309,BIOL,2021,2018,Fall,b,B+
+309,BIOL,2325,2018,Fall,a,B+
+309,CS,1030,2020,Spring,c,B+
+309,CS,2100,2018,Fall,b,B+
+310,PHYS,3210,2020,Spring,a,B+
+311,CS,2100,2017,Fall,a,B+
+311,PHYS,2210,2019,Spring,a,B+
+312,BIOL,1006,2016,Summer,a,B+
+312,CS,1030,2016,Spring,a,B+
+312,CS,1410,2020,Spring,a,B+
+312,CS,2100,2019,Spring,b,B+
+312,CS,3810,2018,Summer,d,B+
+312,MATH,1220,2018,Spring,a,B+
+312,MATH,3210,2020,Summer,a,B+
+313,CS,3810,2018,Spring,a,B+
+313,CS,4400,2017,Spring,c,B+
+313,PHYS,2140,2016,Spring,b,B+
+314,BIOL,1010,2019,Spring,d,B+
+314,CS,3505,2019,Spring,b,B+
+314,PHYS,2040,2017,Fall,c,B+
+317,PHYS,2140,2016,Summer,a,B+
+318,MATH,2280,2019,Fall,b,B+
+318,PHYS,2140,2019,Fall,b,B+
+321,PHYS,2100,2015,Spring,b,B+
+323,BIOL,1010,2020,Summer,d,B+
+326,BIOL,1006,2017,Fall,a,B+
+326,CS,2420,2017,Fall,a,B+
+329,CS,1410,2020,Spring,b,B+
+332,BIOL,1030,2020,Summer,a,B+
+332,PHYS,2210,2018,Fall,c,B+
+333,CS,3505,2020,Fall,b,B+
+333,PHYS,3210,2019,Summer,c,B+
+339,CS,4970,2020,Summer,c,B+
+340,CS,4970,2019,Fall,d,B+
+344,PHYS,2220,2018,Summer,a,B+
+345,BIOL,1006,2017,Fall,a,B+
+345,BIOL,1010,2018,Fall,a,B+
+345,CS,4500,2018,Spring,d,B+
+345,MATH,2270,2019,Summer,c,B+
+345,PHYS,3220,2017,Fall,b,B+
+348,BIOL,2420,2017,Summer,b,B+
+348,CS,2420,2016,Spring,a,B+
+348,MATH,2210,2015,Summer,c,B+
+355,BIOL,2030,2017,Spring,d,B+
+355,CS,3500,2017,Fall,b,B+
+355,PHYS,2060,2016,Spring,a,B+
+356,BIOL,2325,2018,Fall,c,B+
+357,MATH,1220,2016,Spring,a,B+
+359,CS,2100,2019,Summer,b,B+
+360,BIOL,2210,2020,Fall,a,B+
+361,CS,2100,2018,Spring,a,B+
+362,PHYS,2210,2018,Fall,c,B+
+364,CS,4000,2020,Spring,a,B+
+364,MATH,1260,2019,Fall,a,B+
+366,CS,1030,2018,Fall,a,B+
+366,CS,2100,2017,Fall,a,B+
+366,CS,4970,2019,Spring,a,B+
+368,CS,3505,2018,Summer,a,B+
+369,CS,3200,2016,Fall,d,B+
+371,CS,4000,2020,Spring,b,B+
+372,CS,3200,2019,Spring,a,B+
+372,CS,3505,2019,Summer,b,B+
+373,BIOL,1006,2018,Spring,b,B+
+373,BIOL,2325,2018,Spring,a,B+
+373,PHYS,2140,2015,Summer,c,B+
+374,MATH,3210,2015,Fall,a,B+
+374,PHYS,3210,2018,Spring,c,B+
+377,BIOL,2210,2019,Summer,a,B+
+377,CS,3505,2018,Summer,a,B+
+377,CS,4400,2019,Fall,b,B+
+378,BIOL,1006,2020,Fall,b,B+
+378,BIOL,2020,2018,Fall,b,B+
+378,CS,3100,2016,Fall,a,B+
+378,PHYS,3210,2017,Summer,a,B+
+379,BIOL,1030,2015,Spring,d,B+
+379,CS,3200,2016,Summer,a,B+
+379,MATH,2280,2019,Fall,b,B+
+380,BIOL,1030,2019,Summer,a,B+
+380,BIOL,2210,2019,Fall,a,B+
+384,BIOL,1010,2020,Summer,b,B+
+384,BIOL,2021,2018,Fall,c,B+
+384,MATH,2210,2020,Fall,a,B+
+385,BIOL,2325,2017,Fall,a,B+
+385,CS,3500,2017,Fall,c,B+
+385,MATH,1220,2017,Spring,c,B+
+388,CS,4400,2017,Spring,c,B+
+389,MATH,1220,2016,Spring,a,B+
+390,BIOL,1006,2020,Fall,a,B+
+390,BIOL,2010,2020,Summer,b,B+
+392,BIOL,1010,2018,Summer,a,B+
+392,PHYS,3220,2017,Summer,a,B+
+393,PHYS,3210,2017,Summer,a,B+
+394,BIOL,2021,2015,Spring,a,B+
+395,CS,1030,2016,Spring,a,B+
+396,BIOL,2030,2019,Summer,b,B+
+397,CS,4400,2019,Summer,a,B+
+397,MATH,1220,2020,Summer,a,B+
+397,PHYS,2210,2019,Summer,a,B+
+398,CS,1030,2019,Fall,a,B+
+399,BIOL,2030,2019,Summer,c,B+
+101,PHYS,2210,2018,Fall,a,B-
+102,CS,1030,2016,Fall,a,B-
+102,CS,3200,2016,Fall,b,B-
+106,CS,4400,2020,Fall,b,B-
+106,MATH,2280,2020,Spring,b,B-
+106,PHYS,2220,2020,Summer,a,B-
+107,CS,4970,2016,Fall,a,B-
+109,BIOL,2030,2019,Summer,c,B-
+109,CS,3200,2018,Spring,c,B-
+109,CS,3500,2017,Fall,b,B-
+109,MATH,1250,2018,Spring,a,B-
+109,MATH,2270,2017,Fall,a,B-
+113,BIOL,1006,2018,Fall,a,B-
+113,PHYS,2220,2020,Spring,a,B-
+115,BIOL,2021,2017,Summer,a,B-
+115,PHYS,2060,2016,Spring,a,B-
+116,CS,1030,2016,Fall,a,B-
+116,CS,4970,2017,Spring,a,B-
+117,BIOL,1030,2016,Spring,a,B-
+117,MATH,1250,2017,Summer,d,B-
+118,CS,3500,2019,Summer,a,B-
+119,CS,2420,2017,Summer,a,B-
+119,CS,4400,2020,Fall,a,B-
+119,MATH,2210,2019,Spring,b,B-
+120,BIOL,2010,2017,Fall,a,B-
+120,MATH,1210,2015,Summer,a,B-
+120,MATH,2210,2015,Summer,c,B-
+120,MATH,3210,2017,Spring,a,B-
+120,PHYS,2210,2018,Fall,b,B-
+122,PHYS,2060,2020,Spring,a,B-
+123,BIOL,1030,2020,Summer,a,B-
+123,CS,1030,2016,Summer,a,B-
+123,CS,3100,2017,Fall,a,B-
+123,CS,4150,2020,Spring,a,B-
+123,PHYS,2210,2019,Fall,c,B-
+124,CS,3810,2020,Fall,a,B-
+127,MATH,1250,2017,Summer,b,B-
+127,PHYS,2210,2017,Summer,c,B-
+128,PHYS,2210,2019,Summer,a,B-
+128,PHYS,2220,2018,Spring,a,B-
+131,CS,1030,2020,Fall,a,B-
+131,CS,4400,2020,Fall,a,B-
+131,MATH,2270,2017,Fall,c,B-
+133,CS,2420,2020,Summer,a,B-
+133,PHYS,3210,2019,Summer,a,B-
+134,PHYS,2220,2018,Spring,a,B-
+134,PHYS,3210,2016,Summer,b,B-
+135,BIOL,2010,2020,Spring,a,B-
+140,BIOL,2420,2015,Spring,c,B-
+144,MATH,1260,2015,Summer,a,B-
+146,BIOL,2355,2019,Spring,c,B-
+146,CS,4400,2019,Summer,a,B-
+151,CS,4000,2017,Spring,a,B-
+151,CS,4970,2020,Summer,d,B-
+152,BIOL,2325,2019,Spring,a,B-
+152,CS,2100,2020,Spring,a,B-
+152,CS,3505,2019,Spring,a,B-
+152,CS,4400,2020,Fall,a,B-
+153,PHYS,2060,2020,Spring,b,B-
+155,BIOL,2355,2017,Fall,b,B-
+156,CS,3505,2018,Fall,a,B-
+163,CS,4970,2018,Summer,c,B-
+164,CS,3200,2019,Spring,a,B-
+165,MATH,3220,2018,Spring,c,B-
+169,BIOL,2210,2018,Summer,a,B-
+169,MATH,2210,2019,Spring,a,B-
+170,BIOL,1030,2020,Summer,a,B-
+171,CS,4970,2020,Summer,d,B-
+173,MATH,1260,2020,Spring,a,B-
+177,CS,2420,2016,Fall,a,B-
+178,CS,2100,2019,Fall,b,B-
+179,CS,4970,2016,Fall,b,B-
+179,MATH,1220,2017,Spring,b,B-
+179,PHYS,2210,2017,Summer,b,B-
+182,BIOL,2420,2017,Summer,a,B-
+187,BIOL,2330,2017,Fall,b,B-
+187,CS,3505,2019,Spring,b,B-
+187,MATH,3210,2020,Summer,a,B-
+187,PHYS,2140,2017,Fall,a,B-
+192,MATH,1220,2015,Summer,a,B-
+194,CS,4500,2019,Fall,d,B-
+194,MATH,2270,2019,Summer,b,B-
+195,BIOL,1030,2016,Summer,a,B-
+195,BIOL,2010,2015,Summer,a,B-
+197,BIOL,1010,2018,Summer,b,B-
+199,BIOL,2021,2018,Fall,a,B-
+199,CS,4970,2019,Summer,a,B-
+200,CS,4970,2019,Fall,c,B-
+208,MATH,1250,2017,Summer,d,B-
+208,PHYS,2210,2017,Summer,d,B-
+210,BIOL,2420,2020,Spring,a,B-
+213,BIOL,1030,2016,Fall,a,B-
+213,CS,3100,2016,Fall,a,B-
+214,BIOL,2010,2018,Spring,a,B-
+215,BIOL,1030,2017,Spring,c,B-
+215,MATH,1220,2017,Summer,a,B-
+217,BIOL,1030,2019,Spring,b,B-
+220,CS,4970,2018,Summer,c,B-
+221,CS,4970,2020,Summer,a,B-
+223,MATH,2270,2020,Spring,a,B-
+228,BIOL,1010,2019,Spring,b,B-
+228,BIOL,2030,2019,Summer,b,B-
+228,CS,3500,2019,Summer,a,B-
+229,CS,3200,2016,Fall,c,B-
+229,MATH,1210,2016,Spring,b,B-
+230,CS,3810,2018,Spring,a,B-
+230,PHYS,2060,2019,Summer,a,B-
+230,PHYS,3220,2017,Fall,c,B-
+231,CS,1410,2018,Spring,a,B-
+231,CS,3200,2020,Summer,a,B-
+235,BIOL,2420,2020,Spring,a,B-
+235,CS,2100,2019,Fall,b,B-
+238,PHYS,2210,2019,Spring,b,B-
+239,MATH,1250,2018,Summer,b,B-
+239,PHYS,2060,2018,Fall,a,B-
+244,BIOL,1010,2020,Summer,d,B-
+244,BIOL,2355,2020,Summer,b,B-
+246,BIOL,2355,2015,Summer,a,B-
+246,CS,3500,2015,Fall,b,B-
+247,BIOL,2030,2019,Summer,c,B-
+247,PHYS,2220,2020,Summer,a,B-
+248,BIOL,2010,2020,Summer,a,B-
+248,MATH,2280,2019,Fall,c,B-
+252,PHYS,3220,2017,Fall,d,B-
+254,CS,4000,2020,Spring,a,B-
+255,BIOL,2325,2018,Summer,a,B-
+255,CS,4500,2019,Fall,b,B-
+256,BIOL,2355,2017,Fall,b,B-
+256,CS,4940,2019,Fall,a,B-
+256,MATH,1260,2019,Spring,a,B-
+258,BIOL,1010,2018,Fall,a,B-
+258,BIOL,2210,2018,Summer,c,B-
+258,CS,2100,2018,Summer,a,B-
+258,CS,4940,2020,Summer,b,B-
+259,CS,3505,2018,Summer,b,B-
+259,PHYS,2060,2018,Fall,d,B-
+260,BIOL,1010,2018,Summer,a,B-
+260,BIOL,1030,2019,Summer,a,B-
+260,CS,3200,2020,Summer,a,B-
+261,PHYS,2060,2018,Fall,b,B-
+264,BIOL,2021,2017,Fall,a,B-
+267,CS,3505,2020,Summer,a,B-
+267,PHYS,3220,2020,Spring,a,B-
+268,CS,4970,2016,Fall,a,B-
+270,BIOL,1010,2020,Summer,a,B-
+270,PHYS,2140,2015,Summer,b,B-
+271,BIOL,2210,2020,Fall,a,B-
+275,CS,4400,2019,Spring,b,B-
+276,BIOL,2210,2018,Spring,a,B-
+276,PHYS,2140,2015,Fall,a,B-
+277,CS,4400,2015,Summer,a,B-
+277,MATH,2210,2017,Summer,a,B-
+277,PHYS,3220,2016,Summer,b,B-
+282,BIOL,2021,2015,Spring,a,B-
+282,CS,3810,2016,Fall,a,B-
+282,MATH,1220,2015,Summer,c,B-
+285,BIOL,2210,2017,Summer,c,B-
+288,CS,4150,2016,Summer,b,B-
+290,BIOL,1006,2015,Summer,b,B-
+290,BIOL,1010,2015,Fall,b,B-
+290,BIOL,2420,2015,Fall,a,B-
+290,MATH,1250,2016,Spring,a,B-
+292,CS,3500,2017,Summer,a,B-
+296,CS,2420,2018,Spring,a,B-
+296,PHYS,2040,2019,Spring,a,B-
+298,CS,4400,2019,Summer,b,B-
+299,BIOL,1210,2017,Spring,a,B-
+300,CS,3505,2019,Summer,b,B-
+303,CS,1030,2019,Fall,b,B-
+306,BIOL,1010,2020,Summer,b,B-
+306,BIOL,2010,2020,Summer,b,B-
+309,MATH,1250,2020,Summer,a,B-
+309,MATH,2210,2018,Spring,b,B-
+309,PHYS,2220,2020,Summer,a,B-
+310,PHYS,2060,2020,Spring,a,B-
+312,CS,3500,2020,Summer,a,B-
+312,CS,4940,2020,Summer,b,B-
+313,CS,2100,2015,Summer,a,B-
+313,CS,4000,2018,Spring,a,B-
+313,CS,4500,2018,Spring,d,B-
+314,CS,3500,2017,Fall,a,B-
+314,CS,4150,2020,Spring,a,B-
+318,MATH,1260,2019,Summer,a,B-
+321,BIOL,2020,2018,Spring,a,B-
+321,BIOL,2325,2015,Spring,a,B-
+321,BIOL,2355,2016,Spring,b,B-
+321,CS,2420,2016,Summer,a,B-
+321,PHYS,3210,2016,Fall,a,B-
+325,BIOL,1030,2020,Spring,a,B-
+329,MATH,3210,2020,Fall,a,B-
+329,PHYS,3220,2017,Summer,a,B-
+332,BIOL,1010,2019,Spring,c,B-
+332,BIOL,1210,2018,Spring,a,B-
+332,CS,2100,2018,Summer,c,B-
+336,CS,3200,2015,Fall,c,B-
+341,CS,4970,2020,Fall,d,B-
+341,PHYS,3220,2020,Spring,d,B-
+342,BIOL,2020,2018,Fall,d,B-
+342,BIOL,2021,2018,Fall,c,B-
+342,CS,4000,2017,Fall,a,B-
+345,BIOL,2020,2018,Fall,b,B-
+345,BIOL,2355,2019,Spring,c,B-
+347,BIOL,1030,2019,Summer,a,B-
+347,CS,2100,2019,Summer,a,B-
+348,BIOL,2021,2017,Summer,a,B-
+348,BIOL,2210,2017,Spring,b,B-
+348,MATH,1210,2019,Spring,a,B-
+348,PHYS,3210,2020,Spring,a,B-
+348,PHYS,3220,2020,Spring,b,B-
+353,PHYS,2100,2017,Summer,c,B-
+355,BIOL,2330,2017,Fall,a,B-
+356,BIOL,1006,2019,Summer,a,B-
+356,CS,3505,2019,Summer,d,B-
+356,MATH,1250,2018,Summer,a,B-
+356,MATH,1260,2019,Spring,b,B-
+359,CS,4970,2019,Summer,b,B-
+360,BIOL,1030,2020,Summer,a,B-
+361,CS,4000,2017,Fall,b,B-
+361,MATH,1250,2018,Spring,a,B-
+362,BIOL,2020,2018,Fall,c,B-
+362,CS,4940,2020,Summer,a,B-
+362,MATH,1250,2018,Summer,c,B-
+364,CS,4500,2020,Spring,a,B-
+365,CS,4500,2019,Fall,d,B-
+366,BIOL,2210,2020,Fall,a,B-
+368,BIOL,2420,2020,Summer,a,B-
+369,MATH,1210,2016,Fall,c,B-
+371,BIOL,2210,2020,Fall,a,B-
+373,BIOL,2010,2018,Spring,a,B-
+373,CS,2100,2018,Fall,a,B-
+373,CS,4970,2020,Summer,b,B-
+374,BIOL,2210,2017,Summer,c,B-
+374,CS,2100,2016,Summer,b,B-
+374,CS,3505,2018,Summer,a,B-
+374,PHYS,2210,2015,Fall,b,B-
+375,BIOL,1010,2019,Spring,a,B-
+375,CS,3200,2020,Summer,a,B-
+375,MATH,1260,2019,Fall,a,B-
+376,PHYS,2060,2020,Fall,a,B-
+377,MATH,1250,2016,Spring,a,B-
+377,PHYS,3220,2018,Summer,a,B-
+378,BIOL,1006,2020,Fall,c,B-
+378,BIOL,1010,2018,Summer,b,B-
+378,BIOL,2210,2017,Summer,b,B-
+378,CS,4970,2019,Summer,a,B-
+379,BIOL,2020,2018,Fall,d,B-
+385,CS,2420,2016,Spring,a,B-
+390,CS,4970,2020,Summer,d,B-
+391,BIOL,2210,2018,Spring,a,B-
+391,CS,3100,2017,Fall,a,B-
+391,MATH,1260,2019,Summer,a,B-
+391,MATH,3210,2020,Summer,a,B-
+394,MATH,3220,2016,Spring,d,B-
+397,CS,4000,2020,Fall,a,B-
+398,CS,3505,2020,Fall,a,B-
+398,CS,4970,2018,Summer,a,B-
+100,BIOL,1030,2020,Spring,a,C
+100,CS,1410,2018,Spring,b,C
+102,MATH,1210,2018,Spring,a,C
+102,MATH,1260,2019,Spring,c,C
+106,BIOL,2355,2020,Summer,a,C
+107,CS,3810,2016,Fall,a,C
+107,MATH,2270,2017,Fall,a,C
+109,CS,4400,2019,Spring,b,C
+109,PHYS,2220,2020,Fall,a,C
+109,PHYS,3210,2018,Fall,a,C
+112,CS,4970,2020,Summer,a,C
+115,PHYS,3220,2016,Summer,a,C
+116,CS,3200,2017,Spring,a,C
+117,CS,4500,2016,Fall,a,C
+119,BIOL,2030,2016,Summer,a,C
+119,BIOL,2355,2018,Summer,a,C
+120,CS,3100,2016,Spring,b,C
+120,CS,4000,2020,Fall,a,C
+120,MATH,1220,2019,Fall,b,C
+123,CS,3200,2016,Fall,c,C
+123,CS,4500,2019,Summer,a,C
+124,BIOL,2325,2018,Fall,a,C
+124,CS,3100,2017,Fall,a,C
+124,MATH,1210,2019,Summer,a,C
+126,CS,3505,2015,Fall,c,C
+127,CS,3505,2018,Summer,b,C
+127,CS,3810,2019,Fall,a,C
+128,CS,3810,2018,Summer,c,C
+130,PHYS,3210,2020,Fall,c,C
+131,BIOL,1006,2018,Fall,a,C
+131,BIOL,2355,2018,Summer,a,C
+131,CS,4970,2019,Fall,b,C
+131,PHYS,2140,2020,Fall,a,C
+131,PHYS,3220,2017,Fall,a,C
+133,BIOL,2325,2018,Fall,a,C
+133,CS,3200,2018,Spring,a,C
+133,CS,4500,2018,Spring,c,C
+133,PHYS,2220,2018,Fall,a,C
+134,CS,3100,2016,Spring,d,C
+135,BIOL,2030,2019,Summer,c,C
+135,MATH,2270,2020,Fall,b,C
+135,PHYS,2210,2019,Fall,a,C
+136,MATH,2210,2020,Fall,a,C
+138,BIOL,1006,2015,Summer,a,C
+139,MATH,3220,2017,Fall,b,C
+143,MATH,2270,2017,Summer,a,C
+146,CS,2100,2019,Fall,d,C
+146,MATH,3210,2020,Spring,a,C
+151,CS,3810,2018,Summer,c,C
+152,CS,3500,2019,Summer,a,C
+152,CS,4500,2020,Summer,a,C
+153,CS,2420,2020,Summer,a,C
+157,PHYS,2210,2019,Spring,b,C
+163,BIOL,1010,2015,Summer,d,C
+163,CS,2100,2017,Fall,a,C
+163,CS,3505,2016,Summer,a,C
+163,CS,4000,2017,Fall,b,C
+164,BIOL,1006,2018,Spring,a,C
+164,BIOL,2010,2020,Spring,b,C
+164,BIOL,2420,2017,Summer,a,C
+164,CS,4500,2018,Spring,c,C
+164,MATH,1260,2020,Spring,a,C
+165,BIOL,2020,2018,Fall,a,C
+165,MATH,2280,2018,Fall,b,C
+167,MATH,1250,2020,Summer,a,C
+167,MATH,2210,2020,Fall,a,C
+169,BIOL,2010,2020,Spring,a,C
+169,BIOL,2021,2018,Summer,a,C
+169,CS,4400,2019,Spring,c,C
+171,BIOL,2030,2020,Spring,a,C
+171,CS,2100,2020,Fall,a,C
+171,PHYS,2060,2019,Fall,b,C
+172,MATH,1250,2015,Fall,a,C
+172,PHYS,2140,2015,Summer,b,C
+172,PHYS,2220,2016,Summer,a,C
+172,PHYS,3210,2016,Summer,b,C
+175,BIOL,2010,2020,Summer,a,C
+175,CS,1030,2020,Spring,a,C
+177,MATH,3210,2015,Spring,b,C
+178,MATH,1210,2018,Fall,b,C
+178,MATH,2270,2020,Spring,a,C
+179,BIOL,1210,2018,Fall,b,C
+179,CS,2100,2016,Summer,b,C
+179,MATH,2270,2015,Fall,a,C
+181,BIOL,2355,2020,Fall,a,C
+181,PHYS,2060,2020,Fall,a,C
+182,BIOL,2030,2017,Spring,a,C
+182,BIOL,2325,2015,Fall,a,C
+182,CS,3500,2017,Fall,a,C
+182,MATH,2270,2017,Fall,d,C
+183,BIOL,2330,2020,Spring,a,C
+185,CS,2100,2018,Spring,a,C
+185,MATH,1210,2018,Fall,a,C
+186,CS,4970,2020,Fall,d,C
+187,MATH,1260,2019,Spring,b,C
+187,PHYS,2220,2017,Spring,a,C
+191,CS,2100,2020,Fall,a,C
+192,BIOL,1010,2016,Summer,a,C
+194,MATH,1260,2019,Summer,b,C
+195,BIOL,2330,2016,Spring,a,C
+202,CS,4970,2020,Fall,d,C
+203,CS,4000,2018,Spring,a,C
+207,CS,3100,2016,Summer,a,C
+210,BIOL,2020,2015,Summer,a,C
+210,MATH,3210,2015,Summer,a,C
+211,BIOL,1010,2015,Fall,b,C
+212,BIOL,2020,2016,Spring,a,C
+214,CS,3505,2017,Fall,a,C
+214,CS,3810,2018,Summer,b,C
+215,BIOL,2030,2015,Fall,a,C
+215,PHYS,2100,2017,Summer,c,C
+219,BIOL,2210,2020,Fall,a,C
+220,CS,4940,2019,Fall,a,C
+223,CS,3505,2019,Summer,b,C
+227,PHYS,3210,2018,Fall,a,C
+228,PHYS,2220,2020,Fall,a,C
+229,MATH,3210,2016,Spring,a,C
+230,MATH,3210,2019,Fall,a,C
+230,PHYS,2040,2017,Fall,c,C
+231,CS,3810,2018,Summer,b,C
+231,MATH,1250,2020,Summer,a,C
+237,CS,3100,2017,Fall,a,C
+237,PHYS,2040,2017,Fall,a,C
+239,BIOL,2210,2018,Summer,a,C
+239,MATH,2210,2018,Spring,b,C
+240,BIOL,2210,2020,Fall,a,C
+241,PHYS,2060,2019,Fall,b,C
+241,PHYS,2220,2019,Spring,a,C
+241,PHYS,3220,2020,Spring,b,C
+242,BIOL,2420,2020,Spring,a,C
+248,CS,4500,2019,Summer,a,C
+249,MATH,2280,2015,Summer,a,C
+250,CS,4970,2019,Fall,a,C
+251,BIOL,2010,2020,Summer,a,C
+252,CS,2100,2018,Fall,c,C
+252,PHYS,2060,2018,Fall,d,C
+255,BIOL,2020,2018,Fall,a,C
+255,CS,4940,2019,Fall,a,C
+255,PHYS,2140,2017,Summer,a,C
+256,PHYS,2220,2017,Spring,a,C
+258,BIOL,1030,2019,Spring,c,C
+259,MATH,2270,2017,Fall,b,C
+260,PHYS,3210,2016,Fall,a,C
+261,BIOL,1006,2018,Spring,b,C
+261,CS,4970,2017,Summer,a,C
+263,BIOL,1010,2020,Summer,d,C
+267,BIOL,2020,2018,Fall,b,C
+270,BIOL,2210,2017,Summer,b,C
+270,CS,3810,2018,Summer,d,C
+270,CS,4150,2018,Fall,a,C
+270,CS,4500,2018,Spring,b,C
+270,MATH,1250,2016,Summer,a,C
+274,MATH,1250,2018,Spring,a,C
+274,MATH,2210,2020,Spring,c,C
+275,BIOL,1030,2018,Fall,a,C
+275,MATH,1210,2019,Spring,b,C
+275,PHYS,2040,2019,Spring,a,C
+277,BIOL,2210,2017,Spring,c,C
+277,MATH,1210,2016,Spring,d,C
+277,PHYS,2060,2019,Summer,a,C
+281,MATH,1220,2020,Summer,a,C
+282,BIOL,1010,2016,Summer,a,C
+282,BIOL,2330,2016,Spring,a,C
+282,PHYS,2140,2015,Spring,a,C
+285,CS,1030,2019,Fall,a,C
+285,CS,4970,2016,Fall,a,C
+285,MATH,2210,2019,Spring,b,C
+288,CS,2420,2017,Summer,c,C
+289,MATH,3210,2020,Fall,a,C
+290,BIOL,2355,2017,Spring,a,C
+291,CS,4000,2017,Fall,a,C
+292,BIOL,2020,2018,Spring,a,C
+292,PHYS,2210,2019,Spring,c,C
+293,CS,3505,2020,Fall,c,C
+293,MATH,1260,2019,Spring,c,C
+295,CS,2420,2016,Fall,b,C
+295,MATH,1210,2016,Spring,d,C
+296,BIOL,2325,2017,Fall,b,C
+298,BIOL,1010,2018,Fall,b,C
+298,BIOL,1030,2019,Spring,c,C
+300,BIOL,2021,2019,Fall,a,C
+301,BIOL,1010,2015,Summer,c,C
+303,BIOL,2021,2019,Fall,a,C
+303,CS,4970,2019,Summer,d,C
+307,BIOL,2355,2020,Summer,b,C
+307,CS,1030,2020,Spring,a,C
+307,CS,3505,2019,Summer,a,C
+307,CS,4970,2020,Summer,d,C
+307,MATH,1210,2019,Spring,a,C
+307,PHYS,3220,2017,Fall,c,C
+309,BIOL,2030,2019,Summer,b,C
+309,BIOL,2355,2020,Spring,a,C
+309,CS,4150,2020,Fall,a,C
+309,MATH,2280,2018,Fall,c,C
+311,BIOL,2021,2018,Spring,a,C
+311,BIOL,2355,2018,Summer,b,C
+311,CS,3200,2020,Summer,a,C
+311,CS,4940,2017,Fall,a,C
+312,CS,3505,2017,Summer,a,C
+312,PHYS,2210,2019,Fall,b,C
+313,BIOL,1006,2020,Fall,a,C
+313,BIOL,1006,2020,Fall,c,C
+313,CS,3500,2015,Fall,b,C
+313,PHYS,3210,2019,Summer,b,C
+318,CS,2100,2019,Fall,c,C
+318,CS,3505,2019,Summer,d,C
+323,BIOL,2420,2020,Summer,a,C
+323,CS,4970,2020,Fall,d,C
+325,BIOL,2325,2019,Summer,a,C
+329,CS,3505,2016,Fall,b,C
+329,CS,4000,2017,Fall,b,C
+331,MATH,2270,2020,Fall,a,C
+332,CS,3200,2020,Spring,c,C
+333,BIOL,1006,2020,Fall,a,C
+333,BIOL,2010,2020,Summer,a,C
+333,MATH,1210,2019,Spring,a,C
+335,CS,2100,2016,Summer,b,C
+335,CS,3505,2015,Fall,b,C
+340,BIOL,1030,2020,Summer,a,C
+340,CS,3505,2019,Summer,b,C
+340,CS,3810,2020,Fall,a,C
+341,PHYS,2060,2019,Fall,b,C
+345,CS,3505,2018,Fall,a,C
+345,PHYS,2140,2020,Fall,a,C
+348,CS,3810,2016,Fall,a,C
+356,BIOL,2021,2018,Summer,a,C
+356,CS,2420,2019,Summer,a,C
+357,CS,3200,2016,Summer,a,C
+361,BIOL,2021,2018,Spring,a,C
+362,MATH,1220,2018,Spring,b,C
+363,BIOL,2355,2020,Summer,b,C
+364,CS,4970,2019,Spring,b,C
+365,CS,3500,2020,Summer,a,C
+366,BIOL,1010,2018,Summer,b,C
+369,BIOL,2330,2016,Fall,a,C
+371,BIOL,2030,2018,Summer,b,C
+371,CS,4150,2018,Fall,b,C
+372,BIOL,1030,2018,Summer,a,C
+372,BIOL,2030,2017,Spring,b,C
+372,MATH,3210,2017,Summer,a,C
+372,PHYS,2040,2019,Spring,a,C
+373,BIOL,2021,2018,Spring,a,C
+373,CS,4000,2017,Summer,a,C
+373,CS,4500,2020,Spring,a,C
+373,MATH,2270,2020,Fall,a,C
+373,PHYS,2210,2017,Summer,a,C
+374,BIOL,1010,2018,Summer,c,C
+374,CS,3500,2016,Spring,a,C
+374,PHYS,2060,2016,Summer,b,C
+374,PHYS,2220,2015,Spring,a,C
+375,BIOL,1006,2018,Spring,b,C
+375,CS,3500,2019,Fall,b,C
+377,CS,2100,2017,Spring,a,C
+378,BIOL,2010,2020,Summer,a,C
+378,CS,3505,2016,Summer,a,C
+378,CS,4150,2016,Summer,a,C
+378,MATH,1210,2016,Fall,b,C
+378,MATH,2270,2019,Summer,b,C
+379,CS,3505,2016,Fall,a,C
+379,PHYS,2140,2017,Fall,b,C
+379,PHYS,2210,2015,Fall,c,C
+381,CS,2100,2018,Summer,c,C
+382,BIOL,1010,2015,Summer,b,C
+385,CS,3100,2017,Spring,b,C
+385,MATH,1250,2018,Spring,a,C
+386,PHYS,2140,2018,Fall,a,C
+387,MATH,2210,2017,Summer,a,C
+387,PHYS,2040,2015,Fall,c,C
+387,PHYS,2140,2016,Fall,a,C
+388,MATH,1220,2017,Spring,b,C
+389,CS,2420,2016,Spring,a,C
+390,PHYS,3210,2020,Spring,a,C
+391,BIOL,1010,2017,Spring,a,C
+391,BIOL,1030,2018,Fall,a,C
+391,CS,1410,2017,Spring,a,C
+391,CS,4400,2019,Summer,b,C
+391,MATH,3220,2017,Spring,a,C
+392,BIOL,2210,2016,Summer,a,C
+392,CS,3505,2015,Fall,b,C
+392,PHYS,2210,2015,Fall,b,C
+393,CS,4000,2016,Fall,a,C
+393,PHYS,2220,2018,Summer,a,C
+394,BIOL,2325,2016,Summer,a,C
+394,CS,4970,2016,Fall,a,C
+396,PHYS,2210,2019,Fall,b,C
+397,BIOL,2325,2018,Summer,a,C
+397,CS,3505,2017,Fall,a,C
+397,MATH,1210,2016,Fall,a,C
+398,PHYS,3220,2018,Summer,a,C
+399,BIOL,2325,2018,Fall,c,C
+399,MATH,2270,2019,Summer,c,C
+100,BIOL,1010,2020,Summer,d,C+
+100,MATH,2280,2019,Fall,a,C+
+101,BIOL,2020,2018,Fall,d,C+
+102,BIOL,2030,2020,Spring,b,C+
+102,MATH,2210,2019,Spring,a,C+
+102,MATH,2280,2019,Fall,a,C+
+102,PHYS,3220,2020,Spring,a,C+
+105,BIOL,2325,2018,Spring,a,C+
+105,CS,2420,2016,Fall,b,C+
+105,PHYS,2040,2018,Spring,a,C+
+107,BIOL,2420,2017,Summer,b,C+
+107,CS,2100,2019,Spring,a,C+
+107,MATH,2270,2017,Fall,d,C+
+108,BIOL,2010,2020,Spring,b,C+
+108,MATH,1210,2020,Spring,b,C+
+108,PHYS,2210,2019,Fall,b,C+
+109,CS,4000,2020,Fall,a,C+
+109,PHYS,3220,2017,Fall,b,C+
+113,BIOL,2355,2018,Summer,c,C+
+113,PHYS,3210,2019,Spring,a,C+
+117,MATH,1220,2017,Spring,b,C+
+118,CS,3505,2019,Fall,b,C+
+118,PHYS,2220,2020,Summer,a,C+
+119,CS,4970,2019,Summer,d,C+
+119,PHYS,2220,2017,Spring,d,C+
+119,PHYS,3220,2017,Summer,a,C+
+120,BIOL,2030,2017,Spring,d,C+
+120,BIOL,2355,2020,Fall,a,C+
+120,CS,2420,2017,Fall,a,C+
+122,MATH,2210,2020,Fall,a,C+
+123,BIOL,1006,2016,Spring,b,C+
+123,CS,3500,2016,Spring,a,C+
+123,CS,3810,2016,Summer,a,C+
+123,PHYS,2220,2018,Fall,a,C+
+123,PHYS,3210,2016,Fall,a,C+
+124,PHYS,2220,2020,Spring,a,C+
+124,PHYS,3220,2020,Spring,d,C+
+127,BIOL,2010,2017,Summer,a,C+
+127,CS,3500,2020,Summer,a,C+
+128,MATH,1210,2018,Fall,b,C+
+131,CS,4500,2019,Fall,c,C+
+133,BIOL,1010,2018,Summer,b,C+
+133,MATH,1260,2019,Spring,a,C+
+134,BIOL,1210,2018,Spring,a,C+
+134,CS,3200,2015,Fall,b,C+
+134,PHYS,2140,2016,Spring,c,C+
+135,BIOL,1030,2020,Spring,a,C+
+135,CS,1030,2020,Spring,c,C+
+138,CS,1030,2016,Spring,a,C+
+138,CS,3100,2016,Spring,d,C+
+138,PHYS,2140,2015,Summer,c,C+
+139,CS,3100,2017,Fall,a,C+
+139,MATH,1250,2018,Summer,c,C+
+140,CS,2420,2015,Summer,c,C+
+140,PHYS,2140,2015,Summer,a,C+
+148,BIOL,1010,2020,Summer,a,C+
+149,CS,4400,2016,Spring,a,C+
+151,BIOL,1030,2017,Spring,c,C+
+151,BIOL,2030,2016,Fall,a,C+
+153,BIOL,1030,2020,Spring,a,C+
+155,BIOL,2330,2017,Fall,a,C+
+158,PHYS,2060,2018,Fall,b,C+
+163,CS,2420,2016,Fall,a,C+
+163,CS,3100,2015,Summer,a,C+
+164,BIOL,1030,2020,Summer,a,C+
+164,BIOL,2021,2019,Fall,a,C+
+164,CS,1410,2018,Spring,b,C+
+165,BIOL,1006,2017,Fall,b,C+
+165,BIOL,1010,2019,Spring,b,C+
+165,MATH,1220,2018,Spring,a,C+
+167,BIOL,1030,2019,Summer,a,C+
+167,MATH,1210,2018,Fall,a,C+
+169,BIOL,2420,2018,Spring,a,C+
+170,CS,1030,2020,Spring,b,C+
+171,MATH,3210,2020,Summer,a,C+
+173,BIOL,2030,2019,Summer,b,C+
+173,CS,4400,2019,Summer,a,C+
+175,BIOL,2355,2020,Fall,a,C+
+175,MATH,2210,2020,Fall,a,C+
+176,BIOL,2020,2015,Fall,c,C+
+176,PHYS,2100,2016,Fall,b,C+
+177,BIOL,1210,2018,Spring,a,C+
+177,BIOL,2010,2020,Summer,b,C+
+177,MATH,2270,2020,Fall,a,C+
+177,PHYS,2210,2017,Summer,a,C+
+178,BIOL,2355,2019,Spring,a,C+
+178,CS,3200,2020,Fall,a,C+
+178,PHYS,2060,2020,Fall,a,C+
+179,CS,3200,2015,Fall,b,C+
+179,MATH,2210,2020,Fall,a,C+
+182,BIOL,2210,2017,Spring,b,C+
+182,CS,3505,2015,Fall,b,C+
+182,CS,4500,2018,Spring,a,C+
+182,MATH,2280,2018,Spring,a,C+
+183,BIOL,1030,2018,Fall,a,C+
+183,BIOL,2020,2018,Fall,a,C+
+185,BIOL,1030,2020,Summer,a,C+
+185,CS,3505,2018,Summer,b,C+
+185,CS,4500,2019,Summer,a,C+
+187,MATH,1220,2017,Spring,a,C+
+187,PHYS,2060,2020,Fall,a,C+
+187,PHYS,3220,2017,Fall,d,C+
+194,CS,3505,2019,Fall,c,C+
+194,CS,4940,2020,Summer,b,C+
+195,MATH,1210,2016,Fall,c,C+
+196,CS,2100,2018,Fall,c,C+
+197,MATH,2210,2018,Spring,b,C+
+199,CS,2420,2019,Summer,a,C+
+200,PHYS,3210,2020,Fall,b,C+
+203,CS,3500,2017,Fall,c,C+
+204,BIOL,2330,2015,Fall,d,C+
+210,CS,1030,2019,Fall,b,C+
+210,PHYS,2060,2019,Fall,a,C+
+211,CS,3200,2015,Spring,b,C+
+213,BIOL,2030,2016,Fall,a,C+
+214,BIOL,1006,2016,Summer,d,C+
+214,BIOL,2325,2018,Spring,a,C+
+214,CS,2100,2016,Spring,a,C+
+215,CS,2100,2017,Fall,a,C+
+215,CS,2420,2016,Fall,b,C+
+219,CS,3505,2020,Summer,a,C+
+221,CS,1030,2020,Spring,a,C+
+223,BIOL,2010,2020,Spring,a,C+
+225,CS,2420,2020,Fall,a,C+
+225,CS,3810,2020,Fall,a,C+
+227,MATH,2280,2018,Fall,b,C+
+227,PHYS,2140,2019,Fall,a,C+
+227,PHYS,3220,2020,Spring,d,C+
+228,CS,2100,2020,Spring,a,C+
+229,MATH,1260,2016,Fall,a,C+
+229,MATH,2210,2018,Spring,b,C+
+231,BIOL,2010,2020,Spring,a,C+
+231,MATH,1260,2020,Spring,a,C+
+234,MATH,1220,2019,Fall,a,C+
+235,CS,4400,2020,Fall,b,C+
+238,MATH,3220,2018,Spring,d,C+
+241,CS,4400,2019,Fall,a,C+
+241,PHYS,3220,2020,Spring,c,C+
+242,BIOL,2010,2020,Summer,a,C+
+243,CS,2420,2016,Fall,c,C+
+245,CS,4150,2016,Summer,a,C+
+245,MATH,1220,2015,Summer,c,C+
+246,PHYS,2100,2017,Fall,a,C+
+246,PHYS,2210,2015,Fall,a,C+
+247,BIOL,2325,2018,Fall,a,C+
+247,MATH,2280,2019,Fall,b,C+
+248,BIOL,2355,2019,Spring,c,C+
+248,CS,3200,2020,Spring,c,C+
+249,CS,3505,2016,Fall,b,C+
+249,CS,4970,2016,Fall,b,C+
+249,PHYS,2220,2017,Spring,d,C+
+250,CS,3505,2020,Fall,c,C+
+253,CS,2100,2018,Fall,d,C+
+254,CS,4500,2019,Fall,d,C+
+255,BIOL,2010,2018,Spring,a,C+
+255,CS,3500,2019,Fall,a,C+
+255,MATH,1250,2018,Summer,a,C+
+255,PHYS,2210,2019,Spring,d,C+
+256,CS,4500,2019,Fall,c,C+
+256,PHYS,2040,2017,Fall,b,C+
+257,BIOL,2020,2018,Fall,a,C+
+257,BIOL,2021,2018,Summer,a,C+
+257,CS,4000,2020,Spring,a,C+
+257,MATH,1260,2019,Summer,a,C+
+257,PHYS,2060,2018,Fall,b,C+
+258,BIOL,1030,2019,Spring,b,C+
+258,CS,3500,2019,Summer,a,C+
+258,PHYS,3210,2019,Spring,c,C+
+260,BIOL,2325,2017,Fall,b,C+
+261,BIOL,2020,2018,Fall,a,C+
+262,BIOL,2020,2018,Fall,b,C+
+266,BIOL,2330,2017,Fall,b,C+
+270,BIOL,2355,2017,Spring,b,C+
+274,BIOL,2020,2018,Fall,a,C+
+275,CS,4970,2019,Spring,a,C+
+276,BIOL,1006,2016,Spring,a,C+
+276,CS,3100,2015,Summer,a,C+
+276,CS,3505,2019,Spring,a,C+
+277,BIOL,1010,2015,Summer,a,C+
+277,MATH,1210,2016,Spring,c,C+
+281,CS,4970,2020,Fall,c,C+
+282,CS,3505,2015,Spring,a,C+
+282,CS,4000,2015,Fall,a,C+
+285,MATH,1220,2017,Spring,b,C+
+285,MATH,3220,2016,Spring,a,C+
+285,PHYS,2210,2017,Summer,b,C+
+287,CS,4400,2019,Summer,a,C+
+289,BIOL,2210,2019,Fall,b,C+
+291,CS,1030,2016,Spring,a,C+
+291,CS,1410,2016,Spring,b,C+
+292,BIOL,1030,2020,Spring,a,C+
+292,MATH,2270,2017,Fall,a,C+
+292,MATH,3210,2017,Summer,a,C+
+295,CS,4970,2017,Spring,a,C+
+297,PHYS,2140,2020,Fall,a,C+
+298,CS,2100,2018,Summer,c,C+
+300,CS,4970,2019,Summer,a,C+
+304,MATH,3210,2017,Spring,a,C+
+307,BIOL,1030,2019,Spring,c,C+
+307,CS,1410,2018,Spring,d,C+
+309,BIOL,2210,2017,Spring,b,C+
+309,CS,2420,2017,Summer,b,C+
+309,CS,3500,2017,Fall,c,C+
+309,CS,4500,2016,Fall,a,C+
+309,MATH,1220,2018,Spring,b,C+
+309,MATH,3210,2017,Summer,a,C+
+311,BIOL,1010,2018,Summer,b,C+
+311,CS,4970,2019,Spring,b,C+
+312,PHYS,2100,2016,Fall,a,C+
+313,BIOL,1010,2018,Summer,c,C+
+313,BIOL,2010,2019,Fall,a,C+
+313,BIOL,2020,2016,Spring,a,C+
+313,MATH,1260,2019,Spring,b,C+
+314,BIOL,1030,2019,Summer,a,C+
+314,BIOL,2210,2019,Summer,a,C+
+314,CS,4970,2017,Spring,a,C+
+314,MATH,2270,2017,Fall,d,C+
+316,BIOL,2010,2019,Fall,a,C+
+318,BIOL,1010,2018,Summer,a,C+
+318,BIOL,2030,2019,Summer,a,C+
+318,BIOL,2210,2019,Summer,b,C+
+321,BIOL,2420,2020,Fall,a,C+
+321,CS,2100,2019,Fall,b,C+
+329,PHYS,3210,2019,Spring,c,C+
+331,MATH,2270,2020,Fall,b,C+
+332,BIOL,2355,2018,Summer,a,C+
+332,CS,4400,2019,Summer,b,C+
+332,MATH,1220,2018,Spring,a,C+
+333,BIOL,2325,2019,Spring,a,C+
+333,CS,4970,2019,Summer,c,C+
+335,CS,4970,2016,Fall,a,C+
+340,BIOL,1010,2020,Summer,a,C+
+342,BIOL,1210,2019,Spring,a,C+
+342,BIOL,2420,2020,Fall,a,C+
+348,BIOL,2330,2020,Spring,a,C+
+348,CS,4500,2017,Summer,a,C+
+348,MATH,2270,2020,Fall,a,C+
+348,PHYS,2040,2017,Fall,c,C+
+355,MATH,1220,2017,Spring,c,C+
+356,MATH,2270,2017,Fall,b,C+
+356,PHYS,2220,2016,Fall,a,C+
+366,BIOL,2030,2020,Spring,a,C+
+368,MATH,3210,2020,Summer,a,C+
+368,PHYS,2060,2019,Fall,b,C+
+369,PHYS,2140,2018,Summer,b,C+
+371,BIOL,1010,2020,Summer,c,C+
+371,PHYS,2140,2019,Fall,a,C+
+372,BIOL,2010,2017,Fall,a,C+
+372,PHYS,2220,2017,Spring,a,C+
+373,BIOL,2210,2020,Fall,a,C+
+374,CS,3810,2018,Summer,a,C+
+375,PHYS,3210,2019,Spring,c,C+
+377,BIOL,2020,2015,Fall,c,C+
+377,PHYS,2100,2017,Summer,b,C+
+378,CS,3200,2020,Spring,c,C+
+378,CS,4000,2016,Fall,a,C+
+378,MATH,1220,2017,Spring,d,C+
+379,BIOL,1006,2020,Fall,a,C+
+379,BIOL,2030,2015,Fall,a,C+
+380,BIOL,2355,2018,Fall,a,C+
+381,PHYS,3210,2018,Spring,c,C+
+382,BIOL,1010,2015,Summer,a,C+
+386,CS,4150,2020,Fall,a,C+
+387,CS,2100,2018,Spring,a,C+
+387,MATH,3220,2018,Spring,b,C+
+388,CS,2100,2016,Summer,b,C+
+389,BIOL,1006,2016,Summer,d,C+
+390,MATH,2280,2019,Fall,b,C+
+391,BIOL,1006,2018,Fall,a,C+
+391,CS,3505,2019,Fall,c,C+
+391,MATH,2210,2018,Spring,a,C+
+391,PHYS,2060,2020,Spring,b,C+
+392,BIOL,1030,2016,Spring,a,C+
+392,BIOL,2330,2017,Fall,b,C+
+392,CS,2100,2018,Summer,c,C+
+394,CS,1410,2016,Summer,a,C+
+395,BIOL,2355,2016,Spring,b,C+
+396,CS,4150,2020,Spring,a,C+
+397,BIOL,2420,2020,Spring,a,C+
+397,CS,2100,2019,Fall,b,C+
+397,CS,2100,2019,Fall,c,C+
+398,MATH,2270,2020,Fall,a,C+
+398,MATH,2280,2020,Spring,b,C+
+399,BIOL,2020,2018,Fall,a,C+
+399,BIOL,2021,2019,Spring,a,C+
+399,CS,2100,2018,Fall,d,C+
+399,MATH,3210,2019,Spring,a,C+
+100,CS,3505,2018,Summer,a,C-
+101,BIOL,2030,2018,Summer,b,C-
+102,MATH,1220,2019,Fall,b,C-
+105,BIOL,1010,2018,Summer,a,C-
+106,CS,2100,2019,Summer,b,C-
+107,BIOL,2210,2017,Spring,c,C-
+107,CS,4000,2017,Fall,a,C-
+108,CS,4500,2020,Spring,a,C-
+109,CS,1410,2018,Spring,b,C-
+109,CS,3505,2020,Fall,c,C-
+112,BIOL,1010,2020,Summer,c,C-
+113,CS,4400,2020,Spring,a,C-
+115,BIOL,2420,2017,Summer,a,C-
+118,BIOL,2355,2020,Spring,a,C-
+118,CS,2100,2019,Fall,c,C-
+118,MATH,1220,2020,Summer,a,C-
+119,MATH,2280,2018,Fall,b,C-
+120,BIOL,2020,2015,Summer,a,C-
+120,CS,4150,2020,Spring,a,C-
+120,PHYS,2040,2020,Spring,a,C-
+121,BIOL,1010,2020,Summer,d,C-
+121,BIOL,2420,2020,Spring,b,C-
+121,CS,4970,2018,Fall,d,C-
+121,PHYS,3210,2020,Fall,b,C-
+122,CS,1030,2020,Spring,a,C-
+123,BIOL,2010,2017,Summer,a,C-
+123,CS,4000,2020,Spring,b,C-
+123,MATH,1220,2019,Fall,b,C-
+124,CS,1030,2020,Spring,c,C-
+124,CS,3200,2020,Fall,a,C-
+125,PHYS,3210,2020,Spring,a,C-
+127,BIOL,1006,2019,Spring,a,C-
+127,PHYS,2060,2018,Fall,b,C-
+131,BIOL,2420,2020,Summer,a,C-
+133,CS,3500,2019,Fall,b,C-
+133,MATH,1220,2019,Fall,a,C-
+133,MATH,2280,2019,Fall,b,C-
+135,BIOL,2325,2019,Summer,a,C-
+136,CS,4970,2020,Fall,b,C-
+137,CS,3505,2020,Summer,a,C-
+138,CS,4000,2016,Fall,a,C-
+138,CS,4400,2016,Fall,a,C-
+138,MATH,1210,2015,Summer,a,C-
+139,BIOL,2021,2019,Spring,b,C-
+139,CS,4500,2017,Summer,a,C-
+139,PHYS,3210,2017,Summer,a,C-
+143,BIOL,1006,2019,Summer,a,C-
+143,CS,1030,2019,Fall,a,C-
+143,PHYS,3220,2018,Summer,a,C-
+145,CS,4970,2016,Fall,a,C-
+146,BIOL,2420,2020,Fall,a,C-
+151,CS,3505,2017,Summer,a,C-
+151,PHYS,2060,2019,Fall,c,C-
+151,PHYS,2100,2017,Summer,c,C-
+151,PHYS,2210,2018,Fall,b,C-
+151,PHYS,2220,2017,Spring,c,C-
+152,BIOL,2020,2018,Fall,a,C-
+152,BIOL,2021,2018,Fall,a,C-
+152,MATH,1220,2019,Fall,b,C-
+152,PHYS,3210,2019,Summer,c,C-
+160,BIOL,1006,2016,Spring,a,C-
+161,CS,3200,2020,Fall,a,C-
+163,CS,4000,2017,Fall,a,C-
+164,BIOL,2325,2018,Fall,a,C-
+164,CS,4000,2018,Spring,a,C-
+164,CS,4970,2019,Summer,d,C-
+165,PHYS,2060,2018,Fall,c,C-
+167,BIOL,2325,2018,Fall,b,C-
+167,PHYS,3210,2019,Summer,c,C-
+171,CS,3505,2020,Fall,c,C-
+172,MATH,2210,2015,Fall,a,C-
+172,MATH,3210,2015,Fall,d,C-
+173,CS,4000,2018,Spring,a,C-
+173,MATH,1220,2018,Spring,a,C-
+176,CS,3200,2016,Summer,b,C-
+176,MATH,3220,2017,Spring,a,C-
+177,BIOL,2020,2018,Fall,a,C-
+177,CS,3200,2020,Summer,a,C-
+177,CS,3505,2017,Fall,b,C-
+177,CS,4970,2016,Fall,b,C-
+177,PHYS,2140,2017,Summer,a,C-
+179,BIOL,2325,2019,Summer,a,C-
+179,MATH,1260,2019,Spring,b,C-
+179,PHYS,2220,2015,Fall,a,C-
+181,MATH,2270,2020,Fall,a,C-
+182,CS,3810,2018,Summer,d,C-
+182,MATH,1220,2017,Spring,b,C-
+185,CS,3200,2020,Summer,a,C-
+185,PHYS,3210,2020,Spring,a,C-
+187,CS,4400,2019,Spring,c,C-
+188,PHYS,3210,2019,Summer,c,C-
+189,BIOL,2420,2020,Summer,a,C-
+192,BIOL,2355,2015,Summer,a,C-
+194,CS,3200,2020,Spring,a,C-
+195,MATH,1210,2016,Fall,d,C-
+195,MATH,3210,2016,Fall,a,C-
+195,PHYS,2060,2016,Spring,a,C-
+196,BIOL,2021,2019,Spring,a,C-
+199,BIOL,1030,2018,Fall,a,C-
+200,CS,4940,2020,Summer,a,C-
+202,CS,1030,2020,Fall,a,C-
+203,MATH,3220,2018,Spring,d,C-
+204,BIOL,1030,2015,Summer,a,C-
+207,BIOL,2021,2017,Fall,a,C-
+210,CS,3100,2017,Spring,b,C-
+211,MATH,1220,2015,Summer,a,C-
+214,BIOL,2210,2017,Summer,a,C-
+217,PHYS,3210,2019,Spring,b,C-
+220,BIOL,2325,2018,Fall,a,C-
+221,BIOL,2010,2020,Summer,a,C-
+222,PHYS,2140,2020,Fall,a,C-
+223,CS,2100,2019,Fall,a,C-
+223,CS,3500,2019,Fall,b,C-
+227,BIOL,1006,2018,Spring,b,C-
+227,CS,4970,2018,Summer,b,C-
+228,CS,3200,2020,Spring,a,C-
+228,PHYS,3210,2020,Fall,b,C-
+230,BIOL,2030,2018,Summer,a,C-
+230,CS,2420,2020,Fall,a,C-
+231,CS,4940,2020,Summer,b,C-
+231,CS,4970,2018,Summer,a,C-
+231,PHYS,3210,2019,Spring,c,C-
+233,BIOL,2355,2020,Fall,a,C-
+233,CS,2420,2020,Summer,a,C-
+235,MATH,1260,2020,Spring,a,C-
+235,PHYS,2220,2020,Spring,a,C-
+237,PHYS,3220,2017,Fall,a,C-
+242,MATH,1260,2020,Spring,a,C-
+242,PHYS,2220,2020,Summer,b,C-
+243,BIOL,1030,2017,Spring,b,C-
+247,CS,4940,2020,Summer,b,C-
+248,BIOL,2420,2020,Spring,b,C-
+248,CS,4400,2019,Spring,c,C-
+249,MATH,1210,2016,Fall,a,C-
+251,CS,4940,2020,Summer,a,C-
+251,PHYS,3220,2020,Spring,b,C-
+253,BIOL,1030,2018,Fall,a,C-
+256,BIOL,1030,2019,Spring,c,C-
+256,MATH,2280,2018,Spring,a,C-
+257,CS,4970,2020,Summer,c,C-
+257,PHYS,2220,2018,Summer,a,C-
+259,BIOL,2010,2017,Summer,a,C-
+259,CS,4000,2017,Summer,a,C-
+259,MATH,2280,2018,Fall,a,C-
+260,CS,3810,2018,Summer,a,C-
+260,MATH,2270,2020,Fall,b,C-
+261,CS,2420,2017,Summer,c,C-
+261,CS,3100,2017,Spring,a,C-
+261,MATH,2210,2017,Spring,a,C-
+262,CS,2100,2016,Summer,b,C-
+266,MATH,3220,2017,Fall,a,C-
+267,MATH,2280,2019,Fall,b,C-
+268,CS,3200,2016,Fall,a,C-
+270,CS,1410,2015,Summer,d,C-
+270,MATH,2210,2017,Spring,a,C-
+270,MATH,2280,2019,Fall,a,C-
+270,MATH,2280,2019,Fall,c,C-
+270,MATH,3220,2016,Summer,a,C-
+270,PHYS,2210,2018,Fall,b,C-
+271,BIOL,2355,2020,Fall,a,C-
+271,PHYS,3220,2020,Spring,c,C-
+275,BIOL,1010,2018,Summer,b,C-
+275,BIOL,2355,2018,Summer,c,C-
+275,CS,1410,2018,Spring,d,C-
+275,CS,4000,2018,Spring,a,C-
+276,CS,3810,2015,Spring,a,C-
+276,MATH,1260,2019,Summer,a,C-
+276,MATH,3210,2016,Spring,a,C-
+276,PHYS,3210,2018,Fall,a,C-
+277,BIOL,1010,2015,Summer,c,C-
+277,CS,3100,2016,Fall,a,C-
+278,BIOL,2210,2016,Summer,a,C-
+278,MATH,1260,2016,Fall,a,C-
+281,MATH,1250,2020,Summer,a,C-
+285,CS,4970,2016,Fall,b,C-
+285,MATH,1210,2016,Fall,c,C-
+285,MATH,2270,2019,Spring,a,C-
+285,MATH,2280,2020,Spring,b,C-
+285,PHYS,2100,2018,Fall,a,C-
+285,PHYS,3220,2016,Summer,a,C-
+288,MATH,1210,2018,Summer,a,C-
+290,CS,3505,2016,Summer,a,C-
+290,CS,4400,2015,Summer,a,C-
+291,MATH,2270,2017,Fall,d,C-
+292,BIOL,1006,2018,Spring,b,C-
+294,CS,3500,2017,Fall,c,C-
+294,CS,3505,2017,Fall,b,C-
+294,CS,4940,2017,Fall,a,C-
+295,CS,2100,2016,Spring,a,C-
+296,CS,3100,2017,Fall,a,C-
+296,MATH,3220,2018,Spring,a,C-
+297,PHYS,3210,2020,Fall,a,C-
+300,PHYS,2220,2020,Summer,b,C-
+305,CS,1030,2018,Fall,a,C-
+307,BIOL,2330,2019,Fall,a,C-
+307,PHYS,2040,2015,Fall,c,C-
+309,MATH,1260,2019,Fall,a,C-
+309,PHYS,2140,2020,Fall,a,C-
+311,PHYS,2060,2018,Fall,c,C-
+311,PHYS,2060,2018,Fall,d,C-
+312,BIOL,2325,2015,Fall,c,C-
+313,BIOL,2030,2017,Spring,a,C-
+313,MATH,1210,2019,Summer,a,C-
+313,MATH,2270,2015,Fall,b,C-
+313,MATH,3210,2015,Fall,a,C-
+314,CS,4940,2019,Fall,a,C-
+314,PHYS,2040,2017,Fall,a,C-
+314,PHYS,2100,2016,Fall,a,C-
+317,CS,4500,2016,Spring,a,C-
+318,MATH,2270,2017,Summer,a,C-
+321,PHYS,2060,2020,Spring,a,C-
+321,PHYS,2060,2020,Spring,b,C-
+325,BIOL,2020,2018,Fall,d,C-
+325,BIOL,2355,2020,Summer,b,C-
+329,CS,1030,2019,Fall,b,C-
+329,CS,4500,2018,Spring,a,C-
+329,PHYS,2210,2018,Fall,a,C-
+332,CS,1030,2020,Spring,b,C-
+332,CS,3500,2019,Fall,a,C-
+335,CS,3810,2016,Fall,b,C-
+340,PHYS,2210,2019,Fall,a,C-
+341,CS,4500,2019,Fall,d,C-
+342,BIOL,2325,2019,Spring,a,C-
+342,BIOL,2330,2017,Fall,a,C-
+342,PHYS,2220,2018,Summer,a,C-
+344,BIOL,2020,2018,Fall,b,C-
+344,BIOL,2021,2018,Summer,a,C-
+347,BIOL,2030,2020,Spring,b,C-
+347,CS,4970,2019,Fall,d,C-
+348,BIOL,1010,2020,Summer,c,C-
+348,BIOL,2010,2018,Spring,a,C-
+348,CS,1030,2016,Spring,a,C-
+348,CS,3100,2019,Spring,b,C-
+351,PHYS,3210,2019,Spring,a,C-
+355,CS,1410,2016,Spring,a,C-
+355,MATH,1250,2017,Summer,a,C-
+356,CS,2100,2017,Fall,a,C-
+362,MATH,3220,2018,Spring,a,C-
+364,BIOL,2021,2019,Fall,a,C-
+364,CS,4400,2019,Fall,b,C-
+365,MATH,3210,2020,Fall,a,C-
+366,PHYS,2210,2019,Fall,b,C-
+368,CS,2420,2020,Summer,a,C-
+368,CS,4400,2019,Summer,b,C-
+368,MATH,1250,2018,Summer,b,C-
+369,BIOL,2355,2017,Spring,c,C-
+371,CS,4500,2019,Summer,a,C-
+371,MATH,1210,2018,Summer,a,C-
+371,PHYS,2210,2019,Fall,c,C-
+372,BIOL,1010,2019,Spring,a,C-
+373,BIOL,2030,2018,Summer,a,C-
+373,CS,3500,2020,Summer,a,C-
+373,MATH,1210,2020,Spring,a,C-
+373,MATH,2210,2015,Fall,a,C-
+374,BIOL,2420,2015,Summer,a,C-
+374,MATH,1250,2016,Fall,c,C-
+375,BIOL,1030,2019,Spring,c,C-
+375,BIOL,2010,2020,Summer,a,C-
+375,BIOL,2030,2020,Spring,b,C-
+375,CS,1410,2020,Spring,b,C-
+375,PHYS,2100,2017,Summer,a,C-
+376,BIOL,1006,2020,Fall,a,C-
+377,BIOL,2325,2019,Spring,a,C-
+377,BIOL,2355,2015,Summer,a,C-
+377,MATH,1220,2015,Summer,c,C-
+377,MATH,3220,2017,Fall,a,C-
+378,CS,4500,2017,Summer,a,C-
+378,CS,4970,2019,Summer,b,C-
+378,PHYS,2100,2017,Summer,b,C-
+379,CS,3500,2016,Summer,a,C-
+379,CS,3810,2018,Summer,d,C-
+384,BIOL,2010,2020,Spring,a,C-
+385,BIOL,2010,2018,Spring,a,C-
+385,CS,1030,2016,Summer,a,C-
+385,CS,3505,2017,Fall,b,C-
+385,PHYS,2220,2016,Fall,a,C-
+388,BIOL,2021,2017,Summer,a,C-
+388,CS,3200,2018,Spring,c,C-
+390,BIOL,1010,2020,Summer,c,C-
+391,BIOL,2325,2019,Spring,a,C-
+391,CS,4150,2018,Fall,a,C-
+392,BIOL,2355,2016,Spring,b,C-
+393,BIOL,2210,2017,Spring,c,C-
+394,MATH,1210,2017,Spring,a,C-
+396,CS,3505,2018,Fall,c,C-
+397,BIOL,1030,2019,Spring,c,C-
+397,CS,4970,2016,Fall,b,C-
+397,MATH,2210,2020,Fall,a,C-
+398,BIOL,1010,2020,Summer,c,C-
+398,BIOL,2355,2018,Summer,b,C-
+398,CS,4400,2019,Summer,a,C-
+399,CS,4970,2019,Summer,d,C-
+100,CS,4940,2020,Summer,a,D
+101,MATH,1250,2018,Summer,b,D
+106,CS,4150,2020,Spring,a,D
+107,CS,3200,2016,Fall,d,D
+107,MATH,2280,2020,Spring,a,D
+109,BIOL,1010,2019,Spring,b,D
+109,CS,4500,2019,Fall,d,D
+113,BIOL,2020,2018,Fall,b,D
+113,PHYS,2140,2018,Summer,a,D
+116,MATH,1220,2017,Spring,a,D
+117,BIOL,2020,2016,Spring,a,D
+117,CS,4940,2017,Fall,a,D
+117,PHYS,2140,2016,Spring,b,D
+118,CS,4000,2020,Fall,a,D
+119,CS,4500,2016,Spring,b,D
+119,MATH,1250,2018,Summer,b,D
+119,PHYS,2040,2017,Fall,b,D
+119,PHYS,2140,2020,Fall,a,D
+120,BIOL,2420,2020,Spring,a,D
+120,CS,3505,2020,Fall,c,D
+120,MATH,2270,2017,Fall,c,D
+121,PHYS,2220,2020,Summer,a,D
+123,BIOL,2330,2016,Spring,a,D
+123,PHYS,2140,2016,Spring,a,D
+125,CS,4150,2020,Spring,a,D
+129,BIOL,2325,2018,Fall,b,D
+129,BIOL,2325,2018,Fall,c,D
+131,CS,3500,2017,Fall,b,D
+131,PHYS,2060,2018,Summer,a,D
+132,BIOL,2420,2017,Summer,a,D
+132,MATH,3220,2018,Spring,b,D
+132,PHYS,2220,2018,Spring,a,D
+133,MATH,1210,2019,Spring,a,D
+134,BIOL,2010,2018,Spring,a,D
+136,PHYS,2140,2020,Fall,a,D
+138,BIOL,2330,2015,Fall,b,D
+138,CS,2420,2015,Spring,a,D
+138,CS,4500,2016,Spring,b,D
+139,BIOL,2325,2019,Summer,a,D
+143,CS,4400,2019,Summer,b,D
+144,BIOL,2355,2016,Spring,a,D
+146,BIOL,2010,2020,Summer,a,D
+148,CS,4970,2020,Fall,c,D
+151,CS,3200,2016,Fall,d,D
+152,CS,4000,2020,Spring,b,D
+152,PHYS,2040,2019,Spring,b,D
+160,CS,2100,2016,Summer,b,D
+162,CS,4500,2016,Spring,b,D
+163,BIOL,2020,2018,Fall,a,D
+163,BIOL,2355,2017,Spring,d,D
+163,MATH,1220,2017,Spring,b,D
+165,CS,3200,2018,Spring,a,D
+169,MATH,1220,2018,Spring,b,D
+170,BIOL,2010,2020,Summer,a,D
+171,PHYS,2210,2019,Fall,b,D
+172,CS,3100,2015,Summer,a,D
+172,MATH,3210,2015,Fall,a,D
+173,PHYS,2100,2017,Summer,c,D
+173,PHYS,3210,2019,Spring,d,D
+175,BIOL,1010,2020,Summer,b,D
+176,MATH,2210,2017,Spring,a,D
+177,CS,4500,2016,Fall,a,D
+177,PHYS,2100,2017,Summer,a,D
+178,BIOL,1010,2020,Summer,a,D
+178,MATH,1220,2020,Spring,a,D
+178,MATH,2280,2018,Fall,c,D
+179,BIOL,2010,2020,Spring,b,D
+179,BIOL,2021,2016,Fall,a,D
+182,CS,3100,2016,Fall,a,D
+182,MATH,1210,2016,Spring,c,D
+183,CS,4400,2019,Fall,a,D
+183,MATH,2280,2020,Spring,a,D
+183,PHYS,3210,2020,Spring,a,D
+185,BIOL,2355,2018,Summer,a,D
+185,CS,4400,2020,Fall,b,D
+185,MATH,2210,2018,Spring,b,D
+185,PHYS,3220,2020,Spring,c,D
+187,PHYS,2210,2019,Spring,d,D
+188,BIOL,2030,2019,Summer,c,D
+193,CS,4000,2015,Spring,a,D
+194,BIOL,1006,2020,Spring,a,D
+194,PHYS,2040,2020,Spring,a,D
+197,BIOL,2010,2018,Spring,a,D
+199,PHYS,2140,2018,Summer,b,D
+199,PHYS,2210,2019,Spring,b,D
+200,CS,4500,2020,Spring,a,D
+203,CS,1410,2018,Spring,b,D
+204,MATH,2280,2015,Summer,a,D
+208,CS,2420,2017,Summer,c,D
+208,PHYS,3210,2017,Summer,b,D
+210,BIOL,1010,2015,Fall,a,D
+214,PHYS,2040,2017,Fall,c,D
+214,PHYS,3220,2017,Fall,a,D
+216,MATH,3220,2016,Spring,c,D
+219,CS,2420,2020,Summer,a,D
+219,CS,4970,2020,Summer,b,D
+220,CS,4500,2019,Fall,d,D
+220,MATH,1210,2020,Spring,a,D
+228,BIOL,2010,2020,Summer,a,D
+228,BIOL,2010,2020,Summer,b,D
+229,BIOL,1006,2017,Fall,b,D
+230,BIOL,2420,2020,Spring,b,D
+231,CS,2420,2020,Summer,a,D
+231,PHYS,2220,2018,Summer,a,D
+233,CS,4970,2020,Summer,c,D
+235,BIOL,2010,2020,Summer,b,D
+235,PHYS,2140,2020,Fall,a,D
+238,CS,3505,2019,Summer,b,D
+239,BIOL,2325,2018,Fall,c,D
+240,BIOL,2330,2019,Fall,a,D
+240,PHYS,2060,2020,Spring,b,D
+241,BIOL,2030,2019,Summer,d,D
+242,CS,3200,2020,Summer,a,D
+244,BIOL,1010,2020,Summer,b,D
+245,CS,1030,2016,Summer,a,D
+245,CS,2420,2016,Fall,a,D
+245,MATH,3220,2016,Fall,b,D
+245,PHYS,2220,2016,Fall,a,D
+246,BIOL,1006,2015,Summer,a,D
+246,CS,3200,2016,Summer,b,D
+247,PHYS,2060,2018,Fall,b,D
+248,BIOL,1030,2019,Spring,c,D
+252,MATH,1260,2017,Fall,a,D
+253,PHYS,2140,2018,Fall,a,D
+253,PHYS,2210,2019,Spring,c,D
+254,CS,4970,2020,Summer,d,D
+254,MATH,2270,2019,Fall,a,D
+256,BIOL,1210,2019,Spring,a,D
+256,CS,1030,2018,Fall,a,D
+256,MATH,2210,2018,Spring,b,D
+257,BIOL,2030,2017,Spring,a,D
+257,BIOL,2210,2017,Summer,a,D
+257,CS,2100,2018,Fall,b,D
+257,CS,2100,2018,Fall,c,D
+257,CS,3810,2018,Summer,c,D
+257,CS,4400,2019,Spring,d,D
+258,MATH,2270,2020,Spring,a,D
+259,BIOL,2210,2017,Summer,b,D
+259,CS,4400,2019,Fall,b,D
+259,CS,4970,2019,Fall,c,D
+260,CS,4500,2019,Fall,a,D
+260,MATH,1220,2017,Spring,d,D
+262,PHYS,3210,2017,Fall,a,D
+270,CS,2100,2018,Summer,a,D
+274,PHYS,2210,2018,Fall,b,D
+274,PHYS,3210,2018,Spring,b,D
+276,BIOL,2030,2018,Summer,b,D
+276,PHYS,2220,2015,Fall,a,D
+277,BIOL,2030,2016,Fall,a,D
+277,CS,3500,2016,Spring,a,D
+277,MATH,1250,2018,Summer,c,D
+278,PHYS,2060,2016,Summer,a,D
+284,CS,3505,2019,Fall,a,D
+285,BIOL,2355,2017,Spring,d,D
+285,CS,4940,2019,Fall,a,D
+285,PHYS,2060,2016,Summer,b,D
+288,BIOL,1006,2017,Fall,a,D
+292,CS,3505,2019,Summer,d,D
+294,MATH,1210,2019,Spring,a,D
+297,BIOL,1006,2020,Fall,a,D
+298,CS,3505,2019,Summer,b,D
+298,PHYS,2060,2018,Fall,a,D
+301,BIOL,1210,2016,Spring,a,D
+303,CS,4500,2019,Fall,a,D
+303,PHYS,2210,2019,Fall,d,D
+304,PHYS,2210,2017,Summer,d,D
+305,CS,3505,2018,Fall,a,D
+307,CS,2100,2019,Spring,a,D
+307,CS,4500,2016,Spring,b,D
+307,MATH,3210,2015,Fall,a,D
+309,PHYS,3210,2019,Summer,c,D
+311,BIOL,2210,2018,Summer,b,D
+312,BIOL,2010,2019,Fall,a,D
+312,BIOL,2355,2017,Spring,a,D
+312,CS,4400,2019,Spring,d,D
+312,MATH,1250,2018,Summer,a,D
+313,BIOL,2021,2019,Spring,b,D
+313,BIOL,2210,2018,Summer,b,D
+313,CS,4940,2020,Summer,a,D
+313,MATH,1220,2016,Spring,a,D
+320,BIOL,2030,2019,Summer,c,D
+321,MATH,1260,2019,Spring,c,D
+321,PHYS,2220,2015,Fall,a,D
+325,MATH,2270,2019,Summer,b,D
+325,PHYS,3220,2020,Spring,c,D
+329,CS,2100,2018,Summer,c,D
+329,CS,2420,2016,Fall,b,D
+332,BIOL,1006,2019,Fall,b,D
+332,PHYS,2220,2018,Fall,a,D
+333,CS,4400,2019,Spring,a,D
+335,BIOL,2355,2017,Fall,a,D
+335,CS,1410,2016,Spring,a,D
+335,CS,4500,2017,Summer,a,D
+335,MATH,1210,2016,Spring,b,D
+339,PHYS,3210,2020,Fall,a,D
+341,BIOL,1030,2020,Spring,a,D
+342,MATH,2210,2019,Spring,b,D
+342,MATH,2270,2017,Fall,b,D
+342,PHYS,2210,2017,Summer,c,D
+344,CS,3810,2018,Summer,b,D
+345,CS,4940,2020,Summer,b,D
+345,MATH,1210,2017,Summer,a,D
+345,MATH,2210,2020,Fall,a,D
+347,CS,1030,2020,Fall,a,D
+347,MATH,1260,2019,Spring,a,D
+347,MATH,2210,2020,Spring,a,D
+348,CS,3505,2015,Fall,d,D
+348,CS,4150,2015,Summer,b,D
+348,CS,4400,2020,Spring,a,D
+348,CS,4970,2018,Summer,b,D
+355,CS,3505,2017,Fall,a,D
+358,MATH,1220,2019,Fall,c,D
+364,BIOL,1006,2019,Fall,a,D
+365,BIOL,1006,2020,Spring,a,D
+366,CS,4500,2018,Spring,d,D
+372,BIOL,1210,2017,Spring,a,D
+373,CS,3505,2019,Summer,b,D
+374,BIOL,2030,2017,Spring,a,D
+375,CS,4000,2020,Spring,a,D
+375,PHYS,2060,2019,Fall,c,D
+377,CS,2420,2015,Spring,a,D
+377,CS,3200,2017,Spring,a,D
+377,PHYS,2060,2015,Spring,a,D
+378,CS,2100,2017,Spring,a,D
+379,BIOL,1010,2019,Spring,d,D
+379,MATH,3220,2018,Spring,a,D
+379,PHYS,2060,2016,Spring,a,D
+379,PHYS,3210,2017,Summer,b,D
+385,CS,3200,2016,Fall,d,D
+386,BIOL,1030,2020,Summer,a,D
+386,BIOL,2010,2020,Spring,a,D
+386,CS,2100,2019,Fall,a,D
+386,CS,4000,2020,Spring,a,D
+386,CS,4940,2020,Summer,a,D
+387,BIOL,2210,2017,Summer,b,D
+387,BIOL,2330,2017,Fall,a,D
+391,MATH,2270,2020,Fall,b,D
+392,CS,1410,2018,Spring,d,D
+392,PHYS,2140,2016,Summer,b,D
+393,BIOL,2325,2018,Summer,a,D
+393,CS,2100,2018,Summer,c,D
+393,MATH,2280,2016,Fall,a,D
+393,PHYS,2060,2016,Summer,a,D
+397,BIOL,2355,2017,Spring,c,D
+397,MATH,2270,2017,Fall,d,D
+397,PHYS,2060,2019,Summer,b,D
+100,PHYS,2060,2019,Fall,c,D+
+102,BIOL,2355,2017,Spring,d,D+
+102,CS,4970,2018,Fall,d,D+
+102,PHYS,2210,2019,Spring,c,D+
+102,PHYS,3210,2018,Spring,c,D+
+105,BIOL,2355,2017,Spring,a,D+
+107,CS,1410,2018,Spring,b,D+
+107,CS,2420,2018,Spring,a,D+
+107,CS,4500,2019,Summer,a,D+
+109,CS,1410,2018,Spring,c,D+
+109,MATH,3210,2020,Fall,a,D+
+113,BIOL,2010,2020,Spring,a,D+
+113,MATH,1260,2019,Summer,a,D+
+118,BIOL,1006,2020,Fall,c,D+
+119,BIOL,2420,2016,Spring,a,D+
+119,PHYS,2210,2019,Spring,a,D+
+119,PHYS,2220,2017,Spring,a,D+
+120,BIOL,2325,2019,Summer,a,D+
+120,CS,1030,2020,Spring,c,D+
+120,CS,2100,2019,Fall,d,D+
+120,MATH,2280,2018,Fall,c,D+
+121,CS,3505,2020,Fall,b,D+
+122,BIOL,2355,2020,Summer,a,D+
+122,BIOL,2420,2020,Fall,a,D+
+124,CS,3500,2020,Summer,a,D+
+124,CS,3505,2017,Fall,a,D+
+125,PHYS,2040,2020,Spring,a,D+
+128,CS,4400,2019,Spring,b,D+
+128,MATH,2270,2017,Fall,d,D+
+128,PHYS,2140,2018,Summer,a,D+
+128,PHYS,3220,2018,Summer,a,D+
+129,MATH,3220,2018,Spring,d,D+
+130,MATH,2270,2020,Fall,a,D+
+131,BIOL,2030,2019,Summer,d,D+
+131,MATH,1220,2018,Spring,b,D+
+131,PHYS,2040,2020,Spring,a,D+
+132,MATH,2270,2017,Summer,a,D+
+132,PHYS,2040,2017,Fall,c,D+
+135,CS,4970,2019,Summer,d,D+
+135,MATH,1210,2020,Spring,a,D+
+135,MATH,1250,2020,Summer,a,D+
+138,CS,3100,2016,Spring,b,D+
+138,CS,3200,2015,Fall,a,D+
+138,MATH,1250,2016,Fall,b,D+
+139,BIOL,1030,2019,Spring,b,D+
+139,CS,3200,2019,Spring,a,D+
+139,PHYS,3220,2017,Summer,a,D+
+142,BIOL,2355,2020,Fall,a,D+
+143,BIOL,1030,2019,Spring,c,D+
+143,BIOL,2210,2018,Summer,a,D+
+144,BIOL,1210,2016,Spring,a,D+
+146,CS,4970,2020,Summer,d,D+
+146,PHYS,3210,2019,Summer,a,D+
+149,CS,1030,2016,Spring,a,D+
+156,PHYS,2060,2018,Fall,a,D+
+162,MATH,1260,2015,Summer,a,D+
+163,PHYS,2220,2017,Spring,d,D+
+164,BIOL,2210,2017,Summer,b,D+
+164,CS,2100,2018,Fall,d,D+
+164,CS,4970,2019,Summer,b,D+
+164,MATH,1220,2020,Summer,a,D+
+165,CS,4970,2019,Spring,a,D+
+167,CS,2420,2020,Summer,a,D+
+172,CS,1410,2015,Summer,d,D+
+173,BIOL,2210,2018,Summer,c,D+
+173,CS,4970,2019,Summer,c,D+
+175,MATH,2270,2020,Spring,a,D+
+177,PHYS,2040,2015,Spring,a,D+
+177,PHYS,2060,2019,Fall,c,D+
+178,BIOL,2021,2019,Spring,b,D+
+178,CS,3505,2019,Fall,a,D+
+179,BIOL,1010,2015,Fall,a,D+
+179,CS,4150,2020,Spring,a,D+
+179,PHYS,3210,2017,Summer,a,D+
+182,BIOL,2010,2015,Summer,a,D+
+182,BIOL,2355,2017,Fall,b,D+
+183,MATH,2270,2019,Summer,c,D+
+185,CS,4970,2018,Summer,c,D+
+185,PHYS,3220,2020,Spring,d,D+
+187,BIOL,2355,2018,Summer,b,D+
+192,BIOL,2420,2015,Spring,d,D+
+192,MATH,3220,2016,Spring,d,D+
+192,PHYS,2140,2015,Spring,b,D+
+194,PHYS,2060,2019,Summer,b,D+
+199,CS,3505,2017,Fall,a,D+
+204,CS,4150,2015,Summer,a,D+
+208,CS,3505,2017,Fall,b,D+
+209,PHYS,3210,2018,Spring,c,D+
+210,BIOL,2210,2018,Summer,c,D+
+210,BIOL,2330,2020,Spring,a,D+
+210,CS,3810,2018,Summer,d,D+
+211,CS,3505,2015,Fall,a,D+
+213,CS,3810,2016,Fall,a,D+
+214,BIOL,2030,2018,Summer,b,D+
+214,BIOL,2330,2017,Summer,a,D+
+214,PHYS,2220,2018,Spring,a,D+
+215,PHYS,3220,2017,Summer,a,D+
+217,CS,2100,2018,Fall,c,D+
+220,BIOL,1210,2018,Fall,a,D+
+220,BIOL,2210,2020,Fall,a,D+
+227,BIOL,2355,2018,Summer,a,D+
+227,MATH,3210,2020,Fall,a,D+
+228,CS,3505,2019,Spring,a,D+
+228,MATH,1220,2020,Spring,a,D+
+230,BIOL,2210,2018,Summer,b,D+
+230,MATH,1210,2017,Summer,b,D+
+231,CS,4400,2017,Spring,b,D+
+231,PHYS,2060,2018,Fall,d,D+
+233,CS,3810,2020,Fall,a,D+
+238,CS,2100,2019,Summer,b,D+
+239,BIOL,1010,2018,Fall,a,D+
+240,CS,4500,2020,Summer,a,D+
+241,BIOL,1006,2020,Fall,a,D+
+241,PHYS,2210,2019,Fall,a,D+
+247,MATH,1220,2019,Fall,c,D+
+249,BIOL,2021,2015,Summer,b,D+
+251,CS,4940,2020,Summer,b,D+
+254,BIOL,1030,2020,Spring,a,D+
+254,MATH,1220,2019,Fall,a,D+
+255,BIOL,1006,2020,Fall,b,D+
+255,BIOL,1210,2018,Fall,a,D+
+255,CS,4970,2018,Fall,d,D+
+255,MATH,1210,2019,Summer,a,D+
+255,MATH,3210,2020,Fall,a,D+
+255,PHYS,2060,2020,Spring,a,D+
+255,PHYS,2210,2019,Spring,a,D+
+255,PHYS,3220,2020,Spring,b,D+
+256,CS,2100,2017,Spring,a,D+
+256,CS,3505,2018,Fall,a,D+
+256,PHYS,2210,2019,Summer,a,D+
+257,CS,4970,2020,Summer,a,D+
+259,CS,2100,2018,Fall,d,D+
+259,MATH,1220,2017,Spring,b,D+
+260,CS,1030,2019,Fall,a,D+
+260,CS,3500,2020,Summer,a,D+
+260,MATH,1260,2017,Fall,a,D+
+262,BIOL,1006,2017,Fall,a,D+
+262,BIOL,2355,2018,Summer,a,D+
+262,CS,4000,2017,Fall,a,D+
+264,CS,3810,2016,Fall,b,D+
+264,CS,4150,2016,Summer,b,D+
+264,MATH,3220,2016,Fall,b,D+
+267,CS,4970,2018,Fall,d,D+
+270,BIOL,2325,2019,Spring,b,D+
+270,CS,3505,2019,Summer,d,D+
+270,MATH,1210,2016,Spring,a,D+
+270,MATH,3210,2019,Spring,a,D+
+273,CS,1410,2016,Spring,b,D+
+275,CS,3500,2017,Fall,c,D+
+276,BIOL,2355,2018,Summer,d,D+
+276,CS,4400,2017,Spring,c,D+
+276,MATH,2280,2015,Fall,a,D+
+276,PHYS,3220,2017,Fall,d,D+
+277,BIOL,1006,2020,Fall,b,D+
+277,CS,3505,2020,Spring,a,D+
+277,MATH,2270,2017,Summer,a,D+
+285,BIOL,2325,2019,Summer,a,D+
+285,CS,2100,2018,Summer,b,D+
+285,CS,3810,2016,Fall,a,D+
+285,MATH,2210,2019,Spring,a,D+
+288,CS,4970,2017,Summer,a,D+
+288,PHYS,2100,2018,Fall,a,D+
+288,PHYS,2220,2017,Spring,d,D+
+289,BIOL,2020,2019,Summer,a,D+
+289,CS,2100,2020,Fall,a,D+
+289,CS,3505,2019,Fall,b,D+
+290,CS,1030,2016,Spring,a,D+
+291,BIOL,2330,2016,Fall,a,D+
+291,MATH,1260,2017,Fall,a,D+
+292,BIOL,2325,2019,Spring,a,D+
+292,BIOL,2420,2020,Summer,a,D+
+292,CS,4000,2020,Fall,a,D+
+292,MATH,2280,2019,Fall,c,D+
+292,PHYS,2040,2017,Summer,a,D+
+294,BIOL,1006,2018,Spring,b,D+
+294,CS,4400,2019,Summer,a,D+
+295,PHYS,2040,2015,Fall,b,D+
+296,CS,4500,2019,Fall,d,D+
+298,BIOL,2210,2018,Summer,a,D+
+298,PHYS,2140,2018,Summer,a,D+
+299,BIOL,2355,2017,Spring,c,D+
+300,BIOL,2030,2019,Summer,d,D+
+302,CS,3100,2015,Summer,a,D+
+303,BIOL,1006,2019,Summer,a,D+
+305,CS,4500,2018,Spring,b,D+
+305,MATH,2210,2019,Spring,b,D+
+305,PHYS,2040,2019,Spring,b,D+
+305,PHYS,2140,2018,Summer,b,D+
+305,PHYS,2210,2019,Spring,a,D+
+307,PHYS,2100,2017,Summer,b,D+
+309,BIOL,2010,2019,Fall,a,D+
+309,CS,4000,2020,Spring,b,D+
+309,CS,4970,2020,Summer,b,D+
+310,MATH,1210,2020,Spring,a,D+
+311,CS,3500,2017,Summer,a,D+
+312,BIOL,2210,2016,Summer,a,D+
+312,CS,2420,2016,Spring,a,D+
+312,CS,3200,2015,Fall,c,D+
+312,MATH,3220,2018,Spring,b,D+
+312,PHYS,3220,2016,Summer,a,D+
+313,BIOL,1030,2017,Spring,a,D+
+313,CS,4970,2016,Fall,b,D+
+313,PHYS,2060,2019,Summer,b,D+
+314,CS,3100,2016,Fall,a,D+
+318,BIOL,1006,2017,Fall,b,D+
+318,BIOL,2021,2018,Summer,a,D+
+318,CS,4000,2017,Summer,a,D+
+321,BIOL,1006,2017,Fall,a,D+
+321,BIOL,1010,2017,Summer,a,D+
+321,CS,3505,2017,Fall,b,D+
+321,CS,4940,2020,Summer,b,D+
+323,MATH,1220,2019,Fall,a,D+
+325,CS,4970,2019,Summer,b,D+
+326,CS,4940,2017,Fall,a,D+
+326,PHYS,2210,2017,Summer,b,D+
+329,BIOL,1010,2018,Summer,c,D+
+329,BIOL,2355,2017,Spring,d,D+
+329,BIOL,2420,2020,Fall,a,D+
+329,CS,4970,2019,Summer,d,D+
+329,PHYS,2060,2018,Fall,c,D+
+331,CS,3500,2020,Summer,a,D+
+331,CS,4940,2020,Summer,a,D+
+332,BIOL,2020,2018,Spring,a,D+
+332,MATH,1250,2018,Summer,a,D+
+333,BIOL,1030,2019,Spring,a,D+
+333,CS,4500,2019,Summer,a,D+
+335,CS,3500,2017,Fall,a,D+
+339,CS,4150,2020,Fall,a,D+
+342,CS,3200,2020,Spring,a,D+
+345,CS,4000,2017,Fall,b,D+
+345,PHYS,3210,2019,Summer,c,D+
+347,BIOL,2355,2018,Summer,d,D+
+347,CS,3810,2020,Fall,a,D+
+355,PHYS,2220,2017,Spring,b,D+
+356,BIOL,2210,2016,Summer,a,D+
+356,CS,3810,2018,Summer,d,D+
+356,CS,4970,2018,Fall,d,D+
+356,PHYS,3210,2019,Spring,a,D+
+361,CS,4000,2017,Fall,a,D+
+361,MATH,1260,2017,Summer,a,D+
+362,BIOL,1010,2018,Fall,a,D+
+363,BIOL,2420,2020,Summer,a,D+
+365,CS,2420,2020,Summer,a,D+
+366,BIOL,1006,2018,Spring,a,D+
+369,CS,4500,2018,Spring,c,D+
+371,BIOL,1006,2020,Fall,b,D+
+371,CS,3505,2018,Fall,a,D+
+372,CS,2420,2017,Summer,c,D+
+373,MATH,1250,2016,Summer,a,D+
+373,MATH,3220,2016,Spring,a,D+
+373,PHYS,3220,2020,Spring,d,D+
+374,BIOL,2355,2017,Spring,d,D+
+375,MATH,2210,2019,Spring,b,D+
+377,CS,3500,2019,Fall,a,D+
+377,PHYS,2140,2019,Fall,b,D+
+378,BIOL,2021,2018,Summer,a,D+
+378,BIOL,2355,2017,Spring,d,D+
+378,MATH,2210,2020,Fall,a,D+
+379,BIOL,2325,2015,Fall,b,D+
+379,CS,2100,2019,Spring,a,D+
+379,CS,4400,2019,Spring,a,D+
+379,MATH,1210,2016,Fall,c,D+
+380,CS,3505,2019,Summer,b,D+
+386,MATH,1210,2020,Spring,b,D+
+386,MATH,3210,2020,Fall,a,D+
+387,BIOL,1030,2018,Fall,a,D+
+387,BIOL,2355,2018,Summer,d,D+
+387,CS,2420,2017,Fall,a,D+
+388,CS,1410,2018,Spring,c,D+
+389,PHYS,2040,2016,Spring,a,D+
+390,BIOL,2355,2020,Fall,a,D+
+391,CS,4500,2018,Spring,d,D+
+391,PHYS,2210,2019,Spring,c,D+
+392,BIOL,2020,2015,Fall,b,D+
+392,CS,2420,2016,Fall,a,D+
+394,BIOL,1006,2015,Spring,b,D+
+397,BIOL,2021,2018,Fall,b,D+
+397,CS,2420,2016,Fall,c,D+
+397,CS,3100,2017,Fall,a,D+
+397,CS,4500,2020,Summer,a,D+
+397,CS,4940,2020,Summer,b,D+
+398,CS,4500,2019,Fall,b,D+
+399,PHYS,2040,2019,Spring,a,D+
+100,BIOL,2030,2019,Summer,b,F
+100,CS,4940,2020,Summer,b,F
+101,CS,4500,2018,Spring,a,F
+101,MATH,3220,2018,Spring,b,F
+101,PHYS,3210,2018,Fall,a,F
+102,CS,3810,2019,Fall,b,F
+102,MATH,3210,2016,Fall,a,F
+104,MATH,1210,2018,Fall,b,F
+106,BIOL,2355,2020,Summer,b,F
+106,PHYS,2060,2019,Summer,b,F
+107,MATH,1210,2016,Fall,c,F
+108,CS,3500,2019,Fall,b,F
+112,PHYS,2060,2020,Fall,a,F
+113,BIOL,1010,2020,Summer,a,F
+113,MATH,3210,2020,Summer,a,F
+115,BIOL,2210,2017,Spring,a,F
+116,CS,3505,2016,Fall,b,F
+117,BIOL,1210,2017,Spring,a,F
+119,BIOL,2210,2019,Summer,b,F
+119,BIOL,2325,2018,Spring,a,F
+119,CS,1030,2020,Fall,a,F
+119,MATH,2270,2020,Fall,b,F
+120,BIOL,1030,2016,Fall,a,F
+120,BIOL,2330,2016,Spring,a,F
+120,CS,1410,2018,Spring,a,F
+120,CS,3200,2016,Fall,a,F
+120,MATH,1260,2019,Summer,b,F
+121,CS,1410,2020,Spring,a,F
+121,CS,4970,2018,Fall,c,F
+122,CS,1410,2020,Spring,a,F
+123,CS,4400,2020,Fall,a,F
+127,CS,4400,2020,Fall,b,F
+127,CS,4500,2020,Summer,a,F
+128,BIOL,1030,2019,Summer,a,F
+128,CS,4500,2018,Spring,d,F
+129,BIOL,2355,2018,Summer,c,F
+129,PHYS,3210,2020,Fall,b,F
+131,BIOL,1030,2020,Summer,a,F
+131,BIOL,2210,2018,Summer,a,F
+131,BIOL,2325,2018,Fall,b,F
+131,MATH,1260,2019,Summer,a,F
+131,PHYS,3210,2020,Spring,a,F
+132,BIOL,2030,2018,Summer,b,F
+133,PHYS,3210,2019,Summer,b,F
+137,BIOL,2355,2020,Summer,b,F
+139,BIOL,1010,2019,Spring,a,F
+139,BIOL,2020,2018,Spring,a,F
+139,CS,2100,2018,Summer,c,F
+142,CS,3505,2020,Fall,a,F
+142,CS,4500,2020,Spring,a,F
+143,BIOL,2030,2019,Summer,d,F
+143,PHYS,2210,2019,Fall,a,F
+146,CS,3505,2020,Spring,a,F
+146,MATH,2280,2019,Fall,c,F
+149,CS,4500,2016,Spring,b,F
+149,MATH,1250,2015,Fall,a,F
+151,BIOL,2210,2017,Summer,a,F
+152,MATH,2270,2020,Fall,a,F
+158,BIOL,2020,2018,Fall,a,F
+158,PHYS,2100,2018,Fall,a,F
+162,CS,1030,2016,Spring,a,F
+162,CS,3505,2015,Fall,c,F
+163,BIOL,2030,2016,Summer,b,F
+163,PHYS,2140,2018,Fall,a,F
+164,PHYS,2220,2020,Summer,b,F
+167,BIOL,2010,2020,Summer,a,F
+167,CS,4940,2019,Fall,a,F
+167,PHYS,2060,2020,Spring,a,F
+167,PHYS,2140,2019,Fall,a,F
+169,BIOL,1006,2019,Fall,b,F
+169,BIOL,2355,2018,Spring,a,F
+175,PHYS,2220,2020,Spring,a,F
+177,BIOL,2210,2017,Spring,c,F
+177,CS,2100,2020,Fall,a,F
+177,CS,4940,2020,Summer,b,F
+177,MATH,1210,2018,Fall,a,F
+178,BIOL,1006,2019,Summer,a,F
+178,CS,4970,2018,Fall,d,F
+178,PHYS,2220,2018,Fall,a,F
+179,BIOL,1006,2016,Summer,b,F
+179,BIOL,2030,2017,Spring,a,F
+179,CS,3505,2018,Fall,a,F
+181,CS,1030,2020,Fall,a,F
+182,BIOL,1006,2015,Summer,a,F
+182,BIOL,1210,2016,Spring,a,F
+182,CS,1410,2015,Summer,c,F
+182,CS,2100,2018,Summer,b,F
+185,BIOL,2420,2018,Spring,a,F
+185,CS,4000,2020,Fall,a,F
+187,CS,3200,2020,Spring,b,F
+192,MATH,3210,2015,Fall,c,F
+194,CS,2100,2019,Summer,b,F
+195,BIOL,1010,2016,Summer,a,F
+195,CS,2420,2016,Summer,a,F
+195,PHYS,3220,2016,Summer,a,F
+197,BIOL,1030,2018,Summer,a,F
+199,BIOL,2030,2020,Spring,b,F
+199,CS,3505,2017,Fall,b,F
+199,CS,4400,2020,Spring,a,F
+200,CS,1410,2020,Spring,b,F
+200,MATH,2210,2020,Spring,b,F
+207,BIOL,2420,2017,Summer,b,F
+210,BIOL,2010,2018,Spring,a,F
+210,CS,3100,2017,Spring,a,F
+210,CS,4150,2019,Spring,a,F
+211,BIOL,1010,2015,Fall,c,F
+211,BIOL,1030,2015,Spring,c,F
+211,MATH,1250,2015,Spring,c,F
+213,PHYS,3220,2017,Fall,c,F
+220,BIOL,2355,2020,Fall,a,F
+220,CS,3505,2019,Summer,a,F
+221,BIOL,2355,2020,Summer,a,F
+221,CS,4970,2020,Summer,b,F
+223,MATH,3210,2019,Fall,a,F
+229,PHYS,3210,2018,Spring,a,F
+230,BIOL,1210,2019,Spring,a,F
+230,MATH,2280,2018,Spring,a,F
+231,BIOL,1030,2019,Spring,d,F
+231,MATH,1210,2018,Fall,a,F
+231,PHYS,2140,2018,Summer,a,F
+237,MATH,3220,2018,Spring,a,F
+238,BIOL,1010,2018,Summer,b,F
+240,CS,2100,2019,Spring,a,F
+243,BIOL,2021,2016,Fall,a,F
+246,BIOL,1030,2015,Summer,a,F
+247,CS,1030,2019,Fall,b,F
+247,CS,3500,2020,Summer,a,F
+247,CS,3505,2018,Summer,b,F
+248,BIOL,2420,2020,Spring,a,F
+250,PHYS,2060,2020,Fall,a,F
+252,BIOL,1010,2018,Fall,a,F
+252,CS,4000,2017,Fall,a,F
+255,BIOL,2355,2019,Spring,c,F
+255,BIOL,2420,2020,Fall,a,F
+255,CS,3505,2018,Summer,a,F
+255,MATH,2280,2020,Spring,a,F
+256,BIOL,2021,2018,Fall,a,F
+256,MATH,3210,2020,Fall,a,F
+257,MATH,1210,2018,Summer,a,F
+257,PHYS,2210,2019,Spring,d,F
+258,CS,2100,2018,Summer,c,F
+259,BIOL,1010,2018,Summer,c,F
+259,PHYS,2140,2017,Fall,a,F
+260,BIOL,2020,2018,Fall,b,F
+260,CS,4940,2017,Fall,a,F
+260,PHYS,3220,2018,Summer,a,F
+261,MATH,1250,2018,Summer,c,F
+261,PHYS,2210,2017,Summer,d,F
+262,CS,1030,2016,Fall,a,F
+267,CS,1410,2020,Spring,a,F
+267,CS,4970,2018,Fall,b,F
+268,BIOL,2021,2016,Fall,a,F
+270,BIOL,1010,2020,Summer,b,F
+270,BIOL,2030,2019,Summer,b,F
+270,BIOL,2030,2019,Summer,c,F
+270,CS,2420,2016,Fall,c,F
+272,CS,4400,2020,Fall,a,F
+274,CS,4970,2018,Fall,a,F
+276,BIOL,1010,2015,Summer,a,F
+276,BIOL,2355,2018,Summer,b,F
+276,CS,3500,2019,Summer,a,F
+276,CS,4970,2016,Fall,a,F
+276,MATH,1250,2015,Spring,c,F
+278,BIOL,1010,2017,Spring,a,F
+278,MATH,3220,2016,Summer,a,F
+280,MATH,1220,2015,Summer,b,F
+281,CS,3500,2020,Summer,a,F
+282,CS,3200,2015,Fall,d,F
+282,CS,3500,2016,Summer,a,F
+282,PHYS,2220,2015,Fall,b,F
+285,CS,4500,2016,Spring,b,F
+289,CS,4970,2019,Summer,a,F
+290,BIOL,2325,2015,Fall,b,F
+290,CS,4500,2015,Summer,b,F
+290,MATH,3220,2016,Spring,d,F
+292,BIOL,1010,2018,Summer,b,F
+292,BIOL,2355,2018,Fall,a,F
+292,CS,2100,2018,Fall,a,F
+293,PHYS,2220,2020,Summer,a,F
+299,MATH,1220,2017,Spring,a,F
+301,CS,1410,2015,Summer,d,F
+303,CS,3200,2020,Spring,a,F
+303,MATH,2270,2019,Summer,b,F
+303,PHYS,3220,2020,Spring,d,F
+304,BIOL,2010,2017,Fall,a,F
+304,MATH,1260,2017,Summer,a,F
+307,CS,4000,2015,Fall,a,F
+309,CS,3505,2019,Fall,b,F
+309,CS,4400,2017,Spring,b,F
+311,BIOL,2010,2020,Summer,a,F
+311,MATH,1210,2018,Fall,a,F
+311,PHYS,2060,2018,Fall,b,F
+312,BIOL,2030,2019,Summer,a,F
+312,MATH,1210,2018,Fall,a,F
+312,PHYS,2040,2015,Fall,b,F
+313,CS,3200,2016,Fall,b,F
+313,MATH,2280,2020,Spring,a,F
+313,PHYS,2040,2020,Spring,a,F
+314,PHYS,3210,2016,Summer,b,F
+320,CS,3505,2019,Spring,a,F
+329,BIOL,1030,2016,Summer,a,F
+329,BIOL,2210,2019,Summer,a,F
+329,BIOL,2325,2019,Spring,a,F
+329,CS,4150,2020,Fall,a,F
+329,PHYS,2140,2019,Fall,a,F
+332,CS,3505,2020,Spring,a,F
+333,BIOL,2420,2020,Summer,a,F
+335,BIOL,2330,2016,Fall,a,F
+339,CS,4940,2020,Summer,b,F
+339,PHYS,2220,2020,Summer,a,F
+340,BIOL,1006,2020,Spring,a,F
+340,CS,4500,2019,Summer,a,F
+341,MATH,1220,2019,Fall,a,F
+342,MATH,1210,2017,Summer,c,F
+344,CS,2100,2018,Fall,b,F
+345,CS,3200,2020,Fall,a,F
+345,MATH,2280,2018,Fall,c,F
+345,MATH,3220,2018,Spring,a,F
+347,PHYS,2210,2019,Fall,c,F
+348,PHYS,2060,2016,Summer,b,F
+353,CS,2420,2017,Summer,a,F
+355,BIOL,2010,2017,Fall,a,F
+355,CS,3100,2017,Spring,b,F
+355,PHYS,2100,2017,Summer,c,F
+356,BIOL,1210,2019,Spring,a,F
+358,CS,3505,2019,Spring,b,F
+359,PHYS,2210,2019,Summer,a,F
+361,CS,2420,2017,Summer,a,F
+362,BIOL,2355,2020,Spring,a,F
+363,CS,3500,2019,Fall,c,F
+365,PHYS,3220,2020,Spring,c,F
+366,PHYS,2060,2019,Summer,b,F
+366,PHYS,2100,2019,Summer,a,F
+368,BIOL,2210,2019,Summer,a,F
+368,CS,4970,2019,Summer,a,F
+368,MATH,1210,2018,Summer,a,F
+371,MATH,2270,2020,Fall,b,F
+372,MATH,1250,2018,Summer,a,F
+373,BIOL,1010,2017,Spring,a,F
+373,BIOL,2020,2018,Fall,b,F
+377,PHYS,3210,2017,Fall,a,F
+378,CS,4940,2020,Summer,a,F
+379,BIOL,2355,2018,Summer,d,F
+384,BIOL,1030,2019,Spring,d,F
+385,MATH,1210,2016,Fall,d,F
+386,CS,3505,2018,Summer,a,F
+386,PHYS,2100,2019,Summer,a,F
+386,PHYS,2220,2020,Fall,a,F
+387,BIOL,2325,2017,Fall,b,F
+387,MATH,1210,2017,Summer,c,F
+389,CS,1410,2016,Summer,a,F
+390,MATH,1210,2020,Spring,a,F
+391,BIOL,2330,2017,Fall,a,F
+391,CS,1030,2020,Spring,b,F
+391,CS,4970,2019,Summer,b,F
+391,MATH,1250,2020,Summer,a,F
+391,MATH,2270,2020,Fall,a,F
+391,PHYS,2220,2016,Summer,a,F
+392,MATH,3220,2016,Spring,b,F
+392,PHYS,2100,2016,Fall,a,F
+393,CS,1030,2016,Summer,a,F
+396,MATH,1220,2019,Fall,c,F
+397,BIOL,2210,2017,Spring,c,F
+399,BIOL,1010,2018,Summer,a,F
diff --git a/tests/data/Section.csv b/tests/data/Section.csv
new file mode 100644
index 000000000..8dc95361b
--- /dev/null
+++ b/tests/data/Section.csv
@@ -0,0 +1,757 @@
+dept,course,term_year,term,section,auditorium
+BIOL,1006,2015,Spring,a,C68
+BIOL,1006,2015,Spring,b,C22
+BIOL,1006,2015,Summer,a,D38
+BIOL,1006,2015,Summer,b,C15
+BIOL,1006,2016,Spring,a,B87
+BIOL,1006,2016,Spring,b,D72
+BIOL,1006,2016,Summer,a,A34
+BIOL,1006,2016,Summer,b,D48
+BIOL,1006,2016,Summer,c,F34
+BIOL,1006,2016,Summer,d,F48
+BIOL,1006,2017,Fall,a,E42
+BIOL,1006,2017,Fall,b,B83
+BIOL,1006,2018,Spring,a,F39
+BIOL,1006,2018,Spring,b,A18
+BIOL,1006,2018,Fall,a,A13
+BIOL,1006,2019,Spring,a,D59
+BIOL,1006,2019,Summer,a,F70
+BIOL,1006,2019,Fall,a,B54
+BIOL,1006,2019,Fall,b,D79
+BIOL,1006,2020,Spring,a,A89
+BIOL,1006,2020,Fall,a,C13
+BIOL,1006,2020,Fall,b,C70
+BIOL,1006,2020,Fall,c,F46
+BIOL,1010,2015,Summer,a,D12
+BIOL,1010,2015,Summer,b,F82
+BIOL,1010,2015,Summer,c,A7
+BIOL,1010,2015,Summer,d,B17
+BIOL,1010,2015,Fall,a,B9
+BIOL,1010,2015,Fall,b,E27
+BIOL,1010,2015,Fall,c,B43
+BIOL,1010,2015,Fall,d,E1
+BIOL,1010,2016,Summer,a,B70
+BIOL,1010,2017,Spring,a,A17
+BIOL,1010,2017,Summer,a,B76
+BIOL,1010,2018,Summer,a,E15
+BIOL,1010,2018,Summer,b,D58
+BIOL,1010,2018,Summer,c,E76
+BIOL,1010,2018,Fall,a,E6
+BIOL,1010,2018,Fall,b,F67
+BIOL,1010,2019,Spring,a,A8
+BIOL,1010,2019,Spring,b,D55
+BIOL,1010,2019,Spring,c,D92
+BIOL,1010,2019,Spring,d,A11
+BIOL,1010,2020,Summer,a,E71
+BIOL,1010,2020,Summer,b,D77
+BIOL,1010,2020,Summer,c,D65
+BIOL,1010,2020,Summer,d,A90
+BIOL,1030,2015,Spring,a,E93
+BIOL,1030,2015,Spring,b,D58
+BIOL,1030,2015,Spring,c,D44
+BIOL,1030,2015,Spring,d,D54
+BIOL,1030,2015,Summer,a,C55
+BIOL,1030,2016,Spring,a,F61
+BIOL,1030,2016,Summer,a,A56
+BIOL,1030,2016,Fall,a,B72
+BIOL,1030,2017,Spring,a,E43
+BIOL,1030,2017,Spring,b,D46
+BIOL,1030,2017,Spring,c,D93
+BIOL,1030,2018,Summer,a,B85
+BIOL,1030,2018,Fall,a,C72
+BIOL,1030,2019,Spring,a,E29
+BIOL,1030,2019,Spring,b,E99
+BIOL,1030,2019,Spring,c,E87
+BIOL,1030,2019,Spring,d,A78
+BIOL,1030,2019,Summer,a,F35
+BIOL,1030,2020,Spring,a,C45
+BIOL,1030,2020,Summer,a,E85
+BIOL,1210,2015,Spring,a,A12
+BIOL,1210,2015,Spring,b,B49
+BIOL,1210,2016,Spring,a,E77
+BIOL,1210,2017,Spring,a,F11
+BIOL,1210,2017,Summer,a,D78
+BIOL,1210,2018,Spring,a,A45
+BIOL,1210,2018,Fall,a,D68
+BIOL,1210,2018,Fall,b,A29
+BIOL,1210,2019,Spring,a,A27
+BIOL,2010,2015,Spring,a,B17
+BIOL,2010,2015,Summer,a,E72
+BIOL,2010,2015,Summer,b,C10
+BIOL,2010,2015,Fall,a,D3
+BIOL,2010,2017,Summer,a,C15
+BIOL,2010,2017,Fall,a,B80
+BIOL,2010,2018,Spring,a,C12
+BIOL,2010,2019,Fall,a,F44
+BIOL,2010,2020,Spring,a,A66
+BIOL,2010,2020,Spring,b,E66
+BIOL,2010,2020,Summer,a,C94
+BIOL,2010,2020,Summer,b,F19
+BIOL,2020,2015,Summer,a,F10
+BIOL,2020,2015,Fall,a,D60
+BIOL,2020,2015,Fall,b,E58
+BIOL,2020,2015,Fall,c,E83
+BIOL,2020,2015,Fall,d,E42
+BIOL,2020,2016,Spring,a,F41
+BIOL,2020,2018,Spring,a,C60
+BIOL,2020,2018,Fall,a,A83
+BIOL,2020,2018,Fall,b,A79
+BIOL,2020,2018,Fall,c,D60
+BIOL,2020,2018,Fall,d,F6
+BIOL,2020,2019,Summer,a,F25
+BIOL,2021,2015,Spring,a,C92
+BIOL,2021,2015,Summer,a,A32
+BIOL,2021,2015,Summer,b,D68
+BIOL,2021,2015,Summer,c,B47
+BIOL,2021,2016,Fall,a,F83
+BIOL,2021,2017,Summer,a,D37
+BIOL,2021,2017,Fall,a,E20
+BIOL,2021,2018,Spring,a,B45
+BIOL,2021,2018,Summer,a,F51
+BIOL,2021,2018,Fall,a,A40
+BIOL,2021,2018,Fall,b,F43
+BIOL,2021,2018,Fall,c,F90
+BIOL,2021,2018,Fall,d,F88
+BIOL,2021,2019,Spring,a,A83
+BIOL,2021,2019,Spring,b,E47
+BIOL,2021,2019,Fall,a,C99
+BIOL,2030,2015,Spring,a,A65
+BIOL,2030,2015,Spring,b,F68
+BIOL,2030,2015,Fall,a,B77
+BIOL,2030,2016,Summer,a,E22
+BIOL,2030,2016,Summer,b,A53
+BIOL,2030,2016,Fall,a,D79
+BIOL,2030,2017,Spring,a,D30
+BIOL,2030,2017,Spring,b,C61
+BIOL,2030,2017,Spring,c,B48
+BIOL,2030,2017,Spring,d,E57
+BIOL,2030,2018,Summer,a,B26
+BIOL,2030,2018,Summer,b,B33
+BIOL,2030,2019,Summer,a,F67
+BIOL,2030,2019,Summer,b,C11
+BIOL,2030,2019,Summer,c,C58
+BIOL,2030,2019,Summer,d,B56
+BIOL,2030,2020,Spring,a,D45
+BIOL,2030,2020,Spring,b,D7
+BIOL,2210,2016,Summer,a,C19
+BIOL,2210,2017,Spring,a,F18
+BIOL,2210,2017,Spring,b,D58
+BIOL,2210,2017,Spring,c,A3
+BIOL,2210,2017,Summer,a,E94
+BIOL,2210,2017,Summer,b,D15
+BIOL,2210,2017,Summer,c,B39
+BIOL,2210,2018,Spring,a,E59
+BIOL,2210,2018,Summer,a,D77
+BIOL,2210,2018,Summer,b,F66
+BIOL,2210,2018,Summer,c,F19
+BIOL,2210,2019,Summer,a,B86
+BIOL,2210,2019,Summer,b,E47
+BIOL,2210,2019,Fall,a,E65
+BIOL,2210,2019,Fall,b,D61
+BIOL,2210,2020,Fall,a,C9
+BIOL,2325,2015,Spring,a,F14
+BIOL,2325,2015,Spring,b,F97
+BIOL,2325,2015,Fall,a,F23
+BIOL,2325,2015,Fall,b,F60
+BIOL,2325,2015,Fall,c,D81
+BIOL,2325,2016,Summer,a,D5
+BIOL,2325,2017,Fall,a,E51
+BIOL,2325,2017,Fall,b,E61
+BIOL,2325,2018,Spring,a,B37
+BIOL,2325,2018,Summer,a,F43
+BIOL,2325,2018,Fall,a,D52
+BIOL,2325,2018,Fall,b,D44
+BIOL,2325,2018,Fall,c,D89
+BIOL,2325,2019,Spring,a,E35
+BIOL,2325,2019,Spring,b,F55
+BIOL,2325,2019,Summer,a,B70
+BIOL,2330,2015,Spring,a,B89
+BIOL,2330,2015,Fall,a,C79
+BIOL,2330,2015,Fall,b,C82
+BIOL,2330,2015,Fall,c,A10
+BIOL,2330,2015,Fall,d,D47
+BIOL,2330,2016,Spring,a,F87
+BIOL,2330,2016,Fall,a,F57
+BIOL,2330,2017,Summer,a,C47
+BIOL,2330,2017,Fall,a,E20
+BIOL,2330,2017,Fall,b,C48
+BIOL,2330,2019,Fall,a,A95
+BIOL,2330,2020,Spring,a,E16
+BIOL,2355,2015,Spring,a,C89
+BIOL,2355,2015,Spring,b,D26
+BIOL,2355,2015,Summer,a,D23
+BIOL,2355,2015,Summer,b,D12
+BIOL,2355,2015,Summer,c,C86
+BIOL,2355,2016,Spring,a,C21
+BIOL,2355,2016,Spring,b,F82
+BIOL,2355,2017,Spring,a,B31
+BIOL,2355,2017,Spring,b,A47
+BIOL,2355,2017,Spring,c,C60
+BIOL,2355,2017,Spring,d,E17
+BIOL,2355,2017,Summer,a,A9
+BIOL,2355,2017,Fall,a,F62
+BIOL,2355,2017,Fall,b,D74
+BIOL,2355,2018,Spring,a,F10
+BIOL,2355,2018,Summer,a,C17
+BIOL,2355,2018,Summer,b,E82
+BIOL,2355,2018,Summer,c,B56
+BIOL,2355,2018,Summer,d,A16
+BIOL,2355,2018,Fall,a,C22
+BIOL,2355,2019,Spring,a,B45
+BIOL,2355,2019,Spring,b,E37
+BIOL,2355,2019,Spring,c,C26
+BIOL,2355,2019,Spring,d,E36
+BIOL,2355,2020,Spring,a,E83
+BIOL,2355,2020,Summer,a,B22
+BIOL,2355,2020,Summer,b,F78
+BIOL,2355,2020,Fall,a,A4
+BIOL,2420,2015,Spring,a,E34
+BIOL,2420,2015,Spring,b,E54
+BIOL,2420,2015,Spring,c,A64
+BIOL,2420,2015,Spring,d,E38
+BIOL,2420,2015,Summer,a,C62
+BIOL,2420,2015,Fall,a,D39
+BIOL,2420,2016,Spring,a,B57
+BIOL,2420,2017,Summer,a,C94
+BIOL,2420,2017,Summer,b,C52
+BIOL,2420,2018,Spring,a,C31
+BIOL,2420,2020,Spring,a,B21
+BIOL,2420,2020,Spring,b,E93
+BIOL,2420,2020,Summer,a,D66
+BIOL,2420,2020,Fall,a,D3
+CS,1030,2016,Spring,a,A7
+CS,1030,2016,Summer,a,F87
+CS,1030,2016,Fall,a,A56
+CS,1030,2018,Fall,a,C71
+CS,1030,2019,Fall,a,E88
+CS,1030,2019,Fall,b,B13
+CS,1030,2020,Spring,a,C72
+CS,1030,2020,Spring,b,B26
+CS,1030,2020,Spring,c,D65
+CS,1030,2020,Fall,a,D67
+CS,1410,2015,Spring,a,E18
+CS,1410,2015,Summer,a,B51
+CS,1410,2015,Summer,b,F39
+CS,1410,2015,Summer,c,E66
+CS,1410,2015,Summer,d,F73
+CS,1410,2016,Spring,a,C43
+CS,1410,2016,Spring,b,D75
+CS,1410,2016,Summer,a,F81
+CS,1410,2017,Spring,a,E74
+CS,1410,2018,Spring,a,F80
+CS,1410,2018,Spring,b,D19
+CS,1410,2018,Spring,c,B5
+CS,1410,2018,Spring,d,F15
+CS,1410,2020,Spring,a,E61
+CS,1410,2020,Spring,b,F94
+CS,2100,2015,Summer,a,E49
+CS,2100,2016,Spring,a,C70
+CS,2100,2016,Summer,a,F88
+CS,2100,2016,Summer,b,F34
+CS,2100,2016,Summer,c,B32
+CS,2100,2017,Spring,a,C99
+CS,2100,2017,Fall,a,C62
+CS,2100,2018,Spring,a,F36
+CS,2100,2018,Summer,a,E49
+CS,2100,2018,Summer,b,D45
+CS,2100,2018,Summer,c,B38
+CS,2100,2018,Fall,a,A45
+CS,2100,2018,Fall,b,F33
+CS,2100,2018,Fall,c,B26
+CS,2100,2018,Fall,d,C72
+CS,2100,2019,Spring,a,B14
+CS,2100,2019,Spring,b,E31
+CS,2100,2019,Summer,a,E29
+CS,2100,2019,Summer,b,A13
+CS,2100,2019,Fall,a,A88
+CS,2100,2019,Fall,b,A71
+CS,2100,2019,Fall,c,B53
+CS,2100,2019,Fall,d,D62
+CS,2100,2020,Spring,a,C42
+CS,2100,2020,Fall,a,F74
+CS,2420,2015,Spring,a,A23
+CS,2420,2015,Summer,a,A51
+CS,2420,2015,Summer,b,B96
+CS,2420,2015,Summer,c,C5
+CS,2420,2015,Fall,a,A43
+CS,2420,2016,Spring,a,E68
+CS,2420,2016,Summer,a,E60
+CS,2420,2016,Fall,a,C21
+CS,2420,2016,Fall,b,F33
+CS,2420,2016,Fall,c,A95
+CS,2420,2017,Summer,a,B23
+CS,2420,2017,Summer,b,F52
+CS,2420,2017,Summer,c,E42
+CS,2420,2017,Fall,a,B18
+CS,2420,2018,Spring,a,A34
+CS,2420,2019,Summer,a,E2
+CS,2420,2020,Summer,a,D40
+CS,2420,2020,Fall,a,F99
+CS,3100,2015,Summer,a,C48
+CS,3100,2015,Summer,b,B18
+CS,3100,2016,Spring,a,C54
+CS,3100,2016,Spring,b,D97
+CS,3100,2016,Spring,c,F28
+CS,3100,2016,Spring,d,F97
+CS,3100,2016,Summer,a,A68
+CS,3100,2016,Fall,a,A73
+CS,3100,2017,Spring,a,E26
+CS,3100,2017,Spring,b,B22
+CS,3100,2017,Summer,a,A88
+CS,3100,2017,Fall,a,A66
+CS,3100,2019,Spring,a,E60
+CS,3100,2019,Spring,b,C93
+CS,3200,2015,Spring,a,E8
+CS,3200,2015,Spring,b,A61
+CS,3200,2015,Fall,a,F94
+CS,3200,2015,Fall,b,D48
+CS,3200,2015,Fall,c,D58
+CS,3200,2015,Fall,d,D49
+CS,3200,2016,Summer,a,E18
+CS,3200,2016,Summer,b,C16
+CS,3200,2016,Fall,a,E17
+CS,3200,2016,Fall,b,B1
+CS,3200,2016,Fall,c,C60
+CS,3200,2016,Fall,d,E55
+CS,3200,2017,Spring,a,B32
+CS,3200,2018,Spring,a,A5
+CS,3200,2018,Spring,b,D79
+CS,3200,2018,Spring,c,A31
+CS,3200,2019,Spring,a,F7
+CS,3200,2020,Spring,a,A18
+CS,3200,2020,Spring,b,C30
+CS,3200,2020,Spring,c,F74
+CS,3200,2020,Summer,a,F42
+CS,3200,2020,Fall,a,F67
+CS,3500,2015,Fall,a,F23
+CS,3500,2015,Fall,b,D72
+CS,3500,2016,Spring,a,F86
+CS,3500,2016,Summer,a,F54
+CS,3500,2017,Summer,a,B29
+CS,3500,2017,Fall,a,D8
+CS,3500,2017,Fall,b,D72
+CS,3500,2017,Fall,c,D32
+CS,3500,2019,Summer,a,B7
+CS,3500,2019,Fall,a,E6
+CS,3500,2019,Fall,b,B98
+CS,3500,2019,Fall,c,F72
+CS,3500,2020,Summer,a,C2
+CS,3505,2015,Spring,a,F97
+CS,3505,2015,Fall,a,B51
+CS,3505,2015,Fall,b,E42
+CS,3505,2015,Fall,c,D60
+CS,3505,2015,Fall,d,C40
+CS,3505,2016,Summer,a,D60
+CS,3505,2016,Fall,a,D98
+CS,3505,2016,Fall,b,B48
+CS,3505,2017,Summer,a,F19
+CS,3505,2017,Fall,a,E75
+CS,3505,2017,Fall,b,C20
+CS,3505,2018,Summer,a,B64
+CS,3505,2018,Summer,b,F44
+CS,3505,2018,Fall,a,F83
+CS,3505,2018,Fall,b,D22
+CS,3505,2018,Fall,c,C22
+CS,3505,2019,Spring,a,B70
+CS,3505,2019,Spring,b,A68
+CS,3505,2019,Summer,a,F7
+CS,3505,2019,Summer,b,D18
+CS,3505,2019,Summer,c,B9
+CS,3505,2019,Summer,d,A28
+CS,3505,2019,Fall,a,C8
+CS,3505,2019,Fall,b,F79
+CS,3505,2019,Fall,c,F63
+CS,3505,2020,Spring,a,D2
+CS,3505,2020,Summer,a,E37
+CS,3505,2020,Fall,a,F56
+CS,3505,2020,Fall,b,B14
+CS,3505,2020,Fall,c,E20
+CS,3810,2015,Spring,a,C46
+CS,3810,2016,Summer,a,F29
+CS,3810,2016,Fall,a,A84
+CS,3810,2016,Fall,b,F98
+CS,3810,2018,Spring,a,F22
+CS,3810,2018,Summer,a,F43
+CS,3810,2018,Summer,b,A68
+CS,3810,2018,Summer,c,B28
+CS,3810,2018,Summer,d,F73
+CS,3810,2019,Fall,a,E73
+CS,3810,2019,Fall,b,B41
+CS,3810,2020,Fall,a,D10
+CS,4000,2015,Spring,a,E50
+CS,4000,2015,Spring,b,E43
+CS,4000,2015,Summer,a,F93
+CS,4000,2015,Fall,a,C7
+CS,4000,2016,Fall,a,E77
+CS,4000,2017,Spring,a,A82
+CS,4000,2017,Summer,a,D30
+CS,4000,2017,Fall,a,D24
+CS,4000,2017,Fall,b,F49
+CS,4000,2018,Spring,a,B92
+CS,4000,2019,Spring,a,B95
+CS,4000,2020,Spring,a,D47
+CS,4000,2020,Spring,b,A17
+CS,4000,2020,Fall,a,E53
+CS,4150,2015,Summer,a,E77
+CS,4150,2015,Summer,b,D2
+CS,4150,2016,Summer,a,B74
+CS,4150,2016,Summer,b,F49
+CS,4150,2018,Fall,a,C33
+CS,4150,2018,Fall,b,F81
+CS,4150,2019,Spring,a,D14
+CS,4150,2020,Spring,a,D43
+CS,4150,2020,Fall,a,F77
+CS,4400,2015,Summer,a,B62
+CS,4400,2015,Fall,a,C38
+CS,4400,2015,Fall,b,F63
+CS,4400,2015,Fall,c,B42
+CS,4400,2016,Spring,a,D47
+CS,4400,2016,Summer,a,E70
+CS,4400,2016,Fall,a,A94
+CS,4400,2017,Spring,a,D38
+CS,4400,2017,Spring,b,A53
+CS,4400,2017,Spring,c,B82
+CS,4400,2019,Spring,a,E52
+CS,4400,2019,Spring,b,F54
+CS,4400,2019,Spring,c,C90
+CS,4400,2019,Spring,d,E77
+CS,4400,2019,Summer,a,A14
+CS,4400,2019,Summer,b,F86
+CS,4400,2019,Fall,a,A73
+CS,4400,2019,Fall,b,F83
+CS,4400,2020,Spring,a,D14
+CS,4400,2020,Fall,a,E72
+CS,4400,2020,Fall,b,E29
+CS,4500,2015,Summer,a,E89
+CS,4500,2015,Summer,b,C4
+CS,4500,2016,Spring,a,A15
+CS,4500,2016,Spring,b,F19
+CS,4500,2016,Fall,a,E62
+CS,4500,2017,Summer,a,D41
+CS,4500,2018,Spring,a,A44
+CS,4500,2018,Spring,b,F22
+CS,4500,2018,Spring,c,F32
+CS,4500,2018,Spring,d,E21
+CS,4500,2019,Summer,a,F24
+CS,4500,2019,Fall,a,D4
+CS,4500,2019,Fall,b,B58
+CS,4500,2019,Fall,c,D1
+CS,4500,2019,Fall,d,B36
+CS,4500,2020,Spring,a,A74
+CS,4500,2020,Summer,a,B47
+CS,4940,2015,Summer,a,E82
+CS,4940,2017,Fall,a,C79
+CS,4940,2017,Fall,b,F18
+CS,4940,2019,Fall,a,E50
+CS,4940,2020,Summer,a,F23
+CS,4940,2020,Summer,b,D37
+CS,4970,2016,Fall,a,E65
+CS,4970,2016,Fall,b,D88
+CS,4970,2017,Spring,a,D63
+CS,4970,2017,Summer,a,B38
+CS,4970,2018,Summer,a,E96
+CS,4970,2018,Summer,b,D71
+CS,4970,2018,Summer,c,E15
+CS,4970,2018,Fall,a,C70
+CS,4970,2018,Fall,b,A98
+CS,4970,2018,Fall,c,E28
+CS,4970,2018,Fall,d,A95
+CS,4970,2019,Spring,a,B39
+CS,4970,2019,Spring,b,A58
+CS,4970,2019,Summer,a,A57
+CS,4970,2019,Summer,b,A100
+CS,4970,2019,Summer,c,B95
+CS,4970,2019,Summer,d,C91
+CS,4970,2019,Fall,a,D22
+CS,4970,2019,Fall,b,B27
+CS,4970,2019,Fall,c,E45
+CS,4970,2019,Fall,d,E69
+CS,4970,2020,Summer,a,C38
+CS,4970,2020,Summer,b,E87
+CS,4970,2020,Summer,c,B97
+CS,4970,2020,Summer,d,A36
+CS,4970,2020,Fall,a,B90
+CS,4970,2020,Fall,b,B19
+CS,4970,2020,Fall,c,B98
+CS,4970,2020,Fall,d,D63
+MATH,1210,2015,Summer,a,F54
+MATH,1210,2016,Spring,a,A52
+MATH,1210,2016,Spring,b,C89
+MATH,1210,2016,Spring,c,C59
+MATH,1210,2016,Spring,d,C75
+MATH,1210,2016,Fall,a,F12
+MATH,1210,2016,Fall,b,D82
+MATH,1210,2016,Fall,c,C9
+MATH,1210,2016,Fall,d,D28
+MATH,1210,2017,Spring,a,B64
+MATH,1210,2017,Summer,a,C71
+MATH,1210,2017,Summer,b,E63
+MATH,1210,2017,Summer,c,F98
+MATH,1210,2018,Spring,a,D3
+MATH,1210,2018,Summer,a,D59
+MATH,1210,2018,Fall,a,B89
+MATH,1210,2018,Fall,b,F39
+MATH,1210,2019,Spring,a,C12
+MATH,1210,2019,Spring,b,C11
+MATH,1210,2019,Summer,a,B7
+MATH,1210,2020,Spring,a,B55
+MATH,1210,2020,Spring,b,F13
+MATH,1220,2015,Summer,a,A2
+MATH,1220,2015,Summer,b,A55
+MATH,1220,2015,Summer,c,D10
+MATH,1220,2016,Spring,a,A41
+MATH,1220,2017,Spring,a,B83
+MATH,1220,2017,Spring,b,B9
+MATH,1220,2017,Spring,c,A79
+MATH,1220,2017,Spring,d,D45
+MATH,1220,2017,Summer,a,F96
+MATH,1220,2018,Spring,a,B12
+MATH,1220,2018,Spring,b,B97
+MATH,1220,2018,Summer,a,C55
+MATH,1220,2019,Fall,a,E93
+MATH,1220,2019,Fall,b,F4
+MATH,1220,2019,Fall,c,F39
+MATH,1220,2020,Spring,a,B96
+MATH,1220,2020,Summer,a,B64
+MATH,1250,2015,Spring,a,A68
+MATH,1250,2015,Spring,b,A47
+MATH,1250,2015,Spring,c,B50
+MATH,1250,2015,Spring,d,E54
+MATH,1250,2015,Fall,a,D99
+MATH,1250,2016,Spring,a,A34
+MATH,1250,2016,Summer,a,D65
+MATH,1250,2016,Fall,a,D55
+MATH,1250,2016,Fall,b,A82
+MATH,1250,2016,Fall,c,E20
+MATH,1250,2017,Summer,a,B20
+MATH,1250,2017,Summer,b,D76
+MATH,1250,2017,Summer,c,F88
+MATH,1250,2017,Summer,d,C90
+MATH,1250,2018,Spring,a,B8
+MATH,1250,2018,Summer,a,A59
+MATH,1250,2018,Summer,b,A40
+MATH,1250,2018,Summer,c,F95
+MATH,1250,2020,Summer,a,F34
+MATH,1260,2015,Spring,a,C94
+MATH,1260,2015,Spring,b,A43
+MATH,1260,2015,Spring,c,C68
+MATH,1260,2015,Summer,a,E81
+MATH,1260,2016,Fall,a,C21
+MATH,1260,2017,Summer,a,F15
+MATH,1260,2017,Fall,a,A2
+MATH,1260,2019,Spring,a,A71
+MATH,1260,2019,Spring,b,F95
+MATH,1260,2019,Spring,c,B42
+MATH,1260,2019,Summer,a,C35
+MATH,1260,2019,Summer,b,E48
+MATH,1260,2019,Fall,a,A23
+MATH,1260,2020,Spring,a,A52
+MATH,2210,2015,Spring,a,C12
+MATH,2210,2015,Spring,b,A48
+MATH,2210,2015,Summer,a,C95
+MATH,2210,2015,Summer,b,D48
+MATH,2210,2015,Summer,c,D99
+MATH,2210,2015,Summer,d,F70
+MATH,2210,2015,Fall,a,B20
+MATH,2210,2017,Spring,a,A43
+MATH,2210,2017,Summer,a,F94
+MATH,2210,2018,Spring,a,D63
+MATH,2210,2018,Spring,b,B92
+MATH,2210,2019,Spring,a,D90
+MATH,2210,2019,Spring,b,D96
+MATH,2210,2020,Spring,a,A76
+MATH,2210,2020,Spring,b,D85
+MATH,2210,2020,Spring,c,B38
+MATH,2210,2020,Fall,a,F95
+MATH,2270,2015,Fall,a,B100
+MATH,2270,2015,Fall,b,A20
+MATH,2270,2017,Summer,a,D40
+MATH,2270,2017,Fall,a,A21
+MATH,2270,2017,Fall,b,C91
+MATH,2270,2017,Fall,c,A28
+MATH,2270,2017,Fall,d,C19
+MATH,2270,2019,Spring,a,F39
+MATH,2270,2019,Summer,a,A52
+MATH,2270,2019,Summer,b,E96
+MATH,2270,2019,Summer,c,A60
+MATH,2270,2019,Fall,a,A2
+MATH,2270,2020,Spring,a,B17
+MATH,2270,2020,Fall,a,F11
+MATH,2270,2020,Fall,b,C10
+MATH,2280,2015,Summer,a,D17
+MATH,2280,2015,Fall,a,C16
+MATH,2280,2016,Fall,a,F51
+MATH,2280,2018,Spring,a,C36
+MATH,2280,2018,Fall,a,E32
+MATH,2280,2018,Fall,b,D53
+MATH,2280,2018,Fall,c,D8
+MATH,2280,2019,Fall,a,E32
+MATH,2280,2019,Fall,b,E3
+MATH,2280,2019,Fall,c,F46
+MATH,2280,2020,Spring,a,C73
+MATH,2280,2020,Spring,b,D35
+MATH,3210,2015,Spring,a,C8
+MATH,3210,2015,Spring,b,D68
+MATH,3210,2015,Summer,a,B21
+MATH,3210,2015,Fall,a,C69
+MATH,3210,2015,Fall,b,F8
+MATH,3210,2015,Fall,c,B74
+MATH,3210,2015,Fall,d,D46
+MATH,3210,2016,Spring,a,B23
+MATH,3210,2016,Fall,a,C76
+MATH,3210,2017,Spring,a,E73
+MATH,3210,2017,Summer,a,D70
+MATH,3210,2019,Spring,a,A43
+MATH,3210,2019,Spring,b,B17
+MATH,3210,2019,Fall,a,C8
+MATH,3210,2020,Spring,a,B100
+MATH,3210,2020,Summer,a,C10
+MATH,3210,2020,Fall,a,D76
+MATH,3220,2016,Spring,a,F63
+MATH,3220,2016,Spring,b,B91
+MATH,3220,2016,Spring,c,F79
+MATH,3220,2016,Spring,d,B86
+MATH,3220,2016,Summer,a,B49
+MATH,3220,2016,Fall,a,B23
+MATH,3220,2016,Fall,b,F74
+MATH,3220,2017,Spring,a,E5
+MATH,3220,2017,Fall,a,E29
+MATH,3220,2017,Fall,b,A64
+MATH,3220,2018,Spring,a,B45
+MATH,3220,2018,Spring,b,B82
+MATH,3220,2018,Spring,c,A91
+MATH,3220,2018,Spring,d,F43
+PHYS,2040,2015,Spring,a,B53
+PHYS,2040,2015,Fall,a,A62
+PHYS,2040,2015,Fall,b,E84
+PHYS,2040,2015,Fall,c,B21
+PHYS,2040,2016,Spring,a,A38
+PHYS,2040,2017,Summer,a,B94
+PHYS,2040,2017,Fall,a,A44
+PHYS,2040,2017,Fall,b,E62
+PHYS,2040,2017,Fall,c,D84
+PHYS,2040,2018,Spring,a,B7
+PHYS,2040,2019,Spring,a,F94
+PHYS,2040,2019,Spring,b,F37
+PHYS,2040,2020,Spring,a,D20
+PHYS,2060,2015,Spring,a,F77
+PHYS,2060,2016,Spring,a,A61
+PHYS,2060,2016,Spring,b,C51
+PHYS,2060,2016,Summer,a,C12
+PHYS,2060,2016,Summer,b,D24
+PHYS,2060,2018,Summer,a,E8
+PHYS,2060,2018,Fall,a,A11
+PHYS,2060,2018,Fall,b,E53
+PHYS,2060,2018,Fall,c,E30
+PHYS,2060,2018,Fall,d,D67
+PHYS,2060,2019,Summer,a,D74
+PHYS,2060,2019,Summer,b,D39
+PHYS,2060,2019,Fall,a,F5
+PHYS,2060,2019,Fall,b,E74
+PHYS,2060,2019,Fall,c,E19
+PHYS,2060,2020,Spring,a,B22
+PHYS,2060,2020,Spring,b,B17
+PHYS,2060,2020,Fall,a,B81
+PHYS,2100,2015,Spring,a,C94
+PHYS,2100,2015,Spring,b,A12
+PHYS,2100,2016,Fall,a,F80
+PHYS,2100,2016,Fall,b,D15
+PHYS,2100,2017,Summer,a,A14
+PHYS,2100,2017,Summer,b,A37
+PHYS,2100,2017,Summer,c,C53
+PHYS,2100,2017,Fall,a,E78
+PHYS,2100,2018,Fall,a,F89
+PHYS,2100,2019,Summer,a,F31
+PHYS,2140,2015,Spring,a,C36
+PHYS,2140,2015,Spring,b,F88
+PHYS,2140,2015,Summer,a,B39
+PHYS,2140,2015,Summer,b,D100
+PHYS,2140,2015,Summer,c,C94
+PHYS,2140,2015,Fall,a,B57
+PHYS,2140,2016,Spring,a,F63
+PHYS,2140,2016,Spring,b,C8
+PHYS,2140,2016,Spring,c,B9
+PHYS,2140,2016,Summer,a,B100
+PHYS,2140,2016,Summer,b,E4
+PHYS,2140,2016,Fall,a,B8
+PHYS,2140,2017,Summer,a,F26
+PHYS,2140,2017,Fall,a,E51
+PHYS,2140,2017,Fall,b,A88
+PHYS,2140,2018,Summer,a,B61
+PHYS,2140,2018,Summer,b,C45
+PHYS,2140,2018,Fall,a,F89
+PHYS,2140,2019,Fall,a,B29
+PHYS,2140,2019,Fall,b,F27
+PHYS,2140,2020,Fall,a,F2
+PHYS,2210,2015,Fall,a,B33
+PHYS,2210,2015,Fall,b,C92
+PHYS,2210,2015,Fall,c,F36
+PHYS,2210,2017,Summer,a,E51
+PHYS,2210,2017,Summer,b,A66
+PHYS,2210,2017,Summer,c,C72
+PHYS,2210,2017,Summer,d,E37
+PHYS,2210,2018,Fall,a,F42
+PHYS,2210,2018,Fall,b,C84
+PHYS,2210,2018,Fall,c,F39
+PHYS,2210,2019,Spring,a,B8
+PHYS,2210,2019,Spring,b,E52
+PHYS,2210,2019,Spring,c,F18
+PHYS,2210,2019,Spring,d,F64
+PHYS,2210,2019,Summer,a,C54
+PHYS,2210,2019,Fall,a,E91
+PHYS,2210,2019,Fall,b,B44
+PHYS,2210,2019,Fall,c,B88
+PHYS,2210,2019,Fall,d,D86
+PHYS,2220,2015,Spring,a,E24
+PHYS,2220,2015,Fall,a,F72
+PHYS,2220,2015,Fall,b,B88
+PHYS,2220,2015,Fall,c,F12
+PHYS,2220,2016,Summer,a,D43
+PHYS,2220,2016,Fall,a,D16
+PHYS,2220,2017,Spring,a,E75
+PHYS,2220,2017,Spring,b,A61
+PHYS,2220,2017,Spring,c,E16
+PHYS,2220,2017,Spring,d,D68
+PHYS,2220,2018,Spring,a,B26
+PHYS,2220,2018,Summer,a,D19
+PHYS,2220,2018,Fall,a,A63
+PHYS,2220,2019,Spring,a,C82
+PHYS,2220,2020,Spring,a,E98
+PHYS,2220,2020,Summer,a,A17
+PHYS,2220,2020,Summer,b,F55
+PHYS,2220,2020,Fall,a,D1
+PHYS,3210,2016,Summer,a,B3
+PHYS,3210,2016,Summer,b,F94
+PHYS,3210,2016,Fall,a,C40
+PHYS,3210,2017,Summer,a,B9
+PHYS,3210,2017,Summer,b,C38
+PHYS,3210,2017,Fall,a,E44
+PHYS,3210,2018,Spring,a,B44
+PHYS,3210,2018,Spring,b,D46
+PHYS,3210,2018,Spring,c,B52
+PHYS,3210,2018,Fall,a,B94
+PHYS,3210,2019,Spring,a,A47
+PHYS,3210,2019,Spring,b,A49
+PHYS,3210,2019,Spring,c,C99
+PHYS,3210,2019,Spring,d,A77
+PHYS,3210,2019,Summer,a,F14
+PHYS,3210,2019,Summer,b,A7
+PHYS,3210,2019,Summer,c,D57
+PHYS,3210,2019,Fall,a,D90
+PHYS,3210,2020,Spring,a,F2
+PHYS,3210,2020,Summer,a,F67
+PHYS,3210,2020,Fall,a,B54
+PHYS,3210,2020,Fall,b,A66
+PHYS,3210,2020,Fall,c,A37
+PHYS,3220,2016,Summer,a,B46
+PHYS,3220,2016,Summer,b,C21
+PHYS,3220,2017,Summer,a,C31
+PHYS,3220,2017,Fall,a,A74
+PHYS,3220,2017,Fall,b,B12
+PHYS,3220,2017,Fall,c,A93
+PHYS,3220,2017,Fall,d,C83
+PHYS,3220,2018,Summer,a,C34
+PHYS,3220,2020,Spring,a,C55
+PHYS,3220,2020,Spring,b,A98
+PHYS,3220,2020,Spring,c,A18
+PHYS,3220,2020,Spring,d,B43
diff --git a/tests/data/Student.csv b/tests/data/Student.csv
new file mode 100644
index 000000000..bdcf87846
--- /dev/null
+++ b/tests/data/Student.csv
@@ -0,0 +1,301 @@
+student_id,first_name,last_name,sex,date_of_birth,home_address,home_city,home_state,home_zip,home_phone
+100,Allison,Hill,F,1991-05-09,819 Anthony Fields Suite 083,Jacquelinebury,IN,01352,+1-542-351-1615
+101,Lindsey,Roman,F,1995-05-18,618 Courtney Tunnel Apt. 310,Kendrashire,UT,50324,(525)534-1928x327
+102,William,Bowman,M,2005-01-07,030 Morales Centers Suite 953,Randallside,IL,32826,(969)653-2871x01226
+103,Janice,Carlson,F,1989-07-16,0184 Peterson Green,North Jenniferchester,PA,67043,+1-489-325-2880x9570
+104,Sherry,Decker,F,2004-04-08,117 Spence Mountain,New Staceyville,NJ,28261,001-346-578-7133
+105,Alisha,Spencer,F,1994-03-10,031 Heath Circle,New Jasonland,NH,62454,+1-631-165-6670x106
+106,Rebecca,Rodriguez,F,1987-11-30,24731 Michelle Orchard Apt. 801,Allisonville,GA,53066,(064)746-8723
+107,Tracy,Riley,F,2005-02-24,97882 William Summit Apt. 136,Port Johnstad,MA,77004,(435)346-2475x10799
+108,Mr.,Daniel,M,1995-07-04,2784 Archer Ports Apt. 841,Taylorland,NV,36198,534.874.0164x0052
+109,Deborah,Figueroa,F,1994-05-30,12805 Hernandez Creek,Port Laura,VT,28036,586.923.2260x25634
+110,Meredith,Reyes,F,1997-03-09,75433 James Heights,Rasmussenburgh,MD,70783,001-142-940-1965x569
+111,Stephanie,Lee,F,1997-01-06,8356 Elizabeth Highway,Lake Jennifer,IA,54029,482-366-2994x68044
+112,Rachel,Lawson,F,1990-12-07,872 Campbell Prairie,Clarenceshire,IA,26601,3791769367
+113,Brittany,Watts,F,2003-02-04,632 Dominguez Lodge Suite 172,Contrerasshire,WV,58509,872-774-3487x34714
+114,Gabriella,Orozco,F,1998-11-11,2316 Amy Lakes,West Rebeccastad,TX,75957,(546)688-9373x467
+115,Gabriella,Shelton,F,1997-01-15,2980 Vargas Prairie,South Michelleville,KS,60099,646-417-0805x310
+116,Travis,Gonzalez,M,1996-07-14,19374 Jackson Place,Dannyfort,CO,03866,663.193.1491x905
+117,Mary,Jones,F,2002-05-15,7165 Poole Road,Lake Tammy,SD,71040,(945)314-7379x965
+118,Samuel,White,M,1994-03-13,9480 Lee Forest Apt. 837,Travisfort,HI,91174,957.885.6855
+119,Devin,King,M,1986-05-27,82337 Brittany Skyway,Tinafort,LA,40119,+1-240-084-2710
+120,Julie,Alexander,F,1993-08-06,711 Charles Plaza,East Annaburgh,CT,55049,+1-677-496-4990x913
+121,Deborah,Miller,F,1993-07-27,67974 Keith Gateway Suite 134,Weberfurt,MA,71877,421.024.9947x17464
+122,Johnny,Miller,M,1995-05-20,40139 Smith Spring,Johnstonmouth,MT,58464,(967)175-6551
+123,Gary,Steele,M,1987-09-04,807 Johnny Cove Suite 808,North April,MO,58440,(824)771-0932
+124,Adam,Russell,M,2000-01-14,12748 Perry Manors Apt. 782,Port William,UT,36709,840-449-9727x875
+125,Patricia,Williams,F,1988-06-19,627 Martinez Vista Apt. 171,Stephenchester,NC,20733,(459)615-8657x809
+126,Jade,Thomas,F,2004-07-08,221 Reyes Rapid Apt. 923,East Jonathan,SD,38201,759-464-7436
+127,Ashley,James,F,1997-11-27,064 Michelle Spur,Lozanomouth,VA,30663,(394)210-4709
+128,Carlos,Browning,M,1990-09-16,85884 Scott Stream,Lake Julie,CO,10370,001-368-516-0481
+129,Megan,Chambers,F,2002-09-06,137 Nicole Park Suite 317,Turnerbury,WV,40394,382-675-8692
+130,Matthew,Bass,M,1986-08-24,53773 Garcia Rapids Suite 506,Port Stacy,CA,28302,5329318393
+131,David,Schroeder,M,1998-03-28,22842 Michelle Crescent Apt. 395,East Davidbury,AR,59257,(178)390-8470x0766
+132,John,Browning,M,1989-10-24,1249 Kelley Heights,Schmidtview,CO,92484,+1-836-736-5766x1565
+133,Brittany,Leblanc,F,2002-04-29,15280 Hoffman Highway Apt. 560,Burkeborough,GA,86580,(158)514-9368
+134,Dr.,Louis,M,1993-03-28,402 Kathryn Valleys Apt. 229,Chadmouth,CA,70032,752-545-9910x2290
+135,Denise,Stanley,F,1993-02-08,81561 Erika Meadow,Brandonbury,AL,40008,+1-445-107-6226x838
+136,Michael,Gomez,M,1994-03-14,7159 Richard Port Apt. 605,Port Stevechester,MI,14376,681-645-3521x81883
+137,Hannah,Luna,F,1996-11-30,24329 Katherine Circles Suite 779,Coleside,NY,82358,+1-527-177-4490x5814
+138,Anthony,Decker,M,1997-08-09,998 Betty Villages Suite 079,Marcport,AR,14067,001-182-037-7889x255
+139,George,Harper,M,1988-10-20,18644 Douglas Underpass Suite 519,Sabrinaburgh,NC,17402,652.816.8505
+140,Tiffany,Peterson,F,1998-09-26,214 Garcia Springs,Stephensontown,RI,17677,292-706-5379
+141,Nicole,Cole,F,1990-08-18,735 Hudson Loaf,Stricklandport,DC,26675,+1-075-818-1412x4782
+142,Susan,Velasquez,F,1986-02-05,6853 Christopher Flat Apt. 152,West Mariachester,OH,59300,001-043-289-8614x341
+143,Jennifer,Bauer,F,1988-10-31,980 Andrews Roads,North Michael,FL,88085,(518)888-8067x06540
+144,Austin,Allen,M,2001-06-29,5205 Li Drives,Marshallchester,SD,08771,3030548687
+145,Nicole,Lee,F,2000-05-12,541 Kim Knoll Apt. 652,South Sandra,SC,95801,9284511544
+146,Michelle,Jackson,F,2000-10-29,596 Tina Village,New Michaelfort,WV,19215,1355690927
+147,Jacqueline,Hines,F,2001-04-19,4310 Porter Junctions Suite 447,New Heathershire,CT,10207,(715)518-8442
+148,Timothy,Little,M,1988-06-05,32370 Ashley Loop Suite 291,West Jenniferport,MD,75854,517-785-2892
+149,Carl,Shaw,M,1991-08-28,4225 Perez Village Suite 414,Port Joshuastad,CA,84516,922.995.9001x094
+150,Randall,Butler,M,1996-10-13,4473 Cohen Green,North Scottport,NJ,41471,001-562-588-1537
+151,Jerry,Thomas,M,1994-02-09,632 Peck Roads Apt. 278,Port Tyler,MD,60431,(500)479-7480
+152,Jessica,Khan,F,2004-11-24,6098 Angela Circles Suite 849,Davidshire,SC,44945,001-239-868-0002x578
+153,Jordan,Hicks,M,2005-10-09,0551 Silva Squares Suite 097,New Teresa,HI,07232,(896)230-9130x7562
+154,Christina,Shaw,F,1994-11-30,028 Mark Prairie,Leeville,KY,46938,334.843.4437x5758
+155,Robert,Hill,M,1994-01-22,6524 Stephanie Cliff Suite 473,South Sarahchester,NM,77418,833.016.5712
+156,Krista,Hickman,F,1987-02-26,734 Debbie Union Apt. 938,Melissatown,MA,23541,001-672-400-4991x547
+157,Teresa,Rosales,F,1997-01-28,27420 Gibbs Parks,Thompsonhaven,TN,68039,122-753-0463
+158,Debra,Rivera,F,1998-08-19,53017 Richard Mills Suite 414,East Susan,MN,79896,878-339-1878x51910
+159,Stephanie,Harris,F,2001-08-26,713 Burns Turnpike,North David,NV,73743,406.403.9106x51801
+160,John,Mitchell,M,1986-09-10,656 Sally Isle Apt. 825,Port Phillipland,TN,99614,001-786-863-3752x431
+161,Timothy,Small,M,2005-07-09,7903 Morales Ford,Port Brianport,SD,96382,953.428.3644
+162,Jamie,Webster,F,1998-10-02,27086 Grant Crest Apt. 351,Booneton,FL,35688,901.398.3735x40331
+163,Paul,Rocha,M,1987-06-23,3854 Amanda Island Apt. 877,Port Terrancefort,LA,54755,320.489.9642x353
+164,Sandra,Porter,F,1993-10-17,77725 Jennifer Meadow Suite 808,Lake Sierrafurt,MA,83168,2038750997
+165,Alexis,Patel,F,2003-10-31,840 Wolfe Lane,Whiteside,ID,81736,546.156.7933
+166,Jonathan,Hamilton,M,1986-06-14,180 Rachel Rest Suite 401,Juanmouth,FL,41721,001-926-142-9396x856
+167,William,Brown,M,1988-06-02,9965 Joshua Well Apt. 586,New Donna,NM,32803,262-655-1104
+168,Philip,Garcia,M,2004-12-15,8610 Angela Pine,Shieldstown,RI,95507,001-398-262-2444x721
+169,Desiree,Evans,F,2000-07-27,799 Daniel Grove,Cookstad,KS,44375,+1-924-593-7526x5479
+170,Erika,Ramirez,F,1999-11-03,398 Katrina Burg,Sherryville,TN,09565,243.426.6179x79688
+171,Sergio,Barnes,M,1989-07-10,891 John Prairie Apt. 909,Byrdbury,WI,56921,4388899375
+172,Patricia,Chapman,F,2001-04-24,14611 Cross Inlet,Lake Adriana,CA,95134,401.051.2382
+173,Gary,Simmons,M,1992-04-12,2660 Ware Locks Apt. 033,New Laura,SC,70872,371-478-5969x6915
+174,Jimmy,Thompson,M,1991-10-25,912 John Cove Apt. 286,North Patrick,NY,91390,(742)257-9050x72368
+175,Jon,Cohen,M,2004-05-12,1903 Joshua Mountains Apt. 797,Danielland,SD,48586,+1-078-361-3407x4517
+176,Autumn,Cain,F,2003-06-04,962 Glover Stravenue Suite 958,South Mario,IN,35542,001-126-042-2325x367
+177,Mark,Brooks,M,1999-06-14,684 Wiley Locks Apt. 901,Stephenfurt,AR,70549,(637)454-5892
+178,Karina,Cooper,F,1989-02-04,70127 Victoria Lane,Blankenshiphaven,UT,36417,415.206.4361x10371
+179,Courtney,Frazier,F,2005-01-31,627 Patrick Row Apt. 554,Lake Karenland,DE,70035,2753269731
+180,Charles,Martinez,M,2003-07-15,2341 Carolyn Roads,Port Anthony,UT,27429,364.037.6137x9180
+181,Timothy,Anderson,M,2000-05-01,710 Smith Field,Frybury,OK,54952,+1-188-924-1418
+182,William,Moore,M,1990-08-03,146 Mathis Center Apt. 617,Brianfurt,DC,02161,+1-275-884-2524
+183,Bruce,Yoder,M,1989-11-04,4917 Michael Mill,Michaelberg,NH,95237,(800)030-7562
+184,Toni,Johnson,F,1996-06-28,3536 Flores Stream Suite 180,Lake Tinashire,MN,37503,870-534-9493x759
+185,Dr.,Patty,F,1989-01-31,60385 Steele Branch Apt. 641,Port Robertshire,DE,37178,3865719182
+186,James,Vargas,M,1996-05-29,44565 Joseph Circles Apt. 912,South Leeland,RI,59734,(112)490-3521x356
+187,Amy,Norman,F,1987-05-16,1994 Jones Wells,New Lisaton,SD,16560,001-029-667-0662x532
+188,Sophia,Johnson,F,1998-02-20,68701 Derrick Extensions,Foxstad,SC,50635,(759)856-4205x930
+189,Whitney,Robinson,F,2002-08-10,2239 Joanna Island Suite 599,Port Maryfort,NE,23511,0393087059
+190,Teresa,Foster,F,1995-12-10,26752 Hoffman Tunnel,Michaelfurt,ME,96707,096-902-9593
+191,Brian,Crawford,M,2000-01-03,5215 Joseph Forges,East Danieltown,OR,22303,(658)617-9327x1040
+192,Trevor,Jones,M,1992-05-20,815 Austin Manors,Port Frederickhaven,CO,27442,884-443-1069x87205
+193,Brandon,Colon,M,1998-06-27,32417 Parker Keys,New Christopher,FL,50497,(047)743-4902
+194,Michael,Miller,M,2005-05-13,938 Paul Mount Suite 793,North Raven,MO,68241,921.722.3320x61632
+195,Lisa,Mills,F,1987-03-12,99119 Floyd Track,Humphreyburgh,NH,62504,(629)960-6530
+196,Thomas,Prince,M,2003-06-14,47132 Julia Springs Apt. 691,East Madisonmouth,UT,07868,+1-148-628-9023x303
+197,Anthony,Ward,M,1988-12-29,6103 Brooke Drives,Matthewsborough,VT,98668,602.933.3346
+198,Sharon,Coffey,F,2001-10-19,29034 Hahn Road,Joshuaside,MN,29102,896.910.8589
+199,Edwin,Rodriguez,M,1999-09-08,4443 Kathy Turnpike Suite 965,Jenniferfurt,IL,55363,099-353-8758x4282
+200,John,Figueroa,M,1988-05-05,513 Julie Groves Suite 554,Stevenland,NY,76563,(381)684-6022x356
+201,Stephanie,Hatfield,F,2000-07-12,52500 Jason Springs,Ericmouth,CT,57348,760-083-5058x30033
+202,Gregory,Anderson,M,1990-05-20,04478 Morgan Tunnel Suite 575,Martinside,AL,29903,(098)215-0648
+203,Linda,Williams,F,2003-04-29,16761 Wells Dale Suite 046,Elaineburgh,CT,14252,+1-141-173-9348
+204,Mr.,Jason,M,1995-12-29,753 Emily Union Suite 721,Joneschester,NY,60368,012.045.5611
+205,Stefanie,Smith,F,1991-05-06,79415 White Knoll Suite 467,Banksfort,OH,08187,979-729-6590
+206,Sheryl,Acosta,F,1997-06-06,6701 Leon River,Katrinamouth,WI,88298,(916)375-6289x0028
+207,Samuel,Booth,M,2002-11-04,40838 Powell Ford,Lake Shane,MI,16060,001-016-608-8019
+208,Miss,Stefanie,F,1998-01-01,0375 Harvey Mall,Jenniferland,HI,45243,+1-488-510-2726x1493
+209,Tara,Long,F,2005-10-29,160 Monroe Path Suite 779,Taylorport,AZ,57230,(829)221-6995x8669
+210,Stacey,Hunt,F,2000-02-15,83339 Parks Valleys Apt. 288,Marcusland,MS,75295,846.081.0620x03424
+211,Brianna,Brown,F,1987-07-09,5719 Stevenson Trace,Annaberg,SC,38202,001-665-800-4397x359
+212,Craig,Hardy,M,1991-03-10,122 Wilson Camp,East Eugene,AL,61623,5909479851
+213,Evan,Robinson,M,1986-03-21,6886 Jeffrey Field,West Jeffery,NE,74076,573-993-0561
+214,Carol,Huber,F,1997-03-16,36138 Johns Run,Lake Charles,AK,94462,1024819346
+215,Mark,Hamilton,M,2004-01-26,9190 Jones Via Apt. 491,Port Patrick,AK,20990,(684)245-0882
+216,Aaron,Carlson,M,1988-03-18,53682 Jeffrey Street Apt. 290,Randolphshire,NV,38597,397.552.3149
+217,Cheryl,Tucker,F,1998-02-15,299 Leslie Lane Apt. 336,West Erin,MS,58874,+1-781-291-4283x411
+218,Sarah,Welch,F,1998-04-20,308 Patricia Mountains Suite 256,Lake Jessicaburgh,MT,52508,(392)827-2299x2750
+219,Katherine,Brown,F,1991-11-01,56770 Deborah Course,Schultzburgh,NH,75233,659-184-6386x5577
+220,Adriana,Macias,F,1993-02-01,4322 Carolyn Stravenue,Robertborough,ND,63287,603.029.9228x092
+221,Roberto,Valentine,M,1990-06-02,7236 Norton Stravenue Apt. 842,Matthewview,HI,51024,388-629-1279
+222,Sherry,Schmidt,F,2005-07-09,9806 Wood Camp,Jeromefort,ME,77708,247-314-9864
+223,Michelle,Clarke,F,1992-11-06,35651 Denise Fork,Hendersonborough,ND,99456,872-588-7449x56213
+224,Melissa,Martin,F,1988-08-22,8902 Cynthia Squares,Ruizstad,IL,49107,669.849.0277x0384
+225,Richard,Dixon,M,2005-10-02,530 Miller Gardens Apt. 669,North Janeside,OR,73785,439-376-9042x681
+226,Kathy,Morgan,F,1993-09-28,89476 Carrillo Shores Suite 779,Olsonberg,SC,29386,+1-658-804-3416x5182
+227,Hayden,Shannon,M,1987-05-11,373 John Fort Apt. 395,North Samanthafurt,NM,71473,+1-595-794-7284x6392
+228,Jay,Ayers,M,1994-11-11,271 Stevens Rest,East Biancaborough,IL,72402,(795)527-6365
+229,Jennifer,Hayes,F,1996-02-16,143 Chase Extensions Suite 270,South Wendyhaven,OK,64283,906.120.3471
+230,Felicia,Ward,F,2001-09-12,06159 Barbara Ports Apt. 455,Tonychester,ME,38056,225.699.6112x5355
+231,Michael,Jacobs,M,2003-10-01,598 Gutierrez Estates Apt. 341,West Codyside,AZ,52538,+1-114-921-6433x472
+232,Ryan,Johnson,M,1988-12-19,77848 Tara Ridge Apt. 979,New Amanda,MS,30271,(564)240-0825x478
+233,Thomas,Arroyo,M,1994-11-13,4930 Lopez Trail,East Jennifer,TN,29414,3894484631
+234,Dylan,Walsh,M,1993-04-23,3502 Amanda Estates,East Jenniferchester,DE,65195,475-705-1204x618
+235,Corey,Skinner,M,2003-08-24,36730 Jill Corner Suite 376,Larryborough,AZ,72535,743-503-1365
+236,Rebecca,Richards,F,1987-12-15,979 Kelli Forge,New Matthew,PA,08372,281-273-5857x306
+237,Brandy,Roach,F,1994-11-17,73928 Jessica Garden,Rochamouth,DE,39255,(708)620-9593x51863
+238,Kathleen,Arnold,F,2003-10-23,1181 Sharon Estate,North Jamestown,ME,64714,940.539.1037x1705
+239,Teresa,Perry,F,1992-01-03,480 Davenport Cliff Apt. 811,Amandaville,ID,82463,(861)957-6122x86852
+240,Krista,Garner,F,1995-04-23,004 Holmes Well,West Jeffrey,AK,90903,001-889-921-0752x245
+241,Danielle,Scott,F,2000-02-03,3157 Margaret Rest Suite 194,Lake Patrickmouth,KY,57426,001-139-060-4805x892
+242,Connie,Williams,F,2000-09-13,9981 Keith Key,North Ashleytown,CA,66275,+1-227-837-6938x983
+243,Deborah,Jordan,F,1988-11-02,66553 Brittney Brooks Apt. 597,Scottside,ND,20947,039-240-5147
+244,Evelyn,Singh,F,1986-03-15,879 Thomas Ridges Apt. 980,North James,IL,61444,4510463681
+245,Kari,Harper,F,2002-12-22,800 Alyssa Hill,East Michael,NM,31460,046.084.3256
+246,Jessica,Edwards,F,1988-03-23,29832 Janet Mount,Port Theresaland,VA,42115,(125)205-6647x42312
+247,Pamela,Salazar,F,1995-02-06,33051 Woods Mills Suite 526,North James,PA,02468,001-333-127-9757x366
+248,Roger,Cortez,M,1992-05-18,8808 Stephen Trail Suite 388,Lake Angela,NY,06962,644.726.4908
+249,Julie,Lucas,F,1989-01-08,98266 Angel Locks Suite 371,New Rebecca,OK,16694,751-868-9268
+250,Patricia,Barr,F,2002-09-16,22064 Kayla Lock Suite 123,Lake Alexanderport,SD,80190,(977)671-9903
+251,Donald,Fuller,M,2005-05-23,05020 Massey Greens,Williamsbury,ND,80597,+1-279-501-4556x168
+252,John,Martinez,M,2000-06-13,3390 Jessica Plaza,Webbchester,WY,38143,548.995.2997x8772
+253,Crystal,Roberts,F,1996-02-19,1396 Matthew Park,Alexville,SC,40841,(501)556-9902x3557
+254,Rebecca,Brewer,F,1988-03-04,857 Gutierrez Shoal Suite 495,Andrewmouth,VA,46847,001-405-682-9962x914
+255,Brandon,Wiley,M,2003-06-25,84215 Strickland Unions Apt. 078,West Timothyhaven,KS,13379,230.768.1040x91570
+256,Pamela,Reese,F,2004-08-11,3533 Amanda Springs Suite 422,North Cindy,GA,46417,249.321.4958
+257,Carlos,Ruiz,M,2001-10-06,66299 Vaughn Lock,West James,SD,10796,171.747.7332x945
+258,Michael,Ortega,M,1996-03-13,0171 Steven Drive Suite 992,Richardchester,NV,09797,(696)393-8276x15396
+259,Jessica,Cobb,F,1998-10-24,1971 Ford Oval,Thompsonshire,CO,78673,013-290-2278x469
+260,Christina,Maldonado,F,1989-08-26,465 Aguilar Plain Suite 240,South Brian,SD,47587,+1-036-965-6666x8327
+261,Janice,Middleton,F,2001-06-08,220 Alfred Roads,South Veronica,NY,55008,001-969-278-6876x532
+262,Adam,Jimenez,M,1988-12-05,89500 Bush Courts Apt. 128,Terrellmouth,AR,80464,189.490.5807
+263,Taylor,Berry,M,1995-11-05,442 Sandra Shoals,Anneton,DC,07266,+1-904-712-8144x2944
+264,Adrian,Rodriguez,M,2000-11-23,75243 Lauren Throughway Apt. 129,Mooreport,RI,31689,001-239-504-1027
+265,Eric,Reese,M,1995-03-12,6742 Graham Glen Suite 658,Blakeside,WV,57096,414-967-3938x525
+266,Michael,Decker,M,1990-01-01,75344 Andrew Common,Douglasfort,NY,93309,926-921-2447
+267,Robin,Thompson,F,1985-12-12,62712 Reynolds Plains Apt. 741,North Jessicamouth,MO,86073,001-642-569-0877x661
+268,Janice,Norris,F,1992-10-30,5546 Wendy Port,Lake Matthew,PA,38506,(063)461-5717
+269,Charles,Lee,M,2001-07-07,1847 Flowers Locks Suite 050,Lake Richard,NC,69067,001-829-310-2707x903
+270,Mark,Conway,M,1990-01-11,9111 Lauren Fields,Simmonsfort,ND,42999,001-982-530-9251x142
+271,Ann,Pearson,F,1996-03-02,723 Joseph Locks,East Heatherstad,NM,12038,083-318-1958x837
+272,Mary,Hill,F,1991-11-27,772 Sandra Causeway Apt. 364,Lake Katherine,OR,70933,078-113-7995
+273,Nicole,Villanueva,F,1992-07-11,36363 Brenda Causeway,East Chelsea,ME,60497,435.209.0421x7762
+274,Daniel,Phillips,M,2000-09-10,298 Miller Terrace Apt. 397,Ramirezchester,ID,43400,929.060.0780x686
+275,Rebecca,Nicholson,F,2001-09-12,0632 John Wells,New Evanview,NH,60117,+1-625-701-6580x464
+276,Logan,Johnston,M,1994-01-14,5085 Rodriguez Islands Suite 552,Janetmouth,DE,44400,(793)355-4864x01557
+277,Kelsey,Martinez,F,1990-12-14,4795 Dougherty Station Suite 137,West Haroldshire,DC,15184,(380)468-2756x7043
+278,John,Wade,M,1991-11-20,9242 Perez Islands Apt. 025,Port Christine,NE,24392,+1-223-105-9274x5238
+279,Mary,Spence,F,1995-12-23,841 Sullivan Mill,South Luketown,WI,43922,(492)975-1702x814
+280,Lisa,Robinson,F,1996-09-24,3983 Wang Extensions,Lake Ericashire,MD,64787,805.626.5650x4554
+281,Shannon,Miller,M,1998-09-15,426 Perry Street Suite 234,Port Valerie,WV,99606,646-287-9232
+282,Donna,Henry,F,1992-01-09,7873 Aaron Fort,Flowersview,VT,55178,(301)471-9597x9647
+283,Dr.,Jacqueline,F,2003-05-28,2572 Brian Island,Stephanietown,NY,10570,(219)285-5445
+284,Lauren,Morrow,F,1989-11-19,7652 Eric Fields Apt. 898,Marquezchester,MA,10514,+1-075-452-7985x2401
+285,Shannon,Thomas,F,1996-03-07,16110 Todd Camp,Lake Williamton,ID,09184,119.393.2501x24955
+286,Kathryn,Chandler,F,1992-01-27,90833 Jackson Shore Apt. 138,Wellschester,ND,14568,+1-663-836-1517x1827
+287,Michele,Hawkins,F,1992-01-08,47947 Richard Way,Lake Patricia,WA,48662,7167811266
+288,William,Figueroa,M,1999-07-16,3539 Powell Ford,South Kathy,NJ,99631,967-842-7114x773
+289,Chad,Garcia,M,2002-11-10,269 Hernandez Plains,North Karenmouth,GA,87282,(485)880-0616x7567
+290,Andrew,Hawkins,M,1991-03-28,762 Paul Skyway,Tracymouth,MN,74196,(647)969-5450x0902
+291,Hannah,Harmon,F,1987-03-11,1655 Brian Forest Apt. 491,Jonesburgh,AK,43245,(698)640-7905x696
+292,Brent,Freeman,M,1996-01-14,5294 Ryan Mews,Cobbfort,IN,06731,001-639-191-9541x987
+293,Angela,Colon,F,1993-03-01,5366 Zachary Ramp,Nicolestad,FL,65932,748.969.0835x72324
+294,Alexis,Robles,M,1986-08-06,603 Derek Forks,Hopkinsville,WI,64181,1594165162
+295,Laura,Mason,F,1994-07-28,8471 David Station Apt. 963,Robinsonland,IN,54027,+1-078-515-8673x4257
+296,Alex,Rasmussen,M,1996-02-27,0348 Danielle Ridges Suite 183,Priceside,WI,33994,343-275-6041
+297,Todd,Ruiz,M,1999-07-21,124 Bell Pines Suite 570,Davidsonville,NY,00904,(459)112-3829
+298,Ricky,Flores,M,1992-08-31,95431 Hunter Trail Suite 930,Leblancfurt,VA,61111,206.969.4215
+299,Keith,Smith,M,1992-01-21,713 Lee Throughway Suite 476,Lake Carolshire,ND,55332,204-439-7359x71072
+300,William,Sanders,M,1987-06-20,9411 Williams Viaduct,West Catherine,SC,93505,8964652809
+301,Christopher,Vasquez,M,1994-11-23,86241 Tiffany Mill,Campbellborough,VA,35001,(625)728-7032x0320
+302,Carla,Mcdonald,F,2005-11-05,7587 Daniel Roads Apt. 513,Whiteville,IL,87419,(089)261-3715
+303,Melanie,Becker,F,2005-04-14,520 Mariah Prairie Apt. 490,North Cindy,WV,96749,045-018-9616
+304,David,Wise,M,2003-05-13,66421 Laurie Rue,Mckeestad,CA,48664,(767)499-6165
+305,Jessica,Simmons,F,1994-05-19,3278 Warren Glens,Port Tim,CT,39876,(490)810-8186x61794
+306,Lauren,Mack,F,1994-09-28,2601 Janet Harbor Suite 794,Port Lisa,AR,79675,+1-168-006-1027x7697
+307,Valerie,Ward,F,1988-11-06,4122 Daniel Bridge Suite 037,Debraview,SC,25524,727.601.2277
+308,Scott,Richards,M,2002-07-09,050 Melanie Light Apt. 799,Yolandatown,MT,95477,(080)695-8146
+309,Audrey,Dean,F,1995-11-26,2437 Jesse Fields,Morganstad,NC,17692,001-665-729-3417
+310,Christina,Obrien,F,1997-05-30,433 Kidd Island,New Gregg,MO,08845,931-837-4550x84289
+311,Michael,House,M,1991-04-06,119 Garrison Corners,Williamville,GA,47901,001-787-125-5213
+312,Jennifer,Mack,F,1998-03-25,8214 Kari Island Suite 286,Taylorview,VT,68154,001-720-811-5562x606
+313,Margaret,Orr,F,1992-11-24,846 Erin Oval Apt. 550,Mcculloughstad,MD,84895,001-997-563-4108x562
+314,Kimberly,Lewis,F,2003-03-10,2008 Allen Springs,Valerieland,ME,82681,017-490-7539x989
+315,Elizabeth,Estrada,F,1999-08-16,68315 Lee Spur Apt. 266,North Pamelaport,LA,69478,864.976.7762x282
+316,Judith,Faulkner,F,1995-12-03,770 Raymond Islands Suite 961,New Billyland,WY,40249,(229)604-4327x0185
+317,Amanda,Olson,F,1999-11-09,6792 Wagner Lodge,South Michelle,SC,87598,658-074-1209x4818
+318,Tina,Weaver,F,1997-06-27,7801 Schmidt Vista Apt. 339,Lake Catherine,AZ,03550,608-564-1118x24224
+319,Christian,Farley,M,2005-11-10,200 Corey Crossroad,Scottside,AZ,31908,(886)140-5786
+320,Sarah,Mason,F,2002-04-29,2386 Peters Camp,Woodwardstad,DC,08388,465.398.4028
+321,Elizabeth,Foster,F,1996-11-11,4639 Pham Trail,Reidshire,IL,87306,795-020-9700x268
+322,Michele,Farmer,F,2001-01-17,1807 Gomez Station Suite 562,Cainshire,LA,25796,0453194337
+323,Mr.,Johnathan,M,1988-02-18,614 Snyder Oval,Arielfurt,AR,17310,938-430-8948
+324,Aaron,Simmons,M,2005-05-17,566 Erin Lodge Apt. 030,West Shane,FL,11223,+1-361-332-5411x0760
+325,Mark,Cook,M,1998-10-05,50583 Parsons Plains,Garrettmouth,AR,04871,120.704.9611
+326,Kristin,Phillips,F,2003-07-08,399 Patrick Square,Harveyborough,RI,60017,311-091-9392x845
+327,Nathaniel,Wallace,M,2003-03-05,49685 Nicole Springs Apt. 495,Port Zachary,DE,31615,+1-806-533-3153x7795
+328,Kylie,Rogers,F,1992-03-09,07303 Owens Ferry,Lake Lisa,ME,52970,+1-050-150-8124x7395
+329,Allen,Gonzalez,M,1998-08-03,583 Andrew Streets Suite 026,Nicoleborough,MN,48950,896.112.2338x65596
+330,David,Williams,M,2003-03-30,530 Ramirez Creek Suite 973,Kristenfort,DC,51372,872-558-7774x9690
+331,Stephanie,Hayes,F,2000-06-01,6925 Christopher Shore,South Jerry,MT,44590,(665)754-6027x341
+332,Bradley,Kirby,M,2004-05-25,311 Benjamin Fall Apt. 544,Kaylahaven,NJ,18571,001-044-566-9078x263
+333,Paul,Wells,M,1986-04-01,751 Jacob Springs Suite 377,Johnsonland,IA,97206,(553)666-8459x0902
+334,Troy,Rivera,M,1988-04-13,6636 Paul Mall Apt. 741,New Gregoryfort,AK,26584,001-643-348-1705x802
+335,Michelle,Wells,F,2001-06-11,8743 Douglas Centers Apt. 385,Suarezview,OR,38238,469-263-2967x629
+336,Michael,Williams,M,2003-01-30,841 Bowen Field,Port Angela,AR,14292,+1-567-243-8070x176
+337,Jennifer,Lee,F,1989-05-04,257 Carlos Orchard,Port Donaldfort,DC,02868,(186)210-4275
+338,Michelle,Stafford,F,1986-11-14,81647 Adam Springs,Mcfarlandbury,CA,55771,001-531-312-2068x155
+339,Taylor,Foster,F,1996-03-06,52065 Jason Fields,Joshuastad,VT,54384,+1-718-924-1956x252
+340,Stephen,Stewart,M,2000-07-01,9976 Harmon Mills,Alexandertown,CT,31485,001-910-257-4326
+341,Amanda,Mclean,F,1993-06-27,524 Kristin Bypass Suite 640,Lake Matthewville,VA,33051,685.270.1713x0232
+342,Christina,Coleman,F,1986-08-05,3471 Ward Isle,West Chelsea,DE,63677,+1-614-982-8246x747
+343,Kristina,Castillo,F,1999-01-05,30085 Sara Views Suite 567,Port Charles,WY,16816,001-236-458-7506x633
+344,Robert,Mccoy,M,1992-05-05,4972 Carrie Villages Suite 011,Sabrinabury,VT,68466,+1-264-488-6946x1195
+345,Daniel,Goodman,M,2005-03-19,70116 Pena Row,West Janeville,WV,59570,+1-230-234-6791x2141
+346,Destiny,Peterson,F,1994-12-18,100 Stephanie Prairie,Williamsberg,ME,68668,001-759-655-5535x669
+347,Shane,Drake,M,1999-12-23,209 Alyssa Village,Wrightview,UT,67991,050.505.7397x69156
+348,Todd,Alvarez,M,2001-02-07,64932 Walter Spurs Suite 027,Turnerfurt,UT,22528,001-783-332-1160x256
+349,Greg,Kent,M,1988-01-10,8633 Kelly Courts Apt. 931,Davidburgh,OR,41238,366.552.8993x160
+350,Nicole,Sweeney,F,1993-07-30,81497 Lewis Glens,Brownfort,OK,96531,+1-027-642-0865
+351,John,Bailey,M,2005-07-22,438 David Shore,Lindahaven,MN,21956,742-333-0591
+352,Kara,Landry,F,1986-04-25,6263 John Meadow Suite 261,Hancockfurt,NC,48646,117-830-9997
+353,Nichole,Bauer,F,2003-12-15,6492 Bryan Union,Lopezfort,NV,70810,(898)131-2920x8751
+354,Kenneth,Delgado,M,2004-02-03,118 Tammy Drive,Barrettberg,WV,38957,(975)859-8831x030
+355,Jennifer,Pierce,F,1998-10-24,71462 Jones Row Suite 359,Loristad,DE,57337,9314181861
+356,Brandon,Blankenship,M,1989-03-03,401 Tanya Isle,Port Gregorychester,SD,64676,(948)491-0256x25889
+357,Jennifer,Vargas,F,1995-04-21,226 Adams Valley Suite 539,South Scott,MN,38095,001-834-146-5111x312
+358,Patrick,Spencer,M,1997-08-29,682 Zachary Wells Suite 160,Rhondamouth,OH,98761,890.972.8321
+359,Casey,Gomez,M,1987-02-15,15381 Timothy Fort,New Phillipside,WV,68072,001-970-509-7545x105
+360,Adam,Jordan,M,1991-06-05,617 Kayla Forges Apt. 545,East Lisa,MI,58088,605-313-4026
+361,Erin,Johnson,F,1993-12-19,416 Tyler Rapid Apt. 686,Port Lauraland,AL,90211,5690674471
+362,Danielle,Hernandez,F,1990-12-24,436 Jasmine Station,Wayneville,NJ,83663,(260)432-6093
+363,Anthony,Russell,M,1995-08-17,56708 Brett Court Apt. 563,North Blake,OR,28285,(916)247-5541x108
+364,Carlos,Ward,M,1988-06-19,9534 Patrick Tunnel Apt. 910,Rhondafurt,OH,13429,001-954-738-2023x684
+365,James,Lawson,M,1994-01-09,9087 Le Forks,Phillipsburgh,HI,70436,242.403.3810
+366,Mackenzie,Compton,F,1989-07-16,426 Phillips Way Suite 053,Joshuaberg,NC,76950,001-649-837-3543
+367,Robert,Mullins,M,1996-06-21,527 Hunter Estates,Lopezport,NC,03259,(269)312-1637
+368,Tracy,Garcia,F,1989-07-15,916 Daniel Bridge Suite 023,Adamsside,SC,01732,(513)279-7245x72308
+369,Mark,Martinez,M,2002-08-27,86203 Ronald Curve,Jeremiahhaven,VT,15234,(131)451-9515
+370,Thomas,Huang,M,1988-07-08,9262 Mcdaniel Plaza,Port Joseph,LA,35287,+1-225-267-7119x642
+371,Wendy,White,F,1988-10-06,6952 Valdez Forge,South Amanda,SD,50914,689.313.5030x587
+372,Tammie,Brown,F,1998-07-26,247 Melissa Walk Suite 333,North Suzannechester,AK,56168,1917920252
+373,Angela,Carroll,F,1986-04-16,28476 Wallace Port,North Brianfurt,DC,21518,678-498-4362x4186
+374,Beth,Lewis,F,1995-02-07,891 Mcdonald Harbor,Margaretville,NY,26024,159-503-4281
+375,Linda,Avila,F,1999-03-18,0341 Cunningham Park Suite 005,West Tinamouth,MO,41719,001-215-681-8209
+376,John,Melton,M,2003-09-22,113 Aguirre Ports,Martinshire,OR,85880,001-572-545-9606x339
+377,Brittany,Burton,F,1990-09-12,48171 Geoffrey Green Apt. 955,East Kelseyberg,IL,58440,001-970-546-6927x589
+378,Michael,Hunter,M,2001-11-10,903 Castro Dale Apt. 629,North Paul,CA,61564,711.216.6365x15597
+379,Natalie,Wilson,F,1988-10-06,235 Huerta Springs Apt. 567,East Andrewmouth,ID,23583,461-476-8342
+380,Anna,Valenzuela,F,1996-12-07,56778 Martin Ridge Apt. 960,Patriciaville,NH,19456,502.727.5164x80727
+381,Kenneth,Johnson,M,2003-01-01,296 Jason Extension,Stephaniebury,IA,40735,+1-177-665-5868x5127
+382,Christopher,Larson,M,2004-06-14,649 Bullock Corners,Lake Christophertown,CO,98797,789-046-3378
+383,Christina,Harrison,F,2003-07-30,660 Casey Mission Apt. 446,Adamside,AK,49575,+1-955-296-3863x9609
+384,Todd,Myers,M,1989-02-03,26312 Welch Spurs,Burtonberg,WV,27208,609-209-8196
+385,Morgan,Lucero,F,1990-02-03,34383 Roman Isle Apt. 041,Burtonfurt,CO,60679,442-117-5361
+386,Joanne,Martin,F,1993-04-12,9015 Webb Plains Suite 284,Leetown,MT,20469,+1-130-523-1244x7315
+387,John,Lamb,M,1996-10-06,423 Clay Gateway Apt. 994,East Jenniferview,NJ,36109,966.395.5172x0849
+388,Charlene,Sanchez,F,1989-06-03,51050 Lewis Parks,East Carl,GA,29004,919.665.5330x770
+389,Jennifer,Martinez,F,2001-11-27,4090 Mitchell Streets,Port Samantha,NY,09604,644-556-1857
+390,Jennifer,Horton,F,1987-09-15,159 Jeffrey Stream Apt. 563,East Rachelbury,WY,90710,010.414.5964
+391,Tammy,Silva,F,1988-09-26,96718 Lane Prairie,Morrischester,IL,39329,331-170-3037x637
+392,Daniel,Garza,M,2005-07-23,472 Garcia Crescent Suite 679,Kimberlyville,DC,40759,271.130.7240x78754
+393,Krista,Gomez,F,2002-09-18,5074 Brandon Junction,Leeville,IN,80120,(103)131-0094x3181
+394,Sonya,Lyons,F,1994-01-14,47323 Keith Pine,Clintonport,MS,40520,(122)572-0765
+395,William,Ibarra,M,2001-04-27,57907 Kennedy Canyon Apt. 438,Karimouth,SC,44498,(584)745-7054x5897
+396,Michael,Chandler,M,2001-03-16,257 Becky Ridge Apt. 313,Grayland,NM,71924,001-824-556-9644x309
+397,Barbara,Pope,F,1990-02-13,1072 Edward Vista Suite 247,Lake Alexis,IN,78236,4065004254
+398,Jonathan,Mullen,M,1991-10-25,236 Miller Fields Apt. 536,Port Corey,IA,41229,592.342.6834x414
+399,Lori,Gardner,F,1996-03-17,2875 Jennings Island Apt. 766,Port Anthony,CA,18927,+1-985-298-9406x260
diff --git a/tests/data/StudentMajor.csv b/tests/data/StudentMajor.csv
new file mode 100644
index 000000000..644a46492
--- /dev/null
+++ b/tests/data/StudentMajor.csv
@@ -0,0 +1,227 @@
+student_id,dept,declare_date
+100,BIOL,2010-01-10
+102,CS,2019-01-13
+103,PHYS,2018-10-04
+104,CS,2010-11-04
+105,CS,2018-11-20
+107,MATH,2020-01-04
+108,PHYS,2012-09-26
+111,MATH,2001-04-19
+112,MATH,2000-07-12
+113,PHYS,2000-01-02
+114,MATH,2004-06-01
+115,BIOL,2006-11-19
+116,CS,2002-04-14
+117,PHYS,2002-08-13
+118,CS,2015-12-29
+120,MATH,2015-03-18
+121,BIOL,2010-01-05
+122,MATH,2006-11-17
+123,PHYS,2007-01-19
+124,MATH,2002-08-03
+125,CS,2004-12-02
+126,PHYS,2012-01-26
+127,CS,2013-04-17
+128,MATH,2001-03-10
+129,BIOL,2001-02-08
+130,CS,2019-10-27
+131,MATH,2007-07-10
+132,PHYS,2002-11-23
+134,CS,2000-04-10
+135,MATH,2001-06-24
+136,MATH,2014-01-09
+137,CS,2011-09-26
+139,CS,2019-08-21
+141,BIOL,2020-06-24
+142,CS,2000-01-02
+143,PHYS,2004-12-03
+144,CS,2009-12-05
+147,CS,2002-08-30
+148,PHYS,2014-04-18
+150,BIOL,2011-11-07
+151,PHYS,2003-07-14
+153,PHYS,2020-09-08
+156,PHYS,2018-07-10
+159,PHYS,2017-12-07
+160,MATH,2005-10-18
+161,MATH,2005-08-29
+162,MATH,2007-08-04
+163,BIOL,2015-09-17
+164,CS,2013-11-20
+165,CS,2008-09-25
+166,BIOL,2006-09-03
+167,MATH,2005-11-05
+168,PHYS,2004-07-07
+169,PHYS,2013-10-08
+171,PHYS,2016-12-25
+172,MATH,2005-07-17
+174,PHYS,2001-12-04
+175,CS,2018-10-22
+176,MATH,1999-10-29
+177,BIOL,2020-05-28
+178,PHYS,2002-04-10
+181,BIOL,2005-12-04
+182,PHYS,2000-02-18
+183,PHYS,2003-10-13
+184,MATH,1999-03-07
+185,CS,2011-03-27
+187,PHYS,2012-11-18
+188,PHYS,2018-05-03
+189,BIOL,2017-08-06
+191,MATH,2001-06-13
+194,CS,2010-08-05
+195,BIOL,2005-04-21
+196,CS,2020-11-07
+197,BIOL,2016-12-20
+198,CS,2015-11-19
+200,CS,2005-06-20
+203,BIOL,2006-01-22
+204,MATH,2018-05-29
+205,PHYS,2015-02-13
+206,CS,2016-01-16
+207,CS,2010-12-24
+210,BIOL,2011-02-17
+211,PHYS,2020-01-17
+212,BIOL,2018-01-04
+213,MATH,2003-09-10
+215,BIOL,2001-04-14
+216,MATH,2013-12-07
+217,PHYS,2013-07-18
+218,PHYS,2020-04-13
+219,MATH,2011-10-19
+220,PHYS,2001-05-30
+221,MATH,2018-05-14
+223,BIOL,2001-08-29
+224,PHYS,2003-04-30
+225,PHYS,2016-08-07
+226,PHYS,2009-02-23
+228,CS,2002-06-08
+230,MATH,2003-01-05
+231,MATH,2015-12-20
+232,CS,2006-11-05
+233,PHYS,2000-10-01
+234,CS,2019-06-20
+235,PHYS,2017-05-23
+236,BIOL,2010-04-05
+237,CS,1999-10-08
+238,CS,2006-08-16
+239,MATH,2008-11-11
+240,MATH,2007-07-22
+241,MATH,2012-04-14
+242,PHYS,2011-03-06
+243,MATH,2001-04-24
+244,CS,2004-05-15
+245,CS,2008-10-19
+246,PHYS,2001-07-18
+248,CS,2017-03-08
+249,MATH,2018-07-30
+250,BIOL,2007-03-19
+251,CS,2016-08-13
+252,BIOL,2019-10-19
+253,CS,2016-01-06
+254,PHYS,2009-08-16
+255,BIOL,2012-08-01
+256,PHYS,2020-01-19
+257,MATH,2000-12-04
+258,BIOL,2017-07-29
+259,PHYS,2002-10-09
+260,BIOL,2018-10-30
+261,BIOL,2015-01-10
+262,BIOL,2007-12-14
+263,MATH,2000-01-08
+264,CS,2000-02-06
+265,PHYS,2010-07-03
+267,PHYS,2013-05-04
+268,PHYS,2007-11-17
+269,PHYS,2005-10-27
+270,BIOL,2010-05-20
+272,CS,2001-01-08
+273,MATH,2003-09-28
+274,CS,2005-12-13
+275,BIOL,2017-08-12
+276,PHYS,2010-03-20
+277,PHYS,2001-02-13
+278,CS,2007-01-07
+279,MATH,2015-10-17
+280,PHYS,2001-06-25
+282,CS,2018-03-09
+283,CS,2019-10-03
+285,BIOL,2000-03-15
+286,MATH,2010-10-08
+287,MATH,2001-05-29
+288,PHYS,2013-02-28
+290,PHYS,2019-05-09
+292,MATH,2019-11-03
+293,BIOL,2001-09-28
+295,MATH,2017-10-05
+296,CS,2015-04-16
+299,PHYS,2003-05-28
+301,PHYS,2008-03-15
+302,MATH,2000-06-02
+304,MATH,2002-07-17
+305,PHYS,2000-03-18
+307,BIOL,2015-11-24
+308,MATH,2016-04-09
+311,BIOL,2006-08-31
+312,PHYS,2010-12-01
+313,CS,2013-09-06
+314,PHYS,2015-04-02
+315,BIOL,2009-04-28
+318,PHYS,2006-10-01
+319,CS,1999-09-24
+320,MATH,2000-11-18
+321,PHYS,1999-11-24
+322,BIOL,2005-09-03
+323,BIOL,2017-03-05
+324,CS,2019-09-10
+325,MATH,2011-11-28
+326,MATH,1999-08-13
+328,CS,2017-10-19
+329,CS,2015-05-29
+332,PHYS,2000-10-09
+334,MATH,2012-03-04
+336,PHYS,2011-11-02
+337,MATH,2003-04-06
+338,PHYS,2013-08-15
+340,CS,2013-07-10
+342,PHYS,2017-09-12
+343,PHYS,2003-09-09
+344,PHYS,2002-12-07
+345,CS,2013-11-25
+346,BIOL,2003-01-06
+348,PHYS,2019-12-13
+349,PHYS,2011-07-06
+350,CS,2010-12-20
+351,CS,2005-08-03
+352,MATH,2010-09-04
+353,PHYS,2013-11-07
+357,BIOL,2000-12-20
+358,CS,2007-02-07
+360,BIOL,2006-11-23
+362,BIOL,2002-02-17
+364,BIOL,2019-01-11
+365,BIOL,1999-05-05
+366,MATH,2006-09-23
+367,CS,2013-01-20
+368,CS,2017-03-30
+369,BIOL,2018-04-30
+370,PHYS,2000-07-22
+371,CS,1999-07-05
+372,CS,2007-07-03
+373,MATH,2000-12-07
+376,CS,2001-08-10
+378,MATH,2000-12-05
+379,PHYS,2003-04-24
+382,PHYS,2013-12-03
+383,PHYS,2005-02-22
+385,MATH,2008-08-12
+386,PHYS,2000-06-27
+390,CS,2009-09-08
+391,MATH,2010-11-24
+392,CS,2019-07-01
+393,CS,2007-04-24
+394,BIOL,2008-12-12
+395,PHYS,2003-06-01
+396,MATH,2019-08-16
+398,MATH,2012-07-14
+399,CS,2015-04-16
diff --git a/tests/data/Term.csv b/tests/data/Term.csv
new file mode 100644
index 000000000..91c3400ae
--- /dev/null
+++ b/tests/data/Term.csv
@@ -0,0 +1,19 @@
+term_year,term
+2015,Spring
+2015,Summer
+2015,Fall
+2016,Spring
+2016,Summer
+2016,Fall
+2017,Spring
+2017,Summer
+2017,Fall
+2018,Spring
+2018,Summer
+2018,Fall
+2019,Spring
+2019,Summer
+2019,Fall
+2020,Spring
+2020,Summer
+2020,Fall
diff --git a/tests/schema.py b/tests/schema.py
index 18ae09fda..1fd187637 100644
--- a/tests/schema.py
+++ b/tests/schema.py
@@ -146,7 +146,7 @@ def make(self, key):
         populate with random data
         """
         from datetime import date, timedelta
-        users = User().fetch()['username']
+        users = [None, None] + list(User().fetch()['username'])
         random.seed('Amazing Seed')
         self.insert(
             dict(key,
@@ -155,7 +155,6 @@ def make(self, key):
                  username=random.choice(users))
             for experiment_id in range(self.fake_experiments_per_subject))
 
-
 @schema
 class Trial(dj.Imported):
     definition = """   # a trial within an experiment
@@ -174,9 +173,7 @@ class Condition(dj.Part):
         """
 
     def make(self, key):
-        """
-        populate with random data (pretend reading from raw files)
-        """
+        """ populate with random data (pretend reading from raw files) """
         random.seed('Amazing Seed')
         trial = self.Condition()
         for trial_id in range(10):
@@ -322,6 +319,7 @@ class IndexRich(dj.Manual):
     index (first_date, value)
     """
 
+
 #  Schema for issue 656
 @schema
 class ThingA(dj.Manual):
@@ -348,3 +346,36 @@ class ThingC(dj.Manual):
     -> [unique, nullable] ThingB
     """
 
+
+@schema
+class Parent(dj.Lookup):
+    definition = """
+    parent_id: int
+    ---
+    name: varchar(30)
+    """
+    contents = [(1, 'Joe')]
+
+
+@schema
+class Child(dj.Lookup):
+    definition = """
+    -> Parent
+    child_id: int
+    ---
+    name: varchar(30)
+    """
+    contents = [(1, 12, 'Dan')]
+
+# Related to issue #886 (8), #883 (5)
+@schema
+class ComplexParent(dj.Lookup):
+    definition = '\n'.join(['parent_id_{}: int'.format(i+1) for i in range(8)])
+    contents = [tuple(i for i in range(8))]
+
+
+@schema
+class ComplexChild(dj.Lookup):
+    definition = '\n'.join(['-> ComplexParent'] + ['child_id_{}: int'.format(i+1)
+                                                   for i in range(1)])
+    contents = [tuple(i for i in range(9))]
diff --git a/tests/schema_external.py b/tests/schema_external.py
index 8f77e1ce0..7db44bc49 100644
--- a/tests/schema_external.py
+++ b/tests/schema_external.py
@@ -9,7 +9,6 @@
 import numpy as np
 
 schema = dj.Schema(PREFIX + '_extern', connection=dj.conn(**CONN_INFO))
-dj.config['enable_python_native_blobs'] = True
 
 
 stores_config = {
@@ -134,4 +133,4 @@ class FilepathS3(dj.Manual):
     img : filepath@repo_s3  # managed files 
     """
 
-dj.errors._switch_filepath_types(False)
\ No newline at end of file
+dj.errors._switch_filepath_types(False)
diff --git a/tests/schema_university.py b/tests/schema_university.py
new file mode 100644
index 000000000..bb2675f8d
--- /dev/null
+++ b/tests/schema_university.py
@@ -0,0 +1,111 @@
+import datajoint as dj
+
+schema = dj.Schema()
+
+
+@schema
+class Student(dj.Manual):
+    definition = """
+    student_id : int unsigned   # university-wide ID number
+    ---
+    first_name      : varchar(40)
+    last_name       : varchar(40)
+    sex             : enum('F', 'M', 'U')
+    date_of_birth   : date
+    home_address    : varchar(120) # mailing street address
+    home_city       : varchar(60)  # mailing address
+    home_state      : char(2)      # US state acronym: e.g. OH
+    home_zip        : char(10)     # zipcode e.g. 93979-4979
+    home_phone      : varchar(20)  # e.g. 414.657.6883x0881
+    """
+
+
+@schema
+class Department(dj.Manual):
+    definition = """
+    dept : varchar(6)   # abbreviated department name, e.g. BIOL
+    ---
+    dept_name    : varchar(200)  # full department name
+    dept_address : varchar(200)  # mailing address
+    dept_phone   : varchar(20)
+    """
+
+
+@schema
+class StudentMajor(dj.Manual):
+    definition = """
+    -> Student
+    ---
+    -> Department
+    declare_date :  date  # when student declared her major
+    """
+
+
+@schema
+class Course(dj.Manual):
+    definition = """
+    -> Department
+    course  : int unsigned   # course number, e.g. 1010
+    ---
+    course_name :  varchar(200)  # e.g. "Neurobiology of Sensation and Movement."
+    credits     :  decimal(3,1)  # number of credits earned by completing the course
+    """
+
+
+@schema
+class Term(dj.Manual):
+    definition = """
+    term_year : year
+    term      : enum('Spring', 'Summer', 'Fall')
+    """
+
+
+@schema
+class Section(dj.Manual):
+    definition = """
+    -> Course
+    -> Term
+    section : char(1)
+    ---
+    auditorium   :  varchar(12)
+    """
+
+
+@schema
+class CurrentTerm(dj.Manual):
+    definition = """
+    omega=0 : tinyint
+    ---
+    -> Term
+    """
+
+
+@schema
+class Enroll(dj.Manual):
+    definition = """
+    -> Student
+    -> Section
+    """
+
+
+@schema
+class LetterGrade(dj.Lookup):
+    definition = """
+    grade : char(2)
+    ---
+    points : decimal(3,2)
+    """
+    contents = [
+        ['A', 4.00], ['A-', 3.67],
+        ['B+', 3.33], ['B', 3.00], ['B-', 2.67],
+        ['C+', 2.33], ['C', 2.00], ['C-', 1.67],
+        ['D+', 1.33], ['D', 1.00], ['F', 0.00]]
+
+
+@schema
+class Grade(dj.Manual):
+    definition = """
+    -> Enroll
+    ---
+    -> LetterGrade
+    """
\ No newline at end of file
diff --git a/tests/test_adapted_attributes.py b/tests/test_adapted_attributes.py
index dfaa370de..17acc897d 100644
--- a/tests/test_adapted_attributes.py
+++ b/tests/test_adapted_attributes.py
@@ -3,12 +3,11 @@
 from itertools import zip_longest
 from nose.tools import assert_true, assert_equal, assert_dict_equal
 from . import schema_adapted as adapted
-from .schema_adapted import graph, layout_to_filepath
+from .schema_adapted import graph
 
 
-def test_adapted_type():
+def test_adapted_type(c=adapted.Connectivity()):
     dj.errors._switch_adapted_types(True)
-    c = adapted.Connectivity()
     graphs = [nx.lollipop_graph(4, 2), nx.star_graph(5), nx.barbell_graph(3, 1), nx.cycle_graph(5)]
     c.insert((i, g) for i, g in enumerate(graphs))
     returned_graphs = c.fetch('conn_graph', order_by='connid')
@@ -83,6 +82,3 @@ def test_adapted_virtual():
             assert_true(0 == len(nx.symmetric_difference(g1, g2).edges))
     c.delete()
     dj.errors._switch_adapted_types(False)
-
-
-
diff --git a/tests/test_aggr_regressions.py b/tests/test_aggr_regressions.py
new file mode 100644
index 000000000..1cadfdf34
--- /dev/null
+++ b/tests/test_aggr_regressions.py
@@ -0,0 +1,105 @@
+"""
+Regression tests for issues 386, 449, 484, and 558 — all related to processing complex aggregations and projections.
+"""
+
+import itertools
+from nose.tools import assert_equal, raises
+import datajoint as dj
+from . import PREFIX, CONN_INFO
+
+schema = dj.Schema(PREFIX + '_aggr_regress', connection=dj.conn(**CONN_INFO))
+
+# --------------- ISSUE 386 -------------------
+# Issue 386 resulted from the loss of aggregated attributes when the aggregation was used as the restrictor
+#     Q & (R.aggr(S, n='count(*)') & 'n=2')
+#     Error: Unknown column 'n' in HAVING
+
+
+@schema
+class R(dj.Lookup):
+    definition = """
+    r : char(1)
+    """
+    contents = zip('ABCDFGHIJKLMNOPQRST')
+
+
+@schema
+class Q(dj.Lookup):
+    definition = """
+    -> R
+    """
+    contents = zip('ABCDFGH')
+
+
+@schema
+class S(dj.Lookup):
+    definition = """
+    -> R
+    s : int
+    """
+    contents = itertools.product('ABCDF', range(10))
+
+
+def test_issue386():
+    result = R.aggr(S, n='count(*)') & 'n=10'
+    result = Q & result
+    result.fetch()
+
+# ---------------- ISSUE 449 ------------------
+# Issue 449 arises from incorrect group by attributes after joining with a dj.U()
+
+
+def test_issue449():
+    result = dj.U('n') * R.aggr(S, n='max(s)')
+    result.fetch()
+
+
+# ---------------- ISSUE 484 -----------------
+# Issue 484
+def test_issue484():
+    q = dj.U().aggr(S, n='max(s)')
+    n = q.fetch('n')
+    n = q.fetch1('n')
+    q = dj.U().aggr(S, n='avg(s)')
+    result = dj.U().aggr(q, m='max(n)')
+    result.fetch()
+
+# ---------------  ISSUE 558 ------------------
+#  Issue 558 resulted from the fact that DataJoint saves subqueries and often combines a restriction followed
+#  by a projection into a single SELECT statement, which in several unusual cases produces unexpected results.
+
+
+@schema
+class A(dj.Lookup):
+    definition = """
+    id: int
+    """
+    contents = zip(range(10))
+
+
+@schema
+class B(dj.Lookup):
+    definition = """
+    -> A
+    id2: int
+    """
+    contents = zip(range(5), range(5, 10))
+
+
+@schema
+class X(dj.Lookup):
+    definition = """
+    id: int
+    """
+    contents = zip(range(10))
+
+
+def test_issue558_part1():
+    q = (A-B).proj(id2='3')
+    assert_equal(len(A - B), len(q))
+
+
+def test_issue558_part2():
+    d = dict(id=3, id2=5)
+    assert_equal(len(X & d), len((X & d).proj(id2='3')))
+
diff --git a/tests/test_alter.py b/tests/test_alter.py
index b188bba0d..7a40ee822 100644
--- a/tests/test_alter.py
+++ b/tests/test_alter.py
@@ -1,5 +1,4 @@
 from nose.tools import assert_equal, assert_not_equal
-
 from .schema import *
 
 
@@ -30,14 +29,13 @@ class Experiment(dj.Imported):
 
 
 def test_alter():
-
     original = schema.connection.query("SHOW CREATE TABLE " + Experiment.full_table_name).fetchone()[1]
     Experiment.definition = Experiment.definition1
     Experiment.alter(prompt=False)
     altered = schema.connection.query("SHOW CREATE TABLE " + Experiment.full_table_name).fetchone()[1]
+    assert_not_equal(original, altered)
     Experiment.definition = Experiment.original_definition
     Experiment().alter(prompt=False)
     restored = schema.connection.query("SHOW CREATE TABLE " + Experiment.full_table_name).fetchone()[1]
+    assert_not_equal(altered, restored)
     assert_equal(original, restored)
-    assert_not_equal(original, altered)
-
diff --git a/tests/test_attach.py b/tests/test_attach.py
index db2f2f1c7..ebe866f0c 100644
--- a/tests/test_attach.py
+++ b/tests/test_attach.py
@@ -1,5 +1,4 @@
 from nose.tools import assert_true, assert_equal, assert_not_equal
-from numpy.testing import assert_array_equal
 import tempfile
 from pathlib import Path
 import os
diff --git a/tests/test_autopopulate.py b/tests/test_autopopulate.py
index d3a382b8c..c9cb7f97d 100644
--- a/tests/test_autopopulate.py
+++ b/tests/test_autopopulate.py
@@ -34,7 +34,7 @@ def test_populate(self):
 
         # test restricted populate
         assert_false(self.trial, 'table already filled?')
-        restriction = dict(subject_id=self.subject.proj().fetch()['subject_id'][0])
+        restriction = self.subject.proj(animal='subject_id').fetch('KEY')[0]
         d = self.trial.connection.dependencies
         d.load()
         self.trial.populate(restriction)
diff --git a/tests/test_blob_migrate.py b/tests/test_blob_migrate.py
index 088dad913..54b0d75d0 100644
--- a/tests/test_blob_migrate.py
+++ b/tests/test_blob_migrate.py
@@ -1,5 +1,4 @@
-from nose.tools import assert_true, assert_false, assert_equal, \
-                        assert_list_equal, raises
+from nose.tools import assert_equal, raises
 
 import datajoint as dj
 import os
@@ -7,7 +6,6 @@
 from . import S3_CONN_INFO, S3_MIGRATE_BUCKET
 from . import CONN_INFO
 from datajoint.migrate import _migrate_dj011_blob
-dj.config['enable_python_native_blobs'] = True
 
 
 class TestBlobMigrate:
diff --git a/tests/test_cascading_delete.py b/tests/test_cascading_delete.py
index 03aeeb2e2..6988d432a 100644
--- a/tests/test_cascading_delete.py
+++ b/tests/test_cascading_delete.py
@@ -1,6 +1,7 @@
-from nose.tools import assert_false, assert_true
+from nose.tools import assert_false, assert_true, assert_equal
 import datajoint as dj
 from .schema_simple import A, B, D, E, L
+from .schema import ComplexChild, ComplexParent
 
 
 class TestDelete:
@@ -54,14 +55,12 @@ def test_delete_tree_restricted():
                      (E() & rel) or
                      (E.F() & rel),
                      'incomplete delete')
-        assert_true(
-            len(A()) == rest['A'] and
-            len(B()) == rest['B'] and
-            len(B.C()) == rest['C'] and
-            len(D()) == rest['D'] and
-            len(E()) == rest['E'] and
-            len(E.F()) == rest['F'],
-            'incorrect restricted delete')
+        assert_equal(len(A()), rest['A'], 'invalid delete restriction')
+        assert_equal(len(B()), rest['B'], 'invalid delete restriction')
+        assert_equal(len(B.C()), rest['C'], 'invalid delete restriction')
+        assert_equal(len(D()), rest['D'], 'invalid delete restriction')
+        assert_equal(len(E()), rest['E'], 'invalid delete restriction')
+        assert_equal(len(E.F()), rest['F'], 'invalid delete restriction')
 
     @staticmethod
     def test_delete_lookup():
@@ -80,3 +79,20 @@ def test_delete_lookup_restricted():
         deleted_count = len(rel)
         rel.delete()
         assert_true(len(L()) == original_count - deleted_count)
+
+    @staticmethod
+    def test_delete_complex_keys():
+        # https://github.com/datajoint/datajoint-python/issues/883
+        # https://github.com/datajoint/datajoint-python/issues/886
+        assert_false(dj.config['safemode'], 'safemode must be off for testing')
+        parent_key_count = 8
+        child_key_count = 1
+        restriction = dict({'parent_id_{}'.format(i+1): i
+                            for i in range(parent_key_count)},
+                           **{'child_id_{}'.format(i+1): (i + parent_key_count)
+                              for i in range(child_key_count)})
+        assert len(ComplexParent & restriction) == 1, 'Parent record missing'
+        assert len(ComplexChild & restriction) == 1, 'Child record missing'
+        (ComplexParent & restriction).delete()
+        assert len(ComplexParent & restriction) == 0, 'Parent record was not deleted'
+        assert len(ComplexChild & restriction) == 0, 'Child record was not deleted'
diff --git a/tests/test_external_class.py b/tests/test_external_class.py
index f8826fe42..e2018dd5b 100644
--- a/tests/test_external_class.py
+++ b/tests/test_external_class.py
@@ -1,4 +1,4 @@
-from nose.tools import assert_true, assert_list_equal, raises
+from nose.tools import assert_true, assert_list_equal
 from numpy.testing import assert_almost_equal
 import datajoint as dj
 from . import schema_external as modu
@@ -46,8 +46,10 @@ def test_populate():
         assert_list_equal(list(img.shape), list(dimensions))
         assert_almost_equal(img, -neg)
     image.delete()
+    dj.errors._switch_filepath_types(True)
     for external_table in image.external.values():
         external_table.delete(display_progress=False, delete_external_files=True)
+    dj.errors._switch_filepath_types(False)
 
 
 
diff --git a/tests/test_fetch.py b/tests/test_fetch.py
index c72c3c896..cb1ba3a4f 100644
--- a/tests/test_fetch.py
+++ b/tests/test_fetch.py
@@ -7,7 +7,7 @@
 import warnings
 from . import schema
 import datajoint as dj
-dj.config['enable_python_native_blobs'] = True
+import os
 
 
 class TestFetch:
@@ -188,7 +188,7 @@ def test_limit_warning(self):
 
     def test_len(self):
         """Tests __len__"""
-        assert_true(len(self.lang.fetch()) == len(self.lang), '__len__ is not behaving properly')
+        assert_equal(len(self.lang.fetch()), len(self.lang), '__len__ is not behaving properly')
 
     @raises(dj.DataJointError)
     def test_fetch1_step2(self):
@@ -223,24 +223,67 @@ def test_nullable_numbers(self):
 
     def test_fetch_format(self):
         """test fetch_format='frame'"""
-        dj.config['fetch_format'] = 'frame'
-        # test if lists are both dicts
-        list1 = sorted(self.subject.proj().fetch(as_dict=True), key=itemgetter('subject_id'))
-        list2 = sorted(self.subject.fetch(dj.key), key=itemgetter('subject_id'))
-        for l1, l2 in zip(list1, list2):
-            assert_dict_equal(l1, l2, 'Primary key is not returned correctly')
-
-        # tests if pandas dataframe
-        tmp = self.subject.fetch(order_by='subject_id')
-        assert_true(isinstance(tmp, pandas.DataFrame))
-        tmp = tmp.to_records()
-
-        subject_notes, key, real_id = self.subject.fetch('subject_notes', dj.key, 'real_id')
-
-        np.testing.assert_array_equal(sorted(subject_notes), sorted(tmp['subject_notes']))
-        np.testing.assert_array_equal(sorted(real_id), sorted(tmp['real_id']))
-        list1 = sorted(key, key=itemgetter('subject_id'))
-        for l1, l2 in zip(list1, list2):
-            assert_dict_equal(l1, l2, 'Primary key is not returned correctly')
-        # revert configuration of fetch format
-        dj.config['fetch_format'] = 'array'
+        with dj.config(fetch_format='frame'):
+            # test if lists are both dicts
+            list1 = sorted(self.subject.proj().fetch(as_dict=True), key=itemgetter('subject_id'))
+            list2 = sorted(self.subject.fetch(dj.key), key=itemgetter('subject_id'))
+            for l1, l2 in zip(list1, list2):
+                assert_dict_equal(l1, l2, 'Primary key is not returned correctly')
+
+            # tests if pandas dataframe
+            tmp = self.subject.fetch(order_by='subject_id')
+            assert_true(isinstance(tmp, pandas.DataFrame))
+            tmp = tmp.to_records()
+
+            subject_notes, key, real_id = self.subject.fetch('subject_notes', dj.key, 'real_id')
+
+            np.testing.assert_array_equal(sorted(subject_notes), sorted(tmp['subject_notes']))
+            np.testing.assert_array_equal(sorted(real_id), sorted(tmp['real_id']))
+            list1 = sorted(key, key=itemgetter('subject_id'))
+            for l1, l2 in zip(list1, list2):
+                assert_dict_equal(l1, l2, 'Primary key is not returned correctly')
+
+    def test_key_fetch1(self):
+        """test KEY fetch1 - issue #976"""
+        with dj.config(fetch_format="array"):
+            k1 = (self.subject & 'subject_id=10').fetch1('KEY')
+        with dj.config(fetch_format="frame"):
+            k2 = (self.subject & 'subject_id=10').fetch1('KEY')
+        assert_equal(k1, k2)
+
+    def test_same_secondary_attribute(self):
+        children = (schema.Child * schema.Parent().proj()).fetch()['name']
+        assert len(children) == 1
+        assert children[0] == 'Dan'
+
+    def test_query_caching(self):
+        # initialize cache directory
+        os.mkdir(os.path.expanduser('~/dj_query_cache'))
+
+        with dj.config(query_cache=os.path.expanduser('~/dj_query_cache')):
+            conn = schema.TTest3.connection
+            # insert sample data and load cache
+            schema.TTest3.insert([dict(key=100+i, value=200+i) for i in range(2)])
+            conn.set_query_cache(query_cache='main')
+            cached_res = schema.TTest3().fetch()
+            # attempt to insert while caching enabled
+            try:
+                schema.TTest3.insert([dict(key=200+i, value=400+i) for i in range(2)])
+                assert False, 'Insert allowed while query caching enabled'
+            except dj.DataJointError:
+                conn.set_query_cache()
+            # insert new data
+            schema.TTest3.insert([dict(key=600+i, value=800+i) for i in range(2)])
+            # re-enable cache to access old results
+            conn.set_query_cache(query_cache='main')
+            previous_cache = schema.TTest3().fetch()
+            # verify properly cached and how to refresh results
+            assert all([c == p for c, p in zip(cached_res, previous_cache)])
+            conn.set_query_cache()
+            uncached_res = schema.TTest3().fetch()
+            assert len(uncached_res) > len(cached_res)
+            # purge query cache
+            conn.purge_query_cache()
+
+        # reset cache directory state (will fail if purge was unsuccessful)
+        os.rmdir(os.path.expanduser('~/dj_query_cache'))
diff --git a/tests/test_fetch_same.py b/tests/test_fetch_same.py
index 737426c9a..976095b84 100644
--- a/tests/test_fetch_same.py
+++ b/tests/test_fetch_same.py
@@ -18,14 +18,13 @@ class ProjData(dj.Manual):
     """
 
 
-with dj.config(enable_python_native_blobs=True):
-    ProjData().insert([
-        {'id': 0, 'resp': 20.33, 'sim': 45.324, 'big': 3, 'blah': 'yes'},
-        {'id': 1, 'resp': 94.3, 'sim': 34.23,
-            'big': {'key1': np.random.randn(20, 10)}, 'blah': 'si'},
-        {'id': 2, 'resp': 1.90, 'sim': 10.23,
-            'big': np.random.randn(4, 2), 'blah': 'sim'}
-    ])
+ProjData().insert([
+    {'id': 0, 'resp': 20.33, 'sim': 45.324, 'big': 3, 'blah': 'yes'},
+    {'id': 1, 'resp': 94.3, 'sim': 34.23,
+        'big': {'key1': np.random.randn(20, 10)}, 'blah': 'si'},
+    {'id': 2, 'resp': 1.90, 'sim': 10.23,
+        'big': np.random.randn(4, 2), 'blah': 'sim'}
+])
 
 
 class TestFetchSame:
diff --git a/tests/test_filepath.py b/tests/test_filepath.py
index b54c82547..e01004827 100644
--- a/tests/test_filepath.py
+++ b/tests/test_filepath.py
@@ -13,6 +13,7 @@ def setUp(self):
 
 def test_path_match(store="repo"):
     """ test file path matches and empty file"""
+    dj.errors._switch_filepath_types(True)
     ext = schema.external[store]
     stage_path = dj.config['stores'][store]['stage']
 
@@ -25,11 +26,11 @@ def test_path_match(store="repo"):
     # put the file
     uuid = ext.upload_filepath(str(managed_file))
 
-    #remove
+    # remove
     managed_file.unlink()
     assert_false(managed_file.exists())
 
-    #check filepath
+    # check filepath
     assert_equal(
         (ext & {'hash': uuid}).fetch1('filepath'),
         str(managed_file.relative_to(stage_path).as_posix()))
@@ -41,10 +42,13 @@ def test_path_match(store="repo"):
 
     # cleanup
     ext.delete(delete_external_files=True)
+    dj.errors._switch_filepath_types(False)
 
 
 def test_filepath(store="repo"):
     """ test file management """
+    dj.errors._switch_filepath_types(True)
+
     ext = schema.external[store]
     stage_path = dj.config['stores'][store]['stage']
     filename = 'picture.dat'
@@ -82,6 +86,8 @@ def test_filepath(store="repo"):
     ext.delete(delete_external_files=True)
     assert_false(ext.exists(ext._make_external_filepath(str(Path(relpath, filename)))))
 
+    dj.errors._switch_filepath_types(False)
+
 
 def test_filepath_s3():
     """ test file management with s3 """
diff --git a/tests/test_jobs.py b/tests/test_jobs.py
index 58e47da66..5990c3019 100644
--- a/tests/test_jobs.py
+++ b/tests/test_jobs.py
@@ -4,7 +4,6 @@
 import random
 import string
 import datajoint as dj
-dj.config['enable_python_native_blobs'] = True
 
 subjects = schema.Subject()
 
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
new file mode 100644
index 000000000..5fb7d46bc
--- /dev/null
+++ b/tests/test_plugin.py
@@ -0,0 +1,51 @@
+import datajoint.errors as djerr
+import datajoint.plugin as p
+import pkg_resources
+from os import path
+
+
+def test_check_pubkey():
+    base_name = 'datajoint'
+    base_meta = pkg_resources.get_distribution(base_name)
+    pubkey_meta = base_meta.get_metadata('{}.pub'.format(base_name))
+
+    with open(path.join(path.abspath(
+            path.dirname(__file__)), '..', 'datajoint.pub'), "r") as f:
+        assert(f.read() == pubkey_meta)
+
+
+def test_normal_djerror():
+    try:
+        raise djerr.DataJointError
+    except djerr.DataJointError as e:
+        assert(e.__cause__ is None)
+
+
+def test_verified_djerror(category='connection'):
+    try:
+        curr_plugins = getattr(p, '{}_plugins'.format(category))
+        setattr(p, '{}_plugins'.format(category), dict(test_plugin_id=dict(
+                                                        verified=True, object='example')))
+        raise djerr.DataJointError
+    except djerr.DataJointError as e:
+        setattr(p, '{}_plugins'.format(category), curr_plugins)
+        assert(e.__cause__ is None)
+
+
+def test_verified_djerror_type():
+    test_verified_djerror(category='type')
+
+
+def test_unverified_djerror(category='connection'):
+    try:
+        curr_plugins = getattr(p, '{}_plugins'.format(category))
+        setattr(p, '{}_plugins'.format(category), dict(test_plugin_id=dict(
+                                                        verified=False, object='example')))
+        raise djerr.DataJointError("hello")
+    except djerr.DataJointError as e:
+        setattr(p, '{}_plugins'.format(category), curr_plugins)
+        assert(isinstance(e.__cause__, djerr.PluginWarning))
+
+
+def test_unverified_djerror_type():
+    test_unverified_djerror(category='type')
diff --git a/tests/test_privileges.py b/tests/test_privileges.py
index 85c899be4..2e483aefa 100644
--- a/tests/test_privileges.py
+++ b/tests/test_privileges.py
@@ -1,6 +1,5 @@
 from nose.tools import assert_true, raises
 import datajoint as dj
-from os import environ
 from . import schema, CONN_INFO
 
 namespace = locals()
@@ -12,7 +11,7 @@ class TestUnprivileged:
     def setup_class(cls):
         """A connection with only SELECT privilege to djtest schemas"""
         cls.connection = dj.conn(host=CONN_INFO['host'], user='djview', password='djview',
-                                    reset=True)
+                                 reset=True)
 
     @raises(dj.DataJointError)
     def test_fail_create_schema(self):
diff --git a/tests/test_reconnection.py b/tests/test_reconnection.py
index b77a67151..4ddef39f7 100644
--- a/tests/test_reconnection.py
+++ b/tests/test_reconnection.py
@@ -2,7 +2,7 @@
 Collection of test cases to test connection module.
 """
 
-from nose.tools import assert_true, assert_false, assert_equal, raises
+from nose.tools import assert_true, assert_false, raises
 import datajoint as dj
 from datajoint import DataJointError
 from . import CONN_INFO
diff --git a/tests/test_relation.py b/tests/test_relation.py
index 8c9c4ee69..37adab7f5 100644
--- a/tests/test_relation.py
+++ b/tests/test_relation.py
@@ -99,12 +99,13 @@ def test_insert_select(self):
         assert_equal(len(schema.TTest2()), len(schema.TTest()))
 
         original_length = len(self.subject)
-        self.subject.insert(self.subject.proj(
-            'real_id', 'date_of_birth', 'subject_notes', subject_id='subject_id+1000', species='"human"'))
+        elements = self.subject.proj(..., s='subject_id')
+        elements = elements.proj('real_id', 'date_of_birth', 'subject_notes', subject_id='s+1000', species='"human"')
+        self.subject.insert(elements, ignore_extra_fields=True)
         assert_equal(len(self.subject), 2*original_length)
 
     def test_insert_pandas_roundtrip(self):
-        ''' ensure fetched frames can be inserted '''
+        """ ensure fetched frames can be inserted """
         schema.TTest2.delete()
         n = len(schema.TTest())
         assert_true(n > 0)
@@ -115,10 +116,10 @@ def test_insert_pandas_roundtrip(self):
         assert_equal(len(schema.TTest2()), n)
 
     def test_insert_pandas_userframe(self):
-        '''
+        """
         ensure simple user-created frames (1 field, non-custom index)
         can be inserted without extra index adjustment
-        '''
+        """
         schema.TTest2.delete()
         n = len(schema.TTest())
         assert_true(n > 0)
diff --git a/tests/test_relation_u.py b/tests/test_relation_u.py
index 24b5a6d24..281b67d38 100644
--- a/tests/test_relation_u.py
+++ b/tests/test_relation_u.py
@@ -5,7 +5,7 @@
 
 class TestU:
     """
-    Test base relations: insert, delete
+    Test tables: insert, delete
     """
 
     @classmethod
diff --git a/tests/test_relational_operand.py b/tests/test_relational_operand.py
index 090eba80f..f37dafb31 100644
--- a/tests/test_relational_operand.py
+++ b/tests/test_relational_operand.py
@@ -8,7 +8,7 @@
 
 import datajoint as dj
 from .schema_simple import A, B, D, E, F, L, DataA, DataB, TTestUpdate, IJ, JI, ReservedWord
-from .schema import Experiment, TTest3
+from .schema import Experiment, TTest3, Trial, Ephys, Child, Parent
 
 
 def setup():
@@ -57,7 +57,8 @@ def test_rename():
         y = x.proj(j='i')
         assert_equal(len(y), len(B() & 'id_a in (1,2,3,4)'),
                      'incorrect projection of restriction')
-        assert_equal(len(y & 'j in (3,4,5,6)'), len(B() & 'id_a in (3,4)'),
+        z = y & 'j in (3, 4, 5, 6)'
+        assert_equal(len(z), len(B() & 'id_a in (3,4)'),
                      'incorrect nested subqueries')
 
     @staticmethod
@@ -110,8 +111,7 @@ def test_join():
         x = B().proj(i='id_a')  # rename the common attribute to achieve full cartesian product
         y = D()
         rel = x * y
-        assert_equal(len(rel), len(x) * len(y),
-                     'incorrect join')
+        assert_equal(len(rel), len(x) * len(y), 'incorrect join')
         assert_equal(set(x.heading.names).union(y.heading.names), set(rel.heading.names),
                      'incorrect join heading')
         assert_equal(set(x.primary_key).union(y.primary_key), set(rel.primary_key),
@@ -176,11 +176,26 @@ def test_project():
 
     @staticmethod
     def test_union():
-        x = set(zip(*IJ.fetch('i','j')))
-        y = set(zip(*JI.fetch('i','j')))
+        x = set(zip(*IJ.fetch('i', 'j')))
+        y = set(zip(*JI.fetch('i', 'j')))
         assert_true(len(x) > 0 and len(y) > 0 and len(IJ() * JI()) < len(x))  # ensure the IJ and JI are non-trivial
-        z = set(zip(*(IJ + JI).fetch('i','j')))   # union
+        z = set(zip(*(IJ + JI).fetch('i', 'j')))   # union
         assert_set_equal(x.union(y), z)
+        assert_equal(len(IJ + JI), len(z))
+
+    @staticmethod
+    @raises(dj.DataJointError)
+    def test_outer_union_fail():
+        """Union of two tables with different primary keys raises an error."""
+        A() + B()
+
+    @staticmethod
+    def test_outer_union_fail():
+        """Union of two tables with different primary keys raises an error."""
+        t = Trial + Ephys
+        t.fetch()
+        assert_set_equal(set(t.heading.names), set(Trial.heading.names) | set(Ephys.heading.names))
+        len(t)
 
     @staticmethod
     def test_preview():
@@ -225,7 +240,9 @@ def test_aggregate():
     @staticmethod
     def test_aggr():
         x = B.aggr(B.C)
-        assert_equal(len(x), len(B() & B.C()))
+        l1 = len(x)
+        l2 = len(B & B.C)
+        assert_equal(l1, l2)
 
         x = B().aggr(B.C(), keep_all_rows=True)
         assert_equal(len(x), len(B()))  # test LEFT join
@@ -273,6 +290,15 @@ def test_pandas_fetch_and_restriction():
         assert_true(isinstance(df, pandas.DataFrame))
         assert_equal(len(E & q), len(E & df))
 
+    @staticmethod
+    def test_restriction_by_null():
+        assert_true(len(Experiment & 'username is null') > 0)
+        assert_true(len(Experiment & 'username is not null') > 0)
+
+    @staticmethod
+    def test_restriction_between():   # see issue
+        assert_true(len(Experiment & 'username between "S" and "Z"') < len(Experiment()))
+
     @staticmethod
     def test_restrictions_by_lists():
         x = D()
@@ -354,7 +380,8 @@ def test_date():
     @staticmethod
     def test_join_project():
         """Test join of projected relations with matching non-primary key"""
-        assert_true(len(DataA.proj() * DataB.proj()) == len(DataA()) == len(DataB()),
+        q = DataA.proj() * DataB.proj()
+        assert_true(len(q) == len(DataA()) == len(DataB()),
                     "Join of projected relations does not work")
 
     @staticmethod
@@ -422,3 +449,13 @@ def test_reserved_words2():
         rel = ReservedWord()
         rel.insert1({'key': 1, 'in': 'ouch', 'from': 'bummer', 'int': 3, 'select': 'major pain'})
         (rel & 'key=1').fetch('in')  # error because reserved word `key` is not in backquotes. See issue #249
+
+    @staticmethod
+    def test_permissive_join_basic():
+        """Verify join compatibility check is skipped for join"""
+        Child @ Parent
+
+    @staticmethod
+    def test_permissive_restriction_basic():
+        """Verify join compatibility check is skipped for restriction"""
+        Child ^ Parent
diff --git a/tests/test_schema.py b/tests/test_schema.py
index 2d868b60d..f7e19356d 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -26,14 +26,27 @@ def test_schema_size_on_disk():
     assert_true(isinstance(number_of_bytes, int))
 
 
+def test_schema_list():
+    schemas = dj.list_schemas()
+    assert_true(schema.schema.database in schemas)
+
+
+@raises(dj.errors.AccessError)
+def test_drop_unauthorized():
+    info_schema = dj.schema('information_schema')
+    info_schema.drop()
+
+
 def test_namespace_population():
     for name, rel in getmembers(schema, relation_selector):
         assert_true(hasattr(schema_empty, name), '{name} not found in schema_empty'.format(name=name))
-        assert_true(rel.__base__ is getattr(schema_empty, name).__base__, 'Wrong tier for {name}'.format(name=name))
+        assert_true(rel.__base__ is getattr(schema_empty, name).__base__,
+                    'Wrong tier for {name}'.format(name=name))
 
         for name_part in dir(rel):
             if name_part[0].isupper() and part_selector(getattr(rel, name_part)):
-                assert_true(getattr(rel, name_part).__base__ is dj.Part, 'Wrong tier for {name}'.format(name=name_part))
+                assert_true(getattr(rel, name_part).__base__ is dj.Part,
+                            'Wrong tier for {name}'.format(name=name_part))
 
 
 @raises(dj.DataJointError)
diff --git a/tests/test_university.py b/tests/test_university.py
new file mode 100644
index 000000000..63321d6ef
--- /dev/null
+++ b/tests/test_university.py
@@ -0,0 +1,122 @@
+from nose.tools import assert_true, assert_list_equal, assert_false, raises
+import hashlib
+from datajoint import DataJointError
+from .schema_university import *
+from . import PREFIX, CONN_INFO
+
+
+def _hash4(table):
+    """hash of table contents"""
+    data = table.fetch(order_by="KEY", as_dict=True)
+    blob = dj.blob.pack(data, compress=False)
+    return hashlib.md5(blob).digest().hex()[:4]
+
+
+@raises(DataJointError)
+def test_activate_unauthorized():
+    schema.activate('unauthorized', connection=dj.conn(**CONN_INFO))
+
+
+def test_activate():
+    schema.activate(PREFIX + '_university',  connection=dj.conn(**CONN_INFO))  # deferred activation
+    # ---------------  Fill University -------------------
+    for table in Student, Department, StudentMajor, Course, Term, CurrentTerm, Section, Enroll, Grade:
+        import csv
+        with open('./data/' + table.__name__ + '.csv') as f:
+            reader = csv.DictReader(f)
+            table().insert(reader)
+
+
+def test_fill():
+    """ check that the randomized tables are consistently defined """
+    # check randomized tables
+    assert_true(len(Student()) == 300 and _hash4(Student) == '1e1a')
+    assert_true(len(StudentMajor()) == 226 and _hash4(StudentMajor) == '3129')
+    assert_true(len(Section()) == 756 and _hash4(Section) == 'dc7e')
+    assert_true(len(Enroll()) == 3364 and _hash4(Enroll) == '177d')
+    assert_true(len(Grade()) == 3027 and _hash4(Grade) == '4a9d')
+
+
+def test_restrict():
+    """
+    test diverse restrictions from the university database.
+    This test relies on a specific instantiation of the database.
+    """
+    utahns1 = Student & {'home_state': 'UT'}
+    utahns2 = Student & 'home_state="UT"'
+    assert_true(len(utahns1) == len(utahns2.fetch('KEY')) == 7)
+
+    # male nonutahns
+    sex1, state1 = ((Student & 'sex="M"') - {'home_state': 'UT'}).fetch(
+        'sex', 'home_state', order_by='student_id')
+    sex2, state2 = ((Student & 'sex="M"') - {'home_state': 'UT'}).fetch(
+        'sex', 'home_state', order_by='student_id')
+    assert_true(len(set(state1)) == len(set(state2)) == 44)
+    assert_true(set(sex1).pop() == set(sex2).pop() == "M")
+
+    # students from OK, NM, TX
+    s1 = (Student & [{'home_state': s} for s in ('OK', 'NM', 'TX')]).fetch(
+        "KEY", order_by="student_id")
+    s2 = (Student & 'home_state in ("OK", "NM", "TX")').fetch('KEY', order_by="student_id")
+    assert_true(len(s1) == 11)
+    assert_list_equal(s1, s2)
+
+    millenials = Student & 'date_of_birth between "1981-01-01" and "1996-12-31"'
+    assert_true(len(millenials) == 170)
+    millenials_no_math = millenials - (Enroll & 'dept="MATH"')
+    assert_true(len(millenials_no_math) == 53)
+
+    inactive_students = Student - (Enroll & CurrentTerm)
+    assert_true(len(inactive_students) == 204)
+
+    # Females who are active or major in non-math
+    special = Student & [Enroll, StudentMajor - {'dept': "MATH"}] & {'sex': "F"}
+    assert_true(len(special) == 158)
+
+
+def test_advanced_join():
+    """test advanced joins"""
+    # Students with ungraded courses in current term
+    ungraded = Enroll * CurrentTerm - Grade
+    assert_true(len(ungraded) == 34)
+
+    # add major
+    major = StudentMajor.proj(..., major='dept')
+    assert_true(len(ungraded.join(major, left=True)) == len(ungraded) == 34)
+    assert_true(len(ungraded.join(major)) == len(ungraded & major) == 31)
+
+
+def test_union():
+    # effective left join Enroll with Major
+    q1 = (Enroll & 'student_id=101') + (Enroll & 'student_id=102')
+    q2 = (Enroll & 'student_id in (101, 102)')
+    assert_true(len(q1) == len(q2) == 41)
+
+
+def test_aggr():
+    avg_grade_per_course = Course.aggr(Grade*LetterGrade, avg_grade='round(avg(points), 2)')
+    assert_true(len(avg_grade_per_course) == 45)
+
+    # GPA
+    student_gpa = Student.aggr(
+        Course * Grade * LetterGrade,
+        gpa='round(sum(points*credits)/sum(credits), 2)')
+    gpa = student_gpa.fetch('gpa')
+    assert_true(len(gpa) == 261)
+    assert_true(2 < gpa.mean() < 3)
+
+    # Sections in biology department with zero students in them
+    section = (Section & {"dept": "BIOL"}).aggr(
+        Enroll, n='count(student_id)', keep_all_rows=True) & 'n=0'
+    assert_true(len(set(section.fetch('dept'))) == 1)
+    assert_true(len(section) == 17)
+    assert_true(bool(section))
+
+    # Test correct use of ellipses in a similar query
+    section = (Section & {"dept": "BIOL"}).aggr(
+        Grade, ..., n='count(student_id)', keep_all_rows=True) & 'n>1'
+    assert_false(
+        any(name in section.heading.names for name in Grade.heading.secondary_attributes))
+    assert_true(len(set(section.fetch('dept'))) == 1)
+    assert_true(len(section) == 168)
+    assert_true(bool(section))
diff --git a/tests/test_update1.py b/tests/test_update1.py
new file mode 100644
index 000000000..e52ff1a01
--- /dev/null
+++ b/tests/test_update1.py
@@ -0,0 +1,115 @@
+from nose.tools import assert_true, assert_false, assert_equal, raises
+import os
+import numpy as np
+from pathlib import Path
+import tempfile
+import datajoint as dj
+from . import PREFIX, CONN_INFO
+from datajoint import DataJointError
+
+schema = dj.Schema(PREFIX + '_update1', connection=dj.conn(**CONN_INFO))
+
+dj.config['stores']['update_store'] = dict(
+    protocol='file',
+    location=tempfile.mkdtemp())
+
+dj.config['stores']['update_repo'] = dict(
+    stage=tempfile.mkdtemp(),
+    protocol='file',
+    location=tempfile.mkdtemp())
+
+
+scratch_folder = tempfile.mkdtemp()
+
+dj.errors._switch_filepath_types(True)
+
+
+@schema
+class Thing(dj.Manual):
+    definition = """
+    thing   :   int    
+    ---
+    number=0  : int   
+    frac    : float
+    picture = null    :   attach@update_store
+    params = null  : longblob 
+    img_file = null: filepath@update_repo
+    timestamp = CURRENT_TIMESTAMP :   datetime
+    """
+
+
+def test_update1():
+    """test normal updates"""
+
+    dj.errors._switch_filepath_types(True)
+    # CHECK 1 -- initial insert
+    key = dict(thing=1)
+    Thing.insert1(dict(key, frac=0.5))
+    check1 = Thing.fetch1()
+
+    # CHECK 2 -- some updates
+    # numbers and datetimes
+    Thing.update1(dict(key, number=3, frac=30, timestamp="2020-01-01 10:00:00"))
+    # attachment
+    attach_file = Path(scratch_folder, 'attach1.dat')
+    buffer1 = os.urandom(100)
+    attach_file.write_bytes(buffer1)
+    Thing.update1(dict(key, picture=attach_file))
+    attach_file.unlink()
+    assert_false(attach_file.is_file())
+
+    # filepath
+    stage_path = dj.config['stores']['update_repo']['stage']
+    relpath, filename = 'one/two/three', 'picture.dat'
+    managed_file = Path(stage_path, relpath, filename)
+    managed_file.parent.mkdir(parents=True, exist_ok=True)
+    original_file_data = os.urandom(3000)
+    with managed_file.open('wb') as f:
+        f.write(original_file_data)
+    Thing.update1(dict(key, img_file=managed_file))
+    managed_file.unlink()
+    assert_false(managed_file.is_file())
+
+    check2 = Thing.fetch1(download_path=scratch_folder)
+    buffer2 = Path(check2['picture']).read_bytes()   # read attachment
+    final_file_data = managed_file.read_bytes()    # read filepath
+
+    # CHECK 3 -- reset to default values using None
+    Thing.update1(dict(key, number=None, timestamp=None, picture=None, img_file=None, params=np.random.randn(3, 3)))
+    check3 = Thing.fetch1()
+
+    assert_true(check1['number'] == 0 and
+                check1['picture'] is None and
+                check1['params'] is None)
+
+    assert_true(check2['number'] == 3 and
+                check2['frac'] == 30.0 and
+                check2['picture'] is not None and
+                check2['params'] is None and buffer1==buffer2)
+
+    assert_true(check3['number'] == 0 and
+                check3['frac'] == 30.0 and
+                check3['picture'] is None and
+                check3['img_file'] is None and
+                isinstance(check3['params'], np.ndarray))
+
+    assert_true(check3['timestamp'] > check2['timestamp'])
+    assert_equal(buffer1, buffer2)
+    assert_equal(original_file_data, final_file_data)
+
+
+@raises(DataJointError)
+def test_update1_nonexistent():
+    Thing.update1(dict(thing=100, frac=0.5))   # updating a non-existent entry
+
+
+@raises(DataJointError)
+def test_update1_noprimary():
+    Thing.update1(dict(number=None))  # missing primary key
+
+
+@raises(DataJointError)
+def test_update1_misspelled_attribute():
+    key = dict(thing=17)
+    Thing.insert1(dict(key, frac=1.5))
+    Thing.update1(dict(key, numer=3))    # misspelled attribute