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

Issue:
Remove login shell



Discussion / hints:

Fix patch:
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 000000000..c965828c6
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,12 @@
+# Note: You can use any Debian/Ubuntu based image you want. 
+FROM mcr.microsoft.com/devcontainers/python:3.7-bullseye
+
+RUN \
+    apt update && \
+    apt-get install bash-completion graphviz default-mysql-client -y && \
+    pip install flake8 black faker ipykernel nose nose-cov datajoint && \
+    pip uninstall datajoint -y
+
+ENV DJ_HOST fakeservices.datajoint.io
+ENV DJ_USER root
+ENV DJ_PASS simple
\ No newline at end of file
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..668e5c409
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,34 @@
+// For format details, see https://aka.ms/devcontainer.json. For config options, see the
+{
+	"name": "Development",
+	"dockerComposeFile": "docker-compose.yaml",
+	"service": "app",
+	"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
+	// Use this environment variable if you need to bind mount your local source code into a new container.
+	"remoteEnv": {
+		"LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"
+	},
+	// https://containers.dev/features
+	"features": {
+		"ghcr.io/devcontainers/features/docker-in-docker:2": {},
+		"ghcr.io/devcontainers/features/git:1": {},
+		"ghcr.io/eitsupi/devcontainer-features/jq-likes:1": {},
+		"ghcr.io/guiyomh/features/vim:0": {}
+	},
+	"onCreateCommand": "pip install -e .",
+	"postStartCommand": "MYSQL_VER=8.0 MINIO_VER=RELEASE.2022-08-11T04-37-28Z docker compose -f local-docker-compose.yml down && MYSQL_VER=8.0 MINIO_VER=RELEASE.2022-08-11T04-37-28Z docker compose -f local-docker-compose.yml up --build --wait",
+	"forwardPorts": [
+		80,
+		443,
+		3306,
+		8080,
+		9000
+	],
+	"customizations": {
+		"vscode": {
+			"extensions": [
+				"ms-python.python"
+			]
+		}
+	}
+}
\ No newline at end of file
diff --git a/.devcontainer/docker-compose.yaml b/.devcontainer/docker-compose.yaml
new file mode 100644
index 000000000..a456ed151
--- /dev/null
+++ b/.devcontainer/docker-compose.yaml
@@ -0,0 +1,10 @@
+version: "3"
+services:
+  app:
+    build: .
+    extra_hosts:
+      - fakeservices.datajoint.io:127.0.0.1
+    volumes:
+      - ../..:/workspaces:cached
+    entrypoint: /usr/local/share/docker-init.sh
+    command: tail -f /dev/null
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index d386d4d4d..d7b33901b 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -2,7 +2,7 @@
 name: Bug report
 about: Create a report to help us improve
 title: ''
-labels: 'bug, awaiting-triage'
+labels: 'bug'
 assignees: ''
 
 ---
diff --git a/.github/workflows/development.yaml b/.github/workflows/development.yaml
index 8d81bc84c..524f066ad 100644
--- a/.github/workflows/development.yaml
+++ b/.github/workflows/development.yaml
@@ -2,44 +2,70 @@ name: Development
 on:
   push:
     branches:
-      - '**' # every branch
-      - '!stage*' # exclude branches beginning with stage
+      - "**" # every branch
+      - "!gh-pages" # exclude gh-pages branch
+      - "!stage*" # exclude branches beginning with stage
+    tags:
+      - '\d+\.\d+\.\d+' # only semver tags
   pull_request:
     branches:
-      - '**' # every branch
-      - '!stage*' # exclude branches beginning with stage
+      - "**" # every branch
+      - "!gh-pages" # exclude gh-pages branch
+      - "!stage*" # exclude branches beginning with stage
 jobs:
-  build-docs:
+  build:
     runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        include:
+          - py_ver: "3.9"
+            distro: debian
+            image: djbase
     env:
+      PY_VER: ${{matrix.py_ver}}
+      DISTRO: ${{matrix.distro}}
+      IMAGE: ${{matrix.image}}
       DOCKER_CLIENT_TIMEOUT: "120"
       COMPOSE_HTTP_TIMEOUT: "120"
     steps:
-      - uses: actions/checkout@v2
-      - name: Compile docs static artifacts
+      - uses: actions/checkout@v3
+      - name: Validate version and release notes
+        run: |
+          DJ_VERSION=$(grep -oP '\d+\.\d+\.\d+' datajoint/version.py)
+          RELEASE_BODY=$(python -c \
+            'print(open("./CHANGELOG.md").read().split("\n\n")[1].split("\n", 1)[1])' \
+          )
+          echo "DJ_VERSION=${DJ_VERSION}" >> $GITHUB_ENV
+          echo "RELEASE_BODY<<EOF" >> $GITHUB_ENV
+          echo "$RELEASE_BODY" >> $GITHUB_ENV
+          echo "EOF" >> $GITHUB_ENV
+      - name: Build pip artifacts
         run: |
           export HOST_UID=$(id -u)
-          docker-compose -f ./docs-api/docker-compose.yaml up --exit-code-from docs-builder --build 
-      - name: Add docs static artifacts
-        uses: actions/upload-artifact@v2
+          docker-compose -f docker-compose-build.yaml up --exit-code-from app --build
+          echo "DJ_VERSION=${DJ_VERSION}" >> $GITHUB_ENV
+      - if: matrix.py_ver == '3.9' && matrix.distro == 'debian'
+        name: Add pip artifacts
+        uses: actions/upload-artifact@v3
         with:
-          name: docs-api-static
-          path: docs-api/build/html
+          name: pip-datajoint-${{env.DJ_VERSION}}
+          path: dist
           retention-days: 1
   test:
-    if: github.event_name == 'push' || github.event_name == 'pull_request'
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        py_ver: ["3.8"]
+        py_ver: ["3.9"]
         mysql_ver: ["8.0", "5.7"]
         include:
+          - py_ver: "3.8"
+            mysql_ver: "5.7"
           - py_ver: "3.7"
             mysql_ver: "5.7"
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - name: Set up Python ${{matrix.py_ver}}
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v4
         with:
           python-version: ${{matrix.py_ver}}
       - name: Install dependencies
@@ -50,58 +76,136 @@ jobs:
         run: flake8 datajoint --count --select=E9,F63,F7,F82 --show-source --statistics
       - name: Run primary tests
         env:
-          UID: "1001"
-          GID: "121"
           PY_VER: ${{matrix.py_ver}}
+          DJ_PASS: simple
           MYSQL_VER: ${{matrix.mysql_ver}}
-          ALPINE_VER: "3.10"
+          DISTRO: alpine
           MINIO_VER: RELEASE.2021-09-03T03-56-13Z
+          DOCKER_CLIENT_TIMEOUT: "120"
           COMPOSE_HTTP_TIMEOUT: "120"
-          COVERALLS_SERVICE_NAME: travis-ci
-          COVERALLS_REPO_TOKEN: fd0BoXG46TPReEem0uMy7BJO5j0w1MQiY
-        run: docker-compose -f LNX-docker-compose.yml up --build --exit-code-from app
+        run: |
+          export HOST_UID=$(id -u)
+          docker-compose -f LNX-docker-compose.yml up --build --exit-code-from app
       - name: Run style tests
         run: |
-          flake8 --ignore=E203,E722,F401,W503 datajoint \
+          flake8 --ignore=E203,E722,W503 datajoint \
                  --count --max-complexity=62 --max-line-length=127 --statistics
           black datajoint --check -v
           black tests --check -v
   publish-docs:
     if: |
       github.event_name == 'push' &&
-      (
-        github.repository_owner == 'datajoint' ||
-        github.repository_owner == 'datajoint-company' ||
-        github.repository_owner == 'dj-sciops'
-      )
-    needs: build-docs
+      startsWith(github.ref, 'refs/tags')
+    needs: test
     runs-on: ubuntu-latest
+    env:
+      DOCKER_CLIENT_TIMEOUT: "120"
+      COMPOSE_HTTP_TIMEOUT: "120"
     steps:
-      - uses: actions/checkout@v2
-      - name: Fetch docs static artifacts
-        uses: actions/download-artifact@v2
+      - uses: actions/checkout@v3
+      - name: Deploy docs
+        run: |
+          export MODE=BUILD
+          export PACKAGE=datajoint
+          export UPSTREAM_REPO=https://github.com/${GITHUB_REPOSITORY}.git
+          export HOST_UID=$(id -u)
+          docker compose -f docs/docker-compose.yaml up --exit-code-from docs --build
+          git push origin gh-pages
+  publish-release:
+    if: |
+      github.event_name == 'push' &&
+      startsWith(github.ref, 'refs/tags')
+    needs: test
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        include:
+          - py_ver: "3.9"
+            distro: debian
+            image: djbase
+    env:
+      PY_VER: ${{matrix.py_ver}}
+      DISTRO: ${{matrix.distro}}
+      IMAGE: ${{matrix.image}}
+      TWINE_USERNAME: ${{secrets.twine_username}}
+      TWINE_PASSWORD: ${{secrets.twine_password}}
+      DOCKER_CLIENT_TIMEOUT: "120"
+      COMPOSE_HTTP_TIMEOUT: "120"
+    outputs:
+      release_upload_url: ${{steps.create_gh_release.outputs.upload_url}}
+    steps:
+      - uses: actions/checkout@v3
+      - name: Set up Python ${{matrix.py_ver}}
+        uses: actions/setup-python@v4
         with:
-          name: docs-api-static
-          path: docs-api/build/html
-      - name: Commit documentation changes
+          python-version: ${{matrix.py_ver}}
+      - name: Determine package version
         run: |
-          git clone https://github.com/${GITHUB_REPOSITORY}.git \
-              --branch gh-pages --single-branch gh-pages
-          rm -R gh-pages/*
-          cp -r docs-api/build/html/* gh-pages/
-          cp .gitignore gh-pages/
-          touch gh-pages/.nojekyll
-          echo "docs-api.datajoint.org" > gh-pages/CNAME
-          cd gh-pages
-          git config --local user.email "action@github.com"
-          git config --local user.name "GitHub Action"
-          git add . --all
-          git commit -m "Update documentation" -a || true
-          # The above command will fail if no changes were present, so we ignore
-          # the return code.
-      - name: Push changes
-        uses: ad-m/github-push-action@master
+          DJ_VERSION=$(grep -oP '\d+\.\d+\.\d+' datajoint/version.py)
+          RELEASE_BODY=$(python -c \
+            'print(open("./CHANGELOG.md").read().split("\n\n")[1].split("\n", 1)[1])' \
+          )
+          echo "DJ_VERSION=${DJ_VERSION}" >> $GITHUB_ENV
+          echo "RELEASE_BODY<<EOF" >> $GITHUB_ENV
+          echo "$RELEASE_BODY" >> $GITHUB_ENV
+          echo "EOF" >> $GITHUB_ENV
+      - name: Create GH release
+        id: create_gh_release
+        uses: actions/create-release@v1
+        env:
+          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
         with:
-          branch: gh-pages
-          directory: gh-pages
-          github_token: ${{secrets.GITHUB_TOKEN}}
+          tag_name: ${{env.DJ_VERSION}}
+          release_name: Release ${{env.DJ_VERSION}}
+          body: ${{env.RELEASE_BODY}}
+          prerelease: false
+          draft: false
+      - name: Fetch pip artifacts
+        uses: actions/download-artifact@v3
+        with:
+          name: pip-datajoint-${{env.DJ_VERSION}}
+          path: dist
+      - name: Determine pip artifact paths
+        run: |
+          echo "DJ_WHEEL_PATH=$(ls dist/datajoint-*.whl)" >> $GITHUB_ENV
+          echo "DJ_SDIST_PATH=$(ls dist/datajoint-*.tar.gz)" >> $GITHUB_ENV
+      - name: Upload pip wheel asset to release
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
+        with:
+          upload_url: ${{steps.create_gh_release.outputs.upload_url}}
+          asset_path: ${{env.DJ_WHEEL_PATH}}
+          asset_name: pip-datajoint-${{env.DJ_VERSION}}.whl
+          asset_content_type: application/zip
+      - name: Upload pip sdist asset to release
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
+        with:
+          upload_url: ${{steps.create_gh_release.outputs.upload_url}}
+          asset_path: ${{env.DJ_SDIST_PATH}}
+          asset_name: pip-datajoint-${{env.DJ_VERSION}}.tar.gz
+          asset_content_type: application/gzip
+      - name: Publish pip release
+        run: |
+          export HOST_UID=$(id -u)
+          docker-compose -f docker-compose-build.yaml run \
+            -e TWINE_USERNAME=${TWINE_USERNAME} -e TWINE_PASSWORD=${TWINE_PASSWORD} app \
+            sh -c "pip install twine && python -m twine upload dist/*"
+      - name: Login to DockerHub
+        uses: docker/login-action@v1
+        with:
+          username: ${{secrets.docker_username}}
+          password: ${{secrets.docker_password}}
+      - name: Publish image
+        run: |
+          IMAGE=$(docker images --filter "reference=datajoint/datajoint*" --format "{{.Repository}}")
+          TAG=$(docker images --filter "reference=datajoint/datajoint*" --format "{{.Tag}}")
+          docker push "${IMAGE}:${TAG}"
+          docker tag "${IMAGE}:${TAG}" "${IMAGE}:${TAG}-${GITHUB_SHA:0:7}"
+          docker push "${IMAGE}:${TAG}-${GITHUB_SHA:0:7}"
+          [ "$PY_VER" == "3.9" ] && [ "$DISTRO" == "debian" ] \
+                  && docker tag "${IMAGE}:${TAG}" "${IMAGE}:latest" \
+                  && docker push "${IMAGE}:latest" \
+              || echo "skipping 'latest' tag..."
diff --git a/.gitignore b/.gitignore
index 7b6b58d7d..4b7bdb2c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,8 +21,13 @@ build/
 *.env
 docker-compose.yml
 notebook
-.vscode
 __main__.py
 jupyter_custom.js
-apk_requirements.txt
-.eggs
\ No newline at end of file
+.eggs
+*.code-workspace
+docs/site
+
+
+!.vscode/settings.json
+!.vscode/launch.json
+!.devcontainer/devcontainer.json
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 000000000..2b2502c69
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,16 @@
+{
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "Python: Current File",
+            "type": "python",
+            "request": "launch",
+            "program": "${file}",
+            "console": "integratedTerminal",
+            "justMyCode": false
+        }
+    ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100755
index 000000000..83eebdcc9
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,15 @@
+{
+    "editor.formatOnPaste": false,
+    "editor.formatOnSave": true,
+    "editor.rulers": [
+        94
+    ],
+    "python.formatting.provider": "black",
+    "[python]": {
+        "editor.defaultFormatter": null
+    },
+    "[markdown]": {
+        "editor.defaultFormatter": "disable"
+    },
+    "files.autoSave": "off"
+}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 616fa5eb1..e107b29bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,280 +1,300 @@
 ## Release notes
 
+### 0.14.0 -- Feb 13, 2023
+- Added - `json` data type ([#245](https://github.com/datajoint/datajoint-python/issues/245)) PR [#1051](https://github.com/datajoint/datajoint-python/pull/1051)
+- Fixed - Activating a schema requires all tables to exist even if `create_tables=False` PR [#1058](https://github.com/datajoint/datajoint-python/pull/1058)
+- Changed - Populate call with `reserve_jobs=True` to exclude `error` and `ignore` keys - PR [#1062](https://github.com/datajoint/datajoint-python/pull/1062)
+- Added - Support for inserting data with CSV files - PR [#1067](https://github.com/datajoint/datajoint-python/pull/1067)
+- Changed - Switch testing image from `pydev` to `djtest` PR [#1012](https://github.com/datajoint/datajoint-python/pull/1012)
+- Added - DevContainer development environment compatible with GH Codespaces PR [1071](https://github.com/datajoint/datajoint-python/pull/1071)
+- Fixed - Convert lingering prints by replacing with logs PR [#1073](https://github.com/datajoint/datajoint-python/pull/1073)
+- Changed - `table.progress()` defaults to no stdout PR [#1073](https://github.com/datajoint/datajoint-python/pull/1073)
+- Changed - `table.describe()` defaults to no stdout PR [#1073](https://github.com/datajoint/datajoint-python/pull/1073)
+- Deprecated - `table._update()` PR [#1073](https://github.com/datajoint/datajoint-python/pull/1073)
+- Deprecated - old-style foreign key syntax PR [#1073](https://github.com/datajoint/datajoint-python/pull/1073)
+- Deprecated - `dj.migrate_dj011_external_blob_storage_to_dj012()` PR [#1073](https://github.com/datajoint/datajoint-python/pull/1073)
+* Added - Method to set job keys to "ignore" status - PR [#1068](https://github.com/datajoint/datajoint-python/pull/1068)
+
+### 0.13.8 -- Sep 21, 2022
+- Added - New documentation structure based on markdown PR [#1052](https://github.com/datajoint/datajoint-python/pull/1052)
+- Fixed - Fix queries with backslashes ([#999](https://github.com/datajoint/datajoint-python/issues/999)) PR [#1052](https://github.com/datajoint/datajoint-python/pull/1052)
+
 ### 0.13.7 -- Jul 13, 2022
-* Bugfix - Fix networkx incompatable change by version pinning to 2.6.3 PR #1036 (#1035)
-* Add - Support for serializing numpy datetime64 types PR #1036 (#1022)
-* Update - Add traceback to default logging PR #1036
+- Fixed - Fix networkx incompatable change by version pinning to 2.6.3 (#1035) PR #1036
+- Added - Support for serializing numpy datetime64 types (#1022) PR #1036
+- Changed - Add traceback to default logging PR #1036
 
 ### 0.13.6 -- Jun 13, 2022
-* Add - Config option to set threshold for when to stop using checksums for filepath stores. PR #1025
-* Add - Unified package level logger for package (#667) PR #1031
-* Update - Swap various datajoint messages, warnings, etc. to use the new logger. (#667) PR #1031
-* Bugfix - Fix query caching deleting non-datajoint files PR #1027
-* Update - Minimum Python version for Datajoint-Python is now 3.7 PR #1027
+- Added - Config option to set threshold for when to stop using checksums for filepath stores. PR #1025
+- Added - Unified package level logger for package (#667) PR #1031
+- Changed - Swap various datajoint messages, warnings, etc. to use the new logger. (#667) PR #1031
+- Fixed - Fix query caching deleting non-datajoint files PR #1027
+- Changed - Minimum Python version for Datajoint-Python is now 3.7 PR #1027
 
 ### 0.13.5 -- May 19, 2022
-* Update - Import ABC from collections.abc for Python 3.10 compatibility
-* Bugfix - Fix multiprocessing value error (#1013) PR #1026
+- Changed - Import ABC from collections.abc for Python 3.10 compatibility
+- Fixed - Fix multiprocessing value error (#1013) PR #1026
 
 ### 0.13.4 -- Mar, 28 2022
-* Add - Allow reading blobs produced by legacy 32-bit compiled mYm library for matlab. PR #995
-* Bugfix - Add missing `jobs` argument for multiprocessing PR #997
-* Add - Test for multiprocessing PR #1008
-* Bugfix - Fix external store key name doesn't allow '-' (#1005) PR #1006
-* Add - Adopted black formatting into code base PR #998
+- Added - Allow reading blobs produced by legacy 32-bit compiled mYm library for matlab. PR #995
+- Fixed - Add missing `jobs` argument for multiprocessing PR #997
+- Added - Test for multiprocessing PR #1008
+- Fixed - Fix external store key name doesn't allow '-' (#1005) PR #1006
+- Added - Adopted black formatting into code base PR #998
 
 ### 0.13.3 -- Feb 9, 2022
-* Bugfix - Fix error in listing ancestors, descendants with part tables.
-* Bugfix - Fix Python 3.10 compatibility (#983) PR #972
-* Bugfix - Allow renaming non-conforming attributes in proj (#982) PR #972
-* Add - Expose proxy feature for S3 external stores (#961) PR #962
-* Add - implement multiprocessing in populate (#695) PR #704, #969
-* Bugfix - Dependencies not properly loaded on populate. (#902) PR #919
-* Bugfix - Replace use of numpy aliases of built-in types with built-in type. (#938) PR #939
-* Bugfix - Deletes and drops must include the master of each part. (#151, #374) PR #957
-* Bugfix - `ExternalTable.delete` should not remove row on error (#953) PR #956
-* Bugfix - Fix error handling of remove_object function in `s3.py` (#952) PR #955
-* Bugfix - Fix regression issue with `DISTINCT` clause and `GROUP_BY` (#914) PR #963
-* Bugfix - Fix sql code generation to comply with sql mode `ONLY_FULL_GROUP_BY` (#916) PR #965
-* Bugfix - Fix count for left-joined `QueryExpressions` (#951) PR #966
-* Bugfix - Fix assertion error when performing a union into a join (#930) PR #967
-* Update `~jobs.error_stack` from blob to mediumblob to allow error stacks >64kB in jobs (#984) PR #986
-* Bugfix - Fix error when performing a union on multiple tables (#926) PR #964
-* Add - Allow optional keyword arguments for `make()` in `populate()` PR #971
+- Fixed - Fix error in listing ancestors, descendants with part tables.
+- Fixed - Fix Python 3.10 compatibility (#983) PR #972
+- Fixed - Allow renaming non-conforming attributes in proj (#982) PR #972
+- Added - Expose proxy feature for S3 external stores (#961) PR #962
+- Added - implement multiprocessing in populate (#695) PR #704, #969
+- Fixed - Dependencies not properly loaded on populate. (#902) PR #919
+- Fixed - Replace use of numpy aliases of built-in types with built-in type. (#938) PR #939
+- Fixed - Deletes and drops must include the master of each part. (#151, #374) PR #957
+- Fixed - `ExternalTable.delete` should not remove row on error (#953) PR #956
+- Fixed - Fix error handling of remove_object function in `s3.py` (#952) PR #955
+- Fixed - Fix regression issue with `DISTINCT` clause and `GROUP_BY` (#914) PR #963
+- Fixed - Fix sql code generation to comply with sql mode `ONLY_FULL_GROUP_BY` (#916) PR #965
+- Fixed - Fix count for left-joined `QueryExpressions` (#951) PR #966
+- Fixed - Fix assertion error when performing a union into a join (#930) PR #967
+- Changed `~jobs.error_stack` from blob to mediumblob to allow error stacks >64kB in jobs (#984) PR #986
+- Fixed - Fix error when performing a union on multiple tables (#926) PR #964
+- Added - Allow optional keyword arguments for `make()` in `populate()` PR #971
 
 ### 0.13.2 -- May 7, 2021
-* Update `setuptools_certificate` dependency to new name `otumat`
-* Bugfix - Explicit calls to `dj.Connection` throw error due to missing `host_input` (#895) PR #907
-* Bugfix - Correct count of deleted items. (#897) PR #912
+- Changed `setuptools_certificate` dependency to new name `otumat`
+- Fixed - Explicit calls to `dj.Connection` throw error due to missing `host_input` (#895) PR #907
+- Fixed - Correct count of deleted items. (#897) PR #912
 
 ### 0.13.1 -- Apr 16, 2021
-* Add `None` as an alias for `IS NULL` comparison in `dict` restrictions (#824) PR #893
-* Drop support for MySQL 5.6 since it has reached EOL PR #893
-* Bugfix - `schema.list_tables()` is not topologically sorted (#838) PR #893
-* Bugfix - Diagram part tables do not show proper class name (#882) PR #893
-* Bugfix - Error in complex restrictions (#892) PR #893
-* Bugfix - WHERE and GROUP BY clases are dropped on joins with aggregation (#898, #899) PR #893
+- Added `None` as an alias for `IS NULL` comparison in `dict` restrictions (#824) PR #893
+- Changed - Drop support for MySQL 5.6 since it has reached EOL PR #893
+- Fixed - `schema.list_tables()` is not topologically sorted (#838) PR #893
+- Fixed - Diagram part tables do not show proper class name (#882) PR #893
+- Fixed - Error in complex restrictions (#892) PR #893
+- Fixed - WHERE and GROUP BY clases are dropped on joins with aggregation (#898, #899) PR #893
 
 ### 0.13.0 -- Mar 24, 2021
-* Re-implement query transpilation into SQL, fixing issues (#386, #449, #450, #484, #558). PR #754
-* Re-implement cascading deletes for better performance. PR #839
-* Add support for deferred schema activation to allow for greater modularity. (#834) PR #839
-* Add query caching mechanism for offline development (#550) PR #839
-* Add table method `.update1` to update a row in the table with new values (#867) PR #763, #889
-* Python datatypes are now enabled by default in blobs (#761). PR #859
-* 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` (#804) PR #862
-* 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 (#829) PR #861
+- Re-implement query transpilation into SQL, fixing issues (#386, #449, #450, #484, #558). PR #754
+- Re-implement cascading deletes for better performance. PR #839
+- Add support for deferred schema activation to allow for greater modularity. (#834) PR #839
+- Add query caching mechanism for offline development (#550) PR #839
+- Add table method `.update1` to update a row in the table with new values (#867) PR #763, #889
+- Python datatypes are now enabled by default in blobs (#761). PR #859
+- 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` (#804) PR #862
+- 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 (#829) PR #861
 
 ### 0.12.9 -- Mar 12, 2021
-* Fix bug with fetch1 with `dj.config['fetch_format']="frame"`. (#876) PR #880
+- Fix bug with fetch1 with `dj.config['fetch_format']="frame"`. (#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
-* Add `schema.list_tables`. (#838) PR #844
-* Fix minio new version regression.  PR #847
-* Add more S3 logging for debugging. (#831) PR #832
-* Convert testing framework from TravisCI to GitHub Actions (#841) PR #840
+- 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
+- Add `schema.list_tables`. (#838) PR #844
+- Fix minio new version regression. PR #847
+- Add more S3 logging for debugging. (#831) PR #832
+- Convert testing framework from TravisCI to GitHub Actions (#841) PR #840
 
 ### 0.12.7 -- Oct 27, 2020
-* Fix case sensitivity issues to adapt to MySQL 8+.  PR #819
-* Fix pymysql regression bug (#814) PR #816
-* Adapted attribute types now have dtype=object in all recarray results. PR #811
+- Fix case sensitivity issues to adapt to MySQL 8+. PR #819
+- Fix pymysql regression bug (#814) PR #816
+- Adapted attribute types now have dtype=object in all recarray results. PR #811
 
 ### 0.12.6 -- May 15, 2020
-* Add `order_by` to `dj.kill` (#668, #779) PR #775, #783
-* Add explicit S3 bucket and file storage location existence checks (#748) PR #781
-* Modify `_update` to allow nullable updates for strings/date (#664) PR #760
-* Avoid logging events on auxiliary tables (#737) PR #753
-* Add `kill_quick` and expand display to include host (#740) PR #741
-* Bugfix - pandas insert fails due to additional `index` field (#666) PR #776
-* Bugfix - `delete_external_files=True` does not remove from S3 (#686) PR #781
-* Bugfix - pandas fetch throws error when `fetch_format='frame'` PR #774
+- Add `order_by` to `dj.kill` (#668, #779) PR #775, #783
+- Add explicit S3 bucket and file storage location existence checks (#748) PR #781
+- Modify `_update` to allow nullable updates for strings/date (#664) PR #760
+- Avoid logging events on auxiliary tables (#737) PR #753
+- Add `kill_quick` and expand display to include host (#740) PR #741
+- Bugfix - pandas insert fails due to additional `index` field (#666) PR #776
+- Bugfix - `delete_external_files=True` does not remove from S3 (#686) PR #781
+- Bugfix - pandas fetch throws error when `fetch_format='frame'` PR #774
 
 ### 0.12.5 -- Feb 24, 2020
-* Rename module `dj.schema` into `dj.schemas`. `dj.schema` remains an alias for class `dj.Schema`. (#731) PR #732
-* `dj.create_virtual_module` is now called `dj.VirtualModule` (#731) PR #732
-* Bugfix - SSL `KeyError` on failed connection (#716) PR #725
-* Bugfix - Unable to run unit tests using nosetests (#723) PR #724
-* Bugfix - `suppress_errors` does not suppress loss of connection error (#720) PR #721
+- Rename module `dj.schema` into `dj.schemas`. `dj.schema` remains an alias for class `dj.Schema`. (#731) PR #732
+- `dj.create_virtual_module` is now called `dj.VirtualModule` (#731) PR #732
+- Bugfix - SSL `KeyError` on failed connection (#716) PR #725
+- Bugfix - Unable to run unit tests using nosetests (#723) PR #724
+- Bugfix - `suppress_errors` does not suppress loss of connection error (#720) PR #721
 
 ### 0.12.4 -- Jan 14, 2020
-* Support for simple scalar datatypes in blobs (#690) PR #709
-* Add support for the `serial` data type in declarations: alias for `bigint unsigned auto_increment` PR #713
-* Improve the log table to avoid primary key collisions PR #713
-* Improve documentation in README PR #713
+- Support for simple scalar datatypes in blobs (#690) PR #709
+- Add support for the `serial` data type in declarations: alias for `bigint unsigned auto_increment` PR #713
+- Improve the log table to avoid primary key collisions PR #713
+- Improve documentation in README PR #713
 
 ### 0.12.3 -- Nov 22, 2019
-* Bugfix - networkx 2.4 causes error in diagrams (#675) PR #705
-* Bugfix - include table definition in doc string and help (#698, #699) PR #706
-* Bugfix - job reservation fails when native python datatype support is disabled (#701) PR #702
+- Bugfix - networkx 2.4 causes error in diagrams (#675) PR #705
+- Bugfix - include table definition in doc string and help (#698, #699) PR #706
+- Bugfix - job reservation fails when native python datatype support is disabled (#701) PR #702
 
 ### 0.12.2 -- Nov 11, 2019
-* Bugfix - Convoluted error thrown if there is a reference to a non-existent table attribute (#691) PR #696
-* Bugfix - Insert into external does not trim leading slash if defined in `dj.config['stores']['<store>']['location']` (#692) PR #693
+- Bugfix - Convoluted error thrown if there is a reference to a non-existent table attribute (#691) PR #696
+- Bugfix - Insert into external does not trim leading slash if defined in `dj.config['stores']['<store>']['location']` (#692) PR #693
 
 ### 0.12.1 -- Nov 2, 2019
-* Bugfix - AttributeAdapter converts into a string (#684) PR #688
+- Bugfix - AttributeAdapter converts into a string (#684) PR #688
 
 ### 0.12.0 -- Oct 31, 2019
-* Dropped support for Python 3.4
-* Support secure connections with TLS (aka SSL) PR #620
-* Convert numpy array from python object to appropriate data type if all elements are of the same type (#587) PR #608
-* Remove expression requirement to have additional attributes (#604) PR #604
-* Support for filepath datatype (#481) PR #603, #659
-* Support file attachment datatype (#480, #592, #637) PR #659
-* Fetch return a dict array when specifying `as_dict=True` for specified attributes. (#595) PR #593
-* Support of ellipsis in `proj`:  `query_expression.proj(.., '-movie')` (#499) PR #578
-* Expand support of blob serialization (#572, #520, #427, #392, #244, #594) PR #577
-* Support for alter (#110) PR #573
-* Support for `conda install datajoint` via `conda-forge` channel (#293)
-* `dj.conn()` accepts a `port` keyword argument (#563) PR #571
-* Support for UUID datatype (#562) PR #567
-* `query_expr.fetch("KEY", as_dict=False)` returns results as `np.recarray`(#414) PR #574
-* `dj.ERD` is now called `dj.Diagram` (#255, #546) PR #565
-* `dj.Diagram` underlines "distinguished" classes (#378) PR #557
-* Accept alias for supported MySQL datatypes (#544) PR #545
-* Support for pandas in `fetch` (#459, #537) PR #534
-* Support for ordering by "KEY" in `fetch` (#541) PR #534
-* Add config to enable python native blobs PR #672, #676
-* Add secure option for external storage (#663) PR #674, #676
-* Add blob migration utility from DJ011 to DJ012 PR #673
-* Improved external storage - a migration script needed from version 0.11  (#467, #475, #480, #497) PR #532
-* Increase default display rows (#523) PR #526
-* Bugfixes (#521, #205, #279, #477, #570, #581, #597, #596, #618, #633, #643, #644, #647, #648, #650, #656)
-* Minor improvements (#538)
+- Dropped support for Python 3.4
+- Support secure connections with TLS (aka SSL) PR #620
+- Convert numpy array from python object to appropriate data type if all elements are of the same type (#587) PR #608
+- Remove expression requirement to have additional attributes (#604) PR #604
+- Support for filepath datatype (#481) PR #603, #659
+- Support file attachment datatype (#480, #592, #637) PR #659
+- Fetch return a dict array when specifying `as_dict=True` for specified attributes. (#595) PR #593
+- Support of ellipsis in `proj`: `query_expression.proj(.., '-movie')` (#499) PR #578
+- Expand support of blob serialization (#572, #520, #427, #392, #244, #594) PR #577
+- Support for alter (#110) PR #573
+- Support for `conda install datajoint` via `conda-forge` channel (#293)
+- `dj.conn()` accepts a `port` keyword argument (#563) PR #571
+- Support for UUID datatype (#562) PR #567
+- `query_expr.fetch("KEY", as_dict=False)` returns results as `np.recarray`(#414) PR #574
+- `dj.ERD` is now called `dj.Diagram` (#255, #546) PR #565
+- `dj.Diagram` underlines "distinguished" classes (#378) PR #557
+- Accept alias for supported MySQL datatypes (#544) PR #545
+- Support for pandas in `fetch` (#459, #537) PR #534
+- Support for ordering by "KEY" in `fetch` (#541) PR #534
+- Add config to enable python native blobs PR #672, #676
+- Add secure option for external storage (#663) PR #674, #676
+- Add blob migration utility from DJ011 to DJ012 PR #673
+- Improved external storage - a migration script needed from version 0.11 (#467, #475, #480, #497) PR #532
+- Increase default display rows (#523) PR #526
+- Bugfixes (#521, #205, #279, #477, #570, #581, #597, #596, #618, #633, #643, #644, #647, #648, #650, #656)
+- Minor improvements (#538)
 
 ### 0.11.3 -- Jul 26, 2019
-* Fix incompatibility with pyparsing 2.4.1 (#629) PR #631
+- Fix incompatibility with pyparsing 2.4.1 (#629) PR #631
 
 ### 0.11.2 -- Jul 25, 2019
-* Fix #628 - incompatibility with pyparsing 2.4.1
+- Fix #628 - incompatibility with pyparsing 2.4.1
 
 ### 0.11.1 -- Nov 15, 2018
-* Fix ordering of attributes in proj (#483, #516)
-* Prohibit direct insert into auto-populated tables (#511)
+- Fix ordering of attributes in proj (#483, #516)
+- Prohibit direct insert into auto-populated tables (#511)
 
 ### 0.11.0 -- Oct 25, 2018
-* Full support of dependencies with renamed attributes using projection syntax (#300, #345, #436, #506, #507)
-* Rename internal class and module names to comply with terminology in documentation (#494, #500)
-* Full support of secondary indexes (#498, 500)
-* ERD no longer shows numbers in nodes corresponding to derived dependencies (#478, #500)
-* Full support of unique and nullable dependencies (#254, #301, #493, #495, #500)
-* Improve memory management in `populate` (#461, #486)
-* Fix query errors and redundancies (#456, #463, #482)
-
-### 0.10.1  -- Aug 28, 2018
-* Fix ERD Tooltip message (#431)
-* Networkx 2.0 support (#443)
-* Fix insert from query with skip_duplicates=True (#451)
-* Sped up queries (#458)
-* Bugfix in restriction of the form (A & B) * B (#463)
-* Improved error messages (#466)
+- Full support of dependencies with renamed attributes using projection syntax (#300, #345, #436, #506, #507)
+- Rename internal class and module names to comply with terminology in documentation (#494, #500)
+- Full support of secondary indexes (#498, 500)
+- ERD no longer shows numbers in nodes corresponding to derived dependencies (#478, #500)
+- Full support of unique and nullable dependencies (#254, #301, #493, #495, #500)
+- Improve memory management in `populate` (#461, #486)
+- Fix query errors and redundancies (#456, #463, #482)
+
+### 0.10.1 -- Aug 28, 2018
+- Fix ERD Tooltip message (#431)
+- Networkx 2.0 support (#443)
+- Fix insert from query with skip_duplicates=True (#451)
+- Sped up queries (#458)
+- Bugfix in restriction of the form (A & B) \* B (#463)
+- Improved error messages (#466)
 
 ### 0.10.0 -- Jan 10, 2018
-* Deletes are more efficient (#424)
-* ERD shows table definition on tooltip hover in Jupyter (#422)
-* S3 external storage
-* Garbage collection for external sorage
-* Most operators and methods of tables can be invoked as class methods rather than instance methods (#407)
-* The schema decorator object no longer requires locals() to specify the context
-* Compatibility with pymysql 0.8.0+
-* More efficient loading of dependencies (#403)
+- Deletes are more efficient (#424)
+- ERD shows table definition on tooltip hover in Jupyter (#422)
+- S3 external storage
+- Garbage collection for external sorage
+- Most operators and methods of tables can be invoked as class methods rather than instance methods (#407)
+- The schema decorator object no longer requires locals() to specify the context
+- Compatibility with pymysql 0.8.0+
+- More efficient loading of dependencies (#403)
 
 ### 0.9.0 -- Nov 17, 2017
-* Made graphviz installation optional
-* Implement file-based external storage
-* Implement union operator +
-* Implement file-based external storage
+- Made graphviz installation optional
+- Implement file-based external storage
+- Implement union operator +
+- Implement file-based external storage
 
 ### 0.8.0 -- Jul 26, 2017
 Documentation and tutorials available at https://docs.datajoint.io and https://tutorials.datajoint.io
-* improved the ERD graphics and features using the graphviz libraries (#207, #333)
-* improved password handling logic (#322, #321)
-* the use of the `contents` property to populate tables now only works in `dj.Lookup` classes (#310).
-* allow suppressing the display of size of query results through the `show_tuple_count` configuration option (#309)
-* implemented renamed foreign keys to spec (#333)
-* added the `limit` keyword argument to populate (#329)
-* reduced the number of displayed messages (#308)
-* added `size_on_disk` property for dj.Schema() objects (#323)
-* job keys are entered in the jobs table (#316, #243)
-* simplified the `fetch` and `fetch1` syntax, deprecating the `fetch[...]` syntax (#319)
-* the jobs tables now store the connection ids to allow identifying abandoned jobs (#288, #317)
+
+- improved the ERD graphics and features using the graphviz libraries (#207, #333)
+- improved password handling logic (#322, #321)
+- the use of the `contents` property to populate tables now only works in `dj.Lookup` classes (#310).
+- allow suppressing the display of size of query results through the `show_tuple_count` configuration option (#309)
+- implemented renamed foreign keys to spec (#333)
+- added the `limit` keyword argument to populate (#329)
+- reduced the number of displayed messages (#308)
+- added `size_on_disk` property for dj.Schema() objects (#323)
+- job keys are entered in the jobs table (#316, #243)
+- simplified the `fetch` and `fetch1` syntax, deprecating the `fetch[...]` syntax (#319)
+- the jobs tables now store the connection ids to allow identifying abandoned jobs (#288, #317)
 
 ### 0.5.0 (#298) -- Mar 8, 2017
-* All fetched integers are now 64-bit long and all fetched floats are double precision.
-* Added `dj.create_virtual_module`
+- All fetched integers are now 64-bit long and all fetched floats are double precision.
+- Added `dj.create_virtual_module`
 
 ### 0.4.10 (#286) -- Feb 6, 2017
-* Removed Vagrant and Readthedocs support
-* Explicit saving of configuration (issue #284)
+- Removed Vagrant and Readthedocs support
+- Explicit saving of configuration (issue #284)
 
 ### 0.4.9 (#285) -- Feb 2, 2017
-* Fixed setup.py for pip install
+- Fixed setup.py for pip install
 
 ### 0.4.7 (#281) -- Jan 24, 2017
-* Fixed issues related to order of attributes in projection.
+- Fixed issues related to order of attributes in projection.
 
 ### 0.4.6 (#277) -- Dec 22, 2016
-* Proper handling of interruptions during populate
+- Proper handling of interruptions during populate
 
 ### 0.4.5 (#274) -- Dec 20, 2016
-* Populate reports how many keys remain to be populated at the start.
+- Populate reports how many keys remain to be populated at the start.
 
-### 0.4.3  (#271) -- Dec 6, 2016
-* Fixed aggregation issues (#270)
-* datajoint no longer attempts to connect to server at import time
-* dropped support of view (reversed #257)
-* more elegant handling of insufficient privileges (#268)
+### 0.4.3 (#271) -- Dec 6, 2016
+- Fixed aggregation issues (#270)
+- datajoint no longer attempts to connect to server at import time
+- dropped support of view (reversed #257)
+- more elegant handling of insufficient privileges (#268)
 
-### 0.4.2 (#267)  -- Dec 6, 2016
-* improved table appearance in Jupyter
+### 0.4.2 (#267) -- Dec 6, 2016
+- improved table appearance in Jupyter
 
 ### 0.4.1 (#266) -- Oct 28, 2016
-* bugfix for very long error messages
+- bugfix for very long error messages
 
 ### 0.3.9 -- Sep 27, 2016
-* Added support for datatype `YEAR`
-* Fixed issues with `dj.U` and the `aggr` operator (#246, #247)
+- Added support for datatype `YEAR`
+- Fixed issues with `dj.U` and the `aggr` operator (#246, #247)
 
-### 0.3.8  -- Aug 2, 2016
-* added the `_update` method in `base_relation`. It allows updating values in existing tuples.
-* bugfix in reading values of type double.  Previously it was cast as float32.
+### 0.3.8 -- Aug 2, 2016
+- added the `_update` method in `base_relation`. It allows updating values in existing tuples.
+- bugfix in reading values of type double. Previously it was cast as float32.
 
-### 0.3.7  -- Jul 31, 2016
-* added parameter `ignore_extra_fields` in `insert`
-* `insert(..., skip_duplicates=True)` now relies on `SELECT IGNORE`.  Previously it explicitly checked if tuple already exists.
-* table previews now include blob attributes displaying the string <BLOB>
+### 0.3.7 -- Jul 31, 2016
+- added parameter `ignore_extra_fields` in `insert`
+- `insert(..., skip_duplicates=True)` now relies on `SELECT IGNORE`. Previously it explicitly checked if tuple already exists.
+- table previews now include blob attributes displaying the string <BLOB>
 
-### 0.3.6  -- Jul 30, 2016
-* bugfix in `schema.spawn_missing_classes`.  Previously, spawned part classes would not show in ERDs.
-* dj.key now causes fetch to return as a list of dicts.  Previously it was a recarray.
+### 0.3.6 -- Jul 30, 2016
+- bugfix in `schema.spawn_missing_classes`. Previously, spawned part classes would not show in ERDs.
+- dj.key now causes fetch to return as a list of dicts. Previously it was a recarray.
 
 ### 0.3.5
-* `dj.set_password()` now asks for user confirmation before changing the password.
-* fixed issue #228
+- `dj.set_password()` now asks for user confirmation before changing the password.
+- fixed issue #228
 
 ### 0.3.4
-* Added method the `ERD.add_parts` method, which adds the part tables of all tables currently in the ERD.
-* `ERD() + arg` and `ERD() - arg` can now accept relation classes as arg.
+- Added method the `ERD.add_parts` method, which adds the part tables of all tables currently in the ERD.
+- `ERD() + arg` and `ERD() - arg` can now accept table classes as arg.
 
 ### 0.3.3
-* Suppressed warnings (redirected them to logging).  Previoiusly, scipy would throw warnings in ERD, for example.
-* Added ERD.from_sequence as a shortcut to combining the ERDs of multiple sources
-* ERD() no longer text the context argument.
-* ERD.draw() now takes an optional context argument.  By default uses the caller's locals.
-
-### 0.3.2.
-* Fixed issue #223:  `insert` can insert relations without fetching.
-* ERD() now takes the `context` argument, which specifies in which context to look for classes. The default is taken from the argument (schema or relation).
-* ERD.draw() no longer has the `prefix` argument: class names are shown as found in the context.
+- Suppressed warnings (redirected them to logging). Previoiusly, scipy would throw warnings in ERD, for example.
+- Added ERD.from_sequence as a shortcut to combining the ERDs of multiple sources
+- ERD() no longer text the context argument.
+- ERD.draw() now takes an optional context argument. By default uses the caller's locals.
+
+### 0.3.2
+- Fixed issue #223: `insert` can insert relations without fetching.
+- ERD() now takes the `context` argument, which specifies in which context to look for classes. The default is taken from the argument (schema or table).
+- ERD.draw() no longer has the `prefix` argument: class names are shown as found in the context.
diff --git a/Dockerfile b/Dockerfile
index 920f3c75c..59da930a5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,9 @@
-FROM datajoint/pydev
-
-COPY --chown=dja . /tmp/src
-RUN pip install --user /tmp/src && \
-    rm -rf /tmp/src
+ARG IMAGE=djbase
+ARG PY_VER=3.9
+ARG DISTRO=debian
+FROM datajoint/${IMAGE}:py${PY_VER}-${DISTRO}
+COPY --chown=anaconda:anaconda ./setup.py ./datajoint.pub ./requirements.txt /main/
+COPY --chown=anaconda:anaconda ./datajoint /main/datajoint
+RUN \
+    pip install --no-cache-dir /main && \
+    rm -r /main/*
diff --git a/LNX-docker-compose.yml b/LNX-docker-compose.yml
index b2b8c7cb9..e0add9bdd 100644
--- a/LNX-docker-compose.yml
+++ b/LNX-docker-compose.yml
@@ -1,21 +1,27 @@
-# docker-compose -f LNX-docker-compose.yml --env-file LNX.env up --exit-code-from app --build
-version: '2.2'
-x-net: &net
+# PY_VER=3.8 MYSQL_VER=5.7 DISTRO=alpine MINIO_VER=RELEASE.2022-08-11T04-37-28Z HOST_UID=$(id -u) docker compose -f LNX-docker-compose.yml up --exit-code-from app --build
+version: "2.4"
+x-net:
+  &net
   networks:
-      - main
+    - main
 services:
   db:
     <<: *net
-    image: datajoint/mysql:$MYSQL_VER
+    image: datajoint/mysql:${MYSQL_VER}
     environment:
-      - MYSQL_ROOT_PASSWORD=simple
+      - MYSQL_ROOT_PASSWORD=${DJ_PASS}
     # ports:
     #   - "3306:3306"
     # volumes:
     #   - ./mysql/data:/var/lib/mysql
+    healthcheck:
+      test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ]
+      timeout: 30s
+      retries: 5
+      interval: 15s
   minio:
     <<: *net
-    image: minio/minio:$MINIO_VER
+    image: minio/minio:${MINIO_VER}
     environment:
       - MINIO_ACCESS_KEY=datajoint
       - MINIO_SECRET_KEY=datajoint
@@ -26,39 +32,44 @@ services:
     #   - ./minio/data:/data
     command: server --address ":9000" /data
     healthcheck:
-      test: ["CMD", "curl", "--fail", "http://minio:9000/minio/health/live"]
-      timeout: 5s
-      retries: 60
-      interval: 1s
+      test:
+        [
+          "CMD",
+          "curl",
+          "--fail",
+          "http://minio:9000/minio/health/live"
+        ]
+      timeout: 30s
+      retries: 5
+      interval: 15s
   fakeservices.datajoint.io:
     <<: *net
-    image: datajoint/nginx:v0.2.1
+    image: datajoint/nginx:v0.2.4
     environment:
-    - ADD_db_TYPE=DATABASE
-    - ADD_db_ENDPOINT=db:3306
-    - ADD_minio_TYPE=MINIO
-    - ADD_minio_ENDPOINT=minio:9000
-    - ADD_minio_PORT=80 # allow unencrypted connections
-    - ADD_minio_PREFIX=/datajoint
+      - ADD_db_TYPE=DATABASE
+      - ADD_db_ENDPOINT=db:3306
+      - ADD_minio_TYPE=MINIO
+      - ADD_minio_ENDPOINT=minio:9000
+      - ADD_minio_PORT=80 # allow unencrypted connections
+      - ADD_minio_PREFIX=/datajoint
     # ports:
     #   - "80:80"
     #   - "443:443"
     #   - "3306:3306"
+  app:
+    <<: *net
+    image: datajoint/djtest:py${PY_VER}-${DISTRO}
     depends_on:
       db:
         condition: service_healthy
       minio:
         condition: service_healthy
-  app:
-    <<: *net
-    image: datajoint/pydev:${PY_VER}-alpine${ALPINE_VER}
-    depends_on:
       fakeservices.datajoint.io:
         condition: service_healthy
     environment:
       - DJ_HOST=fakeservices.datajoint.io
       - DJ_USER=root
-      - DJ_PASS=simple
+      - DJ_PASS
       - DJ_TEST_HOST=fakeservices.datajoint.io
       - DJ_TEST_USER=datajoint
       - DJ_TEST_PASSWORD=datajoint
@@ -69,23 +80,19 @@ services:
       - PYTHON_USER=dja
       - JUPYTER_PASSWORD=datajoint
       - DISPLAY
-      - COVERALLS_SERVICE_NAME
-      - COVERALLS_REPO_TOKEN
     working_dir: /src
     command:
       - sh
       - -c
       - |
         set -e
-        pip install --user -r test_requirements.txt
+        pip install --user nose nose-cov
         pip install -e .
-        pip freeze | grep datajoint
+        pip list --format=freeze | grep datajoint
         nosetests -vsw tests --with-coverage --cover-package=datajoint
-        coveralls
-        # jupyter notebook
     # ports:
     #   - "8888:8888"
-    user: ${UID}:${GID}
+    user: ${HOST_UID}:anaconda
     volumes:
       - .:/src
       - /tmp/.X11-unix:/tmp/.X11-unix:rw
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 85e421b32..000000000
--- a/Makefile
+++ /dev/null
@@ -1,28 +0,0 @@
-all:
-	@echo 'MakeFile for DataJoint packaging                                              '
-	@echo '                                                                              '
-	@echo 'make sdist                              Creates source distribution           '
-	@echo 'make wheel                              Creates Wheel distribution            '
-	@echo 'make pypi                               Package and upload to PyPI            '
-	@echo 'make pypitest                           Package and upload to PyPI test server'
-	@echo 'make clean                              Remove all build related directories  '
-	
-
-sdist:
-	python3 setup.py sdist >/dev/null 2>&1
-
-wheel:
-	python3 setup.py bdist_wheel >/dev/null 2>&1
-
-pypi:clean sdist wheel
-	twine upload dist/*
-	
-pypitest: clean sdist wheel
-	twine upload -r pypitest dist/*
-
-clean:
-	rm -rf dist && rm -rf build && rm -rf *.egg-info
-
-
-
-
diff --git a/README.md b/README.md
index 8d25fac4d..fc290fe53 100644
--- a/README.md
+++ b/README.md
@@ -6,162 +6,28 @@
 [![Slack](https://img.shields.io/badge/slack-chat-green.svg)](https://datajoint.slack.com/)
 
 # Welcome to DataJoint for Python!
+
 DataJoint for Python is a framework for scientific workflow management based on relational principles. DataJoint is built on the foundation of the relational data model and prescribes a consistent method for organizing, populating, computing, and querying data.
 
 DataJoint was initially developed in 2009 by Dimitri Yatsenko in Andreas Tolias' Lab at Baylor College of Medicine for the distributed processing and management of large volumes of data streaming from regular experiments. Starting in 2011, DataJoint has been available as an open-source project adopted by other labs and improved through contributions from several developers.
-Presently, the primary developer of DataJoint open-source software is the company DataJoint (https://datajoint.com). Related resources are listed at https://datajoint.org.
-
-## Installation
-```
-pip3 install datajoint
-```
+Presently, the primary developer of DataJoint open-source software is the company DataJoint (https://datajoint.com).
 
-If you already have an older version of DataJoint installed using `pip`, upgrade with
-```bash
-pip3 install --upgrade datajoint
-```
+- [Getting Started](https://datajoint.com/docs/core/datajoint-python/latest/getting-started/)
+- [DataJoint Elements](https://datajoint.com/docs/elements/) - Catalog of example pipelines
+- [DataJoint CodeBook](https://codebook.datajoint.io) - Interactive online tutorials
+- Contribute
 
-## Documentation and Tutorials
+  - [Development Environment](https://datajoint.com/docs/core/datajoint-python/latest/develop/)
+  - [Guidelines](https://datajoint.com/docs/community/contribute/)
 
-* https://datajoint.org  -- start page
-* https://docs.datajoint.org -- up-to-date documentation
-* https://tutorials.datajoint.io -- step-by-step tutorials
-* https://elements.datajoint.org -- catalog of example pipelines
-* https://codebook.datajoint.io -- interactive online tutorials
+- Legacy Resources (To be replaced by above)
+  - [Documentation](https://docs.datajoint.org)
+  - [Tutorials](https://tutorials.datajoint.org)
 
 ## Citation
-+ If your work uses DataJoint for Python, please cite the following Research Resource Identifier (RRID) and manuscript.
-
-+ DataJoint ([RRID:SCR_014543](https://scicrunch.org/resolver/SCR_014543)) - DataJoint for Python (version `<Enter version number>`)
-
-+ Yatsenko D, Reimer J, Ecker AS, Walker EY, Sinz F, Berens P, Hoenselaar A, Cotton RJ, Siapas AS, Tolias AS. DataJoint: managing big scientific data using MATLAB or Python. bioRxiv. 2015 Jan 1:031658. doi: https://doi.org/10.1101/031658
-
-## Python Native Blobs
-<details>
-<summary>Click to expand details</summary>
-
-DataJoint 0.12 adds full support for all native python data types in blobs: tuples, lists, sets, dicts, strings, bytes, `None`, and all their recursive combinations.
-The new blobs are a superset of the old functionality and are fully backward compatible.
-In previous versions, only MATLAB-style numerical arrays were fully supported.
-Some Python datatypes such as dicts were coerced into numpy recarrays and then fetched as such.
-
-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 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).
-
-The flag is configured by setting the `enable_python_native_blobs` flag in `dj.config`. 
-
-```python
-import datajoint as dj
-dj.config["enable_python_native_blobs"] = True
-```
-
-You can safely enable this setting if both of the following are true:
-
-  * The only kinds of blobs your pipeline have inserted previously were numerical arrays.
-  * You do not need to share blob data between Python and MATLAB.
-
-Otherwise, read the following explanation.
-
-DataJoint v0.12 expands DataJoint's blob serialization mechanism with
-improved support for complex native python datatypes, such as dictionaries
-and lists of strings.
-
-Prior to DataJoint v0.12, certain python native datatypes such as
-dictionaries were 'squashed' into numpy structured arrays when saved into
-blob attributes. This facilitated easier data sharing between MATLAB
-and Python for certain record types. However, this created a discrepancy
-between insert and fetch datatypes which could cause problems in other
-portions of users pipelines.
-
-DataJoint v0.12, removes the squashing behavior, instead encoding native python datatypes in blobs directly. 
-However, this change creates a compatibility problem for pipelines 
-which previously relied on the type squashing behavior since records 
-saved via the old squashing format will continue to fetch
-as structured arrays, whereas new record inserted in DataJoint 0.12 with
-`enable_python_native_blobs` would result in records returned as the
-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`, 
-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.
-
-The exact process to update a specific pipeline will vary depending on
-the situation, but generally the following strategies may apply:
-
-  * Altering code to directly store numpy structured arrays or plain
-    multidimensional arrays. This strategy is likely best one for those 
-    tables requiring compatibility with MATLAB.
-  * Adjust code to deal with both structured array and native fetched data
-    for those tables that are populated with `dict`s in blobs in pre-0.12 version. 
-    In this case, insert logic is not adjusted, but downstream consumers
-    are adjusted to handle records saved under the old and new schemes.
-  * Migrate data into a fresh schema, fetching the old data, converting blobs to 
-    a uniform data type and re-inserting.
-  * Drop/Recompute imported/computed tables to ensure they are in the new
-    format.
-
-As always, be sure that your data is safely backed up before modifying any
-important DataJoint schema or records.
-
-</details>
-
-### API docs
-
-The API documentation can be built using sphinx by running
-
-``` bash
-pip install sphinx sphinx_rtd_theme 
-(cd docs-api/sphinx && make html)
-```
-
-Generated docs are written to `docs-api/docs/html/index.html`.
-More details in [docs-api/README.md](docs-api/README.md).
-
-## Running Tests Locally
-<details>
-<summary>Click to expand details</summary>
-
-* Create an `.env` with desired development environment values e.g.
-``` sh
-PY_VER=3.7
-ALPINE_VER=3.10
-MYSQL_VER=5.7
-MINIO_VER=RELEASE.2021-09-03T03-56-13Z
-UID=1000
-GID=1000
-```
-* `cp local-docker-compose.yml docker-compose.yml`
-* `docker-compose up -d` (Note configured `JUPYTER_PASSWORD`)
-* Select a means of running Tests e.g. Docker Terminal, or Local Terminal (see bottom)
-* Add entry in `/etc/hosts` for `127.0.0.1 fakeservices.datajoint.io`
-* Run desired tests. Some examples are as follows:
-
-| Use Case                     | Shell Code                                                                    |
-| ---------------------------- | ------------------------------------------------------------------------------ |
-| Run all tests                | `nosetests -vsw tests --with-coverage --cover-package=datajoint`                                                              |
-| Run one specific class test       | `nosetests -vs --tests=tests.test_fetch:TestFetch.test_getattribute_for_fetch1`                                                           |
-| Run one specific basic test        | `nosetests -vs --tests=tests.test_external_class:test_insert_and_fetch`                                   |
-
-
-### Launch Docker Terminal
-* Shell into `datajoint-python_app_1` i.e. `docker exec -it datajoint-python_app_1 sh`
-
-
-### Launch Local Terminal
-* See `datajoint-python_app` environment variables in `local-docker-compose.yml`
-* Launch local terminal
-* `export` environment variables in shell
-* Add entry in `/etc/hosts` for `127.0.0.1 fakeservices.datajoint.io`
 
+- If your work uses DataJoint for Python, please cite the following Research Resource Identifier (RRID) and manuscript.
 
-### Launch Jupyter Notebook for Interactive Use
-* Navigate to `localhost:8888`
-* Input Jupyter password
-* Launch a notebook i.e. `New > Python 3`
+- DataJoint ([RRID:SCR_014543](https://scicrunch.org/resolver/SCR_014543)) - DataJoint for Python (version `<Enter version number>`)
 
-</details>
\ No newline at end of file
+- Yatsenko D, Reimer J, Ecker AS, Walker EY, Sinz F, Berens P, Hoenselaar A, Cotton RJ, Siapas AS, Tolias AS. DataJoint: managing big scientific data using MATLAB or Python. bioRxiv. 2015 Jan 1:031658. doi: https://doi.org/10.1101/031658
diff --git a/datajoint/__init__.py b/datajoint/__init__.py
index e512d85d6..b73ade94a 100644
--- a/datajoint/__init__.py
+++ b/datajoint/__init__.py
@@ -50,6 +50,7 @@
     "DataJointError",
     "key",
     "key_hash",
+    "logger",
 ]
 
 from .logging import logger
@@ -69,7 +70,6 @@
 from .attribute_adapter import AttributeAdapter
 from . import errors
 from .errors import DataJointError
-from .migrate import migrate_dj011_external_blob_storage_to_dj012
 
 ERD = Di = Diagram  # Aliases for Diagram
 schema = Schema  # Aliases for Schema
diff --git a/datajoint/admin.py b/datajoint/admin.py
index 667789ef3..3b3933300 100644
--- a/datajoint/admin.py
+++ b/datajoint/admin.py
@@ -3,6 +3,9 @@
 from .connection import conn
 from .settings import config
 from .utils import user_choice
+import logging
+
+logger = logging.getLogger(__name__.split(".")[0])
 
 
 def set_password(
@@ -13,10 +16,10 @@ def set_password(
         new_password = getpass("New password: ")
         confirm_password = getpass("Confirm password: ")
         if new_password != confirm_password:
-            print("Failed to confirm the password! Aborting password change.")
+            logger.warn("Failed to confirm the password! Aborting password change.")
             return
     connection.query("SET PASSWORD = PASSWORD('%s')" % new_password)
-    print("Password updated.")
+    logger.info("Password updated.")
 
     if update_config or (
         update_config is None and user_choice("Update local setting?") == "yes"
@@ -81,7 +84,7 @@ def kill(restriction=None, connection=None, order_by=None):  # pragma: no cover
                 try:
                     connection.query("kill %d" % pid)
                 except pymysql.err.InternalError:
-                    print("Process not found")
+                    logger.warn("Process not found")
 
 
 def kill_quick(restriction=None, connection=None):
diff --git a/datajoint/autopopulate.py b/datajoint/autopopulate.py
index b0c31b6d1..ae8413081 100644
--- a/datajoint/autopopulate.py
+++ b/datajoint/autopopulate.py
@@ -5,6 +5,7 @@
 import random
 import inspect
 from tqdm import tqdm
+from .hash import key_hash
 from .expression import QueryExpression, AndList
 from .errors import DataJointError, LostConnectionError
 import signal
@@ -43,8 +44,8 @@ def _call_populate1(key):
 
 class AutoPopulate:
     """
-    AutoPopulate is a mixin class that adds the method populate() to a Relation class.
-    Auto-populated relations must inherit from both Relation and AutoPopulate,
+    AutoPopulate is a mixin class that adds the method populate() to a Table class.
+    Auto-populated tables must inherit from both Table and AutoPopulate,
     must define the property `key_source`, and must define the callback method `make`.
     """
 
@@ -117,7 +118,7 @@ def _job_key(self, key):
 
     def _jobs_to_do(self, restrictions):
         """
-        :return: the relation containing the keys to be computed (derived from self.key_source)
+        :return: the query yeilding the keys to be computed (derived from self.key_source)
         """
         if self.restriction:
             raise DataJointError(
@@ -202,6 +203,16 @@ def handler(signum, frame):
             old_handler = signal.signal(signal.SIGTERM, handler)
 
         keys = (self._jobs_to_do(restrictions) - self.target).fetch("KEY", limit=limit)
+
+        # exclude "error" or "ignore" jobs
+        if reserve_jobs:
+            exclude_key_hashes = (
+                jobs
+                & {"table_name": self.target.table_name}
+                & 'status in ("error", "ignore")'
+            ).fetch("key_hash")
+            keys = [key for key in keys if key_hash(key) not in exclude_key_hashes]
+
         if order == "reverse":
             keys.reverse()
         elif order == "random":
@@ -314,7 +325,7 @@ def _populate1(
                 finally:
                     self.__class__._allow_insert = False
 
-    def progress(self, *restrictions, display=True):
+    def progress(self, *restrictions, display=False):
         """
         Report the progress of populating the table.
         :return: (remaining, total) -- numbers of tuples to be populated
@@ -323,9 +334,9 @@ def progress(self, *restrictions, display=True):
         total = len(todo)
         remaining = len(todo - self.target)
         if display:
-            print(
-                "%-20s" % self.__class__.__name__,
-                "Completed %d of %d (%2.1f%%)   %s"
+            logger.info(
+                "%-20s" % self.__class__.__name__
+                + " Completed %d of %d (%2.1f%%)   %s"
                 % (
                     total - remaining,
                     total,
@@ -334,6 +345,5 @@ def progress(self, *restrictions, display=True):
                         datetime.datetime.now(), "%Y-%m-%d %H:%M:%S"
                     ),
                 ),
-                flush=True,
             )
         return remaining, total
diff --git a/datajoint/condition.py b/datajoint/condition.py
index 397f68b53..80786c84c 100644
--- a/datajoint/condition.py
+++ b/datajoint/condition.py
@@ -8,8 +8,29 @@
 import decimal
 import numpy
 import pandas
+import json
 from .errors import DataJointError
 
+JSON_PATTERN = re.compile(
+    r"^(?P<attr>\w+)(\.(?P<path>[\w.*\[\]]+))?(:(?P<type>[\w(,\s)]+))?$"
+)
+
+
+def translate_attribute(key):
+    match = JSON_PATTERN.match(key)
+    if match is None:
+        return match, key
+    match = match.groupdict()
+    if match["path"] is None:
+        return match, match["attr"]
+    else:
+        return match, "json_value(`{}`, _utf8mb4'$.{}'{})".format(
+            *[
+                ((f" returning {v}" if k == "type" else v) if v else "")
+                for k, v in match.items()
+            ]
+        )
+
 
 class PromiscuousOperand:
     """
@@ -94,8 +115,19 @@ def make_condition(query_expression, condition, columns):
     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:
+        """prepare SQL condition"""
+        key_match, k = translate_attribute(k)
+        if key_match["path"] is None:
+            k = f"`{k}`"
+        if (
+            query_expression.heading[key_match["attr"]].json
+            and key_match["path"] is not None
+            and isinstance(v, dict)
+        ):
+            return f"{k}='{json.dumps(v)}'"
+        if v is None:
+            return f"{k} IS NULL"
+        if query_expression.heading[key_match["attr"]].uuid:
             if not isinstance(v, uuid.UUID):
                 try:
                     v = uuid.UUID(v)
@@ -103,26 +135,36 @@ def prep_value(k, v):
                     raise DataJointError(
                         "Badly formed UUID {v} in restriction by `{k}`".format(k=k, v=v)
                     )
-            return "X'%s'" % v.bytes.hex()
+            return f"{k}=X'{v.bytes.hex()}'"
         if isinstance(
-            v, (datetime.date, datetime.datetime, datetime.time, decimal.Decimal)
+            v,
+            (
+                datetime.date,
+                datetime.datetime,
+                datetime.time,
+                decimal.Decimal,
+                list,
+            ),
         ):
-            return '"%s"' % v
+            return f'{k}="{v}"'
         if isinstance(v, str):
-            return '"%s"' % v.replace("%", "%%")
-        return "%r" % v
+            v = v.replace("%", "%%").replace("\\", "\\\\")
+            return f'{k}="{v}"'
+        return f"{k}={v}"
+
+    def combine_conditions(negate, conditions):
+        return f"{'NOT ' if negate else ''} ({')AND('.join(conditions)})"
 
     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(
-            "%", "%%"
+        return combine_conditions(
+            negate, conditions=[condition.strip().replace("%", "%%")]
         )  # escape %, see issue #376
 
     # restrict by AndList
@@ -139,7 +181,7 @@ def prep_value(k, v):
             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) + ")")
+        return combine_conditions(negate, conditions=items)
 
     # restriction by dj.U evaluates to True
     if isinstance(condition, U):
@@ -151,23 +193,19 @@ def prep_value(k, v):
 
     # restrict by a mapping/dict -- convert to an AndList of string equality conditions
     if isinstance(condition, collections.abc.Mapping):
-        common_attributes = set(condition).intersection(query_expression.heading.names)
+        common_attributes = set(c.split(".", 1)[0] for c in 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,
-                    " IS NULL"
-                    if condition[k] is None
-                    else f"={prep_value(k, condition[k])}",
-                )
-                for k in common_attributes
-            )
-            + ")"
+        return combine_conditions(
+            negate,
+            conditions=[
+                prep_value(k, v)
+                for k, v in condition.items()
+                if k.split(".", 1)[0] in common_attributes  # handle json indexing
+            ],
         )
 
     # restrict by a numpy record -- convert to an AndList of string equality conditions
@@ -178,12 +216,9 @@ def prep_value(k, v):
         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
-            )
-            + ")"
+        return combine_conditions(
+            negate,
+            conditions=[prep_value(k, condition[k]) for k in common_attributes],
         )
 
     # restrict by a QueryExpression subclass -- trigger instantiation and move on
@@ -231,7 +266,11 @@ def prep_value(k, v):
         ]  # ignore False conditions
         if any(item is True for item in or_list):  # if any item is True, entirely True
             return not negate
-        return template % ("(%s)" % " OR ".join(or_list)) if or_list else negate
+        return (
+            f"{'NOT ' if negate else ''} ({' OR '.join(or_list)})"
+            if or_list
+            else negate
+        )
 
 
 def extract_column_names(sql_expression):
diff --git a/datajoint/declare.py b/datajoint/declare.py
index 4ad1285f2..683e34759 100644
--- a/datajoint/declare.py
+++ b/datajoint/declare.py
@@ -5,9 +5,9 @@
 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 .condition import translate_attribute
 
 UUID_DATA_TYPE = "binary(16)"
 MAX_TABLE_NAME_LENGTH = 64
@@ -24,6 +24,7 @@
         DECIMAL=r"(decimal|numeric)(\s*\(.+\))?(\s+unsigned)?$",
         FLOAT=r"(double|float|real)(\s*\(.+\))?(\s+unsigned)?$",
         STRING=r"(var)?char\s*\(.+\)$",
+        JSON=r"json$",
         ENUM=r"enum\s*\(.+\)$",
         BOOL=r"bool(ean)?$",  # aliased to tinyint(1)
         TEMPORAL=r"(date|datetime|time|timestamp|year)(\s*\(.+\))?$",
@@ -130,25 +131,9 @@ def build_attribute_parser():
     return attribute_name + pp.Optional(default) + colon + data_type + comment
 
 
-def build_index_parser():
-    left = pp.Literal("(").suppress()
-    right = pp.Literal(")").suppress()
-    unique = pp.Optional(pp.CaselessKeyword("unique")).setResultsName("unique")
-    index = pp.CaselessKeyword("index").suppress()
-    attribute_name = pp.Word(pp.srange("[a-z]"), pp.srange("[a-z0-9_]"))
-    return (
-        unique
-        + index
-        + left
-        + pp.delimitedList(attribute_name).setResultsName("attr_list")
-        + right
-    )
-
-
 foreign_key_parser_old = build_foreign_key_parser_old()
 foreign_key_parser = build_foreign_key_parser()
 attribute_parser = build_attribute_parser()
-index_parser = build_index_parser()
 
 
 def is_foreign_key(line):
@@ -178,19 +163,14 @@ def compile_foreign_key(
     from .table import Table
     from .expression import QueryExpression
 
-    obsolete = False  # See issue #436.  Old style to be deprecated in a future release
     try:
         result = foreign_key_parser.parseString(line)
-    except pp.ParseException:
-        try:
-            result = foreign_key_parser_old.parseString(line)
-        except pp.ParseBaseException as err:
-            raise DataJointError('Parsing error in line "%s". %s.' % (line, err))
-        else:
-            obsolete = True
+    except pp.ParseException as err:
+        raise DataJointError('Parsing error in line "%s". %s.' % (line, err))
+
     try:
         ref = eval(result.ref_table, context)
-    except NameError if obsolete else Exception:
+    except Exception:
         raise DataJointError(
             "Foreign key reference %s could not be resolved" % result.ref_table
         )
@@ -206,18 +186,6 @@ def compile_foreign_key(
             'Primary dependencies cannot be nullable in line "{line}"'.format(line=line)
         )
 
-    if obsolete:
-        logger.warning(
-            '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()
 
@@ -233,55 +201,6 @@ def compile_foreign_key(
             % result.ref_table
         )
 
-    if obsolete:
-        # for backward compatibility with old-style dependency declarations.  See issue #436
-        if not isinstance(ref, Table):
-            DataJointError(
-                'Dependency "%s" is not supported. Check documentation.'
-                % result.ref_table
-            )
-        if not all(r in ref.primary_key for r in result.ref_attrs):
-            raise DataJointError('Invalid foreign key attributes in "%s"' % line)
-        try:
-            raise DataJointError(
-                'Duplicate attributes "{attr}" in "{line}"'.format(
-                    attr=next(attr for attr in result.new_attrs if attr in attributes),
-                    line=line,
-                )
-            )
-        except StopIteration:
-            pass  # the normal outcome
-
-        # Match the primary attributes of the referenced table to local attributes
-        new_attrs = list(result.new_attrs)
-        ref_attrs = list(result.ref_attrs)
-
-        # special case, the renamed attribute is implicit
-        if new_attrs and not ref_attrs:
-            if len(new_attrs) != 1:
-                raise DataJointError(
-                    'Renamed foreign key must be mapped to the primary key in "%s"'
-                    % line
-                )
-            if len(ref.primary_key) == 1:
-                # if the primary key has one attribute, allow implicit renaming
-                ref_attrs = ref.primary_key
-            else:
-                # if only one primary key attribute remains, then allow implicit renaming
-                ref_attrs = [attr for attr in ref.primary_key if attr not in attributes]
-                if len(ref_attrs) != 1:
-                    raise DataJointError(
-                        'Could not resolve which primary key attribute should be referenced in "%s"'
-                        % line
-                    )
-
-        if len(new_attrs) != len(ref_attrs):
-            raise DataJointError('Mismatched attributes in foreign key "%s"' % line)
-
-        if ref_attrs:
-            # convert to projected dependency
-            ref = ref.proj(**dict(zip(new_attrs, ref_attrs)))
-
     # declare new foreign key attributes
     for attr in ref.primary_key:
         if attr not in attributes:
@@ -342,7 +261,7 @@ def prepare_declare(definition, context):
                 foreign_key_sql,
                 index_sql,
             )
-        elif re.match(r"^(unique\s+)?index[^:]*$", line, re.I):  # index
+        elif re.match(r"^(unique\s+)?index\s*.*$", line, re.I):  # index
             compile_index(line, index_sql)
         else:
             name, sql, store = compile_attribute(line, in_key, foreign_key_sql, context)
@@ -516,10 +435,22 @@ def alter(definition, old_definition, context):
 
 
 def compile_index(line, index_sql):
-    match = index_parser.parseString(line)
+    def format_attribute(attr):
+        match, attr = translate_attribute(attr)
+        if match is None:
+            return attr
+        if match["path"] is None:
+            return f"`{attr}`"
+        return f"({attr})"
+
+    match = re.match(
+        r"(?P<unique>unique\s+)?index\s*\(\s*(?P<args>.*)\)", line, re.I
+    ).groupdict()
+    attr_list = re.findall(r"(?:[^,(]|\([^)]*\))+", match["args"])
     index_sql.append(
-        "{unique} index ({attrs})".format(
-            unique=match.unique, attrs=",".join("`%s`" % a for a in match.attr_list)
+        "{unique}index ({attrs})".format(
+            unique="unique " if match["unique"] else "",
+            attrs=",".join(format_attribute(a.strip()) for a in attr_list),
         )
     )
 
diff --git a/datajoint/diagram.py b/datajoint/diagram.py
index dd892038a..c58afde47 100644
--- a/datajoint/diagram.py
+++ b/datajoint/diagram.py
@@ -78,7 +78,7 @@ class Diagram(nx.DiGraph):
 
         >>>  diag = Diagram(source)
 
-        source can be a base relation object, a base relation class, a schema, or a module that has a schema.
+        source can be a base table object, a base table class, a schema, or a module that has a schema.
 
         >>> diag.draw()
 
@@ -94,7 +94,6 @@ class Diagram(nx.DiGraph):
         """
 
         def __init__(self, source, context=None):
-
             if isinstance(source, Diagram):
                 # copy constructor
                 self.nodes_to_show = set(source.nodes_to_show)
@@ -302,7 +301,6 @@ def _make_graph(self):
             return graph
 
         def make_dot(self):
-
             graph = self._make_graph()
             graph.nodes()
 
@@ -385,9 +383,7 @@ def make_dot(self):
                 if name.split(".")[0] in self.context:
                     cls = eval(name, self.context)
                     assert issubclass(cls, Table)
-                    description = (
-                        cls().describe(context=self.context, printout=False).split("\n")
-                    )
+                    description = cls().describe(context=self.context).split("\n")
                     description = (
                         "-" * 30
                         if q.startswith("---")
diff --git a/datajoint/expression.py b/datajoint/expression.py
index ab2d27eec..25dd2fe40 100644
--- a/datajoint/expression.py
+++ b/datajoint/expression.py
@@ -14,6 +14,7 @@
     assert_join_compatibility,
     extract_column_names,
     PromiscuousOperand,
+    translate_attribute,
 )
 from .declare import CONSTANT_LITERALS
 
@@ -342,6 +343,9 @@ def proj(self, *attributes, **named_attributes):
         from other attributes available before the projection.
         Each attribute name can only be used once.
         """
+        named_attributes = {
+            k: translate_attribute(v)[1] for k, v in named_attributes.items()
+        }
         # new attributes in parentheses are included again with the new name without removing original
         duplication_pattern = re.compile(
             rf'^\s*\(\s*(?!{"|".join(CONSTANT_LITERALS)})(?P<name>[a-zA-Z_]\w*)\s*\)\s*$'
diff --git a/datajoint/fetch.py b/datajoint/fetch.py
index 41a2cbd46..750939e5e 100644
--- a/datajoint/fetch.py
+++ b/datajoint/fetch.py
@@ -4,6 +4,7 @@
 import pandas
 import itertools
 import re
+import json
 import numpy as np
 import uuid
 import numbers
@@ -47,6 +48,8 @@ def _get(connection, attr, data, squeeze, download_path):
     """
     if data is None:
         return
+    if attr.json:
+        return json.loads(data)
 
     extern = (
         connection.schemas[attr.database].external[attr.store]
@@ -59,7 +62,6 @@ def _get(connection, attr, data, squeeze, download_path):
 
     if attr.is_filepath:
         return adapt(extern.download_filepath(uuid.UUID(bytes=data))[0])
-
     if attr.is_attachment:
         # Steps:
         # 1. get the attachment filename
@@ -158,7 +160,7 @@ def __call__(
         unpacks blob attributes.
 
         :param attrs: zero or more attributes to fetch. If not provided, the call will return all attributes of this
-                        relation. If provided, returns tuples with an entry for each attribute.
+                        table. If provided, returns tuples with an entry for each attribute.
         :param offset: the number of tuples to skip in the returned result
         :param limit: the maximum number of tuples to return
         :param order_by: a single attribute or the list of attributes to order the results. No ordering should be assumed
@@ -170,7 +172,7 @@ def __call__(
                         True for .fetch('KEY')
         :param squeeze:  if True, remove extra dimensions from arrays
         :param download_path: for fetches that download data, e.g. attachments
-        :return: the contents of the relation in the form of a structured numpy.array or a dict list
+        :return: the contents of the table in the form of a structured numpy.array or a dict list
         """
         if order_by is not None:
             # if 'order_by' passed in a string, make into list
@@ -317,7 +319,7 @@ def __call__(self, *attrs, squeeze=False, download_path="."):
                  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
+        :return: the one tuple in the table in the form of a dict
         """
         heading = self._expression.heading
 
diff --git a/datajoint/heading.py b/datajoint/heading.py
index 52f92ccb9..9a782fc0e 100644
--- a/datajoint/heading.py
+++ b/datajoint/heading.py
@@ -28,6 +28,7 @@
         numeric=None,
         string=None,
         uuid=False,
+        json=None,
         is_blob=False,
         is_attachment=False,
         is_filepath=False,
@@ -84,7 +85,7 @@ def original_name(self):
 
 class Heading:
     """
-    Local class for relations' headings.
+    Local class for table headings.
     Heading contains the property attributes, which is an dict in which the keys are
     the attribute names and the values are Attributes.
     """
@@ -142,7 +143,7 @@ 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
+            if not (v.is_blob or v.is_attachment or v.is_filepath or v.json)
         ]
 
     @property
@@ -273,7 +274,6 @@ def _init_from_database(self):
 
         # additional attribute properties
         for attr in attributes:
-
             attr.update(
                 in_key=(attr["in_key"] == "PRI"),
                 database=database,
@@ -291,6 +291,7 @@ def _init_from_database(self):
                 ),
                 is_blob=bool(TYPE_PATTERN["INTERNAL_BLOB"].match(attr["type"])),
                 uuid=False,
+                json=bool(TYPE_PATTERN["JSON"].match(attr["type"])),
                 is_attachment=False,
                 is_filepath=False,
                 adapter=None,
@@ -376,10 +377,15 @@ def _init_from_database(self):
                 )
 
             if attr["in_key"] and any(
-                (attr["is_blob"], attr["is_attachment"], attr["is_filepath"])
+                (
+                    attr["is_blob"],
+                    attr["is_attachment"],
+                    attr["is_filepath"],
+                    attr["json"],
+                )
             ):
                 raise DataJointError(
-                    "Blob, attachment, or filepath attributes are not allowed in the primary key"
+                    "Json, Blob, attachment, or filepath attributes are not allowed in the primary key"
                 )
 
             if (
@@ -420,7 +426,8 @@ def _init_from_database(self):
         ):
             if item["Key_name"] != "PRIMARY":
                 keys[item["Key_name"]][item["Seq_in_index"]] = dict(
-                    column=item["Column_name"],
+                    column=item["Column_name"]
+                    or f"({item['Expression']})".replace(r"\'", "'"),
                     unique=(item["Non_unique"] == 0),
                     nullable=item["Null"].lower() == "yes",
                 )
diff --git a/datajoint/jobs.py b/datajoint/jobs.py
index 8aedded42..2763d51f4 100644
--- a/datajoint/jobs.py
+++ b/datajoint/jobs.py
@@ -12,7 +12,7 @@
 
 class JobTable(Table):
     """
-    A base relation with no definition. Allows reserving jobs
+    A base table with no definition. Allows reserving jobs
     """
 
     def __init__(self, conn, database):
@@ -87,6 +87,37 @@ def reserve(self, table_name, key):
             return False
         return True
 
+    def ignore(self, table_name, key):
+        """
+        Set a job to be ignored for computation.  When a job is ignored, the job table contains an entry for the
+        job key, identified by its hash, with status "ignore".
+
+        Args:
+        table_name:
+            Table name (str) - `database`.`table_name`
+        key:
+            The dict of the job's primary key
+
+        Returns:
+            True if ignore job successfully. False = the jobs is already taken
+        """
+        job = dict(
+            table_name=table_name,
+            key_hash=key_hash(key),
+            status="ignore",
+            host=platform.node(),
+            pid=os.getpid(),
+            connection_id=self.connection.connection_id,
+            key=key,
+            user=self._user,
+        )
+        try:
+            with config(enable_python_native_blobs=True):
+                self.insert1(job, ignore_extra_fields=True)
+        except DuplicateError:
+            return False
+        return True
+
     def complete(self, table_name, key):
         """
         Log a completed job.  When a job is completed, its reservation entry is deleted.
diff --git a/datajoint/logging.py b/datajoint/logging.py
index 5f0f3eb06..b432e1a4b 100644
--- a/datajoint/logging.py
+++ b/datajoint/logging.py
@@ -1,7 +1,6 @@
 import logging
 import os
 import sys
-import io
 
 logger = logging.getLogger(__name__.split(".")[0])
 
diff --git a/datajoint/migrate.py b/datajoint/migrate.py
deleted file mode 100644
index 41654b68e..000000000
--- a/datajoint/migrate.py
+++ /dev/null
@@ -1,202 +0,0 @@
-import datajoint as dj
-from pathlib import Path
-import re
-from .utils import user_choice
-
-
-def migrate_dj011_external_blob_storage_to_dj012(migration_schema, store):
-    """
-    Utility function to migrate external blob data from 0.11 to 0.12.
-
-    :param migration_schema: string of target schema to be migrated
-    :param store: string of target dj.config['store'] to be migrated
-    """
-    if not isinstance(migration_schema, str):
-        raise ValueError(
-            "Expected type {} for migration_schema, not {}.".format(
-                str, type(migration_schema)
-            )
-        )
-
-    do_migration = False
-    do_migration = (
-        user_choice(
-            """
-Warning: Ensure the following are completed before proceeding.
-- Appropriate backups have been taken,
-- Any existing DJ 0.11.X connections are suspended, and
-- External config has been updated to new dj.config['stores'] structure.
-Proceed?
-            """,
-            default="no",
-        )
-        == "yes"
-    )
-    if do_migration:
-        _migrate_dj011_blob(dj.Schema(migration_schema), store)
-        print(
-            "Migration completed for schema: {}, store: {}.".format(
-                migration_schema, store
-            )
-        )
-        return
-    print("No migration performed.")
-
-
-def _migrate_dj011_blob(schema, default_store):
-    query = schema.connection.query
-
-    LEGACY_HASH_SIZE = 43
-
-    legacy_external = dj.FreeTable(
-        schema.connection, "`{db}`.`~external`".format(db=schema.database)
-    )
-
-    # get referencing tables
-    refs = [
-        {k.lower(): v for k, v in elem.items()}
-        for elem in query(
-            """
-    SELECT concat('`', table_schema, '`.`', table_name, '`')
-            as referencing_table, column_name, constraint_name
-    FROM information_schema.key_column_usage
-    WHERE referenced_table_name="{tab}" and referenced_table_schema="{db}"
-    """.format(
-                tab=legacy_external.table_name, db=legacy_external.database
-            ),
-            as_dict=True,
-        ).fetchall()
-    ]
-
-    for ref in refs:
-        # get comment
-        column = query(
-            "SHOW FULL COLUMNS FROM {referencing_table}"
-            'WHERE Field="{column_name}"'.format(**ref),
-            as_dict=True,
-        ).fetchone()
-
-        store, comment = re.match(
-            r":external(-(?P<store>.+))?:(?P<comment>.*)", column["Comment"]
-        ).group("store", "comment")
-
-        # get all the hashes from the reference
-        hashes = {
-            x[0]
-            for x in query(
-                "SELECT `{column_name}` FROM {referencing_table}".format(**ref)
-            )
-        }
-
-        # sanity check make sure that store suffixes match
-        if store is None:
-            assert all(len(_) == LEGACY_HASH_SIZE for _ in hashes)
-        else:
-            assert all(_[LEGACY_HASH_SIZE:] == store for _ in hashes)
-
-        # create new-style external table
-        ext = schema.external[store or default_store]
-
-        # add the new-style reference field
-        temp_suffix = "tempsub"
-
-        try:
-            query(
-                """ALTER TABLE {referencing_table}
-                ADD COLUMN `{column_name}_{temp_suffix}` {type} DEFAULT NULL
-            COMMENT ":blob@{store}:{comment}"
-            """.format(
-                    type=dj.declare.UUID_DATA_TYPE,
-                    temp_suffix=temp_suffix,
-                    store=(store or default_store),
-                    comment=comment,
-                    **ref
-                )
-            )
-        except:
-            print("Column already added")
-            pass
-
-        for _hash, size in zip(*legacy_external.fetch("hash", "size")):
-            if _hash in hashes:
-                relative_path = str(Path(schema.database, _hash).as_posix())
-                uuid = dj.hash.uuid_from_buffer(init_string=relative_path)
-                external_path = ext._make_external_filepath(relative_path)
-                if ext.spec["protocol"] == "s3":
-                    contents_hash = dj.hash.uuid_from_buffer(
-                        ext._download_buffer(external_path)
-                    )
-                else:
-                    contents_hash = dj.hash.uuid_from_file(external_path)
-                ext.insert1(
-                    dict(
-                        filepath=relative_path,
-                        size=size,
-                        contents_hash=contents_hash,
-                        hash=uuid,
-                    ),
-                    skip_duplicates=True,
-                )
-
-                query(
-                    "UPDATE {referencing_table} "
-                    "SET `{column_name}_{temp_suffix}`=%s "
-                    'WHERE `{column_name}` = "{_hash}"'.format(
-                        _hash=_hash, temp_suffix=temp_suffix, **ref
-                    ),
-                    uuid.bytes,
-                )
-
-        # check that all have been copied
-        check = query(
-            "SELECT * FROM {referencing_table} "
-            "WHERE `{column_name}` IS NOT NULL"
-            "  AND `{column_name}_{temp_suffix}` IS NULL".format(
-                temp_suffix=temp_suffix, **ref
-            )
-        ).fetchall()
-
-        assert len(check) == 0, "Some hashes havent been migrated"
-
-        # drop old foreign key, rename, and create new foreign key
-        query(
-            """
-            ALTER TABLE {referencing_table}
-            DROP FOREIGN KEY `{constraint_name}`,
-            DROP COLUMN `{column_name}`,
-            CHANGE COLUMN `{column_name}_{temp_suffix}` `{column_name}`
-                {type} DEFAULT NULL
-                COMMENT ":blob@{store}:{comment}",
-            ADD FOREIGN KEY (`{column_name}`) REFERENCES {ext_table_name}
-                (`hash`)
-            """.format(
-                temp_suffix=temp_suffix,
-                ext_table_name=ext.full_table_name,
-                type=dj.declare.UUID_DATA_TYPE,
-                store=(store or default_store),
-                comment=comment,
-                **ref
-            )
-        )
-
-    # Drop the old external table but make sure it's no longer referenced
-    # get referencing tables
-    refs = [
-        {k.lower(): v for k, v in elem.items()}
-        for elem in query(
-            """
-    SELECT concat('`', table_schema, '`.`', table_name, '`') as
-        referencing_table, column_name, constraint_name
-    FROM information_schema.key_column_usage
-    WHERE referenced_table_name="{tab}" and referenced_table_schema="{db}"
-    """.format(
-                tab=legacy_external.table_name, db=legacy_external.database
-            ),
-            as_dict=True,
-        ).fetchall()
-    ]
-
-    assert not refs, "Some references still exist"
-
-    # drop old external table
-    legacy_external.drop_quick()
diff --git a/datajoint/preview.py b/datajoint/preview.py
index f761cf533..5188cc81f 100644
--- a/datajoint/preview.py
+++ b/datajoint/preview.py
@@ -56,20 +56,20 @@ def repr_html(query_expression):
 
     css = """
     <style type="text/css">
-        .Relation{
+        .Table{
             border-collapse:collapse;
         }
-        .Relation th{
+        .Table th{
             background: #A0A0A0; color: #ffffff; padding:4px; border:#f0e0e0 1px solid;
             font-weight: normal; font-family: monospace; font-size: 100%;
         }
-        .Relation td{
+        .Table td{
             padding:4px; border:#f0e0e0 1px solid; font-size:100%;
         }
-        .Relation tr:nth-child(odd){
+        .Table tr:nth-child(odd){
             background: #ffffff;
         }
-        .Relation tr:nth-child(even){
+        .Table tr:nth-child(even){
             background: #f3f1ff;
         }
         /* Tooltip container */
@@ -111,7 +111,7 @@ def repr_html(query_expression):
     {css}
     {title}
         <div style="max-height:1000px;max-width:1500px;overflow:auto;">
-        <table border="1" class="Relation">
+        <table border="1" class="Table">
             <thead> <tr style="text-align: right;"> <th> {head} </th> </tr> </thead>
             <tbody> <tr> {body} </tr> </tbody>
         </table>
diff --git a/datajoint/schemas.py b/datajoint/schemas.py
index 0e35f10ac..d217c7b2b 100644
--- a/datajoint/schemas.py
+++ b/datajoint/schemas.py
@@ -47,7 +47,7 @@ def __init__(
         connection=None,
         create_schema=True,
         create_tables=True,
-        add_objects=None
+        add_objects=None,
     ):
         """
         Associate database schema `schema_name`. If the schema does not exist, attempt to
@@ -87,7 +87,7 @@ def activate(
         connection=None,
         create_schema=None,
         create_tables=None,
-        add_objects=None
+        add_objects=None,
     ):
         """
         Associate database schema `schema_name`. If the schema does not exist, attempt to
@@ -171,7 +171,7 @@ def __call__(self, cls, *, context=None):
         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"
+                "The schema decorator should not be applied to Part tables."
             )
         if self.is_activated():
             self._decorate_master(cls, context)
@@ -222,9 +222,7 @@ def _decorate_table(self, table_class, context, assert_declared=False):
         # 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)
+        if not is_declared and not assert_declared and self.create_tables:
             instance.declare(context)
             self.connection.dependencies.clear()
         is_declared = is_declared or instance.is_declared
@@ -284,7 +282,7 @@ def size_on_disk(self):
 
     def spawn_missing_classes(self, context=None):
         """
-        Creates the appropriate python user relation classes from tables in the schema and places them
+        Creates the appropriate python user table classes from tables in the schema and places them
         in the context.
 
         :param context: alternative context to place the missing classes into, e.g. locals()
@@ -321,7 +319,7 @@ def spawn_missing_classes(self, context=None):
                     if re.fullmatch(Part.tier_regexp, table_name):
                         part_tables.append(table_name)
                 else:
-                    # declare and decorate master relation classes
+                    # declare and decorate master table classes
                     context[class_name] = self(
                         type(class_name, (cls,), dict()), context=context
                     )
@@ -449,7 +447,7 @@ def replace(s):
                 defi=re.sub(
                     r"`([^`]+)`.`([^`]+)`",
                     replace,
-                    FreeTable(self.connection, table).describe(printout=False),
+                    FreeTable(self.connection, table).describe(),
                 ).replace("\n", "\n    " + indent),
             )
 
@@ -506,7 +504,7 @@ def __init__(
         create_schema=False,
         create_tables=False,
         connection=None,
-        add_objects=None
+        add_objects=None,
     ):
         """
         Creates a python module with the given name from the name of a schema on the server and
diff --git a/datajoint/settings.py b/datajoint/settings.py
index 3da611af6..08c4ae068 100644
--- a/datajoint/settings.py
+++ b/datajoint/settings.py
@@ -62,7 +62,6 @@
 
 
 class Config(collections.abc.MutableMapping):
-
     instance = None
 
     def __init__(self, *args, **kwargs):
@@ -241,9 +240,7 @@ def __getitem__(self, key):
             return self._conf[key]
 
         def __setitem__(self, key, value):
-            logger.debug(
-                logging.DEBUG, "Setting {0:s} to {1:s}".format(str(key), str(value))
-            )
+            logger.debug("Setting {0:s} to {1:s}".format(str(key), str(value)))
             if validators[key](value):
                 self._conf[key] = value
             else:
diff --git a/datajoint/table.py b/datajoint/table.py
index fc919ec12..e047c8adc 100644
--- a/datajoint/table.py
+++ b/datajoint/table.py
@@ -6,7 +6,9 @@
 import pandas
 import logging
 import uuid
+import csv
 import re
+import json
 from pathlib import Path
 from .settings import config
 from .declare import declare, alter
@@ -22,6 +24,7 @@
     UnknownAttributeError,
     IntegrityError,
 )
+from typing import Union
 from .version import __version__ as version
 
 logger = logging.getLogger(__name__.split(".")[0])
@@ -116,11 +119,11 @@ def alter(self, prompt=True, context=None):
             frame = inspect.currentframe().f_back
             context = dict(frame.f_globals, **frame.f_locals)
             del frame
-        old_definition = self.describe(context=context, printout=False)
+        old_definition = self.describe(context=context)
         sql, external_stores = alter(self.definition, old_definition, context)
         if not sql:
             if prompt:
-                print("Nothing to alter.")
+                logger.warn("Nothing to alter.")
         else:
             sql = "ALTER TABLE {tab}\n\t".format(
                 tab=self.full_table_name
@@ -140,7 +143,7 @@ def alter(self, prompt=True, context=None):
                         table_info=self.heading.table_info
                     )
                     if prompt:
-                        print("Table altered")
+                        logger.info("Table altered")
                     self._log("Altered " + self.full_table_name)
 
     def from_clause(self):
@@ -344,17 +347,20 @@ def insert(
         """
         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 self.
+        :param rows: Either (a) 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 self, or (b) a pathlib.Path object specifying a path
+            relative to the current directory with a CSV file, the contents of which
+            will be inserted.
         :param replace: If True, replaces the existing tuple.
         :param skip_duplicates: If True, silently skip duplicate inserts.
         :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.
+        :param allow_direct_insert: Only applies in auto-populated tables. If False (default),
+            insert may only be called from inside the make callback.
 
         Example:
 
-            >>> relation.insert([
+            >>> Table.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")])
         """
@@ -365,6 +371,10 @@ def insert(
                 drop=len(rows.index.names) == 1 and not rows.index.names[0]
             ).to_records(index=False)
 
+        if isinstance(rows, Path):
+            with open(rows, newline="") as data_file:
+                rows = list(csv.DictReader(data_file, delimiter=","))
+
         # prohibit direct inserts into auto-populated tables
         if not allow_direct_insert and not getattr(self, "_allow_insert", True):
             raise DataJointError(
@@ -459,15 +469,30 @@ def delete_quick(self, get_count=False):
         self._log(query[:255])
         return count
 
-    def delete(self, transaction=True, safemode=None, force_parts=False):
+    def delete(
+        self,
+        transaction: bool = True,
+        safemode: Union[bool, None] = None,
+        force_parts: bool = False,
+    ) -> int:
         """
         Deletes the contents of the table and its dependent tables, recursively.
 
-        :param transaction: if True, use the entire delete becomes an atomic transaction. This is the default and
-                            recommended behavior. Set to False if this delete is nested within another transaction.
-        :param safemode: If True, prohibit nested transactions and prompt to confirm. Default is dj.config['safemode'].
-        :param force_parts: Delete from parts even when not deleting from their masters.
-        :return: number of deleted rows (excluding those from dependent tables)
+        Args:
+            transaction: If `True`, use of the entire delete becomes an atomic transaction.
+                This is the default and recommended behavior. Set to `False` if this delete is
+                nested within another transaction.
+            safemode: If `True`, prohibit nested transactions and prompt to confirm. Default
+                is `dj.config['safemode']`.
+            force_parts: Delete from parts even when not deleting from their masters.
+
+        Returns:
+            Number of deleted rows (excluding those from dependent tables).
+
+        Raises:
+            DataJointError: Delete exceeds maximum number of delete attempts.
+            DataJointError: When deleting within an existing transaction.
+            DataJointError: Deleting a part table before its master.
         """
         deleted = set()
 
@@ -579,7 +604,7 @@ def cascade(table):
         # Confirm and commit
         if delete_count == 0:
             if safemode:
-                print("Nothing to delete.")
+                logger.warn("Nothing to delete.")
             if transaction:
                 self.connection.cancel_transaction()
         else:
@@ -587,18 +612,17 @@ def cascade(table):
                 if transaction:
                     self.connection.commit_transaction()
                 if safemode:
-                    print("Deletes committed.")
+                    logger.info("Deletes committed.")
             else:
                 if transaction:
                     self.connection.cancel_transaction()
                 if safemode:
-                    print("Deletes cancelled")
+                    logger.warn("Deletes cancelled")
         return delete_count
 
     def drop_quick(self):
         """
-        Drops the table associated with this relation without cascading and without user prompt.
-        If the table has any dependent table(s), this call will fail with an error.
+        Drops the table without cascading to dependent tables and without user prompt.
         """
         if self.is_declared:
             query = "DROP TABLE %s" % self.full_table_name
@@ -639,12 +663,14 @@ def drop(self):
 
         if config["safemode"]:
             for table in tables:
-                print(table, "(%d tuples)" % len(FreeTable(self.connection, table)))
+                logger.info(
+                    table + " (%d tuples)" % len(FreeTable(self.connection, table))
+                )
             do_drop = user_choice("Proceed?", default="no") == "yes"
         if do_drop:
             for table in reversed(tables):
                 FreeTable(self.connection, table).drop_quick()
-            print("Tables dropped.  Restart kernel.")
+            logger.info("Tables dropped. Restart kernel.")
 
     @property
     def size_on_disk(self):
@@ -664,9 +690,9 @@ def show_definition(self):
             "show_definition is deprecated. Use the describe method instead."
         )
 
-    def describe(self, context=None, printout=True):
+    def describe(self, context=None, printout=False):
         """
-        :return:  the definition string for the relation using DataJoint DDL.
+        :return:  the definition string for the query using DataJoint DDL.
         """
         if context is None:
             frame = inspect.currentframe().f_back
@@ -745,61 +771,9 @@ def describe(self, context=None, printout=True):
                 unique="UNIQUE " if v["unique"] else "", attrs=", ".join(k)
             )
         if printout:
-            print(definition)
+            logger.info("\n" + definition)
         return definition
 
-    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 one existing tuple. self must be restricted to exactly one entry.
-        In DataJoint the principal way of updating data is to delete and re-insert the
-        entire record and updates are reserved for corrective actions.
-        This is because referential integrity is observed on the level of entire
-        records rather than individual attributes.
-
-        Safety constraints:
-           1. self must be restricted to exactly one tuple
-           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
-        """
-        logger.warning(
-            "`_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:
-            raise DataJointError("Invalid attribute name")
-        if attrname in self.heading.primary_key:
-            raise DataJointError("Cannot update a key value.")
-
-        attr = self.heading[attrname]
-
-        if attr.is_blob:
-            value = blob.pack(value)
-            placeholder = "%s"
-        elif attr.numeric:
-            if value is None or np.isnan(float(value)):  # nans are turned into NULLs
-                placeholder = "NULL"
-                value = None
-            else:
-                placeholder = "%s"
-                value = str(int(value) if isinstance(value, bool) else value)
-        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(),
-            attrname=attrname,
-            placeholder=placeholder,
-            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):
         """
@@ -858,6 +832,8 @@ def __make_placeholder(self, name, value, ignore_extra_fields=False):
                 value = self.external[attr.store].upload_filepath(value).bytes
             elif attr.numeric:
                 value = str(int(value) if isinstance(value, bool) else value)
+            elif attr.json:
+                value = json.dumps(value)
         return name, placeholder, value
 
     def __make_row_to_insert(self, row, field_list, ignore_extra_fields):
@@ -989,7 +965,7 @@ def lookup_class_name(name, context, depth=3):
 
 class FreeTable(Table):
     """
-    A base relation without a dedicated class. Each instance is associated with a table
+    A base table without a dedicated class. Each instance is associated with a table
     specified by full_table_name.
 
     :param conn:  a dj.Connection object
diff --git a/datajoint/user_tables.py b/datajoint/user_tables.py
index 3c8a186e2..16084631f 100644
--- a/datajoint/user_tables.py
+++ b/datajoint/user_tables.py
@@ -1,5 +1,5 @@
 """
-Hosts the table tiers, user relations should be derived from.
+Hosts the table tiers, user tables should be derived from.
 """
 
 from .table import Table
@@ -83,7 +83,7 @@ def __iter__(cls):
 
 class UserTable(Table, metaclass=TableMeta):
     """
-    A subclass of UserTable is a dedicated class interfacing a base relation.
+    A subclass of UserTable is a dedicated class interfacing a base table.
     UserTable is initialized by the decorator generated by schema().
     """
 
@@ -164,7 +164,7 @@ class Imported(UserTable, AutoPopulate):
 
 class Computed(UserTable, AutoPopulate):
     """
-    Inherit from this class if the table's values are computed from other relations in the schema.
+    Inherit from this class if the table's values are computed from other tables in the schema.
     The inherited class must at least provide the function `_make_tuples`.
     """
 
@@ -174,10 +174,10 @@ class Computed(UserTable, AutoPopulate):
 
 class Part(UserTable):
     """
-    Inherit from this class if the table's values are details of an entry in another relation
-    and if this table is populated by this relation. For example, the entries inheriting from
+    Inherit from this class if the table's values are details of an entry in another table
+    and if this table is populated by the other table. For example, the entries inheriting from
     dj.Part could be single entries of a matrix, while the parent table refers to the entire matrix.
-    Part relations are implemented as classes inside classes.
+    Part tables are implemented as classes inside classes.
     """
 
     _connection = None
diff --git a/datajoint/version.py b/datajoint/version.py
index 50c8b9c68..697137322 100644
--- a/datajoint/version.py
+++ b/datajoint/version.py
@@ -1,3 +1,3 @@
-__version__ = "0.13.7"
+__version__ = "0.14.0"
 
 assert len(__version__) <= 10  # The log table limits version to the 10 characters
diff --git a/docker-compose-build.yaml b/docker-compose-build.yaml
new file mode 100644
index 000000000..3dc50d4d5
--- /dev/null
+++ b/docker-compose-build.yaml
@@ -0,0 +1,24 @@
+# PY_VER=3.9 IMAGE=djbase DISTRO=debian DJ_VERSION=$(grep -oP '\d+\.\d+\.\d+' datajoint/version.py) HOST_UID=$(id -u) docker compose -f docker-compose-build.yaml up --exit-code-from app --build
+#
+# Intended for updating dependencies and docker image.
+# Used to build release artifacts.
+version: "2.4"
+services:
+  app:
+    build:
+      context: .
+      args:
+        - PY_VER
+        - DISTRO
+        - IMAGE
+    image: datajoint/datajoint:${DJ_VERSION}
+    user: ${HOST_UID}:anaconda
+    volumes:
+      - .:/main
+    command:
+      - sh
+      - -c
+      - |
+        set -e
+        rm -R build dist *.egg-info || echo "No prev build"
+        python setup.py bdist_wheel sdist
\ No newline at end of file
diff --git a/docs-api/Makefile b/docs-api/Makefile
deleted file mode 100644
index d0c3cbf10..000000000
--- a/docs-api/Makefile
+++ /dev/null
@@ -1,20 +0,0 @@
-# Minimal makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line, and also
-# from the environment for the first two.
-SPHINXOPTS    ?=
-SPHINXBUILD   ?= sphinx-build
-SOURCEDIR     = source
-BUILDDIR      = build
-
-# Put it first so that "make" without argument is like "make help".
-help:
-	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-
-.PHONY: help Makefile
-
-# Catch-all target: route all unknown targets to Sphinx using the new
-# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
-%: Makefile
-	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs-api/apt_requirements.txt b/docs-api/apt_requirements.txt
deleted file mode 100644
index ac08cdcf9..000000000
--- a/docs-api/apt_requirements.txt
+++ /dev/null
@@ -1,1 +0,0 @@
-make
\ No newline at end of file
diff --git a/docs-api/docker-compose.yaml b/docs-api/docker-compose.yaml
deleted file mode 100644
index 4124b9139..000000000
--- a/docs-api/docker-compose.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-# HOST_UID=1000 docker-compose -f ./docs-api/docker-compose.yaml up --build
-# docker exec -it docs-api_docs-builder_1 bash -c "pip install -U /main/datajoint-python && rm -R build && make html"
-version: '2.4'
-services:
-  # example how to build
-  docs-builder:
-    # build: .
-    image: datajoint/djbase:py3.9-debian
-    user: ${HOST_UID}:anaconda
-    working_dir: /main/docs-api
-    volumes:
-      - .:/main/docs-api
-      - ./pip_requirements.txt:/tmp/pip_requirements.txt
-      - ./apt_requirements.txt:/tmp/apt_requirements.txt
-      - ../datajoint:/main/datajoint-python/datajoint:ro
-      - ../setup.py:/main/datajoint-python/setup.py:ro
-      - ../requirements.txt:/main/datajoint-python/requirements.txt
-    command:
-      - bash
-      - -c
-      - |
-        pip install /main/datajoint-python
-        make html
-        chmod -R o=rwx /main/docs-api/build/html
-        # tail -f /dev/null
-        # sphinx-quickstart 
-        # sphinx-apidoc -o ./source ../datajoint-python/datajoint
diff --git a/docs-api/make.bat b/docs-api/make.bat
deleted file mode 100644
index 747ffb7b3..000000000
--- a/docs-api/make.bat
+++ /dev/null
@@ -1,35 +0,0 @@
-@ECHO OFF
-
-pushd %~dp0
-
-REM Command file for Sphinx documentation
-
-if "%SPHINXBUILD%" == "" (
-	set SPHINXBUILD=sphinx-build
-)
-set SOURCEDIR=source
-set BUILDDIR=build
-
-%SPHINXBUILD% >NUL 2>NUL
-if errorlevel 9009 (
-	echo.
-	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
-	echo.installed, then set the SPHINXBUILD environment variable to point
-	echo.to the full path of the 'sphinx-build' executable. Alternatively you
-	echo.may add the Sphinx directory to PATH.
-	echo.
-	echo.If you don't have Sphinx installed, grab it from
-	echo.https://www.sphinx-doc.org/
-	exit /b 1
-)
-
-if "%1" == "" goto help
-
-%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
-goto end
-
-:help
-%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
-
-:end
-popd
diff --git a/docs-api/pip_requirements.txt b/docs-api/pip_requirements.txt
deleted file mode 100644
index 856b3f4cb..000000000
--- a/docs-api/pip_requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-sphinx
-sphinx_rtd_theme
\ No newline at end of file
diff --git a/docs-api/source/conf.py b/docs-api/source/conf.py
deleted file mode 100644
index 2b114821e..000000000
--- a/docs-api/source/conf.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# Configuration file for the Sphinx documentation builder.
-#
-# This file only contains a selection of the most common options. For a full
-# list see the documentation:
-# https://www.sphinx-doc.org/en/master/usage/configuration.html
-
-# -- Path setup --------------------------------------------------------------
-
-# If extensions (or modules to document with autodoc) are in another directory,
-# add these directories to sys.path here. If the directory is relative to the
-# documentation root, use os.path.abspath to make it absolute, like shown here.
-#
-import os
-import sys
-sys.path.insert(0, os.path.abspath('../datajoint'))
-
-
-# -- Project information -----------------------------------------------------
-
-project = 'DataJoint API Docs'
-copyright = '2022, DataJoint contributors'
-author = 'DataJoint contributors'
-
-
-# -- General configuration ---------------------------------------------------
-
-# Add any Sphinx extension module names here, as strings. They can be
-# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
-# ones.
-extensions = ['sphinx.ext.autodoc']
-
-# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
-
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-# This pattern also affects html_static_path and html_extra_path.
-exclude_patterns = []
-
-
-# -- Options for HTML output -------------------------------------------------
-
-# The theme to use for HTML and HTML Help pages.  See the documentation for
-# a list of builtin themes.
-#
-html_theme = 'sphinx_rtd_theme'
-
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
\ No newline at end of file
diff --git a/docs-api/source/index.rst b/docs-api/source/index.rst
deleted file mode 100644
index b733b02f8..000000000
--- a/docs-api/source/index.rst
+++ /dev/null
@@ -1,28 +0,0 @@
-.. DataJoint API Docs documentation master file, created by
-   sphinx-quickstart on Tue Apr 26 22:08:07 2022.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-Welcome to DataJoint's API Documentation!
-=========================================
-
-datajoint module
-----------------
-
-.. automodule:: datajoint
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-.. toctree::
-   :maxdepth: 2
-   :caption: Submodules:
-   
-   submodules
-
-Indices and tables
-==================
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
diff --git a/docs-api/source/submodules.rst b/docs-api/source/submodules.rst
deleted file mode 100644
index 45105d3dd..000000000
--- a/docs-api/source/submodules.rst
+++ /dev/null
@@ -1,207 +0,0 @@
-datajoint.admin module
-----------------------
-
-.. automodule:: datajoint.admin
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.attribute\_adapter module
------------------------------------
-
-.. automodule:: datajoint.attribute_adapter
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.autopopulate module
------------------------------
-
-.. automodule:: datajoint.autopopulate
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.blob module
----------------------
-
-.. automodule:: datajoint.blob
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.condition module
---------------------------
-
-.. automodule:: datajoint.condition
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.connection module
----------------------------
-
-.. automodule:: datajoint.connection
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.declare module
-------------------------
-
-.. automodule:: datajoint.declare
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.dependencies module
------------------------------
-
-.. automodule:: datajoint.dependencies
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.diagram module
-------------------------
-
-.. automodule:: datajoint.diagram
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.errors module
------------------------
-
-.. automodule:: datajoint.errors
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.expression module
----------------------------
-
-.. automodule:: datajoint.expression
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.external module
--------------------------
-
-.. automodule:: datajoint.external
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.fetch module
-----------------------
-
-.. automodule:: datajoint.fetch
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.hash module
----------------------
-
-.. automodule:: datajoint.hash
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.heading module
-------------------------
-
-.. automodule:: datajoint.heading
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.jobs module
----------------------
-
-.. automodule:: datajoint.jobs
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.migrate module
-------------------------
-
-.. automodule:: datajoint.migrate
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.plugin module
------------------------
-
-.. automodule:: datajoint.plugin
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.preview module
-------------------------
-
-.. automodule:: datajoint.preview
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.s3 module
--------------------
-
-.. automodule:: datajoint.s3
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.schemas module
-------------------------
-
-.. automodule:: datajoint.schemas
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.settings module
--------------------------
-
-.. automodule:: datajoint.settings
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.table module
-----------------------
-
-.. automodule:: datajoint.table
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.user\_tables module
------------------------------
-
-.. automodule:: datajoint.user_tables
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.utils module
-----------------------
-
-.. automodule:: datajoint.utils
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-datajoint.version module
-------------------------
-
-.. automodule:: datajoint.version
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs-parts/admin/5-blob-config_lang1.rst b/docs-parts/admin/5-blob-config_lang1.rst
deleted file mode 100644
index 0eb0203f5..000000000
--- a/docs-parts/admin/5-blob-config_lang1.rst
+++ /dev/null
@@ -1,17 +0,0 @@
-.. code-block:: python
-
-  dj.config['stores'] = {
-    'external': dict(  # 'regular' external storage for this pipeline
-                  protocol='s3',
-                  endpoint='s3.amazonaws.com:9000',
-                  bucket = 'testbucket',
-                  location = 'datajoint-projects/lab1',
-                  access_key='1234567',
-                  secret_key='foaf1234'),
-    'external-raw': dict( # 'raw' storage for this pipeline
-                  protocol='file',
-                  location='/net/djblobs/myschema')
-  }
-  # external object cache - see fetch operation below for details.
-  dj.config['cache'] = '/net/djcache'
-
diff --git a/docs-parts/admin/5-blob-config_lang2.rst b/docs-parts/admin/5-blob-config_lang2.rst
deleted file mode 100644
index efae4392b..000000000
--- a/docs-parts/admin/5-blob-config_lang2.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Use ``dj.config`` for configuration.
diff --git a/docs-parts/admin/5-blob-config_lang3.rst b/docs-parts/admin/5-blob-config_lang3.rst
deleted file mode 100644
index 5628e385d..000000000
--- a/docs-parts/admin/5-blob-config_lang3.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-  This is done by saving the path in the ``cache`` key of the DataJoint configuration dictionary:
-
-  .. code-block:: python
-
-    dj.config['cache'] = '/temp/dj-cache'
diff --git a/docs-parts/admin/5-blob-config_lang4.rst b/docs-parts/admin/5-blob-config_lang4.rst
deleted file mode 100644
index 7d8422260..000000000
--- a/docs-parts/admin/5-blob-config_lang4.rst
+++ /dev/null
@@ -1,29 +0,0 @@
-
-To remove only the tracking entries in the external table, call ``delete``
-on the ``~external_<storename>`` table for the external configuration with the argument 
-``delete_external_files=False``.
-
-.. note::
-
-  Currently, cleanup operations on a schema's external table are not 100% 
-  transaction safe and so must be run when there is no write activity occurring 
-  in tables which use a given schema / external store pairing.
-
-.. code-block:: python
-
-    >>> schema.external['external_raw'].delete(delete_external_files=False)
-
-To remove the tracking entries as well as the underlying files, call `delete`
-on the external table for the external configuration with the argument
-`delete_external_files=True`.
-
-.. code-block:: python
-
-    >>> schema.external['external_raw'].delete(delete_external_files=True)
-
-.. note::
-
-  Setting ``delete_external_files=True`` will always attempt to delete
-  the underlying data file, and so should not typically be used with 
-  the ``filepath`` datatype.
-
diff --git a/docs-parts/computation/01-autopopulate_lang1.rst b/docs-parts/computation/01-autopopulate_lang1.rst
deleted file mode 100644
index 376feb471..000000000
--- a/docs-parts/computation/01-autopopulate_lang1.rst
+++ /dev/null
@@ -1,18 +0,0 @@
-
-.. code-block:: python
-
-    @schema
-    class FilteredImage(dj.Computed):
-        definition = """
-        # Filtered image
-        -> Image
-        ---
-        filtered_image : longblob
-        """
-
-        def make(self, key):
-            img = (test.Image & key).fetch1('image')
-            key['filtered_image'] = myfilter(img)
-            self.insert1(key)
-
-The ``make`` method receives one argument: the dict ``key`` containing the primary key value of an element of :ref:`key source <keysource>` to be worked on.  
diff --git a/docs-parts/computation/01-autopopulate_lang2.rst b/docs-parts/computation/01-autopopulate_lang2.rst
deleted file mode 100644
index 8af2c1954..000000000
--- a/docs-parts/computation/01-autopopulate_lang2.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-
-.. code-block:: python
-
-    FilteredImage.populate()
-
-The progress of long-running calls to ``populate()`` in datajoint-python can be visualized by adding the ``display_progress=True`` argument to the populate call.
diff --git a/docs-parts/computation/01-autopopulate_lang3.rst b/docs-parts/computation/01-autopopulate_lang3.rst
deleted file mode 100644
index ae3024d71..000000000
--- a/docs-parts/computation/01-autopopulate_lang3.rst
+++ /dev/null
@@ -1,23 +0,0 @@
-The ``populate`` method accepts a number of optional arguments that provide more features and allow greater control over the method's behavior.
-
-- ``restrictions`` - A list of restrictions, restricting as ``(tab.key_source & AndList(restrictions)) - tab.proj()``.
-  Here ``target`` is the table to be populated, usually ``tab`` itself.
-- ``suppress_errors`` - If ``True``, encountering an error will cancel the current ``make`` call, log the error, and continue to the next ``make`` call.
-  Error messages will be logged in the job reservation table (if ``reserve_jobs`` is ``True``) and returned as a list.
-  See also ``return_exception_objects`` and ``reserve_jobs``.
-  Defaults to ``False``.
-- ``return_exception_objects`` - If ``True``, error objects are returned instead of error messages.
-  This applies only when ``suppress_errors`` is ``True``.
-  Defaults to ``False``.
-- ``reserve_jobs`` - If ``True``, reserves job to indicate to other distributed processes.
-  The job reservation table may be access as ``schema.jobs``.
-  Errors are logged in the jobs table.
-  Defaults to ``False``.
-- ``order`` - The order of execution, either ``"original"``, ``"reverse"``, or ``"random"``.
-  Defaults to ``"original"``.
-- ``display_progress`` - If ``True``, displays a progress bar.
-  Defaults to ``False``.
-- ``limit`` - If not ``None``, checks at most this number of keys.
-  Defaults to ``None``.
-- ``max_calls`` - If not ``None``, populates at most this many keys.
-  Defaults to ``None``, which means no limit.
diff --git a/docs-parts/computation/01-autopopulate_lang4.rst b/docs-parts/computation/01-autopopulate_lang4.rst
deleted file mode 100644
index fff832398..000000000
--- a/docs-parts/computation/01-autopopulate_lang4.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-The method ``table.progress`` reports how many ``key_source`` entries have been populated and how many remain.
-Two optional parameters allow more advanced use of the method.
-A parameter of restriction conditions can be provided, specifying which entities to consider.
-A Boolean parameter ``display`` (default is ``True``) allows disabling the output, such that the numbers of remaining and total entities are returned but not printed.
diff --git a/docs-parts/computation/02-keysource_lang1.rst b/docs-parts/computation/02-keysource_lang1.rst
deleted file mode 100644
index 583ceee7d..000000000
--- a/docs-parts/computation/02-keysource_lang1.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-A custom key source can be configured by setting the ``key_source`` property within a table class, after the ``definition`` string.
diff --git a/docs-parts/computation/02-keysource_lang2.rst b/docs-parts/computation/02-keysource_lang2.rst
deleted file mode 100644
index d370ff238..000000000
--- a/docs-parts/computation/02-keysource_lang2.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-.. code-block:: python
-
-  @schema
-  class EEG(dj.Imported):
-    definition = """
-    -> Recording
-    ---
-    sample_rate : float
-    eeg_data : longblob
-    """
-    key_source = Recording & 'recording_type = "EEG"'
diff --git a/docs-parts/computation/04-master-part_lang1.rst b/docs-parts/computation/04-master-part_lang1.rst
deleted file mode 100644
index 3bda5abb9..000000000
--- a/docs-parts/computation/04-master-part_lang1.rst
+++ /dev/null
@@ -1,29 +0,0 @@
-
-In Python, the master-part relationship is expressed by making the part a nested class of the master.
-The part is subclassed from ``dj.Part`` and does not need the ``@schema`` decorator.
-
-
-.. code-block:: python
-
-    @schema
-    class Segmentation(dj.Computed):
-        definition = """  # image segmentation
-        -> Image
-        """
-
-        class ROI(dj.Part):
-            definition = """  # Region of interest resulting from segmentation
-            -> Segmentation
-            roi  : smallint   # roi number
-            ---
-            roi_pixels  : longblob   #  indices of pixels
-            roi_weights : longblob   #  weights of pixels
-            """
-
-        def make(self, key):
-            image = (Image & key).fetch1('image')
-            self.insert1(key)
-            count = itertools.count()
-            Segmentation.ROI.insert(
-                    dict(key, roi=next(count), roi_pixel=roi_pixels, roi_weights=roi_weights)
-                    for roi_pixels, roi_weights in mylib.segment(image))
diff --git a/docs-parts/computation/04-master-part_lang2.rst b/docs-parts/computation/04-master-part_lang2.rst
deleted file mode 100644
index 8bf4ef731..000000000
--- a/docs-parts/computation/04-master-part_lang2.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-
-.. code-block:: python
-
-    Segmentation.populate()
diff --git a/docs-parts/computation/04-master-part_lang3.rst b/docs-parts/computation/04-master-part_lang3.rst
deleted file mode 100644
index 1e9bfe70f..000000000
--- a/docs-parts/computation/04-master-part_lang3.rst
+++ /dev/null
@@ -1,22 +0,0 @@
-
-.. code-block:: python
-
-  @schema
-  class ArrayResponse(dj.Computed):
-  definition = """
-  array: int
-  """
-
-    class ElectrodeResponse(dj.Part):
-    definition = """
-    -> master
-    electrode: int    # electrode number on the probe
-    """
-
-    class ChannelResponse(dj.Part):
-    definition = """
-    -> ElectrodeResponse
-    channel: int
-    ---
-    response: longblob  # response of a channel
-    """
diff --git a/docs-parts/computation/06-distributed-computing_jobs_by_key.rst b/docs-parts/computation/06-distributed-computing_jobs_by_key.rst
deleted file mode 100644
index 9862d06c7..000000000
--- a/docs-parts/computation/06-distributed-computing_jobs_by_key.rst
+++ /dev/null
@@ -1,22 +0,0 @@
-
-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
deleted file mode 100644
index 585f309fc..000000000
--- a/docs-parts/computation/06-distributed-computing_kill_order_by.rst
+++ /dev/null
@@ -1,14 +0,0 @@
-
-For example, to sort the output by hostname in descending order:
-
-.. code-block:: python
-
-    In [3]: dj.kill(None, None, 'host desc')
-    Out[3]:
-      ID USER         HOST          STATE         TIME    INFO
-    +--+ +----------+ +-----------+ +-----------+ +-----+
-      33 chris        localhost:54840                 1261  None
-      17 chris        localhost:54587                 3246  None
-       4 event_scheduler localhost    Waiting on empty queue  187180  None
-    process to kill or "q" to quit > q
-
diff --git a/docs-parts/computation/06-distributed-computing_lang1.rst b/docs-parts/computation/06-distributed-computing_lang1.rst
deleted file mode 100644
index 93906e82d..000000000
--- a/docs-parts/computation/06-distributed-computing_lang1.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-
-Job reservations are activated by setting the keyword argument ``reserve_jobs=True`` in ``populate`` calls.
diff --git a/docs-parts/computation/06-distributed-computing_lang2.rst b/docs-parts/computation/06-distributed-computing_lang2.rst
deleted file mode 100644
index 2df361bf8..000000000
--- a/docs-parts/computation/06-distributed-computing_lang2.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-
-.. code-block:: python
-
-    In [1]: schema.jobs
-    Out[1]:
-    *table_name    *key_hash      status       error_message  user           host           pid       connection_id  timestamp      key        error_stack
-    +------------+ +------------+ +----------+ +------------+ +------------+ +------------+ +-------+ +------------+ +------------+ +--------+ +------------+
-    __job_results  e4da3b7fbbce23 reserved                    datajoint@localhos localhost     15571     59             2017-09-04 14: <BLOB>     <BLOB>
-     (2 tuples)
-
diff --git a/docs-parts/computation/06-distributed-computing_lang3.rst b/docs-parts/computation/06-distributed-computing_lang3.rst
deleted file mode 100644
index 2a26bc51c..000000000
--- a/docs-parts/computation/06-distributed-computing_lang3.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-
-For example, if a Python process is interrupted via the keyboard, a KeyboardError will be logged to the database as follows:
-
-.. code-block:: python
-
-    In [2]: schema.jobs
-    Out[2]:
-    *table_name    *key_hash      status     error_message  user           host           pid       connection_id  timestamp      key        error_stack
-    +------------+ +------------+ +--------+ +------------+ +------------+ +------------+ +-------+ +------------+ +------------+ +--------+ +------------+
-    __job_results  3416a75f4cea91 error      KeyboardInterr datajoint@localhos localhost     15571     59             2017-09-04 14: <BLOB>     <BLOB>
-     (1 tuples)
diff --git a/docs-parts/computation/06-distributed-computing_lang4.rst b/docs-parts/computation/06-distributed-computing_lang4.rst
deleted file mode 100644
index 106fa51f1..000000000
--- a/docs-parts/computation/06-distributed-computing_lang4.rst
+++ /dev/null
@@ -1,23 +0,0 @@
-
-For example, given the above table, errors can be inspected as follows:
-
-.. code-block:: python
-
-    In [3]: (schema.jobs & 'status="error"' ).fetch(as_dict=True)
-    Out[3]:
-    [OrderedDict([('table_name', '__job_results'),
-                  ('key_hash', 'c81e728d9d4c2f636f067f89cc14862c'),
-                  ('status', 'error'),
-                  ('key', rec.array([(2,)],
-                             dtype=[('id', 'O')])),
-                  ('error_message', 'KeyboardInterrupt'),
-                  ('error_stack', None),
-                  ('user', 'datajoint@localhost'),
-                  ('host', 'localhost'),
-                  ('pid', 15571),
-                  ('connection_id', 59),
-                  ('timestamp', datetime.datetime(2017, 9, 4, 15, 3, 53))])]
-
-
-This particular error occurred when processing the record with ID ``2``, resulted from a `KeyboardInterrupt`, and has no additional
-error trace.
diff --git a/docs-parts/computation/06-distributed-computing_lang5.rst b/docs-parts/computation/06-distributed-computing_lang5.rst
deleted file mode 100644
index 31ffaf493..000000000
--- a/docs-parts/computation/06-distributed-computing_lang5.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-
-For example:
-
-.. code-block:: python
-
-   In [4]: (schema.jobs & 'status="error"' ).delete()
diff --git a/docs-parts/concepts/04-Integrity_lang1.rst b/docs-parts/concepts/04-Integrity_lang1.rst
deleted file mode 100644
index dd5a6b710..000000000
--- a/docs-parts/concepts/04-Integrity_lang1.rst
+++ /dev/null
@@ -1,17 +0,0 @@
-.. code-block:: python
-
-  @schema
-  class Mouse(dj.Manual):
-    definition = """
-    mouse_name : varchar(64)
-    ---
-    mouse_dob : datetime
-    """
-
-  @schema
-  class MouseDeath(dj.Manual):
-    definition = """
-    -> Mouse
-    ---
-    death_date : datetime
-    """
diff --git a/docs-parts/concepts/04-Integrity_lang2.rst b/docs-parts/concepts/04-Integrity_lang2.rst
deleted file mode 100644
index c0da41dbe..000000000
--- a/docs-parts/concepts/04-Integrity_lang2.rst
+++ /dev/null
@@ -1,20 +0,0 @@
-.. code-block:: python
-
-  @schema
-  class EEGRecording(dj.Manual):
-    definition = """
-    -> Session
-    eeg_recording_id : int
-    ---
-    eeg_system : varchar(64)
-    num_channels : int
-    """
-
-  @schema
-  class ChannelData(dj.Imported):
-    definition = """
-    -> EEGRecording
-    channel_idx : int
-    ---
-    channel_data : longblob
-    """
diff --git a/docs-parts/concepts/04-Integrity_lang3.rst b/docs-parts/concepts/04-Integrity_lang3.rst
deleted file mode 100644
index da8e07fd1..000000000
--- a/docs-parts/concepts/04-Integrity_lang3.rst
+++ /dev/null
@@ -1,23 +0,0 @@
-.. code-block:: python
-
-  @schema
-  class Mouse(dj.Manual):
-    definition = """
-    mouse_name : varchar(64)
-    ---
-    mouse_dob : datetime
-    """
-
-  @schema
-  class SubjectGroup(dj.Manual):
-    definition = """
-    group_number : int
-    ---
-    group_name : varchar(64)
-    """
-
-    class GroupMember(dj.Part):
-      definition = """
-      -> master
-      -> Mouse
-      """
diff --git a/docs-parts/concepts/04-Integrity_lang4.rst b/docs-parts/concepts/04-Integrity_lang4.rst
deleted file mode 100644
index 6c7f38315..000000000
--- a/docs-parts/concepts/04-Integrity_lang4.rst
+++ /dev/null
@@ -1,20 +0,0 @@
-.. code-block:: python
-
-  @schema
-  class RecordingModality(dj.Lookup):
-    definition = """
-    modality : varchar(64)
-    """
-
-  @schema
-  class MultimodalSession(dj.Manual):
-    definition = """
-    -> Session
-    modes : int
-    """
-
-    class SessionMode(dj.Part):
-      definition = """
-      -> master
-      -> RecordingModality
-      """
diff --git a/docs-parts/definition/01-Creating-Schemas_lang1.rst b/docs-parts/definition/01-Creating-Schemas_lang1.rst
deleted file mode 100644
index 0c3666d57..000000000
--- a/docs-parts/definition/01-Creating-Schemas_lang1.rst
+++ /dev/null
@@ -1,28 +0,0 @@
-
-.. note:: By convention, the ``datajoint`` package is imported as ``dj``.
-  The documentation refers to the package as ``dj`` throughout.
-
-Create a new schema using the ``dj.Schema`` class object:
-
-.. code-block:: python
-
-    import datajoint as dj
-    schema = dj.Schema('alice_experiment')
-
-This statement creates the database schema ``alice_experiment`` on the server.
-
-The returned object ``schema`` will then serve as a decorator for DataJoint classes, as described in :ref:`table`.
-
-It is a common practice to have a separate Python module for each schema.
-Therefore, each such module has only one ``dj.Schema`` object defined and is usually named ``schema``.
-
-The ``dj.Schema`` constructor can take a number of optional parameters after the schema name.
-
-- ``context`` - Dictionary for looking up foreign key references.
-  Defaults to ``None`` to use local context.
-- ``connection`` - Specifies the DataJoint connection object.
-  Defaults to ``dj.conn()``.
-- ``create_schema`` - When ``False``, the schema object will not create a schema on the database and will raise an error if one does not already exist.
-  Defaults to ``True``.
-- ``create_tables`` - When ``False``, the schema object will not create tables on the database and will raise errors when accessing missing tables.
-  Defaults to ``True``.
diff --git a/docs-parts/definition/02-Creating-Tables_lang1.rst b/docs-parts/definition/02-Creating-Tables_lang1.rst
deleted file mode 100644
index 1411d3aa0..000000000
--- a/docs-parts/definition/02-Creating-Tables_lang1.rst
+++ /dev/null
@@ -1,43 +0,0 @@
-
-To define a DataJoint table in Python:
-
-1. Define a class inheriting from the appropriate DataJoint class: ``dj.Lookup``, ``dj.Manual``, ``dj.Imported`` or ``dj.Computed``.
-
-2. Decorate the class with the schema object (see :ref:`schema`)
-
-3. Define the class property ``definition`` to define the table heading.
-
-For example, the following code defines the table ``Person``:
-
-.. code-block:: python
-
-	import datajoint as dj
-	schema = dj.Schema('alice_experiment')
-
-	@schema
-	class Person(dj.Manual):
-	    definition = '''
-			username : varchar(20)   # unique user name
-	    ---
-	    first_name : varchar(30)
-	    last_name  : varchar(30)
-	    '''
-
-
-The ``@schema`` decorator uses the class name and the data tier to check whether an appropriate table exists on the database.
-If a table does not already exist, the decorator creates one on the database using the definition property.
-The decorator attaches the information about the table to the class, and then returns the class.
-
-The class will become usable after you define the ``definition`` property as described in :ref:`definitions`.
-
-DataJoint classes in Python
-^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-DataJoint for Python is implemented through the use of classes providing access to the actual tables stored on the database.
-Since only a single table exists on the database for any class, interactions with all instances of the class are equivalent.
-As such, most methods can be called on the classes themselves rather than on an object, for convenience.
-Whether calling a DataJoint method on a class or on an instance, the result will only depend on or apply to the corresponding table.
-All of the basic functionality of DataJoint is built to operate on the classes themselves, even when called on an instance.
-For example, calling ``Person.insert(...)`` (on the class) and ``Person.insert(...)`` (on an instance) both have the identical effect of inserting data into the table on the database server.
-DataJoint does not prevent a user from working with instances, but the workflow is complete without the need for instantiation.
-It is up to the user whether to implement additional functionality as class methods or methods called on instances.
diff --git a/docs-parts/definition/03-Table-Definition_lang1.rst b/docs-parts/definition/03-Table-Definition_lang1.rst
deleted file mode 100644
index e88fd3484..000000000
--- a/docs-parts/definition/03-Table-Definition_lang1.rst
+++ /dev/null
@@ -1,15 +0,0 @@
-
-The table definition is contained in the ``definition`` property of the class.
-
-.. code-block:: python
-
-	@schema
-	class User(dj.Manual):
-	    definition = """
-	    # database users
-	    username : varchar(20)   # unique user name
-	    ---
-	    first_name : varchar(30)
-	    last_name  : varchar(30)
-	    role : enum('admin', 'contributor', 'viewer')
-	    """
diff --git a/docs-parts/definition/03-Table-Definition_lang2.rst b/docs-parts/definition/03-Table-Definition_lang2.rst
deleted file mode 100644
index e2b8e8377..000000000
--- a/docs-parts/definition/03-Table-Definition_lang2.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-
-Users do not need to do anything special to have a table created in the database.
-Tables are created at the time of class definition.
-In fact, table creation on the database is one of the jobs performed by the decorator ``@schema`` of the class.
diff --git a/docs-parts/definition/03-Table-Definition_lang3.rst b/docs-parts/definition/03-Table-Definition_lang3.rst
deleted file mode 100644
index 480445931..000000000
--- a/docs-parts/definition/03-Table-Definition_lang3.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-
-.. code-block:: python
-
-	s = lab.User.describe()
diff --git a/docs-parts/definition/03-Table-Definition_lang4.rst b/docs-parts/definition/03-Table-Definition_lang4.rst
deleted file mode 100644
index e69de29bb..000000000
diff --git a/docs-parts/definition/07-Primary-Key_lang1.rst b/docs-parts/definition/07-Primary-Key_lang1.rst
deleted file mode 100644
index 58bc636c6..000000000
--- a/docs-parts/definition/07-Primary-Key_lang1.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-.. code-block:: python
-
-    U().aggr(Scan & key, next='max(scan_idx)+1')
-
-    # or
-
-    Session.aggr(Scan, next='max(scan_idx)+1') & key
-
-Note that the first option uses a :ref:`universal set <universal-sets>`.
diff --git a/docs-parts/definition/10-Dependencies_lang1.rst b/docs-parts/definition/10-Dependencies_lang1.rst
deleted file mode 100644
index 108ea6a2f..000000000
--- a/docs-parts/definition/10-Dependencies_lang1.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-
-.. code-block:: python
-
-    mp.BrainSlice.heading
diff --git a/docs-parts/definition/11-ERD_lang1.rst b/docs-parts/definition/11-ERD_lang1.rst
deleted file mode 100644
index 647e77f04..000000000
--- a/docs-parts/definition/11-ERD_lang1.rst
+++ /dev/null
@@ -1,21 +0,0 @@
-
-To plot the Diagram for an entire schema, an Diagram object can be initialized with the schema object (which is normally used to decorate table objects)
-
-.. code-block:: python
-
-    import datajoint as dj
-    schema = dj.Schema('my_database')
-    dj.Diagram(schema).draw()
-
-or alternatively an object that has the schema object as an attribute, such as the module defining a schema:
-
-.. code-block:: python
-
-    import datajoint as dj
-    import seq    # import the sequence module defining the seq database
-    dj.Diagram(seq).draw()   # draw the Diagram
-
-Note that calling the ``.draw()`` method is not necessary when working in a Jupyter notebook.
-You can simply let the object display itself, for example by entering ``dj.Diagram(seq)`` in a notebook cell.
-The Diagram will automatically render in the notebook by calling its ``_repr_html_`` method.
-A Diagram displayed without ``.draw()`` will be rendered as an SVG, and hovering the mouse over a table will reveal a compact version of the output of the ``.describe()`` method.
diff --git a/docs-parts/definition/11-ERD_lang2.rst b/docs-parts/definition/11-ERD_lang2.rst
deleted file mode 100644
index 523b072fa..000000000
--- a/docs-parts/definition/11-ERD_lang2.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-
-.. code-block:: python
-
-    dj.Diagram(seq.Genome).draw()
diff --git a/docs-parts/definition/11-ERD_lang3.rst b/docs-parts/definition/11-ERD_lang3.rst
deleted file mode 100644
index 91632fafb..000000000
--- a/docs-parts/definition/11-ERD_lang3.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-
-.. code-block:: python
-
-    # plot the Diagram with tables Genome and Species from module seq.
-    (dj.Diagram(seq.Genome) + dj.Diagram(seq.Species)).draw()
diff --git a/docs-parts/definition/11-ERD_lang4.rst b/docs-parts/definition/11-ERD_lang4.rst
deleted file mode 100644
index 03936a728..000000000
--- a/docs-parts/definition/11-ERD_lang4.rst
+++ /dev/null
@@ -1,15 +0,0 @@
-
-.. code-block:: python
-
-    # Plot all the tables directly downstream from ``seq.Genome``:
-    (dj.Diagram(seq.Genome)+1).draw()
-
-.. code-block:: python
-
-    # Plot all the tables directly upstream from ``seq.Genome``:
-    (dj.Diagram(seq.Genome)-1).draw()
-
-.. code-block:: python
-
-    # Plot the local neighborhood of ``seq.Genome``
-    (dj.Diagram(seq.Genome)+1-1+1-1).draw()
diff --git a/docs-parts/definition/12-Example_lang1.rst b/docs-parts/definition/12-Example_lang1.rst
deleted file mode 100644
index b595c6a2a..000000000
--- a/docs-parts/definition/12-Example_lang1.rst
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-.. code-block:: python
-
-    @schema
-    class Animal(dj.Manual):
-        definition = """
-        # information about animal
-        animal_id : int  # animal id assigned by the lab
-        ---
-        -> Species
-        date_of_birth=null : date  # YYYY-MM-DD optional
-        sex='' : enum('M', 'F', '')   # leave empty if unspecified
-        """
-
-    @schema
-    class Session(dj.Manual):
-        definition = """
-        # Experiment Session
-        -> Animal
-        session  : smallint  # session number for the animal
-        ---
-        session_date : date  # YYYY-MM-DD
-        -> User
-        -> Anesthesia
-        -> Rig
-        """
-
-    @schema
-    class Scan(dj.Manual):
-        definition = """
-        # Two-photon imaging scan
-        -> Session
-        scan : smallint  # scan number within the session
-        ---
-        -> Lens
-        laser_wavelength : decimal(5,1)  # um
-        laser_power      : decimal(4,1)  # mW
-        """
diff --git a/docs-parts/definition/13-Lookup-Tables_lang1.rst b/docs-parts/definition/13-Lookup-Tables_lang1.rst
deleted file mode 100644
index 104ee6724..000000000
--- a/docs-parts/definition/13-Lookup-Tables_lang1.rst
+++ /dev/null
@@ -1,17 +0,0 @@
-
-.. code-block:: python
-
-    @schema
-    class User(dj.Lookup):
-        definition = """
-        # users in the lab
-        username : varchar(20)   # user in the lab
-        ---
-        first_name  : varchar(20)   # user first name
-        last_name   : varchar(20)   # user last name
-        """
-        contents = [
-            ['cajal', 'Santiago', 'Cajal'],
-            ['hubel', 'David', 'Hubel'],
-            ['wiesel', 'Torsten', 'Wiesel']
-    ]
diff --git a/docs-parts/definition/14-Drop_lang1.rst b/docs-parts/definition/14-Drop_lang1.rst
deleted file mode 100644
index 2851015d3..000000000
--- a/docs-parts/definition/14-Drop_lang1.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-
-.. code-block:: python
-
-    # drop the Person table from its schema
-    Person.drop()
diff --git a/docs-parts/definition/14-Drop_lang2.rst b/docs-parts/definition/14-Drop_lang2.rst
deleted file mode 100644
index 97c2a8b61..000000000
--- a/docs-parts/definition/14-Drop_lang2.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-A :ref:`part table <master-part>` is usually removed as a consequence of calling ``drop`` on its master table.
-To enforce this workflow, calling ``drop`` directly on a part table produces an error.
-In some cases, it may be necessary to override this behavior.
-To remove a part table without removing its master, use the argument ``force=True``.
diff --git a/docs-parts/existing/0-Virtual-Modules_lang1.rst b/docs-parts/existing/0-Virtual-Modules_lang1.rst
deleted file mode 100644
index 8893a4d80..000000000
--- a/docs-parts/existing/0-Virtual-Modules_lang1.rst
+++ /dev/null
@@ -1,14 +0,0 @@
-The class object ``VirtualModule`` of the ``dj.Schema`` class provides access to virtual modules.
-It creates a python module with the given name from the name of a schema on the server, automatically adds classes to it corresponding to the tables in the schema.
-
-The function can take several parameters:
-
-  ``module_name``: displayed module name.
-
-  ``schema_name``: name of the database in MySQL.
-
-  ``create_schema``: if ``True``, create the schema on the database server if it does not already exist; if ``False`` (default), raise an error when the schema is not found.
-
-  ``create_tables``: if ``True``, ``module.schema`` can be used as the decorator for declaring new classes; if ``False``, such use will raise an error stating that the module is intend only to work with existing tables.
-
-The function returns the Python module containing classes from the schema object with all the table classes already declared inside it.
diff --git a/docs-parts/existing/1-Loading-Classes_lang1.rst b/docs-parts/existing/1-Loading-Classes_lang1.rst
deleted file mode 100644
index 7d209b40c..000000000
--- a/docs-parts/existing/1-Loading-Classes_lang1.rst
+++ /dev/null
@@ -1,239 +0,0 @@
-
-This section describes how to work with database schemas without access to the
-original code that generated the schema. These situations often arise when the
-database is created by another user who has not shared the generating code yet
-or when the database schema is created from a programming language other than
-Python.
-
-.. code-block:: python
-
-    import datajoint as dj
-
-
-Working with schemas and their modules
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Typically a DataJoint schema is created as a dedicated Python module. This
-module defines a schema object that is used to link classes declared in the
-module to tables in the database schema. As an example, examine the university
-module: `university.py <https://github.com/vathes/db-programming-with-datajoint/blob/master/notebooks/university.py>`_.
-
-You may then import the module to interact with its tables:
-
-.. code-block:: python
-
-    import university as uni
-
-*Connecting dimitri\@localhost:3306*
-
-.. code-block:: python
-
-    dj.Diagram(uni)
-
-.. figure:: virtual-module-ERD.svg
-   :align: center
-   :alt: query object preview
-
-.. .. raw:: html
-..     :file: virtual-module-ERD.svg
-
-Note that dj.Diagram can extract the diagram from a schema object or from a
-Python module containing its schema object, lending further support to the
-convention of one-to-one correspondence between database schemas and Python
-modules in a DataJoint project:
-
-``dj.Diagram(uni)``
-
-is equvalent to
-
-``dj.Diagram(uni.schema)``
-
-.. code-block:: python
-
-    # students without majors
-    uni.Student - uni.StudentMajor
-
-.. figure:: StudentTable.png
-   :align: center
-   :alt: query object preview
-
-.. .. csv-table::
-..    :file: Student_Table.csv
-..    :widths: 5, 5, 5, 5, 5, 5, 5, 5, 5, 5
-..    :header-rows: 1
-
-Spawning missing classes
-~~~~~~~~~~~~~~~~~~~~~~~~
-Now imagine that you do not have access to ``university.py`` or you do not have
-its latest version. You can still connect to the database schema but you will
-not have classes declared to interact with it.
-
-So let's start over in this scenario.
-
-You may use the ``dj.list_schemas`` function (new in DataJoint 0.12.0) to
-list the names of database schemas available to you.
-
-.. code-block:: python
-
-    import datajoint as dj
-    dj.list_schemas()
-
-*Connecting dimitri@localhost:3306*
-
-*['dimitri_alter','dimitri_attach','dimitri_blob','dimitri_blobs',
-'dimitri_nphoton','dimitri_schema','dimitri_university','dimitri_uuid',
-'university']*
-
-Just as with a new schema, we start by creating a schema object to connect to
-the chosen database schema:
-
-.. code-block:: python
-
-    schema = dj.Schema('dimitri_university')
-
-If the schema already exists, dj.Schema is initialized as usual and you may plot
-the schema diagram. But instead of seeing class names, you will see the raw
-table names as they appear in the database.
-
-.. code-block:: python
-
-    # let's plot its diagram
-    dj.Diagram(schema)
-
-.. figure:: dimitri-ERD.svg
-   :align: center
-   :alt: query object preview
-
-.. .. raw:: html
-..    :file: dimitri-ERD.svg
-
-You may view the diagram but, at this point, there is no way to interact with
-these tables. A similar situation arises when another developer has added new
-tables to the schema but has not yet shared the updated module code with you.
-Then the diagram will show a mixture of class names and database table names.
-
-Now you may use the ``spawn_missing_classes`` method to spawn classes into
-the local namespace for any tables missing their classes:
-
-.. code-block:: python
-
-    schema.spawn_missing_classes()
-    dj.Diagram(schema)
-
-.. figure:: spawned-classes-ERD.svg
-   :align: center
-   :alt: query object preview
-
-.. .. raw:: html
-..    :file: spawned-classes-ERD.svg
-
-Now you may interact with these tables as if they were declared right here in
-this namespace:
-
-.. code-block:: python
-
-    # students without majors
-    Student - StudentMajor
-
-.. figure:: StudentTable.png
-   :align: center
-   :alt: query object preview
-
-Creating a virtual module
-~~~~~~~~~~~~~~~~~~~~~~~~~
-Now ``spawn_missing_classes`` creates the new classes in the local namespace.
-However, it is often more convenient to import a schema with its Python module,
-equivalent to the Python command
-
-.. code-block:: python
-
-    import university as uni
-
-We can mimick this import without having access to ``university.py`` using the
-``VirtualModule`` class object:
-
-.. code-block:: python
-
-    import datajoint as dj
-
-    uni = dj.VirtualModule('university.py', 'dimitri_university')
-
-*Connecting dimitri@localhost:3306*
-
-Now ``uni`` behaves as an imported module complete with the schema object and all
-the table classes.
-
-.. code-block:: python
-
-    dj.Diagram(uni)
-
-.. figure:: added-example-ERD.svg
-   :align: center
-   :alt: query object preview
-
-.. .. raw:: html
-..    :file: added-example-ERD.svg
-
-.. code-block:: python
-
-    uni.Student - uni.StudentMajor
-
-.. figure:: StudentTable.png
-   :align: center
-   :alt: query object preview
-
-``dj.VirtualModule`` takes optional arguments.
-
-First, ``create_schema=False`` assures that an error is raised when the schema
-does not already exist. Set it to ``True`` if you want to create an empty schema.
-
-.. code-block:: python
-
-    dj.VirtualModule('what', 'nonexistent')
-
-.. code-block:: python
-
-    ---------------------------------------------------------------------------
-    DataJointError                            Traceback (most recent call last)
-    .
-    .
-    .
-    DataJointError: Database named `nonexistent` was not defined. Set argument create_schema=True to create it.
-
-
-The other optional argument, ``create_tables=False`` is passed to the schema
-object. It prevents the use of the schema obect of the virtual module for
-creating new tables in the existing schema. This is a precautionary measure
-since virtual modules are often used for completed schemas. You may set this
-argument to ``True`` if you wish to add new tables to the existing schema. A
-more common approach in this scenario would be to create a new schema object and
-to use the ``spawn_missing_classes`` function to make the classes available.
-
-However, you if do decide to create new tables in an existing tables using the
-virtual module, you may do so by using the schema object from the module as the
-decorator for declaring new tables:
-
-.. code-block:: python
-
-    uni = dj.VirtualModule('university.py', 'dimitri_university', create_tables=True)
-
-.. code-block:: python
-
-    @uni.schema
-    class Example(dj.Manual):
-        definition = """
-        -> uni.Student
-        ---
-        example : varchar(255)
-        """
-
-.. code-block:: python
-
-    dj.Diagram(uni)
-
-.. figure:: added-example-ERD.svg
-   :align: center
-   :alt: query object preview
-
-.. .. raw:: html
-..    :file: added-example-ERD.svg
diff --git a/docs-parts/existing/StudentTable.png b/docs-parts/existing/StudentTable.png
deleted file mode 100644
index c8623f2ab..000000000
Binary files a/docs-parts/existing/StudentTable.png and /dev/null differ
diff --git a/docs-parts/existing/added-example-ERD.svg b/docs-parts/existing/added-example-ERD.svg
deleted file mode 100644
index 7603f2c2c..000000000
--- a/docs-parts/existing/added-example-ERD.svg
+++ /dev/null
@@ -1,207 +0,0 @@
-
-<svg height="327pt" viewBox="0.00 0.00 453.50 327.00" width="454pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-<g class="graph" id="graph0" transform="scale(1 1) rotate(0) translate(4 323)">
-<title>%3</title>
-<polygon fill="#ffffff" points="-4,4 -4,-323 449.5,-323 449.5,4 -4,4" stroke="transparent"/>
-<!-- uni.Term -->
-<g class="node" id="node1">
-<title>uni.Term</title>
-<g id="a_node1"><a xlink:title="term_year            
-term                 
-">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="92,-248 30,-248 30,-213 92,-213 92,-248" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="38" y="-228.4">uni.Term</text>
-</a>
-</g>
-</g>
-<!-- uni.Section -->
-<g class="node" id="node7">
-<title>uni.Section</title>
-<g id="a_node7"><a xlink:title="→ uni.Course
-→ uni.Term
-section              
-------------------------------
-auditorium           
-">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="197.5,-177 120.5,-177 120.5,-142 197.5,-142 197.5,-177" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="128.5" y="-157.4">uni.Section</text>
-</a>
-</g>
-</g>
-<!-- uni.Term&#45;&gt;uni.Section -->
-<g class="edge" id="edge1">
-<title>uni.Term-&gt;uni.Section</title>
-<path d="M85.2247,-212.9494C100.3018,-202.0262 119.6258,-188.0262 134.7143,-177.0947" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- uni.CurrentTerm -->
-<g class="node" id="node8">
-<title>uni.CurrentTerm</title>
-<g id="a_node8"><a xlink:title="omega                
-------------------------------
-→ uni.Term
-">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="102,-177 0,-177 0,-142 102,-142 102,-177" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="8" y="-157.4">uni.CurrentTerm</text>
-</a>
-</g>
-</g>
-<!-- uni.Term&#45;&gt;uni.CurrentTerm -->
-<g class="edge" id="edge2">
-<title>uni.Term-&gt;uni.CurrentTerm</title>
-<path d="M58.5281,-212.9494C57.0012,-202.1083 55.0474,-188.2366 53.5129,-177.3418" fill="none" stroke="#000000" stroke-dasharray="5,2" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- uni.Student -->
-<g class="node" id="node2">
-<title>uni.Student</title>
-<g id="a_node2"><a xlink:title="student_id           
-------------------------------
-first_name           
-last_name            
-sex                  
-date_of_birth        
-home_address         
-home_city            
-home_state           
-home_zip             
-home_phone           
-">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="413.5,-319 336.5,-319 336.5,-284 413.5,-284 413.5,-319" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="344.5" y="-299.4">uni.Student</text>
-</a>
-</g>
-</g>
-<!-- uni.Enroll -->
-<g class="node" id="node4">
-<title>uni.Enroll</title>
-<g id="a_node4"><a xlink:title="→ uni.Student
-→ uni.Section
-">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="328,-106 260,-106 260,-71 328,-71 328,-106" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="middle" x="294" y="-85.4">uni.Enroll</text>
-</a>
-</g>
-</g>
-<!-- uni.Student&#45;&gt;uni.Enroll -->
-<g class="edge" id="edge3">
-<title>uni.Student-&gt;uni.Enroll</title>
-<path d="M398.5769,-283.8767C409.0979,-274.5228 420.3731,-262.0577 426,-248 431.7806,-233.5584 432.1623,-227.2829 426,-213 405.2844,-164.9852 355.9321,-126.8909 323.7917,-106.0403" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- uni.StudentMajor -->
-<g class="node" id="node5">
-<title>uni.StudentMajor</title>
-<g id="a_node5"><a xlink:title="→ uni.Student
-------------------------------
-→ uni.Department
-declare_date         
-">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="315.5,-248 208.5,-248 208.5,-213 315.5,-213 315.5,-248" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="middle" x="262" y="-227.4">uni.StudentMajor</text>
-</a>
-</g>
-</g>
-<!-- uni.Student&#45;&gt;uni.StudentMajor -->
-<g class="edge" id="edge4">
-<title>uni.Student-&gt;uni.StudentMajor</title>
-<path d="M347.0674,-283.9494C329.6826,-273.0262 307.4009,-259.0262 290.0029,-248.0947" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width="2"/>
-</g>
-<!-- uni.Example -->
-<g class="node" id="node6">
-<title>uni.Example</title>
-<g id="a_node6"><a xlink:title="→ uni.Student
-------------------------------
-example              
-">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="416.5,-248 333.5,-248 333.5,-213 416.5,-213 416.5,-248" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="middle" x="375" y="-227.4">uni.Example</text>
-</a>
-</g>
-</g>
-<!-- uni.Student&#45;&gt;uni.Example -->
-<g class="edge" id="edge5">
-<title>uni.Student-&gt;uni.Example</title>
-<path d="M375,-283.9494C375,-273.1083 375,-259.2366 375,-248.3418" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width="2"/>
-</g>
-<!-- uni.Grade -->
-<g class="node" id="node3">
-<title>uni.Grade</title>
-<g id="a_node3"><a xlink:title="→ uni.Enroll
-------------------------------
-→ uni.LetterGrade
-">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="379.5,-35 310.5,-35 310.5,0 379.5,0 379.5,-35" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="middle" x="345" y="-14.4">uni.Grade</text>
-</a>
-</g>
-</g>
-<!-- uni.Enroll&#45;&gt;uni.Grade -->
-<g class="edge" id="edge6">
-<title>uni.Enroll-&gt;uni.Grade</title>
-<path d="M306.6067,-70.9494C314.394,-60.1083 324.3582,-46.2366 332.184,-35.3418" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width="2"/>
-</g>
-<!-- uni.Section&#45;&gt;uni.Enroll -->
-<g class="edge" id="edge7">
-<title>uni.Section-&gt;uni.Enroll</title>
-<path d="M192.3708,-141.9494C213.1403,-131.0262 239.76,-117.0262 260.5452,-106.0947" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- uni.Course -->
-<g class="node" id="node9">
-<title>uni.Course</title>
-<g id="a_node9"><a xlink:title="→ uni.Department
-course               
-------------------------------
-course_name          
-credits              
-">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="190.5,-248 115.5,-248 115.5,-213 190.5,-213 190.5,-248" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="123.5" y="-228.4">uni.Course</text>
-</a>
-</g>
-</g>
-<!-- uni.Course&#45;&gt;uni.Section -->
-<g class="edge" id="edge8">
-<title>uni.Course-&gt;uni.Section</title>
-<path d="M154.4831,-212.9494C155.3993,-202.1083 156.5716,-188.2366 157.4922,-177.3418" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- uni.Department -->
-<g class="node" id="node10">
-<title>uni.Department</title>
-<g id="a_node10"><a xlink:title="dept                 
-------------------------------
-dept_name            
-dept_address         
-dept_phone           
-">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="257,-319 159,-319 159,-284 257,-284 257,-319" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="167" y="-299.4">uni.Department</text>
-</a>
-</g>
-</g>
-<!-- uni.Department&#45;&gt;uni.StudentMajor -->
-<g class="edge" id="edge9">
-<title>uni.Department-&gt;uni.StudentMajor</title>
-<path d="M221.3483,-283.9494C229.5936,-273.1083 240.144,-259.2366 248.4302,-248.3418" fill="none" stroke="#000000" stroke-dasharray="5,2" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- uni.Department&#45;&gt;uni.Course -->
-<g class="edge" id="edge10">
-<title>uni.Department-&gt;uni.Course</title>
-<path d="M194.4045,-283.9494C186.0065,-273.1083 175.2607,-259.2366 166.8211,-248.3418" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- uni.LetterGrade -->
-<g class="node" id="node11">
-<title>uni.LetterGrade</title>
-<g id="a_node11"><a xlink:title="grade                
-------------------------------
-points               
-">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="445.5,-106 346.5,-106 346.5,-71 445.5,-71 445.5,-106" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="354.5" y="-86.4">uni.LetterGrade</text>
-</a>
-</g>
-</g>
-<!-- uni.LetterGrade&#45;&gt;uni.Grade -->
-<g class="edge" id="edge11">
-<title>uni.LetterGrade-&gt;uni.Grade</title>
-<path d="M383.3933,-70.9494C375.606,-60.1083 365.6418,-46.2366 357.816,-35.3418" fill="none" stroke="#000000" stroke-dasharray="5,2" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-</g>
-</svg>
\ No newline at end of file
diff --git a/docs-parts/existing/dimitri-ERD.svg b/docs-parts/existing/dimitri-ERD.svg
deleted file mode 100644
index 5c805f8ed..000000000
--- a/docs-parts/existing/dimitri-ERD.svg
+++ /dev/null
@@ -1,117 +0,0 @@
-
-<svg height="327pt" viewBox="0.00 0.00 740.50 327.00" width="741pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-<g class="graph" id="graph0" transform="scale(1 1) rotate(0) translate(4 323)">
-<title>%3</title>
-<polygon fill="#ffffff" points="-4,4 -4,-323 736.5,-323 736.5,4 -4,4" stroke="transparent"/>
-<!-- `dimitri_university`.`course` -->
-<g class="node" id="node1">
-<title>`dimitri_university`.`course`</title>
-<polygon fill="#00ff00" fill-opacity="0.188235" points="528.5,-248 367.5,-248 367.5,-213 528.5,-213 528.5,-248" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="375.5" y="-228.4">`dimitri_university`.`course`</text>
-</g>
-<!-- `dimitri_university`.`section` -->
-<g class="node" id="node7">
-<title>`dimitri_university`.`section`</title>
-<polygon fill="#00ff00" fill-opacity="0.188235" points="523.5,-177 360.5,-177 360.5,-142 523.5,-142 523.5,-177" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="368.5" y="-157.4">`dimitri_university`.`section`</text>
-</g>
-<!-- `dimitri_university`.`course`&#45;&gt;`dimitri_university`.`section` -->
-<g class="edge" id="edge1">
-<title>`dimitri_university`.`course`-&gt;`dimitri_university`.`section`</title>
-<path d="M446.5169,-212.9494C445.6007,-202.1083 444.4284,-188.2366 443.5078,-177.3418" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- `dimitri_university`.`current_term` -->
-<g class="node" id="node2">
-<title>`dimitri_university`.`current_term`</title>
-<polygon fill="#00ff00" fill-opacity="0.188235" points="732.5,-177 541.5,-177 541.5,-142 732.5,-142 732.5,-177" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="549.5" y="-157.4">`dimitri_university`.`current_term`</text>
-</g>
-<!-- `dimitri_university`.`department` -->
-<g class="node" id="node3">
-<title>`dimitri_university`.`department`</title>
-<polygon fill="#00ff00" fill-opacity="0.188235" points="505.5,-319 320.5,-319 320.5,-284 505.5,-284 505.5,-319" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="328.5" y="-299.4">`dimitri_university`.`department`</text>
-</g>
-<!-- `dimitri_university`.`department`&#45;&gt;`dimitri_university`.`course` -->
-<g class="edge" id="edge2">
-<title>`dimitri_university`.`department`-&gt;`dimitri_university`.`course`</title>
-<path d="M421.6517,-283.9494C426.9959,-273.1083 433.8341,-259.2366 439.2047,-248.3418" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- `dimitri_university`.`student_major` -->
-<g class="node" id="node9">
-<title>`dimitri_university`.`student_major`</title>
-<polygon fill="#00ff00" fill-opacity="0.188235" points="349,-248 149,-248 149,-213 349,-213 349,-248" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="middle" x="249" y="-227.4">`dimitri_university`.`student_major`</text>
-</g>
-<!-- `dimitri_university`.`department`&#45;&gt;`dimitri_university`.`student_major` -->
-<g class="edge" id="edge3">
-<title>`dimitri_university`.`department`-&gt;`dimitri_university`.`student_major`</title>
-<path d="M372.4607,-283.9494C347.2296,-273.0262 314.8916,-259.0262 289.6414,-248.0947" fill="none" stroke="#000000" stroke-dasharray="5,2" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- `dimitri_university`.`enroll` -->
-<g class="node" id="node4">
-<title>`dimitri_university`.`enroll`</title>
-<polygon fill="#00ff00" fill-opacity="0.188235" points="361.5,-106 206.5,-106 206.5,-71 361.5,-71 361.5,-106" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="middle" x="284" y="-85.4">`dimitri_university`.`enroll`</text>
-</g>
-<!-- `dimitri_university`.`grade` -->
-<g class="node" id="node5">
-<title>`dimitri_university`.`grade`</title>
-<polygon fill="#00ff00" fill-opacity="0.188235" points="266.5,-35 111.5,-35 111.5,0 266.5,0 266.5,-35" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="middle" x="189" y="-14.4">`dimitri_university`.`grade`</text>
-</g>
-<!-- `dimitri_university`.`enroll`&#45;&gt;`dimitri_university`.`grade` -->
-<g class="edge" id="edge4">
-<title>`dimitri_university`.`enroll`-&gt;`dimitri_university`.`grade`</title>
-<path d="M260.5168,-70.9494C245.9013,-60.0262 227.1689,-46.0262 212.5422,-35.0947" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width="2"/>
-</g>
-<!-- `dimitri_university`.`letter_grade` -->
-<g class="node" id="node6">
-<title>`dimitri_university`.`letter_grade`</title>
-<polygon fill="#00ff00" fill-opacity="0.188235" points="188,-106 0,-106 0,-71 188,-71 188,-106" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="8" y="-86.4">`dimitri_university`.`letter_grade`</text>
-</g>
-<!-- `dimitri_university`.`letter_grade`&#45;&gt;`dimitri_university`.`grade` -->
-<g class="edge" id="edge5">
-<title>`dimitri_university`.`letter_grade`-&gt;`dimitri_university`.`grade`</title>
-<path d="M117.4832,-70.9494C132.0987,-60.0262 150.8311,-46.0262 165.4578,-35.0947" fill="none" stroke="#000000" stroke-dasharray="5,2" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- `dimitri_university`.`section`&#45;&gt;`dimitri_university`.`enroll` -->
-<g class="edge" id="edge6">
-<title>`dimitri_university`.`section`-&gt;`dimitri_university`.`enroll`</title>
-<path d="M402.9438,-141.9494C378.6358,-131.0262 347.4809,-117.0262 323.1545,-106.0947" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- `dimitri_university`.`student` -->
-<g class="node" id="node8">
-<title>`dimitri_university`.`student`</title>
-<polygon fill="#00ff00" fill-opacity="0.188235" points="267,-319 103,-319 103,-284 267,-284 267,-319" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="111" y="-299.4">`dimitri_university`.`student`</text>
-</g>
-<!-- `dimitri_university`.`student`&#45;&gt;`dimitri_university`.`enroll` -->
-<g class="edge" id="edge7">
-<title>`dimitri_university`.`student`-&gt;`dimitri_university`.`enroll`</title>
-<path d="M164.6178,-283.8306C155.1889,-274.2936 144.9894,-261.6831 140,-248 134.671,-233.3857 133.5302,-227.1463 140,-213 162.5422,-163.711 215.2999,-126.4787 250.3081,-106.0837" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- `dimitri_university`.`student`&#45;&gt;`dimitri_university`.`student_major` -->
-<g class="edge" id="edge8">
-<title>`dimitri_university`.`student`-&gt;`dimitri_university`.`student_major`</title>
-<path d="M200.8202,-283.9494C210.5925,-273.1083 223.0966,-259.2366 232.9172,-248.3418" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width="2"/>
-</g>
-<!-- `dimitri_university`.`term` -->
-<g class="node" id="node10">
-<title>`dimitri_university`.`term`</title>
-<polygon fill="#00ff00" fill-opacity="0.188235" points="706,-248 558,-248 558,-213 706,-213 706,-248" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="566" y="-228.4">`dimitri_university`.`term`</text>
-</g>
-<!-- `dimitri_university`.`term`&#45;&gt;`dimitri_university`.`current_term` -->
-<g class="edge" id="edge9">
-<title>`dimitri_university`.`term`-&gt;`dimitri_university`.`current_term`</title>
-<path d="M633.236,-212.9494C633.9994,-202.1083 634.9763,-188.2366 635.7435,-177.3418" fill="none" stroke="#000000" stroke-dasharray="5,2" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- `dimitri_university`.`term`&#45;&gt;`dimitri_university`.`section` -->
-<g class="edge" id="edge10">
-<title>`dimitri_university`.`term`-&gt;`dimitri_university`.`section`</title>
-<path d="M585.0337,-212.9494C555.8026,-202.0262 518.3378,-188.0262 489.0845,-177.0947" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-</g>
-</svg>
\ No newline at end of file
diff --git a/docs-parts/existing/spawned-classes-ERD.svg b/docs-parts/existing/spawned-classes-ERD.svg
deleted file mode 100644
index 65fbd7ccd..000000000
--- a/docs-parts/existing/spawned-classes-ERD.svg
+++ /dev/null
@@ -1,147 +0,0 @@
-
-<svg height="327pt" viewBox="0.00 0.00 337.50 327.00" width="338pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-<g class="graph" id="graph0" transform="scale(1 1) rotate(0) translate(4 323)">
-<title>%3</title>
-<polygon fill="#ffffff" points="-4,4 -4,-323 333.5,-323 333.5,4 -4,4" stroke="transparent"/>
-<!-- Course -->
-<g class="node" id="node1">
-<title>Course</title>
-<g id="a_node1"><a xlink:title="→ Departmentcourse               ------------------------------course_name          credits              ">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="233,-248 177,-248 177,-213 233,-213 233,-248" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="185" y="-228.4">Course</text>
-</a>
-</g>
-</g>
-<!-- Section -->
-<g class="node" id="node7">
-<title>Section</title>
-<g id="a_node7"><a xlink:title="→ Course→ Termsection              ------------------------------auditorium           ">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="228.5,-177 171.5,-177 171.5,-142 228.5,-142 228.5,-177" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="179.5" y="-157.4">Section</text>
-</a>
-</g>
-</g>
-<!-- Course&#45;&gt;Section -->
-<g class="edge" id="edge1">
-<title>Course-&gt;Section</title>
-<path d="M203.764,-212.9494C203.0006,-202.1083 202.0237,-188.2366 201.2565,-177.3418" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- Department -->
-<g class="node" id="node2">
-<title>Department</title>
-<g id="a_node2"><a xlink:title="dept                 ------------------------------dept_name            dept_address         dept_phone           ">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="224.5,-319 145.5,-319 145.5,-284 224.5,-284 224.5,-319" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="153.5" y="-299.4">Department</text>
-</a>
-</g>
-</g>
-<!-- Department&#45;&gt;Course -->
-<g class="edge" id="edge2">
-<title>Department-&gt;Course</title>
-<path d="M189.9438,-283.9494C192.9976,-273.1083 196.9052,-259.2366 199.9741,-248.3418" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- StudentMajor -->
-<g class="node" id="node8">
-<title>StudentMajor</title>
-<g id="a_node8"><a xlink:title="→ Student------------------------------→ Departmentdeclare_date         ">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="159,-248 71,-248 71,-213 159,-213 159,-248" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="middle" x="115" y="-227.4">StudentMajor</text>
-</a>
-</g>
-</g>
-<!-- Department&#45;&gt;StudentMajor -->
-<g class="edge" id="edge3">
-<title>Department-&gt;StudentMajor</title>
-<path d="M167.6966,-283.9494C157.0082,-273.1083 143.3318,-259.2366 132.5905,-248.3418" fill="none" stroke="#000000" stroke-dasharray="5,2" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- Term -->
-<g class="node" id="node3">
-<title>Term</title>
-<g id="a_node3"><a xlink:title="term_year            term                 ">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="304.5,-248 261.5,-248 261.5,-213 304.5,-213 304.5,-248" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="269.5" y="-228.4">Term</text>
-</a>
-</g>
-</g>
-<!-- Term&#45;&gt;Section -->
-<g class="edge" id="edge4">
-<title>Term-&gt;Section</title>
-<path d="M262.4831,-212.9494C249.7138,-202.0262 233.3476,-188.0262 220.5685,-177.0947" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- CurrentTerm -->
-<g class="node" id="node9">
-<title>CurrentTerm</title>
-<g id="a_node9"><a xlink:title="omega                ------------------------------→ Term">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="329.5,-177 246.5,-177 246.5,-142 329.5,-142 329.5,-177" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="254.5" y="-157.4">CurrentTerm</text>
-</a>
-</g>
-</g>
-<!-- Term&#45;&gt;CurrentTerm -->
-<g class="edge" id="edge5">
-<title>Term-&gt;CurrentTerm</title>
-<path d="M284.236,-212.9494C284.9994,-202.1083 285.9763,-188.2366 286.7435,-177.3418" fill="none" stroke="#000000" stroke-dasharray="5,2" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- LetterGrade -->
-<g class="node" id="node4">
-<title>LetterGrade</title>
-<g id="a_node4"><a xlink:title="grade                ------------------------------points               ">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="80,-106 0,-106 0,-71 80,-71 80,-106" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="8" y="-86.4">LetterGrade</text>
-</a>
-</g>
-</g>
-<!-- Grade -->
-<g class="node" id="node10">
-<title>Grade</title>
-<g id="a_node10"><a xlink:title="→ Enroll------------------------------→ LetterGrade">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="106,-35 56,-35 56,0 106,0 106,-35" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="middle" x="81" y="-14.4">Grade</text>
-</a>
-</g>
-</g>
-<!-- LetterGrade&#45;&gt;Grade -->
-<g class="edge" id="edge6">
-<title>LetterGrade-&gt;Grade</title>
-<path d="M50.1348,-70.9494C56.3952,-60.1083 64.4056,-46.2366 70.697,-35.3418" fill="none" stroke="#000000" stroke-dasharray="5,2" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- Enroll -->
-<g class="node" id="node5">
-<title>Enroll</title>
-<g id="a_node5"><a xlink:title="→ Student→ Section">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="146,-106 98,-106 98,-71 146,-71 146,-106" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="middle" x="122" y="-85.4">Enroll</text>
-</a>
-</g>
-</g>
-<!-- Enroll&#45;&gt;Grade -->
-<g class="edge" id="edge7">
-<title>Enroll-&gt;Grade</title>
-<path d="M111.8652,-70.9494C105.6048,-60.1083 97.5944,-46.2366 91.303,-35.3418" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width="2"/>
-</g>
-<!-- Student -->
-<g class="node" id="node6">
-<title>Student</title>
-<g id="a_node6"><a xlink:title="student_id           ------------------------------first_name           last_name            sex                  date_of_birth        home_address         home_city            home_state           home_zip             home_phone           ">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="108,-319 50,-319 50,-284 108,-284 108,-319" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="58" y="-299.4">Student</text>
-</a>
-</g>
-</g>
-<!-- Student&#45;&gt;Enroll -->
-<g class="edge" id="edge8">
-<title>Student-&gt;Enroll</title>
-<path d="M71.2696,-283.7936C64.4603,-265.8736 56.4262,-237.4043 62,-213 71.2959,-172.299 95.6737,-129.4101 110.3369,-106.1102" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- Student&#45;&gt;StudentMajor -->
-<g class="edge" id="edge9">
-<title>Student-&gt;StudentMajor</title>
-<path d="M87.8989,-283.9494C93.3958,-273.1083 100.4293,-259.2366 105.9534,-248.3418" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width="2"/>
-</g>
-<!-- Section&#45;&gt;Enroll -->
-<g class="edge" id="edge10">
-<title>Section-&gt;Enroll</title>
-<path d="M180.7191,-141.9494C168.7189,-131.0262 153.3387,-117.0262 141.3294,-106.0947" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-</g>
-</svg>
\ No newline at end of file
diff --git a/docs-parts/existing/virtual-module-ERD.svg b/docs-parts/existing/virtual-module-ERD.svg
deleted file mode 100644
index 69d98ae2a..000000000
--- a/docs-parts/existing/virtual-module-ERD.svg
+++ /dev/null
@@ -1,147 +0,0 @@
-
-<svg height="327pt" viewBox="0.00 0.00 411.50 327.00" width="412pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-<g class="graph" id="graph0" transform="scale(1 1) rotate(0) translate(4 323)">
-<title>%3</title>
-<polygon fill="#ffffff" points="-4,4 -4,-323 407.5,-323 407.5,4 -4,4" stroke="transparent"/>
-<!-- uni.LetterGrade -->
-<g class="node" id="node1">
-<title>uni.LetterGrade</title>
-<g id="a_node1"><a xlink:title="grade                ------------------------------points               ">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="99,-106 0,-106 0,-71 99,-71 99,-106" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="8" y="-86.4">uni.LetterGrade</text>
-</a>
-</g>
-</g>
-<!-- uni.Grade -->
-<g class="node" id="node3">
-<title>uni.Grade</title>
-<g id="a_node3"><a xlink:title="→ uni.Enroll------------------------------→ uni.LetterGrade">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="135,-35 66,-35 66,0 135,0 135,-35" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="middle" x="100.5" y="-14.4">uni.Grade</text>
-</a>
-</g>
-</g>
-<!-- uni.LetterGrade&#45;&gt;uni.Grade -->
-<g class="edge" id="edge1">
-<title>uni.LetterGrade-&gt;uni.Grade</title>
-<path d="M62.1067,-70.9494C69.894,-60.1083 79.8582,-46.2366 87.684,-35.3418" fill="none" stroke="#000000" stroke-dasharray="5,2" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- uni.Course -->
-<g class="node" id="node2">
-<title>uni.Course</title>
-<g id="a_node2"><a xlink:title="→ uni.Departmentcourse               ------------------------------course_name          credits              ">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="288,-248 213,-248 213,-213 288,-213 288,-248" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="221" y="-228.4">uni.Course</text>
-</a>
-</g>
-</g>
-<!-- uni.Section -->
-<g class="node" id="node5">
-<title>uni.Section</title>
-<g id="a_node5"><a xlink:title="→ uni.Course→ uni.Termsection              ------------------------------auditorium           ">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="283,-177 206,-177 206,-142 283,-142 283,-177" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="214" y="-157.4">uni.Section</text>
-</a>
-</g>
-</g>
-<!-- uni.Course&#45;&gt;uni.Section -->
-<g class="edge" id="edge2">
-<title>uni.Course-&gt;uni.Section</title>
-<path d="M249.0169,-212.9494C248.1007,-202.1083 246.9284,-188.2366 246.0078,-177.3418" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- uni.Term -->
-<g class="node" id="node4">
-<title>uni.Term</title>
-<g id="a_node4"><a xlink:title="term_year            term                 ">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="378.5,-248 316.5,-248 316.5,-213 378.5,-213 378.5,-248" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="324.5" y="-228.4">uni.Term</text>
-</a>
-</g>
-</g>
-<!-- uni.Term&#45;&gt;uni.Section -->
-<g class="edge" id="edge3">
-<title>uni.Term-&gt;uni.Section</title>
-<path d="M322.0393,-212.9494C306.193,-202.0262 285.8831,-188.0262 270.0248,-177.0947" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- uni.CurrentTerm -->
-<g class="node" id="node7">
-<title>uni.CurrentTerm</title>
-<g id="a_node7"><a xlink:title="omega                ------------------------------→ uni.Term">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="403.5,-177 301.5,-177 301.5,-142 403.5,-142 403.5,-177" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="309.5" y="-157.4">uni.CurrentTerm</text>
-</a>
-</g>
-</g>
-<!-- uni.Term&#45;&gt;uni.CurrentTerm -->
-<g class="edge" id="edge4">
-<title>uni.Term-&gt;uni.CurrentTerm</title>
-<path d="M348.736,-212.9494C349.4994,-202.1083 350.4763,-188.2366 351.2435,-177.3418" fill="none" stroke="#000000" stroke-dasharray="5,2" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- uni.Enroll -->
-<g class="node" id="node8">
-<title>uni.Enroll</title>
-<g id="a_node8"><a xlink:title="→ uni.Student→ uni.Section">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="185.5,-106 117.5,-106 117.5,-71 185.5,-71 185.5,-106" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="middle" x="151.5" y="-85.4">uni.Enroll</text>
-</a>
-</g>
-</g>
-<!-- uni.Section&#45;&gt;uni.Enroll -->
-<g class="edge" id="edge5">
-<title>uni.Section-&gt;uni.Enroll</title>
-<path d="M221.5112,-141.9494C207.2034,-131.0262 188.8653,-117.0262 174.5466,-106.0947" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- uni.StudentMajor -->
-<g class="node" id="node6">
-<title>uni.StudentMajor</title>
-<g id="a_node6"><a xlink:title="→ uni.Student------------------------------→ uni.Departmentdeclare_date         ">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="195,-248 88,-248 88,-213 195,-213 195,-248" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="middle" x="141.5" y="-227.4">uni.StudentMajor</text>
-</a>
-</g>
-</g>
-<!-- uni.Enroll&#45;&gt;uni.Grade -->
-<g class="edge" id="edge6">
-<title>uni.Enroll-&gt;uni.Grade</title>
-<path d="M138.8933,-70.9494C131.106,-60.1083 121.1418,-46.2366 113.316,-35.3418" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width="2"/>
-</g>
-<!-- uni.Department -->
-<g class="node" id="node9">
-<title>uni.Department</title>
-<g id="a_node9"><a xlink:title="dept                 ------------------------------dept_name            dept_address         dept_phone           ">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="277.5,-319 179.5,-319 179.5,-284 277.5,-284 277.5,-319" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="187.5" y="-299.4">uni.Department</text>
-</a>
-</g>
-</g>
-<!-- uni.Department&#45;&gt;uni.Course -->
-<g class="edge" id="edge7">
-<title>uni.Department-&gt;uni.Course</title>
-<path d="M233.9382,-283.9494C237.2974,-273.1083 241.5957,-259.2366 244.9715,-248.3418" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- uni.Department&#45;&gt;uni.StudentMajor -->
-<g class="edge" id="edge8">
-<title>uni.Department-&gt;uni.StudentMajor</title>
-<path d="M206.9944,-283.9494C193.6096,-273.0262 176.4547,-259.0262 163.0597,-248.0947" fill="none" stroke="#000000" stroke-dasharray="5,2" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-<!-- uni.Student -->
-<g class="node" id="node10">
-<title>uni.Student</title>
-<g id="a_node10"><a xlink:title="student_id           ------------------------------first_name           last_name            sex                  date_of_birth        home_address         home_city            home_state           home_zip             home_phone           ">
-<polygon fill="#00ff00" fill-opacity="0.188235" points="139,-319 62,-319 62,-284 139,-284 139,-319" stroke="#00ff00" stroke-opacity="0.188235"/>
-<text fill="#006400" font-family="arial" font-size="12.00" text-anchor="start" text-decoration="underline" x="70" y="-299.4">uni.Student</text>
-</a>
-</g>
-</g>
-<!-- uni.Student&#45;&gt;uni.StudentMajor -->
-<g class="edge" id="edge9">
-<title>uni.Student-&gt;uni.StudentMajor</title>
-<path d="M110.6348,-283.9494C116.8952,-273.1083 124.9056,-259.2366 131.197,-248.3418" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width="2"/>
-</g>
-<!-- uni.Student&#45;&gt;uni.Enroll -->
-<g class="edge" id="edge10">
-<title>uni.Student-&gt;uni.Enroll</title>
-<path d="M90.7385,-283.8113C82.1149,-265.905 71.8615,-237.4462 78.5,-213 89.8597,-171.1678 119.2037,-129.0177 137.0374,-106.0834" fill="none" stroke="#000000" stroke-opacity="0.250980" stroke-width=".75"/>
-</g>
-</g>
-</svg>
\ No newline at end of file
diff --git a/docs-parts/index_lang1.rst b/docs-parts/index_lang1.rst
deleted file mode 100644
index dc7dd445c..000000000
--- a/docs-parts/index_lang1.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-This is a detailed manual for active users of DataJoint in Python. 
diff --git a/docs-parts/intro/Releases_lang1.rst b/docs-parts/intro/Releases_lang1.rst
deleted file mode 100644
index 5264a2d32..000000000
--- a/docs-parts/intro/Releases_lang1.rst
+++ /dev/null
@@ -1,313 +0,0 @@
-0.13.7 -- Jul 13, 2022
-----------------------
-* Bugfix - Fix networkx incompatable change by version pinning to 2.6.3 PR #1036 (#1035)
-* Add - Support for serializing numpy datetime64 types PR #1036 (#1022)
-* Update - Add traceback to default logging PR #1036
-
-0.13.6 -- Jun 13, 2022
-----------------------
-* Add - Config option to set threshold for when to stop using checksums for filepath stores. PR #1025
-* Add - Unified package level logger for package (#667) PR #1031
-* Update - Swap various datajoint messages, warnings, etc. to use the new logger. (#667) PR #1031
-* Bugfix - Fix query caching deleting non-datajoint files PR #1027
-* Update - Minimum Python version for Datajoint-Python is now 3.7 PR #1027
-
-0.13.5 -- May 19, 2022
-----------------------
-* Update - Import ABC from collections.abc for Python 3.10 compatibility
-* Bugfix - Fix multiprocessing value error (#1013) PR #1026
-
-0.13.4 -- Mar 28, 2022
-----------------------
-* Add - Allow reading blobs produced by legacy 32-bit compiled mYm library for matlab. PR #995
-* Bugfix - Add missing ``jobs`` argument for multiprocessing PR #997
-* Add - Test for multiprocessing PR #1008
-* Bugfix - Fix external store key name doesn't allow '-' (#1005) PR #1006
-* Add - Adopted black formatting into code base PR #998
-
-0.13.3 -- Feb 9, 2022
-----------------------
-* Bugfix - Fix error in listing ancestors, descendants with part tables.
-* Bugfix - Fix Python 3.10 compatibility (#983) PR #972
-* Bugfix - Allow renaming non-conforming attributes in proj (#982) PR #972
-* Add - Expose proxy feature for S3 external stores (#961) PR #962
-* Add - implement multiprocessing in populate (#695) PR #704, #969
-* Bugfix - Dependencies not properly loaded on populate. (#902) PR #919
-* Bugfix - Replace use of numpy aliases of built-in types with built-in type. (#938) PR #939
-* Bugfix - Deletes and drops must include the master of each part. (#151, #374) PR #957
-* Bugfix - `ExternalTable.delete` should not remove row on error (#953) PR #956
-* Bugfix - Fix error handling of remove_object function in `s3.py` (#952) PR #955
-* Bugfix - Fix sql code generation to comply with sql mode ``ONLY_FULL_GROUP_BY`` (#916) PR #965
-* Bugfix - Fix count for left-joined ``QueryExpressions`` (#951) PR #966
-* Bugfix - Fix assertion error when performing a union into a join (#930) PR #967
-* Bugfix - Fix regression issue with `DISTINCT` clause and `GROUP_BY` (#914) PR #963
-* Update `~jobs.error_stack` from blob to mediumblob to allow error stacks >64kB in jobs (#984) PR #986
-* Bugfix - Fix error when performing a union on multiple tables (#926) PR #964
-* Add - Allow optional keyword arguments for `make()` in `populate()` PR #971
-
-0.13.2 -- May 7, 2021
-----------------------
-* Update `setuptools_certificate` dependency to new name `otumat`
-* Bugfix - Explicit calls to `dj.Connection` throw error due to missing `host_input` (#895) PR #907
-* Bugfix - Correct count of deleted items. (#897) PR #912
-
-0.13.1 -- Apr 16, 2021
-----------------------
-* Add `None` as an alias for `IS NULL` comparison in `dict` restrictions (#824) PR #893
-* Drop support for MySQL 5.6 since it has reached EOL PR #893
-* Bugfix - `schema.list_tables()` is not topologically sorted (#838) PR #893
-* Bugfix - Diagram part tables do not show proper class name (#882) PR #893
-* Bugfix - Error in complex restrictions (#892) PR #893
-* Bugfix - WHERE and GROUP BY clases are dropped on joins with aggregation (#898, #899) PR #893
-
-0.13.0 -- Mar 24, 2021
-----------------------
-* Re-implement query transpilation into SQL, fixing issues (#386, #449, #450, #484, #558). PR #754
-* Re-implement cascading deletes for better performance. PR #839
-* Add support for deferred schema activation to allow for greater modularity. (#834) PR #839
-* Add query caching mechanism for offline development (#550) PR #839
-* Add table method `.update1` to update a row in the table with new values (#867) PR #763, #889
-* Python datatypes are now enabled by default in blobs (#761). PR #859
-* 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` (#804) PR #862
-* 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 (#829) PR #861
-
-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
-* Add `schema.list_tables`. (#838) PR #844
-* Fix minio new version regression.  PR #847
-* Add more S3 logging for debugging. (#831) PR #832
-* Convert testing framework from TravisCI to GitHub Actions (#841) PR #840
-
-0.12.7 -- Oct 27, 2020
-----------------------
-* Fix case sensitivity issues to adapt to MySQL 8+.  PR #819
-* Fix pymysql regression bug (#814) PR #816
-* Adapted attribute types now have `dtype=object` in all recarray results. PR #811
-
-0.12.6 -- May 15, 2020
-----------------------
-* Add `order_by` to `dj.kill` (#668, #779) PR #775, #783
-* Add explicit S3 bucket and file storage location existence checks (#748) PR #781
-* Modify `_update` to allow nullable updates for strings/date (#664) PR #760
-* Avoid logging events on auxiliary tables (#737) PR #753
-* Add `kill_quick` and expand display to include host (#740) PR #741
-* Bugfix - pandas insert fails due to additional `index` field (#666) PR #776
-* Bugfix - `delete_external_files=True` does not remove from S3 (#686) PR #781
-* Bugfix - pandas fetch throws error when `fetch_format='frame'` PR #774
-
-0.12.5 -- Feb 24, 2020
-----------------------
-* Rename module `dj.schema` into `dj.schemas`. `dj.schema` remains an alias for class `dj.Schema`. (#731) PR #732
-* `dj.create_virtual_module` is now called `dj.VirtualModule` (#731) PR #732
-* Bugfix - SSL `KeyError` on failed connection (#716) PR #725
-* Bugfix - Unable to run unit tests using nosetests (#723) PR #724
-* Bugfix - `suppress_errors` does not suppress loss of connection error (#720) PR #721
-
-0.12.4 -- Jan 14, 2020
-----------------------
-* Support for simple scalar datatypes in blobs (#690) PR #709
-* Add support for the `serial` data type in declarations: alias for `bigint unsigned auto_increment` PR #713
-* Improve the log table to avoid primary key collisions PR #713
-* Improve documentation in README PR #713
-
-0.12.3 -- Nov 22, 2019
-----------------------
-* Bugfix - networkx 2.4 causes error in diagrams (#675) PR #705
-* Bugfix - include table definition in doc string and help (#698, #699) PR #706
-* Bugfix - job reservation fails when native python datatype support is disabled (#701) PR #702
-
-0.12.2 -- Nov 11, 2019
--------------------------
-* Bugfix - Convoluted error thrown if there is a reference to a non-existent table attribute (#691) PR #696
-* Bugfix - Insert into external does not trim leading slash if defined in `dj.config['stores']['<store>']['location']` (#692) PR #693
-
-0.12.1 -- Nov 2, 2019
--------------------------
-* Bugfix - AttributeAdapter converts into a string (#684) PR #688
-
-0.12.0 -- Oct 31, 2019
--------------------------
-* Dropped support for Python 3.4
-* Support secure connections with TLS (aka SSL) PR #620
-* Convert numpy array from python object to appropriate data type if all elements are of the same type (#587) PR #608
-* Remove expression requirement to have additional attributes (#604) PR #604
-* Support for filepath datatype (#481) PR #603, #659
-* Support file attachment datatype (#480, #592, #637) PR #659
-* Fetch return a dict array when specifying `as_dict=True` for specified attributes. (#595) PR #593
-* Support of ellipsis in `proj`:  `query_expression.proj(.., '-movie')` (#499) PR #578
-* Expand support of blob serialization (#572, #520, #427, #392, #244, #594) PR #577
-* Support for alter (#110) PR #573
-* Support for `conda install datajoint` via `conda-forge` channel (#293)
-* `dj.conn()` accepts a `port` keyword argument (#563) PR #571
-* Support for UUID datatype (#562) PR #567
-* `query_expr.fetch("KEY", as_dict=False)` returns results as `np.recarray`(#414) PR #574
-* `dj.ERD` is now called `dj.Diagram` (#255, #546) PR #565
-* `dj.Diagram` underlines "distinguished" classes (#378) PR #557
-* Accept alias for supported MySQL datatypes (#544) PR #545
-* Support for pandas in `fetch` (#459, #537) PR #534
-* Support for ordering by "KEY" in `fetch` (#541) PR #534
-* Add config to enable python native blobs PR #672, #676
-* Add secure option for external storage (#663) PR #674, #676
-* Add blob migration utility from DJ011 to DJ012 PR #673
-* Improved external storage - a migration script needed from version 0.11  (#467, #475, #480, #497) PR #532
-* Increase default display rows (#523) PR #526
-* Bugfixes (#521, #205, #279, #477, #570, #581, #597, #596, #618, #633, #643, #644, #647, #648, #650, #656)
-* Minor improvements (#538)
-
-0.11.1 -- Nov 15, 2018
-----------------------
-* Fix ordering of attributes in proj (#483, #516)
-* Prohibit direct insert into auto-populated tables (#511)
-
-0.11.0 -- Oct 25, 2018
-----------------------
-* Full support of dependencies with renamed attributes using projection syntax (#300, #345, #436, #506, #507)
-* Rename internal class and module names to comply with terminology in documentation (#494, #500)
-* Full support of secondary indexes (#498, 500)
-* ERD no longer shows numbers in nodes corresponding to derived dependencies (#478, #500)
-* Full support of unique and nullable dependencies (#254, #301, #493, #495, #500)
-* Improve memory management in ``populate`` (#461, #486)
-* Fix query errors and redundancies (#456, #463, #482)
-
-0.10.1  -- Aug 28, 2018
------------------------
-* Fix ERD Tooltip message (#431)
-* Networkx 2.0 support (#443)
-* Fix insert from query with skip_duplicates=True (#451)
-* Sped up queries (#458)
-* Bugfix in restriction of the form (A & B) * B (#463)
-* Improved error messages (#466)
-
-0.10.0 -- Jan 10, 2018
-----------------------
-* Deletes are more efficient (#424)
-* ERD shows table definition on tooltip hover in Jupyter (#422)
-* S3 external storage
-* Garbage collection for external sorage
-* Most operators and methods of tables can be invoked as class methods rather than instance methods (#407)
-* The schema decorator object no longer requires locals() to specify the context
-* Compatibility with pymysql 0.8.0+
-* More efficient loading of dependencies (#403)
-
-0.9.0 -- Nov 17, 2017
----------------------
-* Made graphviz installation optional
-* Implement file-based external storage
-* Implement union operator +
-* Implement file-based external storage
-
-0.8.0 -- Jul 26, 2017
----------------------
-Documentation and tutorials available at https://docs.datajoint.io and https://tutorials.datajoint.io
-* improved the ERD graphics and features using the graphviz libraries (#207, #333)
-* improved password handling logic (#322, #321)
-* the use of the ``contents`` property to populate tables now only works in ``dj.Lookup`` classes (#310).
-* allow suppressing the display of size of query results through the ``show_tuple_count`` configuration option (#309)
-* implemented renamed foreign keys to spec (#333)
-* added the ``limit`` keyword argument to populate (#329)
-* reduced the number of displayed messages (#308)
-* added ``size_on_disk`` property for dj.Schema() objects (#323)
-* job keys are entered in the jobs table (#316, #243)
-* simplified the ``fetch`` and ``fetch1`` syntax, deprecating the ``fetch[...]`` syntax (#319)
-* the jobs tables now store the connection ids to allow identifying abandoned jobs (#288, #317)
-
-0.5.0 (#298) -- Mar 8, 2017
----------------------------
-* All fetched integers are now 64-bit long and all fetched floats are double precision.
-* Added ``dj.create_virtual_module``
-
-0.4.10 (#286) -- Feb 6, 2017
-----------------------------
-* Removed Vagrant and Readthedocs support
-* Explicit saving of configuration (issue #284)
-
-0.4.9 (#285) -- Feb 2, 2017
----------------------------
-* Fixed setup.py for pip install
-
-0.4.7 (#281) -- Jan 24, 2017
-----------------------------
-* Fixed issues related to order of attributes in projection.
-
-0.4.6 (#277) -- Dec 22, 2016
-----------------------------
-* Proper handling of interruptions during populate
-
-0.4.5 (#274) -- Dec 20, 2016
-----------------------------
-* Populate reports how many keys remain to be populated at the start.
-
-0.4.3  (#271) -- Dec 6, 2016
-----------------------------
-* Fixed aggregation issues (#270)
-* datajoint no longer attempts to connect to server at import time
-* dropped support of view (reversed #257)
-* more elegant handling of insufficient privileges (#268)
-
-0.4.2 (#267)  -- Dec 6, 2016
-----------------------------
-* improved table appearance in Jupyter
-
-0.4.1 (#266) -- Oct 28, 2016
-----------------------------
-* bugfix for very long error messages
-
-0.3.9 -- Sep 27, 2016
----------------------
-* Added support for datatype ``YEAR``
-* Fixed issues with ``dj.U`` and the ``aggr`` operator (#246, #247)
-
-0.3.8  -- Aug 2, 2016
----------------------
-* added the ``_update`` method in ``base_relation``. It allows updating values in existing tuples.
-* bugfix in reading values of type double.  Previously it was cast as float32.
-
-0.3.7  -- Jul 31, 2016
-----------------------
-* added parameter ``ignore_extra_fields`` in ``insert``
-* ``insert(..., skip_duplicates=True)`` now relies on ``SELECT IGNORE``.  Previously it explicitly checked if tuple already exists.
-* table previews now include blob attributes displaying the string <BLOB>
-
-0.3.6  -- Jul 30, 2016
-----------------------
-* bugfix in ``schema.spawn_missing_classes``.  Previously, spawned part classes would not show in ERDs.
-* dj.key now causes fetch to return as a list of dicts.  Previously it was a recarray.
-
-0.3.5
------
-* ``dj.set_password()`` now asks for user confirmation before changing the password.
-* fixed issue #228
-
-0.3.4
------
-* Added method the ``ERD.add_parts`` method, which adds the part tables of all tables currently in the ERD.
-* ``ERD() + arg`` and ``ERD() - arg`` can now accept relation classes as arg.
-
-0.3.3
------
-* Suppressed warnings (redirected them to logging).  Previoiusly, scipy would throw warnings in ERD, for example.
-* Added ERD.from_sequence as a shortcut to combining the ERDs of multiple sources
-* ERD() no longer text the context argument.
-* ERD.draw() now takes an optional context argument.  By default uses the caller's locals.
-
-0.3.2
------
-* Fixed issue #223:  ``insert`` can insert relations without fetching.
-* ERD() now takes the ``context`` argument, which specifies in which context to look for classes. The default is taken from the argument (schema or relation).
-* ERD.draw() no longer has the ``prefix`` argument: class names are shown as found in the context.
diff --git a/docs-parts/manipulation/1-Insert_lang1.rst b/docs-parts/manipulation/1-Insert_lang1.rst
deleted file mode 100644
index bee8417da..000000000
--- a/docs-parts/manipulation/1-Insert_lang1.rst
+++ /dev/null
@@ -1,42 +0,0 @@
-
-In Python there is a separate method ``insert1`` to insert one entity at a time.
-The entity may have the form of a Python dictionary with key names matching the attribute names in the table.
-
-.. code-block:: python
-
-    lab.Person.insert1(
-           dict(username='alice',
-                first_name='Alice',
-                last_name='Cooper'))
-
-The entity also may take the form of a sequence of values in the same order as the attributes in the table.
-
-.. code-block:: python
-
-    lab.Person.insert1(['alice', 'Alice', 'Cooper'])
-
-Additionally, the entity may be inserted as a `NumPy record array <https://docs.scipy.org/doc/numpy/reference/generated/numpy.record.html#numpy.record>`_ or `Pandas DataFrame <https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.html>`_.
-
-The ``insert`` method accepts a sequence or a generator of multiple entities and is used to insert multiple entities at once.
-
-.. code-block:: python
-
-    lab.Person.insert([
-           ['alice',   'Alice',   'Cooper'],
-           ['bob',     'Bob',     'Dylan'],
-           ['carol',   'Carol',   'Douglas']])
-
-Several optional parameters can be used with ``insert``:
-
-  ``replace`` If ``True``, replaces the existing entity.
-  (Default ``False``.)
-
-  ``skip_duplicates`` If ``True``, silently skip duplicate inserts.
-  (Default ``False``.)
-
-  ``ignore_extra_fields`` If ``False``, fields that are not in the heading raise an error.
-  (Default ``False``.)
-
-  ``allow_direct_insert`` If ``True``, allows inserts outside of populate calls.
-  Applies only in auto-populated tables.
-  (Default ``None``.)
diff --git a/docs-parts/manipulation/1-Insert_lang2.rst b/docs-parts/manipulation/1-Insert_lang2.rst
deleted file mode 100644
index 9e5d8616f..000000000
--- a/docs-parts/manipulation/1-Insert_lang2.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-
-.. code-block:: python
-
-  # Server-side inserts are faster...
-  phase_two.Protocol.insert(phase_one.Protocol)
-
-  # ...than fetching before inserting
-  protocols = phase_one.Protocol.fetch()
-  phase_two.Protocol.insert(protocols)
diff --git a/docs-parts/manipulation/1-Update_lang1.rst b/docs-parts/manipulation/1-Update_lang1.rst
deleted file mode 100644
index 0960e2e22..000000000
--- a/docs-parts/manipulation/1-Update_lang1.rst
+++ /dev/null
@@ -1,14 +0,0 @@
-
-.. 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/manipulation/2-Delete_lang1.rst b/docs-parts/manipulation/2-Delete_lang1.rst
deleted file mode 100644
index 1b154cabd..000000000
--- a/docs-parts/manipulation/2-Delete_lang1.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-The ``delete`` method deletes entities from a table and all dependent entries in dependent tables.
diff --git a/docs-parts/manipulation/2-Delete_lang2.rst b/docs-parts/manipulation/2-Delete_lang2.rst
deleted file mode 100644
index a1c9a68da..000000000
--- a/docs-parts/manipulation/2-Delete_lang2.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-
-.. code-block:: python
-
-    # delete all entries from tuning.VonMises
-    tuning.VonMises.delete()
-
-    # delete entries from tuning.VonMises for mouse 1010
-    (tuning.VonMises & 'mouse=1010').delete()
-
-    # delete entries from tuning.VonMises except mouse 1010
-    (tuning.VonMises - 'mouse=1010').delete()
diff --git a/docs-parts/manipulation/2-Delete_lang3.rst b/docs-parts/manipulation/2-Delete_lang3.rst
deleted file mode 100644
index 7e2aa7a66..000000000
--- a/docs-parts/manipulation/2-Delete_lang3.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-To enforce this workflow, calling ``delete`` directly on a part table produces an error.
-In some cases, it may be necessary to override this behavior.
-To remove entities from a part table without calling ``delete`` master, use the argument ``force=True``.
diff --git a/docs-parts/manipulation/3-Transactions_lang1.rst b/docs-parts/manipulation/3-Transactions_lang1.rst
deleted file mode 100644
index 53732b0df..000000000
--- a/docs-parts/manipulation/3-Transactions_lang1.rst
+++ /dev/null
@@ -1,20 +0,0 @@
-Transactions are formed using the ``transaction`` property of the connection object. 
-The connection object may be obtained from any table object.
-The ``transaction`` property can then be used as a context manager in Python's ``with`` statement.
-
-For example, the following code inserts matching entries for the master table ``Session`` and its part table ``Session.Experimenter``.
-
-.. code-block:: python
-
-    # get the connection object 
-    connection = Session.connection
-
-    # insert Session and Session.Experimenter entries in a transaction
-    with connection.transaction:
-        key = {'subject_id': animal_id, 'session_time': session_time}
-        Session.insert1({**key, 'brain_region':region, 'cortical_layer':layer})
-        Session.Experimenter.insert1({**key, 'experimenter': username})
-
-Here, to external observers, both inserts will take effect together upon exiting from the ``with`` block or will not have any effect at all.
-For example, if the second insert fails due to an error, the first insert will be rolled back. 
-
diff --git a/docs-parts/queries/01-Queries_lang1.rst b/docs-parts/queries/01-Queries_lang1.rst
deleted file mode 100644
index 7e0543b1c..000000000
--- a/docs-parts/queries/01-Queries_lang1.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-
-.. code-block:: python
-
-    query  = experiment.Session()
-
diff --git a/docs-parts/queries/01-Queries_lang2.rst b/docs-parts/queries/01-Queries_lang2.rst
deleted file mode 100644
index c334dfa72..000000000
--- a/docs-parts/queries/01-Queries_lang2.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-
-.. code-block:: python
-
-    query = experiment.Session * experiment.Scan & 'animal_id = 102'
-
-Note that for brevity, query operators can be applied directly to class objects rather than instance objects so that ``experiment.Session`` may be used in place of ``experiment.Session()``.
-
diff --git a/docs-parts/queries/01-Queries_lang3.rst b/docs-parts/queries/01-Queries_lang3.rst
deleted file mode 100644
index aafd492c8..000000000
--- a/docs-parts/queries/01-Queries_lang3.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-
-    s = query.fetch()
-
-Here fetching from the ``query`` object produces the NumPy record array ``s`` of the queried data.
-
diff --git a/docs-parts/queries/01-Queries_lang4.rst b/docs-parts/queries/01-Queries_lang4.rst
deleted file mode 100644
index 082409f9a..000000000
--- a/docs-parts/queries/01-Queries_lang4.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-
-The ``bool`` function applied to a query object evaluates to ``True`` if the query returns any entities and to ``False`` if the query result is empty.
-
-The ``len`` function applied to a query object determines the number of entities returned by the query.
-
-.. code-block:: python
-
-    # number of sessions since the start of 2018.
-    n = len(Session & 'session_date >= "2018-01-01"')
-
diff --git a/docs-parts/queries/02-Example-Schema_lang1.rst b/docs-parts/queries/02-Example-Schema_lang1.rst
deleted file mode 100644
index 00a17cee0..000000000
--- a/docs-parts/queries/02-Example-Schema_lang1.rst
+++ /dev/null
@@ -1,100 +0,0 @@
-
-.. warning::
-  Empty primary keys, such as in the ``CurrentTerm`` table, are not yet supported by DataJoint.
-  This feature will become available in a future release.
-  See `Issue #113 <https://github.com/datajoint/datajoint-python/issues/113>`_ for more information.
-
-.. code-block:: python
-
-  @schema
-  class Student (dj.Manual):
-    definition = """
-    student_id      : int unsigned # university ID
-    ---
-    first_name      : varchar(40)
-    last_name       : varchar(40)
-    sex             : enum('F', 'M', 'U')
-    date_of_birth   : date
-    home_address    : varchar(200) # street address
-    home_city       : varchar(30)
-    home_state      : char(2) # two-letter abbreviation
-    home_zipcode    : char(10)
-    home_phone      : varchar(14)
-    """
-
-  @schema
-  class Department (dj.Manual):
-    definition = """
-    dept         : char(6) # abbreviated department name, e.g. BIOL
-    ---
-    dept_name    : varchar(200) # full department name
-    dept_address : varchar(200) # mailing address
-    dept_phone   : varchar(14)
-    """
-
-  @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. "Cell Biology"
-    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)
-    ---
-    room  :  varchar(12) # building and room code
-    """
-
-  @schema
-  class CurrentTerm (dj.Manual):
-    definition = """
-    ---
-    -> Term
-    """
-
-  @schema
-  class Enroll (dj.Manual):
-    definition = """
-    -> Section
-    -> Student
-    """
-
-  @schema
-  class LetterGrade (dj.Manual):
-    definition = """
-    grade : char(2)
-    ---
-    points : decimal(3,2)
-    """
-
-  @schema
-  class Grade (dj.Manual):
-    definition = """
-    -> Enroll
-    ---
-    -> LetterGrade
-    """
-
diff --git a/docs-parts/queries/03-Fetch_lang1.rst b/docs-parts/queries/03-Fetch_lang1.rst
deleted file mode 100644
index b291948c9..000000000
--- a/docs-parts/queries/03-Fetch_lang1.rst
+++ /dev/null
@@ -1,96 +0,0 @@
-
-Entire table
-~~~~~~~~~~~~
-
-The following statement retrieves the entire table as a NumPy `recarray <https://docs.scipy.org/doc/numpy/reference/generated/numpy.recarray.html>`_.
-
-.. code-block:: python
-
-    data = query.fetch()
-
-To retrieve the data as a list of ``dict``:
-
-.. code-block:: python
-
-    data = query.fetch(as_dict=True)
-
-In some cases, the amount of data returned by fetch can be quite large; in these cases it can be useful to use the ``size_on_disk`` attribute to determine if running a bare fetch would be wise.
-Please note that it is only currently possible to query the size of entire tables stored directly in the database at this time.
-
-As separate variables
-~~~~~~~~~~~~~~~~~~~~~
-
-.. code-block:: python
-
-    name, img = query.fetch1('name', 'image')  # when query has exactly one entity
-    name, img = query.fetch('name', 'image')  # [name, ...] [image, ...]
-
-Primary key values
-~~~~~~~~~~~~~~~~~~
-
-.. code-block:: python
-
-    keydict = tab.fetch1("KEY")  # single key dict when tab has exactly one entity
-    keylist = tab.fetch("KEY")  # list of key dictionaries [{}, ...]
-
-``KEY`` can also used when returning attribute values as separate variables, such that one of the returned variables contains the entire primary keys.
-
-Sorting and limiting the results
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-To sort the result, use the ``order_by`` keyword argument.
-
-.. code-block:: python
-
-    # ascending order:
-    data = query.fetch(order_by='name')
-    # descending order:
-    data = query.fetch(order_by='name desc')  
-    # by name first, year second:
-    data = query.fetch(order_by=('name desc', 'year'))
-    # sort by the primary key:
-    data = query.fetch(order_by='KEY')
-    # sort by name but for same names order by primary key:
-    data = query.fetch(order_by=('name', 'KEY desc'))
-
-The ``order_by`` argument can be a string specifying the attribute to sort by. By default the sort is in ascending order. Use ``'attr desc'`` to sort in descending order by attribute ``attr``.  The value can also be a sequence of strings, in which case, the sort performed on all the attributes jointly in the order specified.
-
-The special attribute name ``'KEY'`` represents the primary key attributes in order that they appear in the index. Otherwise, this name can be used as any other argument.
-
-If an attribute happens to be a SQL reserved word, it needs to be enclosed in backquotes.  For example:
-
-.. code-block:: python
-
-    data = query.fetch(order_by='`select` desc')
-
-The ``order_by`` value is eventually passed  to the ``ORDER BY`` `clause <https://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html>`_.
-
-Similarly, the ``limit`` and ``offset`` arguments can be used to limit the result to a subset of entities.
-
-For example, one could do the following:
-
-.. code-block:: python
-
-    data = query.fetch(order_by='name', limit=10, offset=5)
-
-Note that an ``offset`` cannot be used without specifying a ``limit`` as well. 
-
-Usage with Pandas
-~~~~~~~~~~~~~~~~~
-
-The ``pandas`` `library <http://pandas.pydata.org/>`_ is a popular library for data analysis in Python which can easily be used with DataJoint query results.
-Since the records returned by ``fetch()`` are contained within a ``numpy.recarray``, they can be easily converted to ``pandas.DataFrame`` objects by passing them into the ``pandas.DataFrame`` constructor.
-For example:
-
-.. code-block:: python
-
-    import pandas as pd
-    frame = pd.DataFrame(tab.fetch())
-
-Calling ``fetch()`` with the argument ``format="frame"`` returns results as ``pandas.DataFrame`` objects indexed by the table's primary key attributes.
-
-.. code-block:: python
-
-  frame = tab.fetch(format="frame")
-
-Returning results as a ``DataFrame`` is not possible when fetching a particular subset of attributes or when ``as_dict`` is set to ``True``.
diff --git a/docs-parts/queries/04-Iteration_lang1.rst b/docs-parts/queries/04-Iteration_lang1.rst
deleted file mode 100644
index 1afb8b7e4..000000000
--- a/docs-parts/queries/04-Iteration_lang1.rst
+++ /dev/null
@@ -1,24 +0,0 @@
-
-In the simple example below, iteration is used to display the names and values of the attributes of each entity in the simple table or table expression ``tab``.
-
-.. code-block:: python
-
-    for entity in tab:
-        print(entity)
-
-This example illustrates the function of the iterator: DataJoint iterates through the whole table expression, returning the entire entity during each step.
-In this case, each entity will be returned as a ``dict`` containing all attributes.
-
-At the start of the above loop, DataJoint internally fetches only the primary keys of the entities in ``tab``.
-Since only the primary keys are needed to distinguish between entities, DataJoint can then iterate over the list of primary keys to execute the loop.
-At each step of the loop, DataJoint uses a single primary key to fetch an entire entity for use in the iteration, such that ``print(entity)`` will print all attributes of each entity.
-By first fetching only the primary keys and then fetching each entity individually, DataJoint saves memory at the cost of network overhead.
-This can be particularly useful for tables containing large amounts of data in secondary attributes.
-
-The memory savings of the above syntax may not be worth the additional network overhead in all cases, such as for tables with little data stored as secondary attributes.
-In the example below, DataJoint fetches all of the attributes of each entity in a single call and then iterates over the list of entities stored in memory.
-
-.. code-block:: python
-
-    for entity in tab.fetch(as_dict=True):
-        print(entity)
diff --git a/docs-parts/queries/06-Restriction_lang1.rst b/docs-parts/queries/06-Restriction_lang1.rst
deleted file mode 100644
index bff0f5e88..000000000
--- a/docs-parts/queries/06-Restriction_lang1.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-
-* another table
-* a mapping, e.g. ``dict``
-* an expression in a character string
-* a collection of conditions as a ``list``, ``tuple``, or Pandas ``DataFrame``
-* a Boolean expression (``True`` or ``False``)
-* an ``AndList``
-* a ``Not`` object
-* a query expression
diff --git a/docs-parts/queries/06-Restriction_lang2.rst b/docs-parts/queries/06-Restriction_lang2.rst
deleted file mode 100644
index 29482c6fe..000000000
--- a/docs-parts/queries/06-Restriction_lang2.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-
-.. code-block:: python
-
-    Session & {'session_dat': "2018-01-01"}
-
diff --git a/docs-parts/queries/06-Restriction_lang3.rst b/docs-parts/queries/06-Restriction_lang3.rst
deleted file mode 100644
index e04d86151..000000000
--- a/docs-parts/queries/06-Restriction_lang3.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-
-.. code-block:: python
-
-  # All the sessions performed by Alice
-  Session & 'user = "Alice"'
-
-  # All the experiments at least one minute long
-  Experiment & 'duration >= 60'
diff --git a/docs-parts/queries/06-Restriction_lang4.rst b/docs-parts/queries/06-Restriction_lang4.rst
deleted file mode 100644
index 6302e764c..000000000
--- a/docs-parts/queries/06-Restriction_lang4.rst
+++ /dev/null
@@ -1,15 +0,0 @@
-
-A collection can be a list, a tuple, or a Pandas ``DataFrame``.
-
-.. code-block:: python
-
-    # a list:
-    cond_list = ['first_name = "Aaron"', 'last_name = "Aaronson"']
-
-    # a tuple:
-    cond_tuple = ('first_name = "Aaron"', 'last_name = "Aaronson"')
-
-    # a dataframe:
-    import pandas as pd
-    cond_frame = pd.DataFrame(
-                data={'first_name': ['Aaron'], 'last_name': ['Aaronson']})
diff --git a/docs-parts/queries/06-Restriction_lang5.rst b/docs-parts/queries/06-Restriction_lang5.rst
deleted file mode 100644
index a0f9dc2e1..000000000
--- a/docs-parts/queries/06-Restriction_lang5.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-
-.. code-block:: python
-
-    Student() & ['first_name = "Aaron"', 'last_name = "Aaronson"']
-
-.. figure:: ../_static/img/python_collection.png
-    :align: center
-    :alt: restriction by collection
-
-    Restriction by a collection, returning all entities matching any condition in the collection.
-
diff --git a/docs-parts/queries/06-Restriction_lang6.rst b/docs-parts/queries/06-Restriction_lang6.rst
deleted file mode 100644
index c32575ec1..000000000
--- a/docs-parts/queries/06-Restriction_lang6.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-
-``A & True`` and ``A - False`` are equivalent to ``A``.
-
-``A & False`` and ``A - True`` are empty.
diff --git a/docs-parts/queries/06-Restriction_lang7.rst b/docs-parts/queries/06-Restriction_lang7.rst
deleted file mode 100644
index d57ae0630..000000000
--- a/docs-parts/queries/06-Restriction_lang7.rst
+++ /dev/null
@@ -1,18 +0,0 @@
-
-Restriction by an ``AndList``
------------------------------
-
-The special function ``dj.AndList`` represents logical conjunction (logical AND).
-Restriction of table ``A`` by an ``AndList`` will return all entities in ``A`` that meet *all* of the conditions in the list.
-``A & dj.AndList([c1, c2, c3])`` is equivalent to ``A & c1 & c2 & c3``.
-Usually, it is more convenient to simply write out all of the conditions, as ``A & c1 & c2 & c3``.
-However, when a list of conditions has already been generated, the list can simply be passed as the argument to ``dj.AndList``.
-
-Restriction of table ``A`` by an empty ``AndList``, as in ``A & dj.AndList([])``, will return all of the entities in ``A``.
-Exclusion by an empty ``AndList`` will return no entities.
-
-Restriction by a ``Not`` object
--------------------------------
-
-The special function ``dj.Not`` represents logical negation, such that ``A & dj.Not(cond)`` is equivalent to ``A - cond``.
-
diff --git a/docs-parts/queries/06-Restriction_lang8.rst b/docs-parts/queries/06-Restriction_lang8.rst
deleted file mode 100644
index a00bff9cf..000000000
--- a/docs-parts/queries/06-Restriction_lang8.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-.. code-block:: python
-
-  query = Session & 'user = "Alice"'
-  Experiment & query
diff --git a/docs-parts/queries/08-Proj_lang1.rst b/docs-parts/queries/08-Proj_lang1.rst
deleted file mode 100644
index 1843e087d..000000000
--- a/docs-parts/queries/08-Proj_lang1.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-
-This is done using keyword arguments:
-``tab.proj(new_attr='old_attr')``
diff --git a/docs-parts/queries/08-Proj_lang2.rst b/docs-parts/queries/08-Proj_lang2.rst
deleted file mode 100644
index edadb0210..000000000
--- a/docs-parts/queries/08-Proj_lang2.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-
-.. code-block:: python
-
-    tab.proj(animal='mouse', 'stimulus')
diff --git a/docs-parts/queries/08-Proj_lang3.rst b/docs-parts/queries/08-Proj_lang3.rst
deleted file mode 100644
index 08ec43285..000000000
--- a/docs-parts/queries/08-Proj_lang3.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-
-.. code-block:: python
-
-    tab * tab.proj(other='cell')
diff --git a/docs-parts/queries/08-Proj_lang4.rst b/docs-parts/queries/08-Proj_lang4.rst
deleted file mode 100644
index c32bfd67b..000000000
--- a/docs-parts/queries/08-Proj_lang4.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-
-.. code-block:: python
-
-    tab.proj(depth='scan_z-surface_z') & 'depth > 500'
diff --git a/docs-parts/queries/09-Aggr_lang1.rst b/docs-parts/queries/09-Aggr_lang1.rst
deleted file mode 100644
index 6bb99964d..000000000
--- a/docs-parts/queries/09-Aggr_lang1.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-
-.. code-block:: python
-
-  # Number of students in each course section
-  Section.aggr(Enroll, n="count(*)")
-  # Average grade in each course
-  Course.aggr(Grade * LetterGrade, avg_grade="avg(points)")
diff --git a/docs-parts/queries/11-Universal-Sets_lang1.rst b/docs-parts/queries/11-Universal-Sets_lang1.rst
deleted file mode 100644
index f80d231cd..000000000
--- a/docs-parts/queries/11-Universal-Sets_lang1.rst
+++ /dev/null
@@ -1,14 +0,0 @@
-
-.. code-block:: python
-
-  # All home cities of students
-  dj.U('home_city', 'home_state') & Student
-
-  # Total number of students from each city
-  dj.U('home_city', 'home_state').aggr(Student, n="count(*)")
-
-  # Total number of students from each state
-  U('home_state').aggr(Student, n="count(*)")
-
-  # Total number of students in the database
-  U().aggr(Student, n="count(*)")
diff --git a/docs-parts/queries/12-Query-Caching_lang1.rst b/docs-parts/queries/12-Query-Caching_lang1.rst
deleted file mode 100644
index 673eef85b..000000000
--- a/docs-parts/queries/12-Query-Caching_lang1.rst
+++ /dev/null
@@ -1,13 +0,0 @@
-
-.. 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
deleted file mode 100644
index 54661c6b7..000000000
--- a/docs-parts/queries/12-Query-Caching_lang2.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-
-.. 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
deleted file mode 100644
index 34e3784cd..000000000
--- a/docs-parts/queries/12-Query-Caching_lang3.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-
-.. 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
deleted file mode 100644
index e015646e1..000000000
--- a/docs-parts/queries/13-Transpiler-Design_lang1.md
+++ /dev/null
@@ -1,113 +0,0 @@
-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/setup/01-Install-and-Connect_lang1.rst b/docs-parts/setup/01-Install-and-Connect_lang1.rst
deleted file mode 100644
index 6bb1a9e52..000000000
--- a/docs-parts/setup/01-Install-and-Connect_lang1.rst
+++ /dev/null
@@ -1,53 +0,0 @@
-
-DataJoint is implemented for Python 3.4+.
-You may install it from `PyPI <https://pypi.python.org/pypi/datajoint>`_:
-
-::
-
-    pip3 install datajoint
-
-or upgrade
-
-::
-
-    pip3 install --upgrade datajoint
-
-Next configure the connection through DataJoint's ``config`` object:
-
-.. code-block:: python
-
-    In [1]: import datajoint as dj
-    DataJoint 0.4.9 (February 1, 2017)
-    No configuration found. Use `dj.config` to configure and save the configuration.
-
-You may now set the database credentials:
-
-.. code-block:: python
-
-    In [2]: dj.config['database.host'] = "alicelab.datajoint.io"
-    In [3]: dj.config['database.user'] = "alice"
-    In [4]: dj.config['database.password'] = "haha not my real password"
-
-Skip setting the password to make DataJoint prompt to enter the password every time.
-
-You may save the configuration in the local work directory with ``dj.config.save_local()`` or for all your projects in ``dj.config.save_global()``.
-Configuration changes should be made through the ``dj.config`` interface; the config file should not be modified directly by the user.
-
-You may leave the user or the password as ``None``, in which case you will be prompted to enter them manually for every session.
-Setting the password as an empty string allows access without a password.
-
-Note that the system environment variables ``DJ_HOST``, ``DJ_USER``, and ``DJ_PASS`` will overwrite the settings in the config file.
-You can use them to set the connection credentials instead of config files.
-
-To change the password, the ``dj.set_password`` function will walk you through the process:
-
-::
-
-    >>> dj.set_password()
-
-After that, update the password in the configuration and save it as described above:
-
-.. code-block:: python
-
-    dj.config['database.password'] = 'my#cool!new*psswrd'
-    dj.config.save_local()   # or dj.config.save_global()
diff --git a/docs-parts/version_common.json b/docs-parts/version_common.json
deleted file mode 100644
index 2296f52e6..000000000
--- a/docs-parts/version_common.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-    "comm_version": "v0.2"
-}
\ No newline at end of file
diff --git a/docs/.docker/Dockerfile b/docs/.docker/Dockerfile
new file mode 100644
index 000000000..e3acb0f42
--- /dev/null
+++ b/docs/.docker/Dockerfile
@@ -0,0 +1,15 @@
+FROM datajoint/miniconda3:4.10.3-py3.9-alpine
+ARG PACKAGE
+WORKDIR /main
+COPY --chown=anaconda:anaconda ./docs/.docker/apk_requirements.txt ${APK_REQUIREMENTS}
+COPY --chown=anaconda:anaconda ./docs/.docker/pip_requirements.txt ${PIP_REQUIREMENTS}
+RUN \
+    /entrypoint.sh echo "Dependencies installed" && \
+    git config --global user.name "GitHub Action" && \
+    git config --global user.email "action@github.com"&& \
+    git config --global pull.rebase false && \
+    git init
+COPY --chown=anaconda:anaconda ./${PACKAGE} /main/${PACKAGE}
+COPY --chown=anaconda:anaconda ./docs/mkdocs.yaml /main/docs/mkdocs.yaml
+COPY --chown=anaconda:anaconda ./docs/src /main/docs/src
+COPY --chown=anaconda:anaconda ./CHANGELOG.md /main/docs/src/about/changelog.md
\ No newline at end of file
diff --git a/docs/.docker/apk_requirements.txt b/docs/.docker/apk_requirements.txt
new file mode 100644
index 000000000..5664e303b
--- /dev/null
+++ b/docs/.docker/apk_requirements.txt
@@ -0,0 +1,1 @@
+git
diff --git a/docs/.docker/pip_requirements.txt b/docs/.docker/pip_requirements.txt
new file mode 100644
index 000000000..e09eef4cc
--- /dev/null
+++ b/docs/.docker/pip_requirements.txt
@@ -0,0 +1,10 @@
+mkdocs-material
+mkdocs-redirects
+mkdocstrings
+mkdocstrings-python
+mike
+mdx-truly-sane-lists
+mkdocs-gen-files
+mkdocs-literate-nav
+mkdocs-exclude-search
+mkdocs-jupyter
diff --git a/docs/.markdownlint.yaml b/docs/.markdownlint.yaml
new file mode 100644
index 000000000..593bb1d73
--- /dev/null
+++ b/docs/.markdownlint.yaml
@@ -0,0 +1,20 @@
+# https://github.com/DavidAnson/markdownlint
+# https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md
+MD009: false # permit trailing spaces
+MD013: 
+    line_length: "88" # Line length limits
+    tables: false # disable for tables
+    headings: false # disable for headings
+    code_blocks: false # disable for code blocks
+MD030: false # Number of spaces after a list
+MD033: # HTML elements allowed
+    allowed_elements: 
+        - "div"
+        - "span"
+        - "a"
+        - "br"
+        - "sup"  
+        - "figure"
+MD034: false # Bare URLs OK
+MD031: false # Spacing w/code blocks. Conflicts with `??? Note` and code tab styling
+MD046: false # Spacing w/code blocks. Conflicts with `??? Note` and code tab styling
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 000000000..a7da95426
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,16 @@
+# Docs Contributions
+
+Docs contributors should be aware of the following extensions, with the corresponding
+settings files, that were used in developing these docs:
+
+- [MarkdownLinter](https://github.com/DavidAnson/markdownlint):
+  - `.markdownlint.yaml` establishes settings for various
+  [linter rules](https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md)
+  - `.vscode/settings.json` formatting on save to fix linting
+
+- [CSpell](https://github.com/streetsidesoftware/vscode-spell-checker): `cspell.json`
+has various ignored words.
+
+- [ReWrap](https://github.com/stkb/Rewrap/): `.vscode/settings.json` allows toggling
+automated hard wrapping for files at 88 characters. This can also be keymapped to be
+performed on individual paragraphs, see documentation.
diff --git a/docs/docker-compose.yaml b/docs/docker-compose.yaml
new file mode 100644
index 000000000..b599ccd49
--- /dev/null
+++ b/docs/docker-compose.yaml
@@ -0,0 +1,38 @@
+# MODE="LIVE|QA|BUILD" PACKAGE=datajoint UPSTREAM_REPO=https://github.com/datajoint/datajoint-python.git HOST_UID=$(id -u) docker compose -f docs/docker-compose.yaml up --build
+version: "2.4"
+services:
+  docs:
+    build:
+      dockerfile: docs/.docker/Dockerfile
+      context: ../
+      args:
+        - PACKAGE
+    image: ${PACKAGE}-docs
+    environment:
+      - PACKAGE
+      - UPSTREAM_REPO
+      - MODE
+    volumes:
+      - ..:/main
+    user: ${HOST_UID}:anaconda
+    ports:
+      - 8080:80
+    command:
+      - sh
+      - -c
+      - |
+        set -e
+        if echo "$${MODE}" | grep -i live &>/dev/null; then
+            mkdocs serve --config-file ./docs/mkdocs.yaml -a 0.0.0.0:80
+        elif echo "$${MODE}" | grep -iE "qa|build" &>/dev/null; then
+            git branch -D gh-pages || true
+            git fetch $${UPSTREAM_REPO} gh-pages:gh-pages || true
+            mike deploy --config-file ./docs/mkdocs.yaml -u $$(grep -oE '\d+\.\d+' /main/$${PACKAGE}/version.py) latest
+            mike set-default --config-file ./docs/mkdocs.yaml latest
+            if echo "$${MODE}" | grep -i qa &>/dev/null; then
+                mike serve --config-file ./docs/mkdocs.yaml -a 0.0.0.0:80
+            fi
+        else
+            echo "Unexpected mode..."
+            exit 1
+        fi
diff --git a/docs/mkdocs.yaml b/docs/mkdocs.yaml
new file mode 100644
index 000000000..0a00bf067
--- /dev/null
+++ b/docs/mkdocs.yaml
@@ -0,0 +1,134 @@
+# ---------------------- PROJECT SPECIFIC ---------------------------
+
+site_name: DataJoint Python
+repo_url: https://github.com/datajoint/datajoint-python
+repo_name: datajoint/datajoint-python
+nav:
+  - DataJoint Python: getting-started/index.md
+  - Getting Started: getting-started/index.md
+  - Existing Pipelines: concepts/existing-pipelines.md
+  - Query Language:
+      - Common Commands: query-lang/common-commands.md
+      - Operators: query-lang/operators.md
+      - Iteration: query-lang/iteration.md
+      - Query Caching: query-lang/query-caching.md
+  - Reproducibility:
+      - Table Tiers: reproduce/table-tiers.md
+      - Make Method: reproduce/make-method.md
+  - Tutorials:
+      - tutorials/json.ipynb
+  - Develop: develop.md
+  - Changelog: about/changelog.md
+  - API: api/ # defer to gen-files + literate-nav
+
+# ---------------------------- STANDARD -----------------------------
+
+edit_uri: ./edit/master/docs/src
+docs_dir: ./src
+theme:
+  font:
+    text: Roboto Slab
+    code: Source Code Pro
+  name: material
+  custom_dir: src/.overrides
+  icon:
+    logo: main/company-logo
+  favicon: assets/images/company-logo-blue.png
+  features:
+    - toc.integrate
+    - content.code.annotate # Add codeblock annotations
+  palette:
+    - media: "(prefers-color-scheme: light)"
+      scheme: datajoint
+      toggle:
+        icon: material/brightness-7
+        name: Switch to dark mode
+    - media: "(prefers-color-scheme: dark)"
+      scheme: slate
+      toggle:
+        icon: material/brightness-4
+        name: Switch to light mode
+plugins:
+  - search
+  - redirects:
+      redirect_maps:
+        "index.md": "getting-started/index.md"
+  - mkdocstrings:
+      default_handler: python
+      handlers:
+        python:
+          options:
+            filters:
+              - "!^_"
+            docstring_style: sphinx # Replaces google default pending docstring updates
+            members_order: source
+            group_by_category: false
+            line_length: 88
+  - gen-files:
+      scripts:
+        - ./src/api/make_pages.py
+  - literate-nav:
+      nav_file: navigation.md
+  - exclude-search:
+      exclude:
+        - "*/navigation.md"
+        - "*/archive/*md"
+  - mkdocs-jupyter:
+      include: ["*.ipynb"]
+markdown_extensions:
+  - attr_list
+  - toc:
+      permalink: true
+  - pymdownx.emoji:
+      emoji_index: !!python/name:materialx.emoji.twemoji
+      emoji_generator: !!python/name:materialx.emoji.to_svg
+      options:
+        custom_icons:
+          - .overrides/.icons
+  - mdx_truly_sane_lists
+  - pymdownx.tabbed:
+      alternate_style: true
+  - admonition
+  - pymdownx.details
+  - pymdownx.superfences:
+      custom_fences:
+        - name: mermaid
+          class: mermaid
+          format: !!python/name:pymdownx.superfences.fence_code_format
+extra:
+  generator: false # Disable watermark
+  analytics:
+    provider: google
+    property: !ENV GOOGLE_ANALYTICS_KEY
+  version:
+    provider: mike
+  social:
+    - icon: main/company-logo
+      link: https://www.datajoint.com
+      name: DataJoint
+    - icon: fontawesome/brands/slack
+      link: https://datajoint.slack.com
+      name: Slack
+    - icon: fontawesome/brands/linkedin
+      link: https://www.linkedin.com/company/datajoint
+      name: LinkedIn
+    - icon: fontawesome/brands/twitter
+      link: https://twitter.com/datajoint
+      name: Twitter
+    - icon: fontawesome/brands/github
+      link: https://github.com/datajoint
+      name: GitHub
+    - icon: fontawesome/brands/docker
+      link: https://hub.docker.com/u/datajoint
+      name: DockerHub
+    - icon: fontawesome/brands/python
+      link: https://pypi.org/user/datajointbot
+      name: PyPI
+    - icon: fontawesome/brands/stack-overflow
+      link: https://stackoverflow.com/questions/tagged/datajoint
+      name: StackOverflow
+    - icon: fontawesome/brands/youtube
+      link: https://www.youtube.com/channel/UCdeCuFOTCXlVMRzh6Wk-lGg
+      name: YouTube
+extra_css:
+  - assets/stylesheets/extra.css
diff --git a/docs/src/.overrides/.icons/main/company-logo.svg b/docs/src/.overrides/.icons/main/company-logo.svg
new file mode 100644
index 000000000..e876313cd
--- /dev/null
+++ b/docs/src/.overrides/.icons/main/company-logo.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="260.479px" height="365.463px" viewBox="0 0 260.479 365.463" enable-background="new 0 0 260.479 365.463"
+	 xml:space="preserve">
+<g>
+	<path d="M4.638,361.434c0,0,124.071-124.072,135.896-357.434c0,0,29.555,189.053,115.266,256.988
+		C255.799,260.988,173.038,249.174,4.638,361.434z"/>
+</g>
+</svg>
diff --git a/docs/src/.overrides/assets/images/company-logo-blue.png b/docs/src/.overrides/assets/images/company-logo-blue.png
new file mode 100644
index 000000000..d15194b8d
Binary files /dev/null and b/docs/src/.overrides/assets/images/company-logo-blue.png differ
diff --git a/docs/src/.overrides/assets/stylesheets/extra.css b/docs/src/.overrides/assets/stylesheets/extra.css
new file mode 100644
index 000000000..46b6aa597
--- /dev/null
+++ b/docs/src/.overrides/assets/stylesheets/extra.css
@@ -0,0 +1,93 @@
+:root {
+    --dj-primary: #00a0df;
+    --dj-secondary: #ff5113;
+    --dj-background: #808285;
+    --dj-black: #000000;
+    --dj-white: #ffffff;
+}
+
+/* footer previous/next navigation */
+.md-footer__inner:not([hidden]) {
+    display: none
+}
+
+/* footer social icons */
+html a[title="DataJoint"].md-social__link svg {
+    color: var(--dj-primary);
+}
+html a[title="Slack"].md-social__link svg {
+    color: var(--dj-primary);
+}
+html a[title="LinkedIn"].md-social__link svg {
+    color: var(--dj-primary);
+}
+html a[title="Twitter"].md-social__link svg {
+    color: var(--dj-primary);
+}
+html a[title="GitHub"].md-social__link svg {
+    color: var(--dj-primary);
+}
+html a[title="DockerHub"].md-social__link svg {
+    color: var(--dj-primary);
+}
+html a[title="PyPI"].md-social__link svg {
+    color: var(--dj-primary);
+}
+html a[title="StackOverflow"].md-social__link svg {
+    color: var(--dj-primary);
+}
+html a[title="YouTube"].md-social__link svg {
+    color: var(--dj-primary);
+}
+
+[data-md-color-scheme="datajoint"] {
+    /* ribbon */
+    /* ribbon + markdown heading expansion */
+    --md-primary-fg-color: var(--dj-black);
+    /* ribbon text */
+    --md-primary-bg-color: var(--dj-primary);
+
+    /* navigation */
+    /* navigation header + links */
+    --md-typeset-a-color: var(--dj-primary);
+    /* navigation on hover + diagram outline */
+    --md-accent-fg-color: var(--dj-secondary);
+
+    /* main */
+    /* main header + already viewed*/
+    --md-default-fg-color--light: var(--dj-background);
+    /* primary text */
+    --md-typeset-color: var(--dj-black);
+    /* code comments + diagram text */
+    --md-code-fg-color: var(--dj-primary);
+
+    /* footer */
+    /* previous/next text */
+    /* --md-footer-fg-color: var(--dj-primary); */
+}
+
+[data-md-color-scheme="slate"] {
+    /* ribbon */
+    /* ribbon + markdown heading expansion */
+    --md-primary-fg-color: var(--dj-primary);
+    /* ribbon text */
+    --md-primary-bg-color: var(--dj-white);
+
+    /* navigation */
+    /* navigation header + links */
+    --md-typeset-a-color: var(--dj-primary);
+    /* navigation on hover + diagram outline */
+    --md-accent-fg-color: var(--dj-secondary);
+
+    /* main */
+    /* main header + already viewed*/
+    /* --md-default-fg-color--light: var(--dj-background); */
+    /* primary text */
+    --md-typeset-color: var(--dj-white);
+    /* code comments + diagram text */
+    --md-code-fg-color: var(--dj-primary);
+
+    /* footer */
+    /* previous/next text */
+    /* --md-footer-fg-color: var(--dj-white); */
+}
diff --git a/docs/src/.overrides/partials/nav.html b/docs/src/.overrides/partials/nav.html
new file mode 100644
index 000000000..5c090954d
--- /dev/null
+++ b/docs/src/.overrides/partials/nav.html
@@ -0,0 +1,33 @@
+{% set class = "md-nav md-nav--primary" %}
+{% if "navigation.tabs" in features %}
+{% set class = class ~ " md-nav--lifted" %}
+{% endif %}
+{% if "toc.integrate" in features %}
+{% set class = class ~ " md-nav--integrated" %}
+{% endif %}
+<nav class="{{ class }}" aria-label="{{ lang.t('nav.title') }}" data-md-level="0">
+    <label class="md-nav__title" for="__drawer">
+        <a href="{{ config.extra.homepage | d(nav.homepage.url, true) | url }}" title="{{ config.site_name | e }}"
+            class="md-nav__button md-logo" aria-label="{{ config.site_name }}" data-md-component="logo">
+            {% include "partials/logo.html" %}
+        </a>
+        {#-
+        Add DataJoint home link to navigation header, otherwise unchanged
+        -#}
+        <a href="https://datajoint.com/docs/" title="DataJoint">
+            ⬅ Home
+        </a>
+    </label>
+    {% if config.repo_url %}
+    <div class="md-nav__source">
+        {% include "partials/source.html" %}
+    </div>
+    {% endif %}
+    <ul class="md-nav__list" data-md-scrollfix>
+        {% for nav_item in nav %}
+        {% set path = "__nav_" ~ loop.index %}
+        {% set level = 1 %}
+        {% include "partials/nav-item.html" %}
+        {% endfor %}
+    </ul>
+</nav>
\ No newline at end of file
diff --git a/docs/src/about/changelog.md b/docs/src/about/changelog.md
new file mode 120000
index 000000000..79b747aee
--- /dev/null
+++ b/docs/src/about/changelog.md
@@ -0,0 +1,1 @@
+../../../CHANGELOG.md
\ No newline at end of file
diff --git a/docs/src/api/make_pages.py b/docs/src/api/make_pages.py
new file mode 100644
index 000000000..87673d14e
--- /dev/null
+++ b/docs/src/api/make_pages.py
@@ -0,0 +1,18 @@
+"""Generate the api pages and navigation."""
+
+import mkdocs_gen_files
+from pathlib import Path
+import os
+
+package = os.getenv("PACKAGE")
+nav = mkdocs_gen_files.Nav()
+for path in sorted(Path(package).glob("**/*.py")):
+    with mkdocs_gen_files.open(f"api/{path.with_suffix('')}.md", "w") as f:
+        module_path = ".".join(
+            [p for p in path.with_suffix("").parts if p != "__init__"]
+        )
+        print(f"::: {module_path}", file=f)
+    nav[path.parts] = f"{path.with_suffix('')}.md"
+
+with mkdocs_gen_files.open("api/navigation.md", "w") as nav_file:
+    nav_file.writelines(nav.build_literate_nav())
diff --git a/docs/src/concepts/existing-pipelines.md b/docs/src/concepts/existing-pipelines.md
new file mode 100644
index 000000000..f88a3732e
--- /dev/null
+++ b/docs/src/concepts/existing-pipelines.md
@@ -0,0 +1,133 @@
+# Existing Pipelines
+
+This section describes how to work with database schemas without access to the original
+code that generated the schema. These situations often arise when the database is
+created by another user who has not shared the generating code yet or when the database
+schema is created from a programming language other than Python.
+
+## Loading Classes
+
+Typically, a DataJoint schema is created as a dedicated Python module. This module
+defines a schema object that is used to link classes declared in the module to tables
+in the database schema. With the module installed, you can simply import it to interact
+with its tables:
+
+``` python
+import datajoint as dj
+from element_calcium_imaging import scan # (1)
+```
+
+1. This and other [DataJoint Elements](https://datajoint.com/docs/elements/) are 
+installable via `pip` or downloadable via their respective GitHub repositories.
+
+To visualize an unfamiliar schema, see commands for generating [diagrams](../../getting-started/#diagram).
+
+## Spawning Missing Classes
+
+Now, imagine we do not have access to the 
+[Python definition of Scan](https://github.com/datajoint/element-calcium-imaging/blob/main/element_calcium_imaging/scan.py),
+or we're unsure if the version on our server matches the definition available. We can
+use the `dj.list_schemas` function to list the available database schemas.
+
+``` python
+import datajoint as dj
+dj.conn() # (1)
+dj.list_schemas() # (2)
+dj.Schema('schema_name').list_tables() # (3)
+```
+
+1. Establish a connection to the server.
+2. List the available schemas on the server.
+3. List the tables for a given schema from the previous step. These will appear in their
+raw database form, with underscores instead of camelcase and special characters for Part
+tables.
+
+Just as with a new schema, we can create a schema object to connect to the chosen
+database schema. If the schema already exists, `dj.Schema` is initialized as usual.
+
+If a diagram will shows a mixture of class names and database table names, the
+`spawn_missing_classes` method will spawn classes into the local namespace for any
+tables missing their classes. This will allow us to interact with all tables as if
+they were declared in the current namespace.
+
+``` python
+schema.spawn_missing_classes()
+```
+
+## Virtual Modules
+
+While `spawn_missing_classes` creates the new classes in the local namespace, it is
+often more convenient to import a schema with its Python module, equivalent to the
+Python command. We can mimmick this import without having access to the schema using
+the `VirtualModule` class object:
+
+```python
+import datajoint as dj
+subject = dj.create_virtual_module(module_name='subject', schema_name='db_subject')
+```
+
+Now, `subject` behaves as an imported module complete with the schema object and all the
+table classes.
+
+The class object `VirtualModule` of the `dj.Schema` class provides access to virtual
+modules. It creates a python module with the given name from the name of a schema on
+the server, automatically adds classes to it corresponding to the tables in the
+schema.
+
+The function can take several parameters:
+
+- `module_name`: displayed module name.
+
+- `schema_name`: name of the database in MySQL.
+
+ `create_schema`: if `True`, create the schema on the database server if it does not
+ already exist; if `False` (default), raise an error when the schema is not found.
+
+- `create_tables`: if `True`, `module.schema` can be used as the decorator for declaring
+  new classes; if `False`, such use will raise an error stating that the module is
+  intend only to work with existing tables.
+
+The function returns the Python module containing classes from the schema object with
+all the table classes already declared inside it.
+
+`create_schema=False` may be useful if we want to make sure that the schema already 
+exists.  If none exists, `create_schema=True` will create an empty schema.
+
+``` python
+dj.VirtualModule('what', 'nonexistent')
+```
+
+Returns
+
+``` python
+DataJointError: Database named `nonexistent` was not defined. Set argument create_schema=True to create it.
+```
+
+`create_tables=False` prevents the use of the schema object of the virtual module for
+creating new tables in the existing schema. This is a precautionary measure since
+virtual modules are often used for completed schemas. `create_tables=True` will new
+tables to the existing schema. A more common approach in this scenario would be to
+create a new schema object and to use the `spawn_missing_classes` function to make the
+classes available.
+
+However, you if do decide to create new tables in an existing tables using the virtual
+module, you may do so by using the schema object from the module as the decorator for
+declaring new tables:
+
+``` python
+uni = dj.VirtualModule('university.py', 'dimitri_university', create_tables=True)
+```
+
+``` python
+@uni.schema
+class Example(dj.Manual):
+    definition = """
+    -> uni.Student
+    ---
+    example : varchar(255)
+    """
+```
+
+``` python
+dj.Diagram(uni)
+```
diff --git a/docs/src/develop.md b/docs/src/develop.md
new file mode 100644
index 000000000..41ed12c5d
--- /dev/null
+++ b/docs/src/develop.md
@@ -0,0 +1,120 @@
+# Develop
+
+Included with the codebase is the recommended development environment configured using [DevContainer](https://containers.dev/).
+
+## Launch Environment
+
+Here are some options that provide a great developer experience:
+
+- **Cloud-based IDE**: (*recommended*)
+  - Launch using [GitHub Codespaces](https://github.com/features/codespaces) using the option `Create codespace on master` in the codebase repository on your fork.
+  - Build time for a 2-Core codespace is **~6m**. This is done infrequently and cached for convenience.
+  - Start time for a 2-Core codespace is **~2m**. This will pull the built codespace from cache when you need it.
+  - *Tip*: GitHub auto names the codespace but you can rename the codespace so that it is easier to identify later.
+- **Local IDE**:
+  - Ensure you have [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
+  - Ensure you have [Docker](https://docs.docker.com/get-docker/)
+  - Ensure you have [VSCode](https://code.visualstudio.com/)
+  - Install the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
+  - `git clone` the codebase repository and open it in VSCode
+  - Use the `Dev Containers extension` to `Reopen in Container` (More info in the `Getting started` included with the extension)
+
+You will know your environment has finished loading once you see a terminal open related to `Running postStartCommand` with a final message: `Done`.
+
+## Features
+
+Once you've successfully launched the development environment, you'll be able to take advantage of our developer tooling to help improve productivity and quality.
+
+### Syntax Tests
+
+The following will verify that there are no syntax errors.
+
+```
+flake8 datajoint --count --select=E9,F63,F7,F82 --show-source --statistics
+```
+
+### Integration Tests
+
+The following will verify there are no regression errors by running our test suite of unit and integration tests.
+
+- Entire test suite:
+  ```
+  nosetests -vw tests
+  ```
+- A single functional test:
+  ```
+  nosetests -vs --tests=tests.test_external_class:test_insert_and_fetch
+  ```
+- A single class test:
+  ```
+  nosetests -vs --tests=tests.test_fetch:TestFetch.test_getattribute_for_fetch1
+  ```
+
+### Style Tests
+
+The following will verify that there are no code styling errors.
+
+```
+flake8 --ignore=E203,E722,W503 datajoint --count --max-complexity=62 --max-line-length=127 --statistics
+```
+
+The following will ensure the codebase has been formatted with [black](https://black.readthedocs.io/en/stable/).
+
+```
+black datajoint --check -v
+```
+
+The following will ensure the test suite has been formatted with [black](https://black.readthedocs.io/en/stable/).
+
+```
+black tests --check -v
+```
+
+### Jupyter
+
+Jupyter notebooks are supported in this environment. This means that when you `import datajoint`, it will use the current state of the source.
+
+Be sure to see the reference documenation if you are new to [running Jupyter notebooks w/ VSCode](https://code.visualstudio.com/docs/datascience/jupyter-notebooks#_create-or-open-a-jupyter-notebook).
+
+### Debugger
+
+[VSCode Debugger](https://code.visualstudio.com/docs/editor/debugging) is a powerful tool that can really accelerate fixes.
+
+Try it as follows:
+
+- Create a python script of your choice
+- `import datajoint` (This will use the current state of the source)
+- Add breakpoints by adding red dots next to line numbers
+- Select the `Run and Debug` tab
+- Start by clicking the button `Run and Debug`
+
+### MySQL CLI
+
+It is often useful in development to connect to DataJoint's relational database backend directly using the MySQL CLI.
+
+Connect as follows to the database running within your developer environment:
+
+```
+mysql -hfakeservices.datajoint.io -uroot -psimple
+```
+
+### Documentation
+
+Our documentation is built using [MkDocs Material](https://squidfunk.github.io/mkdocs-material/). The easiest way to improve the documentation is by using the `docs/docker-compose.yaml` environment. The source can be modified in `docs/src` using markdown.
+
+The docs environment can be run using 3 modes:
+
+- **LIVE**: (*recommended*) This serves the docs locally. It supports live reloading on saves to `docs/src` files but does not support the docs version dropdown. Useful to see changes live.
+  ```
+  MODE="LIVE" PACKAGE=datajoint UPSTREAM_REPO=https://github.com/datajoint/datajoint-python.git HOST_UID=$(id -u) docker compose -f docs/docker-compose.yaml up --build
+  ```
+- **QA**: This serves the docs locally. It supports the docs version dropdown but does not support live reloading. Useful as a final check.
+  ```
+  MODE="QA" PACKAGE=datajoint UPSTREAM_REPO=https://github.com/datajoint/datajoint-python.git HOST_UID=$(id -u) docker compose -f docs/docker-compose.yaml up --build
+  ```
+- **BUILD**: This compiles the docs. Most useful for the docs deployment automation. Other modes are more useful to new contributors.
+  ```
+  MODE="BUILD" PACKAGE=datajoint UPSTREAM_REPO=https://github.com/datajoint/datajoint-python.git HOST_UID=$(id -u) docker compose -f docs/docker-compose.yaml up --build
+  ```
+
+When the docs are served locally, use the VSCode `PORTS` tab (next to `TERMINAL`) to manage access to the forwarded ports. Docs are served on port `8080`.
diff --git a/docs/src/getting-started/index.md b/docs/src/getting-started/index.md
new file mode 100644
index 000000000..5948f100c
--- /dev/null
+++ b/docs/src/getting-started/index.md
@@ -0,0 +1,277 @@
+# Getting Started
+
+## Installation
+
+First, please [install Python](https://www.python.org/downloads/) version
+3.7 or later. We recommend 3.8.
+
+Next, please install DataJoint via one of the following:
+
+=== "conda"
+
+    !!! note "Pre-Requisites"
+
+        - Ensure you have [conda](https://conda.io/projects/conda/en/latest/user-guide/install/index.html#regular-installation)
+        installed.
+
+    To add the `conda-forge` channel:
+
+    ```bash
+    conda config --add channels conda-forge
+    ```
+
+    To install:
+
+    ```bash
+    conda install -c conda-forge datajoint
+    ```
+
+=== "pip + :fontawesome-brands-windows:"
+
+    !!! note "Pre-Requisites"
+
+        - Ensure you have [pip](https://pip.pypa.io/en/stable/installation/) installed.
+        - Install [graphviz](https://graphviz.org/download/#windows) pre-requisite for
+          diagram visualization.
+
+    To install:
+
+    ```bash
+    pip install datajoint
+    ```
+
+=== "pip + :fontawesome-brands-apple:"
+
+    !!! note "Pre-Requisites"
+
+        - Ensure you have [pip](https://pip.pypa.io/en/stable/installation/) installed.
+        - Install [graphviz](https://graphviz.org/download/#mac) pre-requisite for
+          diagram visualization.
+
+    To install:
+
+    ```bash
+    pip install datajoint
+    ```
+
+=== "pip + :fontawesome-brands-linux:"
+
+    !!! note "Pre-Requisites"
+
+        - Ensure you have [pip](https://pip.pypa.io/en/stable/installation/) installed.
+        - Install [graphviz](https://graphviz.org/download/#linux) pre-requisite for
+          diagram visualization.
+    
+    To install:
+
+    ```bash
+    pip install datajoint
+    ```
+
+## Connection
+
+!!! note
+
+    Although you may connect to any MySQL server of your choice, the DataJoint company
+    offers an online tutorial environment. Simply sign up for a free
+    [DataJoint account](https://accounts.datajoint.io). 
+    You will be granted privileges to create schemas
+    that are prefixed as `{user}_`.
+
+=== "environment variables"
+
+    Before using `datajoint`, set the following environment variables like so:
+
+    ```bash linenums="1"
+    DJ_HOST=tutorial-db.datajoint.io
+    DJ_USER={user}
+    DJ_PASS={password}
+    ```
+
+=== "memory"
+
+    To set connection settings within Python, perform:
+
+    ```python linenums="1"
+    import datajoint as dj
+
+    dj.config["database.host"] = "tutorial-db.datajoint.io"
+    dj.config["database.user"] = "{user}"
+    dj.config["database.password"] = "{password}"
+    ```
+
+    These configuration settings can be saved either locally or system-wide using one 
+    of the following commands:
+    ```python
+    dj.config.save_local()
+    dj.config.save_global()
+    ```
+
+=== "file"
+
+    Before using `datajoint`, create a file named `dj_local_conf.json` in the current
+    directory like so:
+
+    ```json linenums="1"
+    {
+        "database.host": "tutorial-db.datajoint.io",
+        "database.user": "{user}",
+        "database.password": "{password}"
+    }
+    ```
+
+    These settings will be loaded whenever a Python instance is launched from this 
+    directory. To configure settings globally, save a similar file as 
+    `.datajoint_config.json` in your home directory. A local config, if present, will 
+    take precedent over global settings.
+
+## Data Pipeline Definition
+
+Let's definite a simple data pipeline.
+
+```python linenums="1"
+import datajoint as dj
+schema = dj.Schema(f"{dj.config['database.user']}_shapes") # (1)
+
+@schema # (2)
+class Rectangle(dj.Manual):
+    definition = """ # (3)
+    shape_id: int
+    ---
+    shape_height: float
+    shape_width: float
+    """
+
+
+@schema
+class Area(dj.Computed):
+    definition = """
+    -> Rectangle
+    ---
+    shape_area: float
+    """
+    def make(self, key):
+        rectangle = (Rectangle & key).fetch1()
+        Area.insert1(
+            dict(
+                shape_id=rectangle["shape_id"],
+                shape_area=rectangle["shape_height"] * rectangle["shape_width"],
+            )
+        )
+```
+
+1. This statement creates the database schema `{username}_shapes` on the server.
+
+2. The `@schema` decorator for DataJoint classes creates the table on the server.
+
+3. The table is defined by the the `definition` property.
+
+It is a common practice to have a separate Python module for each schema. Therefore,
+each such module has only one `dj.Schema` object defined and is usually named
+`schema`.
+
+The `dj.Schema` constructor can take a number of optional parameters
+after the schema name.
+
+-   `context` - Dictionary for looking up foreign key references.
+    Defaults to `None` to use local context.
+-   `connection` - Specifies the DataJoint connection object. Defaults
+    to `dj.conn()`.
+-   `create_schema` - When `False`, the schema object will not create a
+    schema on the database and will raise an error if one does not
+    already exist. Defaults to `True`.
+-   `create_tables` - When `False`, the schema object will not create
+    tables on the database and will raise errors when accessing missing
+    tables. Defaults to `True`.
+
+The `@schema` decorator uses the class name and the data tier to check whether an
+appropriate table exists on the database. If a table does not already exist, the
+decorator creates one on the database using the definition property. The decorator
+attaches the information about the table to the class, and then returns the class.
+
+## Diagram
+
+### Display
+
+The diagram displays the relationship of the data model in the data pipeline.
+
+This can be done for an entire schema:
+
+```python
+dj.Diagram(schema)
+```
+
+![pipeline](../images/shapes_pipeline.svg)
+
+Or for individual or sets of tables:
+```python
+dj.Diagram(schema.Rectangle)
+dj.Diagram(schema.Rectangle) + dj.Diagram(schema.Area)
+```
+
+??? Note "What if I don't see the diagram?"
+
+    Some Python interfaces may require additional `draw` method.
+
+    ```python
+    dj.Diagram(schema).draw()
+    ```
+
+    Calling the `.draw()` method is not necessary when working in a Jupyter notebook by
+    entering `dj.Diagram(schema)` in a notebook cell. The Diagram will automatically
+    render in the notebook by calling its `_repr_html_` method. A Diagram displayed
+    without `.draw()` will be rendered as an SVG, and hovering the mouse over a table
+    will reveal a compact version of the output of the `.describe()` method.
+
+### Customize
+
+Adding or substracting a number to a diagram object adds nodes downstream or upstream,
+respectively, in the pipeline.
+
+``` python
+(dj.Diagram(schema.Rectangle)+1).draw() # (1)
+```
+
+1. Plot all the tables directly downstream from `schema.Rectangle`
+
+```python
+(dj.Diagram('my_schema')-1+1).draw() # (1)
+```
+
+1. Plot all tables directly downstream of those directly upstream of this schema.
+
+### Save
+
+The diagram can be saved as either `png` or `svg`.
+
+```python
+dj.Diagram(schema).save(filename='my-diagram', format='png')
+```
+
+## Add data
+
+Let's add data for a rectangle:
+
+```python
+Rectangle.insert1(dict(shape_id=1, shape_height=2, shape_width=4))
+```
+
+## Run computation
+
+Let's start the computations on our entity: `Area`.
+
+```python
+Area.populate(display_progress=True)
+```
+
+## Query
+
+Let's inspect the results.
+
+```python
+Area & "shape_area >= 8"
+```
+
+| shaped_id | shape_area |
+| --- | --- |
+| 1 | 8.0 |
diff --git a/docs/src/images/shapes_pipeline.svg b/docs/src/images/shapes_pipeline.svg
new file mode 100644
index 000000000..203b7b47c
--- /dev/null
+++ b/docs/src/images/shapes_pipeline.svg
@@ -0,0 +1,36 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="79pt" height="105pt" viewBox="0.00 0.00 79.00 105.00">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 101)">
+<title>%3</title>
+<polygon fill="white" stroke="transparent" points="-4,4 -4,-101 75,-101 75,4 -4,4"/>
+<!-- Area -->
+<g id="node1" class="node">
+<title>Area</title>
+<g id="a_node1"><a xlink:title="→ Rectangle
+------------------------------
+shape_area           
+">
+<ellipse fill="#ff0000" fill-opacity="0.125490" stroke="#ff0000" stroke-opacity="0.125490" cx="35.5" cy="-13" rx="13" ry="13"/>
+<text text-anchor="middle" x="35.5" y="-9.9" font-family="arial" font-size="12.00" fill="#7f0000" fill-opacity="0.627451">Area</text>
+</a>
+</g>
+</g>
+<!-- Rectangle -->
+<g id="node2" class="node">
+<title>Rectangle</title>
+<g id="a_node2"><a xlink:title="shape_id             
+------------------------------
+shape_height         
+shape_width          
+">
+<polygon fill="#00ff00" fill-opacity="0.188235" stroke="#00ff00" stroke-opacity="0.188235" points="71,-97 0,-97 0,-62 71,-62 71,-97"/>
+<text text-anchor="start" x="8" y="-77.4" font-family="arial" text-decoration="underline" font-size="12.00" fill="darkgreen">Rectangle</text>
+</a>
+</g>
+</g>
+<!-- Rectangle&#45;&gt;Area -->
+<g id="edge1" class="edge">
+<title>Rectangle-&gt;Area</title>
+<path fill="none" stroke="#000000" stroke-width="2" stroke-opacity="0.250980" d="M35.5,-61.89C35.5,-50.69 35.5,-36.17 35.5,-26.07"/>
+</g>
+</g>
+</svg>
\ No newline at end of file
diff --git a/docs/src/query-lang/common-commands.md b/docs/src/query-lang/common-commands.md
new file mode 100644
index 000000000..83f70bc3e
--- /dev/null
+++ b/docs/src/query-lang/common-commands.md
@@ -0,0 +1,177 @@
+
+## Insert
+
+Data entry is as easy as providing the appropriate data structure to a permitted table.
+Given the following table definition, we can insert data as tuples, dicts, pandas
+dataframes, or pathlib `Path` relative paths to local CSV files.
+
+```text      
+    mouse_id: int            # unique mouse id
+    ---
+    dob: date                # mouse date of birth
+    sex: enum('M', 'F', 'U') # sex of mouse - Male, Female, or Unknown
+``` 
+
+=== "Tuple"
+
+    ```python
+    mouse.insert1( (0, '2017-03-01', 'M') ) # Single entry
+    data = [
+        (1, '2016-11-19', 'M'),
+        (2, '2016-11-20', 'U'),
+        (5, '2016-12-25', 'F')
+    ]
+    mouse.insert(data) # Multi-entry
+    ```
+
+=== "Dict"
+
+    ```python
+    mouse.insert1( dict(mouse_id=0, dob='2017-03-01', sex='M') ) # Single entry
+    data = [
+        {'mouse_id':1, 'dob':'2016-11-19', 'sex':'M'},
+        {'mouse_id':2, 'dob':'2016-11-20', 'sex':'U'},
+        {'mouse_id':5, 'dob':'2016-12-25', 'sex':'F'}
+    ]
+    mouse.insert(data) # Multi-entry
+    ```
+
+=== "Pandas"
+
+    ```python
+    import pandas as pd
+    data = pd.DataFrame(
+        [[1, "2016-11-19", "M"], [2, "2016-11-20", "U"], [5, "2016-12-25", "F"]],
+        columns=["mouse_id", "dob", "sex"],
+    )
+    mouse.insert(data)
+    ```
+
+=== "CSV"
+
+    Given the following CSV in the current working directory as `mice.csv`
+    
+    ```console
+    mouse_id,dob,sex
+    1,2016-11-19,M
+    2,2016-11-20,U
+    5,2016-12-25,F
+    ```
+    
+    We can import as follows:
+    
+    ```python
+    from pathlib import Path
+    mouse.insert(Path('./mice.csv'))
+    ```
+    
+## Make
+
+See the article on [`make` methods](../../reproduce/make-method/)
+
+## Fetch
+
+### Entire table
+
+A `fetch` command can either retrieve table data as a NumPy
+[recarray](https://docs.scipy.org/doc/numpy/reference/generated/numpy.recarray.html)
+or a as a list of `dict`
+
+``` python
+data = query.fetch() # (1)
+data = query.fetch(as_dict=True) # (2)
+```
+
+1. NumPy recarray
+2. List of `dict`:
+
+??? Note "For very large tables..."
+
+    In some cases, the amount of data returned by fetch can be quite large; it can be
+    useful to use the `size_on_disk` attribute to determine if running a bare fetch
+    would be wise. Please note that it is only currently possible to query the size of
+    entire tables stored directly in the database at this time.
+
+### Separate variables
+
+``` python
+name, img = query.fetch1('mouse_id', 'dob')  # when query has exactly one entity
+name, img = query.fetch('mouse_id', 'dob')   # [mouse_id, ...] [dob, ...]
+```
+
+### Primary key values
+
+``` python
+keydict = tab.fetch1("KEY")  # single key dict when tab has exactly one entity
+keylist = tab.fetch("KEY")   # list of key dictionaries [{}, ...]
+```
+
+`KEY` can also used when returning attribute values as separate
+variables, such that one of the returned variables contains the entire
+primary keys.
+
+### Sorting results
+
+To sort the result, use the `order_by` keyword argument.
+
+``` python
+data = query.fetch(order_by='mouse_id')                # ascending order
+data = query.fetch(order_by='mouse_id desc')           # descending order
+data = query.fetch(order_by=('mouse_id', 'dob'))       # by ID first, dob second
+data = query.fetch(order_by='KEY')                     # sort by the primary key
+```
+
+The `order_by` argument can be a string specifying the attribute to sort by. By default
+the sort is in ascending order. Use `'attr desc'` to sort in descending order by
+attribute `attr`. The value can also be a sequence of strings, in which case, the sort
+performed on all the attributes jointly in the order specified.
+
+The special attribute named `'KEY'` represents the primary key attributes in order that
+they appear in the index. Otherwise, this name can be used as any other argument.
+
+If an attribute happens to be a SQL reserved word, it needs to be enclosed in
+backquotes. For example:
+
+``` python
+data = query.fetch(order_by='`select` desc')
+```
+
+The `order_by` value is eventually passed to the `ORDER BY`
+[clause](https://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html).
+
+### Limiting results
+
+Similar to sorting, the `limit` and `offset` arguments can be used to limit the result
+to a subset of entities.
+
+``` python
+data = query.fetch(order_by='mouse_id', limit=10, offset=5)
+```
+
+Note that an `offset` cannot be used without specifying a `limit` as
+well.
+
+### Usage with Pandas
+
+The `pandas` [library](http://pandas.pydata.org/) is a popular library for data analysis
+in Python which can easily be used with DataJoint query results. Since the records
+returned by `fetch()` are contained within a `numpy.recarray`, they can be easily
+converted to `pandas.DataFrame` objects by passing them into the `pandas.DataFrame`
+constructor. For example:
+
+``` python
+import pandas as pd
+frame = pd.DataFrame(tab.fetch())
+```
+
+Calling `fetch()` with the argument `format="frame"` returns results as
+`pandas.DataFrame` objects indexed by the table's primary key attributes.
+
+``` python
+frame = tab.fetch(format="frame")
+```
+
+Returning results as a `DataFrame` is not possible when fetching a particular subset of
+attributes or when `as_dict` is set to `True`.
+
+<!-- ## Drop and ## Diagrams are mentioned in general docs here -->
diff --git a/docs/src/query-lang/iteration.md b/docs/src/query-lang/iteration.md
new file mode 100644
index 000000000..640b687a9
--- /dev/null
+++ b/docs/src/query-lang/iteration.md
@@ -0,0 +1,36 @@
+# Iteration
+
+The DataJoint model primarily handles data as sets, in the form of tables. However, it
+can sometimes be useful to access or to perform actions such as visualization upon
+individual entities sequentially. In DataJoint this is accomplished through iteration.
+
+In the simple example below, iteration is used to display the names and values of the
+attributes of each entity in the simple table or table expression.
+
+``` python
+for entity in table:
+    print(entity)
+```
+
+This example illustrates the function of the iterator: DataJoint iterates through the
+whole table expression, returning the entire entity during each step. In this case,
+each entity will be returned as a `dict` containing all attributes.
+
+At the start of the above loop, DataJoint internally fetches only the primary keys of
+the entities. Since only the primary keys are needed to distinguish between entities,
+DataJoint can then iterate over the list of primary keys to execute the loop. At each
+step of the loop, DataJoint uses a single primary key to fetch an entire entity for use
+in the iteration, such that `print(entity)` will print all attributes of each entity.
+By first fetching only the primary keys and then fetching each entity individually,
+DataJoint saves memory at the cost of network overhead. This can be particularly useful
+for tables containing large amounts of data in secondary attributes.
+
+The memory savings of the above syntax may not be worth the additional network overhead
+in all cases, such as for tables with little data stored as secondary attributes. In
+the example below, DataJoint fetches all of the attributes of each entity in a single
+call and then iterates over the list of entities stored in memory.
+
+``` python
+for entity in table.fetch(as_dict=True):
+    print(entity)
+```
diff --git a/docs/src/query-lang/operators.md b/docs/src/query-lang/operators.md
new file mode 100644
index 000000000..cbee4126a
--- /dev/null
+++ b/docs/src/query-lang/operators.md
@@ -0,0 +1,159 @@
+# Operators
+
+The examples below will use the table definitions in [table tiers](../../reproduce/table-tiers).
+
+<!-- ## Join appears here in the general docs -->
+
+## Restriction
+
+`&` and `-` operators permit restriction.
+
+### By a mapping
+
+For a [Session table](../../reproduce/table-tiers#manual-tables), that has the attribute
+`session_date`, we can restrict to sessions from January 1st, 2022:
+
+```python
+Session & {'session_date': "2022-01-01"}
+```
+
+If there were any typos (e.g., using `sess_date` instead of `session_date`), our query
+will return all of the entities of `Session`.
+
+### By a string
+
+Conditions may include arithmetic operations, functions, range tests, etc. Restriction
+of table `A` by a string containing an attribute not found in table `A` produces an
+error.
+
+```python
+Session & 'user = "Alice"' # (1)
+Session & 'session_date >= "2022-01-01"' # (2)
+```
+
+1. All the sessions performed by Alice
+2. All of the sessions on or after January 1st, 2022
+
+### By a collection
+
+When `cond` is a collection of conditions, the conditions are applied by logical
+disjunction (logical OR). Restricting a table by a collection will return all entities
+that meet *any* of the conditions in the collection. 
+
+For example, if we restrict the `Session` table by a collection containing two
+conditions, one for user and one for date, the query will return any sessions with a
+matching user *or* date.
+
+A collection can be a list, a tuple, or a Pandas `DataFrame`.
+
+``` python
+cond_list = ['user = "Alice"', 'session_date = "2022-01-01"'] # (1)
+cond_tuple = ('user = "Alice"', 'session_date = "2022-01-01"') # (2)
+import pandas as pd
+cond_frame = pd.DataFrame(data={'user': ['Alice'], 'session_date': ['2022-01-01']}) # (3)
+
+Session() & ['user = "Alice"', 'session_date = "2022-01-01"']
+```
+
+1. A list
+2. A tuple
+3. A data frame
+
+`dj.AndList` represents logical conjunction(logical AND). Restricting a table by an
+`AndList` will return all entities that meet *all* of the conditions in the list. `A &
+dj.AndList([c1, c2, c3])` is equivalent to `A & c1 & c2 & c3`.
+
+```python
+Student() & dj.AndList(['user = "Alice"', 'session_date = "2022-01-01"'])
+```
+
+The above will show all the sessions that Alice conducted on the given day.
+
+### By a `Not` object
+
+The special function `dj.Not` represents logical negation, such that `A & dj.Not
+(cond)` is equivalent to `A - cond`.
+
+### By a query
+
+Restriction by a query object is a generalization of restriction by a table. The example
+below creates a query object corresponding to all the users named Alice. The `Session`
+table is then restricted by the query object, returning all the sessions performed by
+Alice.
+
+``` python
+query = User & 'user = "Alice"'
+Session & query
+```
+
+## Proj
+
+Renaming an attribute in python can be done via keyword arguments: 
+
+```python
+table.proj(new_attr='old_attr')
+```
+
+This can be done in the context of a table definition:
+
+```python
+@schema
+class Session(dj.Manual):
+    definition = """
+    # Experiment Session
+    -> Animal
+    session             : smallint  # session number for the animal
+    ---
+    session_datetime    : datetime  # YYYY-MM-DD HH:MM:SS
+    session_start_time  : float     # seconds relative to session_datetime
+    session_end_time    : float     # seconds relative to session_datetime
+    -> User.proj(experimenter='username')
+    -> User.proj(supervisor='username')
+    """
+```
+
+Or to rename multiple values in a table with the following syntax: 
+`Table.proj(*existing_attributes,*renamed_attributes)`
+
+```python
+Session.proj('session','session_date',start='session_start_time',end='session_end_time')
+```
+
+Projection can also be used to to compute new attributes from existing ones.
+
+```python
+Session.proj(duration='session_end_time-session_start_time') & 'duration > 10'
+```
+
+## Aggr
+
+For more complicated calculations, we can use aggregation.
+
+``` python
+Subject.aggr(Session,n="count(*)") # (1)
+Subject.aggr(Session,average_start="avg(session_start_time)") # (2)
+```
+
+1. Number of sessions per subject.
+2. Average `session_start_time` for each subject
+
+<!-- ## Union appears here in the general docs -->
+
+## Universal set
+
+Universal sets offer the complete list of combinations of attributes.
+
+``` python
+# All home cities of students
+dj.U('laser_wavelength', 'laser_power') & Scan # (1)
+dj.U('laser_wavelength', 'laser_power').aggr(Scan, n="count(*)") # (2)
+dj.U().aggr(Session, n="max(session)") # (3)
+```
+
+1. All combinations of wavelength and power.
+2. Total number of scans for each combination.
+3. Largest session number.
+
+`dj.U()`, as shown in the last example above, is often useful for integer IDs.
+For an example of this process, see the source code for 
+[Element Array Electrophysiology's `insert_new_params`](https://datajoint.com/docs/elements/element-array-ephys/latest/api/element_array_ephys/ephys_acute/#element_array_ephys.ephys_acute.ClusteringParamSet.insert_new_params).
diff --git a/docs/src/query-lang/query-caching.md b/docs/src/query-lang/query-caching.md
new file mode 100644
index 000000000..7a2f87e27
--- /dev/null
+++ b/docs/src/query-lang/query-caching.md
@@ -0,0 +1,43 @@
+# Query Caching
+
+Query caching allows avoiding repeated queries to the database by caching the results
+locally for faster retrieval.
+
+To enable queries, set the query cache local path in `dj.config`, create the directory,
+and activate the query caching.
+
+``` python
+dj.config['query_cache'] = os.path.expanduser('~/dj_query_cache') # (1)
+# (2)
+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
+
+conn.set_query_cache(query_cache='main') # (3)
+```
+
+1. Set the query cache path
+2. Access the active connection object for the tables
+3. Activate query caching for a namespace called 'main'
+
+The `query_cache` argument is an arbitrary string serving to differentiate cache states;
+setting a new value will effectively start a new cache, triggering retrieval of new
+values once.
+
+To turn off query caching, use the following:
+
+``` python
+conn.set_query_cache(query_cache=None)
+## OR
+conn.set_query_cache()
+```
+
+While query caching is enabled, any insert or delete calls and any transactions are
+disabled and will raise an error. This ensures that stale data are not used for
+updating the database in violation of data integrity.
+
+To clear and remove the query cache, use the following:
+
+``` python
+conn.purge_query_cache() # Purge the cached queries
+```
diff --git a/docs/src/reproduce/make-method.md b/docs/src/reproduce/make-method.md
new file mode 100644
index 000000000..f67c44c76
--- /dev/null
+++ b/docs/src/reproduce/make-method.md
@@ -0,0 +1,55 @@
+# Make Method
+
+Consider the following table definition from the article on 
+[table tiers](./table-tiers):
+
+``` python
+@schema
+class FilteredImage(dj.Computed):
+    definition = """ # Filtered image
+    -> Image
+    ---
+    filtered_image : longblob
+    """
+
+    def make(self, key):
+        img = (test.Image & key).fetch1('image')
+        key['filtered_image'] = my_filter(img)
+        self.insert1(key)
+```
+
+The `FilteredImage` table can be populated as
+
+``` python
+FilteredImage.populate()
+```
+
+The `make` method receives one argument: the dict `key` containing the primary key value
+of an element of `key source` to be worked on. 
+
+## Optional Arguments
+
+The `make` method also accepts a number of optional arguments that provide more features
+and allow greater control over the method's behavior.
+
+| Argument                  | Default    | Description |
+|   ---                     |   ---      | ---         |
+| `restrictions`            |            | A list of restrictions, restricting as `(tab.key_source & AndList (restrictions)) - tab.proj()`. Here `target` is the table to be populated, usually `tab` itself. |
+| `suppress_errors`         | `False`    | If `True`, encountering an error will cancel the current `make` call, log the error, and continue to the next `make` call. Error messages will be logged in the job reservation table (if `reserve_jobs` is `True`) and returned as a list. See also `return_exception_objects` and `reserve_jobs`. |
+| `return_exception_objects`| `False`    | If `True`, error objects are returned instead of error messages. This applies only when `suppress_errors` is `True`. |
+| `reserve_jobs`            | `False`    | If `True`, reserves job to indicate to other distributed processes. The job reservation table may be access as `schema.jobs`. Errors are logged in the jobs table. |
+| `order`                   | `original` | The order of execution, either `"original"`, `"reverse"`, or `"random"`. |
+| `limit`                   | `None`     | If not `None`, checks at most this number of keys. |
+| `max_calls`               | `None`     | If not `None`, populates at most this many keys. Defaults to no limit.
+| `display_progress`        | `False`    | If `True`, displays a progress bar. |
+| `processes`               | `1`        | Number of processes to use. Set to `None` to use all cores |
+| `make_kwargs`             | `None`     | Keyword arguments which do not affect the result of computation to be passed down to each ``make()`` call. Computation arguments should be specified within the pipeline e.g. using a `dj.Lookup` table. |
+
+## Progress
+
+The method `table.progress` reports how many `key_source` entries have been populated
+and how many remain. Two optional parameters allow more advanced use of the method. A
+parameter of restriction conditions can be provided, specifying which entities to
+consider. A Boolean parameter `display` (default is `True`) allows disabling the
+output, such that the numbers of remaining and total entities are returned but not
+printed.
diff --git a/docs/src/reproduce/table-tiers.md b/docs/src/reproduce/table-tiers.md
new file mode 100644
index 000000000..ca644ac8e
--- /dev/null
+++ b/docs/src/reproduce/table-tiers.md
@@ -0,0 +1,152 @@
+# Table Tiers
+
+To define a DataJoint table in Python:
+
+1.  Define a class inheriting from the appropriate DataJoint class:
+   `dj.Lookup`, `dj.Manual`, `dj.Imported` or `dj.Computed`.
+2.  Decorate the class with the schema object (see [schema](./index#data-pipeline-definition))
+3.  Define the class property `definition` to define the table heading.
+
+DataJoint for Python is implemented through the use of classes providing access to the
+actual tables stored on the database. Since only a single table exists on the database
+for any class, interactions with all instances of the class are equivalent. As such,
+most methods can be called on the classes themselves rather than on an object, for
+convenience. Whether calling a DataJoint method on a class or on an instance, the
+result will only depend on or apply to the corresponding table. All of the basic
+functionality of DataJoint is built to operate on the classes themselves, even when
+called on an instance. For example, calling `Person.insert(...)` (on the class) and
+`Person.insert(...)` (on an instance) both have the identical effect of inserting data
+into the table on the database server. DataJoint does not prevent a user from working
+with instances, but the workflow is complete without the need for instantiation. It is
+up to the user whether to implement additional functionality as class methods or
+methods called on instances.
+
+## Manual Tables
+
+The following code defines two manual tables, `Animal` and `Session`:
+
+``` python
+@schema
+class Animal(dj.Manual):
+    definition = """
+    # information about animal
+    animal_id          : int                # animal id assigned by the lab
+    ---
+    -> Species
+    date_of_birth=null : date               # YYYY-MM-DD optional
+    sex=''             : enum('M', 'F', '') # leave empty if unspecified 
+    """
+
+@schema
+class Session(dj.Manual):
+    definition = """
+    # Experiment Session
+    -> Animal
+    session             : smallint  # session number for the animal
+    ---
+    session_datetime    : datetime  # YYYY-MM-DD HH:MM:SS
+    session_start_time  : float     # seconds relative to session_datetime
+    session_end_time    : float     # seconds relative to session_datetime
+    -> [nullable] User
+    """
+```
+
+Note that the notation to permit null entries differs for attributes versus foreign
+key references.
+
+## Lookup Tables
+
+Lookup tables are commonly populated from their `contents` property. 
+
+The table below is declared as a lookup table with its contents property
+provided to generate entities.
+
+``` python
+@schema
+class User(dj.Lookup):
+    definition = """
+    # users in the lab
+    username : varchar(20)   # user in the lab
+    ---
+    first_name  : varchar(20)   # user first name
+    last_name   : varchar(20)   # user last name
+    """
+    contents = [
+        ['cajal', 'Santiago', 'Cajal'],
+        ['hubel', 'David', 'Hubel'],
+        ['wiesel', 'Torsten', 'Wiesel']
+]
+
+@schema
+class ProcessingParamSet(dj.Lookup):
+    definition = """  #  Parameter set used for processing of calcium imaging data
+    paramset_idx:  smallint
+    ---
+    -> ProcessingMethod
+    paramset_desc: varchar(128)
+    param_set_hash: uuid
+    unique index (param_set_hash) (1)
+    params: longblob  # dictionary of all applicable parameters
+    """
+```
+
+1. This syntax enforces uniqueness of a secondary attribute.
+
+## Imported and Computed Tables
+
+Imported and Computed tables provide [`make` methods](./make-method) to determine how
+they are populated, either from files or other tables.
+
+Imagine that there is a table `test.Image` that contains 2D grayscale images in its
+`image` attribute. We can define the Computed table, `test.FilteredImage` that filters
+the image in some way and saves the result in its `filtered_image` attribute.
+
+``` python
+@schema
+class FilteredImage(dj.Computed):
+    definition = """ # Filtered image
+    -> Image
+    ---
+    filtered_image : longblob
+    """
+
+    def make(self, key):
+        img = (test.Image & key).fetch1('image')
+        key['filtered_image'] = my_filter(img)
+        self.insert1(key)
+```
+
+## Part Tables
+
+The following code defines a Imported table with an associated part table. In Python,
+the master-part relationship is expressed by making the part a nested class of the
+master. The part is subclassed from `dj.Part` and does not need the `@schema`
+decorator.
+
+```python
+@schema
+class Scan(dj.Imported):
+    definition = """
+    # Two-photon imaging scan
+    -> Session
+    scan : smallint  # scan number within the session
+    ---
+    -> Lens
+    laser_wavelength : decimal(5,1)  # um
+    laser_power      : decimal(4,1)  # mW
+    """
+
+    class ScanField(dj.Part):
+        definition = """
+        -> master
+        ROI: longblob  # Region of interest
+        """
+
+    def make(self, key):
+        ... # (1)
+        self.insert1(key)
+        self.ScanField.insert1(ROI_information)
+```
+
+1. This make method is truncated for the sake of brevity. For more detailed examples,
+please visit [Element Calcium Imaging table definitions](https://datajoint.com/docs/elements/element-calcium-imaging/0.2/api/element_calcium_imaging/scan/)
diff --git a/docs/src/tutorials/json.ipynb b/docs/src/tutorials/json.ipynb
new file mode 100644
index 000000000..f83b960bc
--- /dev/null
+++ b/docs/src/tutorials/json.ipynb
@@ -0,0 +1,1085 @@
+{
+ "cells": [
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "id": "7fe24127-c0d0-4ff8-96b4-6ab0d9307e73",
+   "metadata": {},
+   "source": [
+    "# Using the `json` type"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "62450023",
+   "metadata": {},
+   "source": [
+    "> ⚠️ Note the following before using the `json` type\n",
+    "> - Supported only for MySQL >= 8.0 when [JSON_VALUE](https://dev.mysql.com/doc/refman/8.0/en/json-search-functions.html#function_json-value) introduced.\n",
+    "> - Equivalent Percona is fully-compatible.\n",
+    "> - MariaDB is not supported since [JSON_VALUE](https://mariadb.com/kb/en/json_value/#syntax) does not allow type specification like MySQL's.\n",
+    "> - Not yet supported in DataJoint MATLAB"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "id": "67cf93d2",
+   "metadata": {},
+   "source": [
+    "First you will need to [install](../../getting-started/#installation) and [connect](../../getting-started/#connection) to a DataJoint [data pipeline](https://datajoint.com/docs/core/glossary/#data-pipeline).\n",
+    "\n",
+    "Now let's start by importing the `datajoint` client."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "bc0b6f54-8f11-45f4-bf8d-e1058ee0056f",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import datajoint as dj\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "3544cab9-f2db-458a-9431-939bea5affc5",
+   "metadata": {},
+   "source": [
+    "## Table Definition"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a2998c71",
+   "metadata": {},
+   "source": [
+    "For this exercise, let's imagine we work for an awesome company that is organizing a fun RC car race across various teams in the company. Let's see which team has the fastest car! 🏎️\n",
+    "\n",
+    "This establishes 2 important entities: a `Team` and a `Car`. Normally we'd map this to their own dedicated table, however, let's assume that `Team` is well-structured but `Car` is less structured then we'd prefer. In other words, the structure for what makes up a *car* is varing too much between entries (perhaps because users of the pipeline haven't agreed yet on the definition? 🤷).\n",
+    "\n",
+    "This would make it a good use-case to keep `Team` as a table but make `Car` actually a `json` type defined within the `Team` table.\n",
+    "\n",
+    "Let's begin."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "dc318298-b819-4f06-abbd-7bb7544dd431",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "[2023-02-12 00:14:33,027][INFO]: Connecting root@fakeservices.datajoint.io:3306\n",
+      "[2023-02-12 00:14:33,039][INFO]: Connected root@fakeservices.datajoint.io:3306\n"
+     ]
+    }
+   ],
+   "source": [
+    "schema = dj.Schema(f\"{dj.config['database.user']}_json\")\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "4aaf96db-85d9-4e94-a4c3-3558f4cc6671",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "@schema\n",
+    "class Team(dj.Lookup):\n",
+    "    definition = \"\"\"\n",
+    "    # A team within a company\n",
+    "    name: varchar(40)  # team name\n",
+    "    ---\n",
+    "    car=null: json  # A car belonging to a team (null to allow registering first but specifying car later)\n",
+    "    \n",
+    "    unique index(car.length:decimal(4, 1))  # Add an index if this key is frequently accessed\n",
+    "    \"\"\"\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "640bf7a7-9e07-4953-9c8a-304e55c467f8",
+   "metadata": {},
+   "source": [
+    "## Insert"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "7081e577",
+   "metadata": {},
+   "source": [
+    "Let's suppose that engineering is first up to register their car."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "30f0d62e",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "Team.insert1(\n",
+    "    {\n",
+    "        \"name\": \"engineering\",\n",
+    "        \"car\": {\n",
+    "            \"name\": \"Rever\",\n",
+    "            \"length\": 20.5,\n",
+    "            \"inspected\": True,\n",
+    "            \"tire_pressure\": [32, 31, 33, 34],\n",
+    "            \"headlights\": [\n",
+    "                {\n",
+    "                    \"side\": \"left\",\n",
+    "                    \"hyper_white\": None,\n",
+    "                },\n",
+    "                {\n",
+    "                    \"side\": \"right\",\n",
+    "                    \"hyper_white\": None,\n",
+    "                },\n",
+    "            ],\n",
+    "        },\n",
+    "    }\n",
+    ")\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "ee5e4dcf",
+   "metadata": {},
+   "source": [
+    "Next, business and marketing teams are up and register their cars.\n",
+    "\n",
+    "A few points to notice below:\n",
+    "- The person signing up on behalf of marketing does not know the specifics of the car during registration but another team member will be updating this soon before the race.\n",
+    "- Notice how the `business` and `engineering` teams appear to specify the same property but refer to it as `safety_inspected` and `inspected` respectfully."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "b532e16c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "Team.insert(\n",
+    "    [\n",
+    "        {\n",
+    "            \"name\": \"marketing\",\n",
+    "            \"car\": None,\n",
+    "        },\n",
+    "        {\n",
+    "            \"name\": \"business\",\n",
+    "            \"car\": {\n",
+    "                \"name\": \"Chaching\",\n",
+    "                \"length\": 100,\n",
+    "                \"safety_inspected\": False,\n",
+    "                \"tire_pressure\": [34, 30, 27, 32],\n",
+    "                \"headlights\": [\n",
+    "                    {\n",
+    "                        \"side\": \"left\",\n",
+    "                        \"hyper_white\": True,\n",
+    "                    },\n",
+    "                    {\n",
+    "                        \"side\": \"right\",\n",
+    "                        \"hyper_white\": True,\n",
+    "                    },\n",
+    "                ],\n",
+    "            },\n",
+    "        },\n",
+    "    ]\n",
+    ")\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "57365de7",
+   "metadata": {},
+   "source": [
+    "We can preview the table data much like normal but notice how the value of `car` behaves like other BLOB-like attributes."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "id": "0e3b517c",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "\n",
+       "    \n",
+       "    <style type=\"text/css\">\n",
+       "        .Table{\n",
+       "            border-collapse:collapse;\n",
+       "        }\n",
+       "        .Table th{\n",
+       "            background: #A0A0A0; color: #ffffff; padding:4px; border:#f0e0e0 1px solid;\n",
+       "            font-weight: normal; font-family: monospace; font-size: 100%;\n",
+       "        }\n",
+       "        .Table td{\n",
+       "            padding:4px; border:#f0e0e0 1px solid; font-size:100%;\n",
+       "        }\n",
+       "        .Table tr:nth-child(odd){\n",
+       "            background: #ffffff;\n",
+       "        }\n",
+       "        .Table tr:nth-child(even){\n",
+       "            background: #f3f1ff;\n",
+       "        }\n",
+       "        /* Tooltip container */\n",
+       "        .djtooltip {\n",
+       "        }\n",
+       "        /* Tooltip text */\n",
+       "        .djtooltip .djtooltiptext {\n",
+       "            visibility: hidden;\n",
+       "            width: 120px;\n",
+       "            background-color: black;\n",
+       "            color: #fff;\n",
+       "            text-align: center;\n",
+       "            padding: 5px 0;\n",
+       "            border-radius: 6px;\n",
+       "            /* Position the tooltip text - see examples below! */\n",
+       "            position: absolute;\n",
+       "            z-index: 1;\n",
+       "        }\n",
+       "        #primary {\n",
+       "            font-weight: bold;\n",
+       "            color: black;\n",
+       "        }\n",
+       "        #nonprimary {\n",
+       "            font-weight: normal;\n",
+       "            color: white;\n",
+       "        }\n",
+       "\n",
+       "        /* Show the tooltip text when you mouse over the tooltip container */\n",
+       "        .djtooltip:hover .djtooltiptext {\n",
+       "            visibility: visible;\n",
+       "        }\n",
+       "    </style>\n",
+       "    \n",
+       "    <b>A team within a company</b>\n",
+       "        <div style=\"max-height:1000px;max-width:1500px;overflow:auto;\">\n",
+       "        <table border=\"1\" class=\"Table\">\n",
+       "            <thead> <tr style=\"text-align: right;\"> <th> <div class=\"djtooltip\">\n",
+       "                            <p id=\"primary\">name</p>\n",
+       "                            <span class=\"djtooltiptext\">team name</span>\n",
+       "                        </div></th><th><div class=\"djtooltip\">\n",
+       "                            <p id=\"nonprimary\">car</p>\n",
+       "                            <span class=\"djtooltiptext\">A car belonging to a team (null to allow registering first but specifying car later)</span>\n",
+       "                        </div> </th> </tr> </thead>\n",
+       "            <tbody> <tr> <td>marketing</td>\n",
+       "<td>=BLOB=</td></tr><tr><td>engineering</td>\n",
+       "<td>=BLOB=</td></tr><tr><td>business</td>\n",
+       "<td>=BLOB=</td> </tr> </tbody>\n",
+       "        </table>\n",
+       "        \n",
+       "        <p>Total: 3</p></div>\n",
+       "        "
+      ],
+      "text/plain": [
+       "*name          car       \n",
+       "+------------+ +--------+\n",
+       "marketing      =BLOB=    \n",
+       "engineering    =BLOB=    \n",
+       "business       =BLOB=    \n",
+       " (Total: 3)"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "Team()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "c95cbbee-4ef7-4870-ad42-a60345a3644f",
+   "metadata": {},
+   "source": [
+    "## Restriction"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "8b454996",
+   "metadata": {},
+   "source": [
+    "Now let's see what kinds of queries we can form to demostrate how we can query this pipeline."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "id": "81efda24",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "\n",
+       "    \n",
+       "    <style type=\"text/css\">\n",
+       "        .Table{\n",
+       "            border-collapse:collapse;\n",
+       "        }\n",
+       "        .Table th{\n",
+       "            background: #A0A0A0; color: #ffffff; padding:4px; border:#f0e0e0 1px solid;\n",
+       "            font-weight: normal; font-family: monospace; font-size: 100%;\n",
+       "        }\n",
+       "        .Table td{\n",
+       "            padding:4px; border:#f0e0e0 1px solid; font-size:100%;\n",
+       "        }\n",
+       "        .Table tr:nth-child(odd){\n",
+       "            background: #ffffff;\n",
+       "        }\n",
+       "        .Table tr:nth-child(even){\n",
+       "            background: #f3f1ff;\n",
+       "        }\n",
+       "        /* Tooltip container */\n",
+       "        .djtooltip {\n",
+       "        }\n",
+       "        /* Tooltip text */\n",
+       "        .djtooltip .djtooltiptext {\n",
+       "            visibility: hidden;\n",
+       "            width: 120px;\n",
+       "            background-color: black;\n",
+       "            color: #fff;\n",
+       "            text-align: center;\n",
+       "            padding: 5px 0;\n",
+       "            border-radius: 6px;\n",
+       "            /* Position the tooltip text - see examples below! */\n",
+       "            position: absolute;\n",
+       "            z-index: 1;\n",
+       "        }\n",
+       "        #primary {\n",
+       "            font-weight: bold;\n",
+       "            color: black;\n",
+       "        }\n",
+       "        #nonprimary {\n",
+       "            font-weight: normal;\n",
+       "            color: white;\n",
+       "        }\n",
+       "\n",
+       "        /* Show the tooltip text when you mouse over the tooltip container */\n",
+       "        .djtooltip:hover .djtooltiptext {\n",
+       "            visibility: visible;\n",
+       "        }\n",
+       "    </style>\n",
+       "    \n",
+       "    <b>A team within a company</b>\n",
+       "        <div style=\"max-height:1000px;max-width:1500px;overflow:auto;\">\n",
+       "        <table border=\"1\" class=\"Table\">\n",
+       "            <thead> <tr style=\"text-align: right;\"> <th> <div class=\"djtooltip\">\n",
+       "                            <p id=\"primary\">name</p>\n",
+       "                            <span class=\"djtooltiptext\">team name</span>\n",
+       "                        </div></th><th><div class=\"djtooltip\">\n",
+       "                            <p id=\"nonprimary\">car</p>\n",
+       "                            <span class=\"djtooltiptext\">A car belonging to a team (null to allow registering first but specifying car later)</span>\n",
+       "                        </div> </th> </tr> </thead>\n",
+       "            <tbody> <tr> <td>business</td>\n",
+       "<td>=BLOB=</td> </tr> </tbody>\n",
+       "        </table>\n",
+       "        \n",
+       "        <p>Total: 1</p></div>\n",
+       "        "
+      ],
+      "text/plain": [
+       "*name        car       \n",
+       "+----------+ +--------+\n",
+       "business     =BLOB=    \n",
+       " (Total: 1)"
+      ]
+     },
+     "execution_count": 7,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# Which team has a `car` equal to 100 inches long?\n",
+    "Team & {'car.length': 100}"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "id": "fd7b855d",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "\n",
+       "    \n",
+       "    <style type=\"text/css\">\n",
+       "        .Table{\n",
+       "            border-collapse:collapse;\n",
+       "        }\n",
+       "        .Table th{\n",
+       "            background: #A0A0A0; color: #ffffff; padding:4px; border:#f0e0e0 1px solid;\n",
+       "            font-weight: normal; font-family: monospace; font-size: 100%;\n",
+       "        }\n",
+       "        .Table td{\n",
+       "            padding:4px; border:#f0e0e0 1px solid; font-size:100%;\n",
+       "        }\n",
+       "        .Table tr:nth-child(odd){\n",
+       "            background: #ffffff;\n",
+       "        }\n",
+       "        .Table tr:nth-child(even){\n",
+       "            background: #f3f1ff;\n",
+       "        }\n",
+       "        /* Tooltip container */\n",
+       "        .djtooltip {\n",
+       "        }\n",
+       "        /* Tooltip text */\n",
+       "        .djtooltip .djtooltiptext {\n",
+       "            visibility: hidden;\n",
+       "            width: 120px;\n",
+       "            background-color: black;\n",
+       "            color: #fff;\n",
+       "            text-align: center;\n",
+       "            padding: 5px 0;\n",
+       "            border-radius: 6px;\n",
+       "            /* Position the tooltip text - see examples below! */\n",
+       "            position: absolute;\n",
+       "            z-index: 1;\n",
+       "        }\n",
+       "        #primary {\n",
+       "            font-weight: bold;\n",
+       "            color: black;\n",
+       "        }\n",
+       "        #nonprimary {\n",
+       "            font-weight: normal;\n",
+       "            color: white;\n",
+       "        }\n",
+       "\n",
+       "        /* Show the tooltip text when you mouse over the tooltip container */\n",
+       "        .djtooltip:hover .djtooltiptext {\n",
+       "            visibility: visible;\n",
+       "        }\n",
+       "    </style>\n",
+       "    \n",
+       "    <b>A team within a company</b>\n",
+       "        <div style=\"max-height:1000px;max-width:1500px;overflow:auto;\">\n",
+       "        <table border=\"1\" class=\"Table\">\n",
+       "            <thead> <tr style=\"text-align: right;\"> <th> <div class=\"djtooltip\">\n",
+       "                            <p id=\"primary\">name</p>\n",
+       "                            <span class=\"djtooltiptext\">team name</span>\n",
+       "                        </div></th><th><div class=\"djtooltip\">\n",
+       "                            <p id=\"nonprimary\">car</p>\n",
+       "                            <span class=\"djtooltiptext\">A car belonging to a team (null to allow registering first but specifying car later)</span>\n",
+       "                        </div> </th> </tr> </thead>\n",
+       "            <tbody> <tr> <td>engineering</td>\n",
+       "<td>=BLOB=</td> </tr> </tbody>\n",
+       "        </table>\n",
+       "        \n",
+       "        <p>Total: 1</p></div>\n",
+       "        "
+      ],
+      "text/plain": [
+       "*name          car       \n",
+       "+------------+ +--------+\n",
+       "engineering    =BLOB=    \n",
+       " (Total: 1)"
+      ]
+     },
+     "execution_count": 8,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# Which team has a `car` less than 50 inches long?\n",
+    "Team & \"car->>'$.length' < 50\""
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "id": "b76ebb75",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "\n",
+       "    \n",
+       "    <style type=\"text/css\">\n",
+       "        .Table{\n",
+       "            border-collapse:collapse;\n",
+       "        }\n",
+       "        .Table th{\n",
+       "            background: #A0A0A0; color: #ffffff; padding:4px; border:#f0e0e0 1px solid;\n",
+       "            font-weight: normal; font-family: monospace; font-size: 100%;\n",
+       "        }\n",
+       "        .Table td{\n",
+       "            padding:4px; border:#f0e0e0 1px solid; font-size:100%;\n",
+       "        }\n",
+       "        .Table tr:nth-child(odd){\n",
+       "            background: #ffffff;\n",
+       "        }\n",
+       "        .Table tr:nth-child(even){\n",
+       "            background: #f3f1ff;\n",
+       "        }\n",
+       "        /* Tooltip container */\n",
+       "        .djtooltip {\n",
+       "        }\n",
+       "        /* Tooltip text */\n",
+       "        .djtooltip .djtooltiptext {\n",
+       "            visibility: hidden;\n",
+       "            width: 120px;\n",
+       "            background-color: black;\n",
+       "            color: #fff;\n",
+       "            text-align: center;\n",
+       "            padding: 5px 0;\n",
+       "            border-radius: 6px;\n",
+       "            /* Position the tooltip text - see examples below! */\n",
+       "            position: absolute;\n",
+       "            z-index: 1;\n",
+       "        }\n",
+       "        #primary {\n",
+       "            font-weight: bold;\n",
+       "            color: black;\n",
+       "        }\n",
+       "        #nonprimary {\n",
+       "            font-weight: normal;\n",
+       "            color: white;\n",
+       "        }\n",
+       "\n",
+       "        /* Show the tooltip text when you mouse over the tooltip container */\n",
+       "        .djtooltip:hover .djtooltiptext {\n",
+       "            visibility: visible;\n",
+       "        }\n",
+       "    </style>\n",
+       "    \n",
+       "    <b>A team within a company</b>\n",
+       "        <div style=\"max-height:1000px;max-width:1500px;overflow:auto;\">\n",
+       "        <table border=\"1\" class=\"Table\">\n",
+       "            <thead> <tr style=\"text-align: right;\"> <th> <div class=\"djtooltip\">\n",
+       "                            <p id=\"primary\">name</p>\n",
+       "                            <span class=\"djtooltiptext\">team name</span>\n",
+       "                        </div></th><th><div class=\"djtooltip\">\n",
+       "                            <p id=\"nonprimary\">car</p>\n",
+       "                            <span class=\"djtooltiptext\">A car belonging to a team (null to allow registering first but specifying car later)</span>\n",
+       "                        </div> </th> </tr> </thead>\n",
+       "            <tbody> <tr> <td>engineering</td>\n",
+       "<td>=BLOB=</td> </tr> </tbody>\n",
+       "        </table>\n",
+       "        \n",
+       "        <p>Total: 1</p></div>\n",
+       "        "
+      ],
+      "text/plain": [
+       "*name          car       \n",
+       "+------------+ +--------+\n",
+       "engineering    =BLOB=    \n",
+       " (Total: 1)"
+      ]
+     },
+     "execution_count": 9,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# Any team that has had their car inspected?\n",
+    "Team & [{'car.inspected:unsigned': True}, {'car.safety_inspected:unsigned': True}]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "id": "b787784c",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "\n",
+       "    \n",
+       "    <style type=\"text/css\">\n",
+       "        .Table{\n",
+       "            border-collapse:collapse;\n",
+       "        }\n",
+       "        .Table th{\n",
+       "            background: #A0A0A0; color: #ffffff; padding:4px; border:#f0e0e0 1px solid;\n",
+       "            font-weight: normal; font-family: monospace; font-size: 100%;\n",
+       "        }\n",
+       "        .Table td{\n",
+       "            padding:4px; border:#f0e0e0 1px solid; font-size:100%;\n",
+       "        }\n",
+       "        .Table tr:nth-child(odd){\n",
+       "            background: #ffffff;\n",
+       "        }\n",
+       "        .Table tr:nth-child(even){\n",
+       "            background: #f3f1ff;\n",
+       "        }\n",
+       "        /* Tooltip container */\n",
+       "        .djtooltip {\n",
+       "        }\n",
+       "        /* Tooltip text */\n",
+       "        .djtooltip .djtooltiptext {\n",
+       "            visibility: hidden;\n",
+       "            width: 120px;\n",
+       "            background-color: black;\n",
+       "            color: #fff;\n",
+       "            text-align: center;\n",
+       "            padding: 5px 0;\n",
+       "            border-radius: 6px;\n",
+       "            /* Position the tooltip text - see examples below! */\n",
+       "            position: absolute;\n",
+       "            z-index: 1;\n",
+       "        }\n",
+       "        #primary {\n",
+       "            font-weight: bold;\n",
+       "            color: black;\n",
+       "        }\n",
+       "        #nonprimary {\n",
+       "            font-weight: normal;\n",
+       "            color: white;\n",
+       "        }\n",
+       "\n",
+       "        /* Show the tooltip text when you mouse over the tooltip container */\n",
+       "        .djtooltip:hover .djtooltiptext {\n",
+       "            visibility: visible;\n",
+       "        }\n",
+       "    </style>\n",
+       "    \n",
+       "    <b>A team within a company</b>\n",
+       "        <div style=\"max-height:1000px;max-width:1500px;overflow:auto;\">\n",
+       "        <table border=\"1\" class=\"Table\">\n",
+       "            <thead> <tr style=\"text-align: right;\"> <th> <div class=\"djtooltip\">\n",
+       "                            <p id=\"primary\">name</p>\n",
+       "                            <span class=\"djtooltiptext\">team name</span>\n",
+       "                        </div></th><th><div class=\"djtooltip\">\n",
+       "                            <p id=\"nonprimary\">car</p>\n",
+       "                            <span class=\"djtooltiptext\">A car belonging to a team (null to allow registering first but specifying car later)</span>\n",
+       "                        </div> </th> </tr> </thead>\n",
+       "            <tbody> <tr> <td>engineering</td>\n",
+       "<td>=BLOB=</td></tr><tr><td>marketing</td>\n",
+       "<td>=BLOB=</td> </tr> </tbody>\n",
+       "        </table>\n",
+       "        \n",
+       "        <p>Total: 2</p></div>\n",
+       "        "
+      ],
+      "text/plain": [
+       "*name          car       \n",
+       "+------------+ +--------+\n",
+       "engineering    =BLOB=    \n",
+       "marketing      =BLOB=    \n",
+       " (Total: 2)"
+      ]
+     },
+     "execution_count": 10,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# Which teams do not have hyper white lights for their first head light?\n",
+    "Team & {\"car.headlights[0].hyper_white\": None}"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5bcf0b5d",
+   "metadata": {},
+   "source": [
+    "Notice that the previous query will satisfy the `None` check if it experiences any of the following scenarious:\n",
+    "- if entire record missing (`marketing` satisfies this)\n",
+    "- JSON key is missing\n",
+    "- JSON value is set to JSON `null` (`engineering` satisfies this)"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "id": "bcf1682e-a0c7-4c2f-826b-0aec9052a694",
+   "metadata": {},
+   "source": [
+    "## Projection"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "id": "daea110e",
+   "metadata": {},
+   "source": [
+    "Projections can be quite useful with the `json` type since we can extract out just what we need. This allows greater query flexibility but more importantly, for us to be able to fetch only what is pertinent."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "id": "8fb8334a",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "\n",
+       "    \n",
+       "    <style type=\"text/css\">\n",
+       "        .Table{\n",
+       "            border-collapse:collapse;\n",
+       "        }\n",
+       "        .Table th{\n",
+       "            background: #A0A0A0; color: #ffffff; padding:4px; border:#f0e0e0 1px solid;\n",
+       "            font-weight: normal; font-family: monospace; font-size: 100%;\n",
+       "        }\n",
+       "        .Table td{\n",
+       "            padding:4px; border:#f0e0e0 1px solid; font-size:100%;\n",
+       "        }\n",
+       "        .Table tr:nth-child(odd){\n",
+       "            background: #ffffff;\n",
+       "        }\n",
+       "        .Table tr:nth-child(even){\n",
+       "            background: #f3f1ff;\n",
+       "        }\n",
+       "        /* Tooltip container */\n",
+       "        .djtooltip {\n",
+       "        }\n",
+       "        /* Tooltip text */\n",
+       "        .djtooltip .djtooltiptext {\n",
+       "            visibility: hidden;\n",
+       "            width: 120px;\n",
+       "            background-color: black;\n",
+       "            color: #fff;\n",
+       "            text-align: center;\n",
+       "            padding: 5px 0;\n",
+       "            border-radius: 6px;\n",
+       "            /* Position the tooltip text - see examples below! */\n",
+       "            position: absolute;\n",
+       "            z-index: 1;\n",
+       "        }\n",
+       "        #primary {\n",
+       "            font-weight: bold;\n",
+       "            color: black;\n",
+       "        }\n",
+       "        #nonprimary {\n",
+       "            font-weight: normal;\n",
+       "            color: white;\n",
+       "        }\n",
+       "\n",
+       "        /* Show the tooltip text when you mouse over the tooltip container */\n",
+       "        .djtooltip:hover .djtooltiptext {\n",
+       "            visibility: visible;\n",
+       "        }\n",
+       "    </style>\n",
+       "    \n",
+       "    \n",
+       "        <div style=\"max-height:1000px;max-width:1500px;overflow:auto;\">\n",
+       "        <table border=\"1\" class=\"Table\">\n",
+       "            <thead> <tr style=\"text-align: right;\"> <th> <div class=\"djtooltip\">\n",
+       "                            <p id=\"primary\">name</p>\n",
+       "                            <span class=\"djtooltiptext\">team name</span>\n",
+       "                        </div></th><th><div class=\"djtooltip\">\n",
+       "                            <p id=\"nonprimary\">car_name</p>\n",
+       "                            <span class=\"djtooltiptext\">calculated attribute</span>\n",
+       "                        </div></th><th><div class=\"djtooltip\">\n",
+       "                            <p id=\"nonprimary\">car_length</p>\n",
+       "                            <span class=\"djtooltiptext\">calculated attribute</span>\n",
+       "                        </div> </th> </tr> </thead>\n",
+       "            <tbody> <tr> <td>business</td>\n",
+       "<td>Chaching</td>\n",
+       "<td>100</td></tr><tr><td>engineering</td>\n",
+       "<td>Rever</td>\n",
+       "<td>20.5</td></tr><tr><td>marketing</td>\n",
+       "<td>None</td>\n",
+       "<td>None</td> </tr> </tbody>\n",
+       "        </table>\n",
+       "        \n",
+       "        <p>Total: 3</p></div>\n",
+       "        "
+      ],
+      "text/plain": [
+       "*name          car_name     car_length    \n",
+       "+------------+ +----------+ +------------+\n",
+       "business       Chaching     100           \n",
+       "engineering    Rever        20.5          \n",
+       "marketing      None         None          \n",
+       " (Total: 3)"
+      ]
+     },
+     "execution_count": 11,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# Only interested in the car names and the length but let the type be inferred\n",
+    "q_untyped = Team.proj(\n",
+    "    car_name='car.name',\n",
+    "    car_length=\"car.length\",\n",
+    ")\n",
+    "q_untyped"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "id": "bb5f0448",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[{'name': 'business', 'car_name': 'Chaching', 'car_length': '100'},\n",
+       " {'name': 'engineering', 'car_name': 'Rever', 'car_length': '20.5'},\n",
+       " {'name': 'marketing', 'car_name': None, 'car_length': None}]"
+      ]
+     },
+     "execution_count": 12,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "q_untyped.fetch(as_dict=True)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "id": "a307dfd7",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "\n",
+       "    \n",
+       "    <style type=\"text/css\">\n",
+       "        .Table{\n",
+       "            border-collapse:collapse;\n",
+       "        }\n",
+       "        .Table th{\n",
+       "            background: #A0A0A0; color: #ffffff; padding:4px; border:#f0e0e0 1px solid;\n",
+       "            font-weight: normal; font-family: monospace; font-size: 100%;\n",
+       "        }\n",
+       "        .Table td{\n",
+       "            padding:4px; border:#f0e0e0 1px solid; font-size:100%;\n",
+       "        }\n",
+       "        .Table tr:nth-child(odd){\n",
+       "            background: #ffffff;\n",
+       "        }\n",
+       "        .Table tr:nth-child(even){\n",
+       "            background: #f3f1ff;\n",
+       "        }\n",
+       "        /* Tooltip container */\n",
+       "        .djtooltip {\n",
+       "        }\n",
+       "        /* Tooltip text */\n",
+       "        .djtooltip .djtooltiptext {\n",
+       "            visibility: hidden;\n",
+       "            width: 120px;\n",
+       "            background-color: black;\n",
+       "            color: #fff;\n",
+       "            text-align: center;\n",
+       "            padding: 5px 0;\n",
+       "            border-radius: 6px;\n",
+       "            /* Position the tooltip text - see examples below! */\n",
+       "            position: absolute;\n",
+       "            z-index: 1;\n",
+       "        }\n",
+       "        #primary {\n",
+       "            font-weight: bold;\n",
+       "            color: black;\n",
+       "        }\n",
+       "        #nonprimary {\n",
+       "            font-weight: normal;\n",
+       "            color: white;\n",
+       "        }\n",
+       "\n",
+       "        /* Show the tooltip text when you mouse over the tooltip container */\n",
+       "        .djtooltip:hover .djtooltiptext {\n",
+       "            visibility: visible;\n",
+       "        }\n",
+       "    </style>\n",
+       "    \n",
+       "    \n",
+       "        <div style=\"max-height:1000px;max-width:1500px;overflow:auto;\">\n",
+       "        <table border=\"1\" class=\"Table\">\n",
+       "            <thead> <tr style=\"text-align: right;\"> <th> <div class=\"djtooltip\">\n",
+       "                            <p id=\"primary\">name</p>\n",
+       "                            <span class=\"djtooltiptext\">team name</span>\n",
+       "                        </div></th><th><div class=\"djtooltip\">\n",
+       "                            <p id=\"nonprimary\">car_name</p>\n",
+       "                            <span class=\"djtooltiptext\">calculated attribute</span>\n",
+       "                        </div></th><th><div class=\"djtooltip\">\n",
+       "                            <p id=\"nonprimary\">car_length</p>\n",
+       "                            <span class=\"djtooltiptext\">calculated attribute</span>\n",
+       "                        </div> </th> </tr> </thead>\n",
+       "            <tbody> <tr> <td>business</td>\n",
+       "<td>Chaching</td>\n",
+       "<td>100.0</td></tr><tr><td>engineering</td>\n",
+       "<td>Rever</td>\n",
+       "<td>20.5</td></tr><tr><td>marketing</td>\n",
+       "<td>None</td>\n",
+       "<td>None</td> </tr> </tbody>\n",
+       "        </table>\n",
+       "        \n",
+       "        <p>Total: 3</p></div>\n",
+       "        "
+      ],
+      "text/plain": [
+       "*name          car_name     car_length    \n",
+       "+------------+ +----------+ +------------+\n",
+       "business       Chaching     100.0         \n",
+       "engineering    Rever        20.5          \n",
+       "marketing      None         None          \n",
+       " (Total: 3)"
+      ]
+     },
+     "execution_count": 13,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# Nevermind, I'll specify the type explicitly\n",
+    "q_typed = Team.proj(\n",
+    "    car_name='car.name',\n",
+    "    car_length=\"car.length:float\",\n",
+    ")\n",
+    "q_typed"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "id": "8a93dbf9",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[{'name': 'business', 'car_name': 'Chaching', 'car_length': 100.0},\n",
+       " {'name': 'engineering', 'car_name': 'Rever', 'car_length': 20.5},\n",
+       " {'name': 'marketing', 'car_name': None, 'car_length': None}]"
+      ]
+     },
+     "execution_count": 14,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "q_typed.fetch(as_dict=True)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "62dd0239-fa70-4369-81eb-3d46c5053fee",
+   "metadata": {},
+   "source": [
+    "## Describe"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "id": "73d9df01",
+   "metadata": {},
+   "source": [
+    "Lastly, the `.describe()` function on the `Team` table can help us generate the table's definition. This is useful if we are connected directly to the pipeline without the original source."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 16,
+   "id": "0e739932",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "# A team within a company\n",
+      "name                 : varchar(40)                  # team name\n",
+      "---\n",
+      "car=null             : json                         # A car belonging to a team (null to allow registering first but specifying car later)\n",
+      "UNIQUE INDEX ((json_value(`car`, _utf8mb4'$.length' returning decimal(4, 1))))\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "rebuilt_definition = Team.describe()\n",
+    "print(rebuilt_definition)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "be1070d5-765b-4bc2-92de-8a6ffd885984",
+   "metadata": {},
+   "source": [
+    "## Cleanup"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "id": "cb959927",
+   "metadata": {},
+   "source": [
+    "Finally, let's clean up what we created in this tutorial."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 17,
+   "id": "d9cc28a3-3ffd-4126-b7e9-bc6365040b93",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "schema.drop()\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "68ad4340",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3.7.16 64-bit",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.7.16"
+  },
+  "vscode": {
+   "interpreter": {
+    "hash": "949777d72b0d2535278d3dc13498b2535136f6dfe0678499012e853ee9abcab1"
+   }
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/local-docker-compose.yml b/local-docker-compose.yml
index 6fdcfec9b..0c9d8f218 100644
--- a/local-docker-compose.yml
+++ b/local-docker-compose.yml
@@ -1,22 +1,28 @@
-# docker-compose -f local-docker-compose.yml --env-file LNX.env up --build
-version: '2.2'
-x-net: &net
+# MYSQL_VER=5.7 MINIO_VER=RELEASE.2022-08-11T04-37-28Z docker compose -f local-docker-compose.yml up --build
+version: "2.4"
+x-net:
+  &net
   networks:
-      - main
+    - main
 services:
   db:
     <<: *net
-    image: datajoint/mysql:$MYSQL_VER
+    image: datajoint/mysql:${MYSQL_VER}
     environment:
-      - MYSQL_ROOT_PASSWORD=simple
+      - MYSQL_ROOT_PASSWORD=${DJ_PASS}
     # ports:
     #   - "3306:3306"
     # To persist MySQL data
     # volumes:
     #   - ./mysql/data:/var/lib/mysql
+    healthcheck:
+      test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ]
+      timeout: 30s
+      retries: 5
+      interval: 15s
   minio:
     <<: *net
-    image: minio/minio:$MINIO_VER
+    image: minio/minio:${MINIO_VER}
     environment:
       - MINIO_ACCESS_KEY=datajoint
       - MINIO_SECRET_KEY=datajoint
@@ -28,87 +34,33 @@ services:
     #   - ./minio/config:/root/.minio
     command: server --address ":9000" /data
     healthcheck:
-      test: ["CMD", "curl", "--fail", "http://minio:9000/minio/health/live"]
-      timeout: 5s
-      retries: 60
-      interval: 1s
+      test:
+        [
+          "CMD",
+          "curl",
+          "--fail",
+          "http://minio:9000/minio/health/live"
+        ]
+      timeout: 30s
+      retries: 5
+      interval: 15s
   fakeservices.datajoint.io:
     <<: *net
-    image: datajoint/nginx:v0.2.1
+    image: datajoint/nginx:v0.2.4
     environment:
-    - ADD_db_TYPE=DATABASE
-    - ADD_db_ENDPOINT=db:3306
-    - ADD_minio_TYPE=MINIO
-    - ADD_minio_ENDPOINT=minio:9000
-    - ADD_minio_PORT=80 # allow unencrypted connections
-    - ADD_minio_PREFIX=/datajoint
-    - ADD_browser_TYPE=MINIOADMIN
-    - ADD_browser_ENDPOINT=minio:9000
-    - ADD_browser_PORT=80 # allow unencrypted connections
+      - ADD_db_TYPE=DATABASE
+      - ADD_db_ENDPOINT=db:3306
+      - ADD_minio_TYPE=MINIO
+      - ADD_minio_ENDPOINT=minio:9000
+      - ADD_minio_PORT=80 # allow unencrypted connections
+      - ADD_minio_PREFIX=/datajoint
+      - ADD_browser_TYPE=MINIOADMIN
+      - ADD_browser_ENDPOINT=minio:9000
+      - ADD_browser_PORT=80 # allow unencrypted connections
     ports:
       - "80:80"
       - "443:443"
       - "3306:3306"
       - "9000:9000"
-    depends_on:
-      db:
-        condition: service_healthy
-      minio:
-        condition: service_healthy
-  app:
-    <<: *net
-    image: datajoint/pydev:${PY_VER}-alpine${ALPINE_VER}
-    depends_on:
-      fakeservices.datajoint.io:
-        condition: service_healthy
-    environment:
-      - DJ_HOST=fakeservices.datajoint.io
-      - DJ_USER=root
-      - DJ_PASS=simple
-      - DJ_TEST_HOST=fakeservices.datajoint.io
-      - DJ_TEST_USER=datajoint
-      - DJ_TEST_PASSWORD=datajoint
-      # If running tests locally, make sure to add entry in /etc/hosts for 127.0.0.1 fakeservices.datajoint.io
-      - S3_ENDPOINT=fakeservices.datajoint.io
-      - S3_ACCESS_KEY=datajoint
-      - S3_SECRET_KEY=datajoint
-      - S3_BUCKET=datajoint.test
-      - PYTHON_USER=dja
-      - JUPYTER_PASSWORD=datajoint
-      - DISPLAY
-    working_dir: /src
-    command:
-      - sh
-      - -c
-      - |
-        set -e
-        pip install --user nose nose-cov coveralls flake8 ptvsd black
-        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
-        # nosetests -vs --tests=tests.test_fetch:TestFetch.test_getattribute_for_fetch1; #run specific Class test
-        # flake8 datajoint --count --select=E9,F63,F7,F82 --show-source --statistics
-        # flake8 --ignore=E203,E722,F401,W503 datajoint --count --max-complexity=62 --max-line-length=127 --statistics
-        # black datajoint --check -v
-        ## 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
-    ports:
-      - "8888:8888"
-      - "5678:5678"
-    user: ${UID}:${GID}
-    volumes:
-      - .:/src
-      - /tmp/.X11-unix:/tmp/.X11-unix:rw
-      # Additional mounted notebooks may go here
-      # - ./notebook:/home/dja/notebooks
-      # - ../dj-python-101/ch1:/home/dja/tutorials
 networks:
   main:
diff --git a/requirements.txt b/requirements.txt
index af5b869ce..c0e8a99c2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,7 +4,7 @@ pyparsing
 ipython
 pandas
 tqdm
-networkx<=2.6.3
+networkx<=2.6.3  # until py3.8 is our minimum version
 pydot
 minio>=7.0.0
 matplotlib
diff --git a/setup.py b/setup.py
index 8127519a8..ecf53d97f 100644
--- a/setup.py
+++ b/setup.py
@@ -5,33 +5,42 @@
 
 min_py_version = (3, 7)
 
-if sys.version_info <  min_py_version:
-    sys.exit('DataJoint is only supported for Python {}.{} or higher'.format(*min_py_version))
+if sys.version_info < min_py_version:
+    sys.exit(
+        "DataJoint is only supported for Python {}.{} or higher".format(*min_py_version)
+    )
 
 here = path.abspath(path.dirname(__file__))
 
-long_description = "A relational data framework for scientific data pipelines with MySQL backend."
+long_description = (
+    "A relational data framework for scientific data pipelines with MySQL backend."
+)
 
 # read in version number into __version__
-with open(path.join(here, 'datajoint', 'version.py')) as f:
+with open(path.join(here, "datajoint", "version.py")) as f:
     exec(f.read())
 
-with open(path.join(here, 'requirements.txt')) as f:
-    requirements = f.read().split()
+with open(path.join(here, "requirements.txt")) as f:
+    requirements = [line.split("#", 1)[0].rstrip() for line in f.readlines()]
 
 setup(
-    name='datajoint',
+    name="datajoint",
     version=__version__,
     description="A relational data pipeline framework.",
     long_description=long_description,
-    author='Dimitri Yatsenko',
-    author_email='info@datajoint.io',
+    author="DataJoint Contributors",
+    author_email="support@datajoint.com",
     license="GNU LGPL",
-    url='https://datajoint.io',
-    keywords='database organization',
-    packages=find_packages(exclude=['contrib', 'docs', 'tests*']),
+    url="https://datajoint.com",
+    keywords=[
+        "database",
+        "data pipelines",
+        "scientific computing",
+        "automated research workflows",
+    ],
+    packages=find_packages(exclude=["contrib", "docs", "tests*"]),
     install_requires=requirements,
-    python_requires='~={}.{}'.format(*min_py_version),
-    setup_requires=['otumat'],  # maybe remove due to conflicts?
-    pubkey_path='./datajoint.pub'
+    python_requires="~={}.{}".format(*min_py_version),
+    setup_requires=["otumat"],  # maybe remove due to conflicts?
+    pubkey_path="./datajoint.pub",
 )


Test patch:
diff --git a/test_requirements.txt b/test_requirements.txt
deleted file mode 100644
index 373bb1a8e..000000000
--- a/test_requirements.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-nose
-nose-cov
-coveralls
-faker
\ No newline at end of file
diff --git a/tests/__init__.py b/tests/__init__.py
index e1795e12d..5211278e3 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -44,13 +44,6 @@
     bucket=environ.get("S3_BUCKET", "datajoint.test"),
 )
 
-S3_MIGRATE_BUCKET = [
-    path.name
-    for path in Path(
-        Path(__file__).resolve().parent, "external-legacy-data", "s3"
-    ).iterdir()
-][0]
-
 # Prefix for all databases used during testing
 PREFIX = environ.get("DJ_TEST_DB_PREFIX", "djtest")
 conn_root = dj.conn(**CONN_INFO_ROOT)
@@ -129,43 +122,7 @@ def setup_package():
             """
         )
 
-    # Add old MySQL
-    source = Path(Path(__file__).resolve().parent, "external-legacy-data")
-    db_name = "djtest_blob_migrate"
-    db_file = "v0_11.sql"
-    conn_root.query(
-        """
-        CREATE DATABASE IF NOT EXISTS {};
-        """.format(
-            db_name
-        )
-    )
-
-    statements = parse_sql(Path(source, db_file))
-    for s in statements:
-        conn_root.query(s)
-
-    # Add old S3
-    source = Path(Path(__file__).resolve().parent, "external-legacy-data", "s3")
     region = "us-east-1"
-    try:
-        minioClient.make_bucket(S3_MIGRATE_BUCKET, location=region)
-    except minio.error.S3Error as e:
-        if e.code != "BucketAlreadyOwnedByYou":
-            raise e
-
-    pathlist = Path(source).glob("**/*")
-    for path in pathlist:
-        if os.path.isfile(str(path)) and ".sql" not in str(path):
-            minioClient.fput_object(
-                S3_MIGRATE_BUCKET,
-                str(
-                    Path(
-                        os.path.relpath(str(path), str(Path(source, S3_MIGRATE_BUCKET)))
-                    ).as_posix()
-                ),
-                str(path),
-            )
     # Add S3
     try:
         minioClient.make_bucket(S3_CONN_INFO["bucket"], location=region)
@@ -173,22 +130,6 @@ def setup_package():
         if e.code != "BucketAlreadyOwnedByYou":
             raise e
 
-    # Add old File Content
-    try:
-        shutil.copytree(
-            str(
-                Path(
-                    Path(__file__).resolve().parent,
-                    "external-legacy-data",
-                    "file",
-                    "temp",
-                )
-            ),
-            str(Path(os.path.expanduser("~"), "temp")),
-        )
-    except FileExistsError:
-        pass
-
 
 def teardown_package():
     """
@@ -210,14 +151,6 @@ def teardown_package():
     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))
-    objs = [
-        minioClient.remove_object(S3_MIGRATE_BUCKET, o.object_name.encode("utf-8"))
-        for o in objs
-    ]
-    minioClient.remove_bucket(S3_MIGRATE_BUCKET)
-
     # Remove S3
     objs = list(minioClient.list_objects(S3_CONN_INFO["bucket"], recursive=True))
     objs = [
@@ -225,6 +158,3 @@ def teardown_package():
         for o in objs
     ]
     minioClient.remove_bucket(S3_CONN_INFO["bucket"])
-
-    # Remove old File Content
-    shutil.rmtree(str(Path(os.path.expanduser("~"), "temp")))
diff --git a/tests/external-legacy-data/file/temp/datajoint.migrate/djtest_blob_migrate/_Fhi2GUBB0fgxcSP2q-isgncIUTdgGK7ivHiySAU_94local b/tests/external-legacy-data/file/temp/datajoint.migrate/djtest_blob_migrate/_Fhi2GUBB0fgxcSP2q-isgncIUTdgGK7ivHiySAU_94local
deleted file mode 100644
index 11a25ad89..000000000
Binary files a/tests/external-legacy-data/file/temp/datajoint.migrate/djtest_blob_migrate/_Fhi2GUBB0fgxcSP2q-isgncIUTdgGK7ivHiySAU_94local and /dev/null differ
diff --git a/tests/external-legacy-data/file/temp/datajoint.migrate/djtest_blob_migrate/e46pnXQW9GaCKbL3WxV1crGHeGqcE0OLInM_TTwAFfwlocal b/tests/external-legacy-data/file/temp/datajoint.migrate/djtest_blob_migrate/e46pnXQW9GaCKbL3WxV1crGHeGqcE0OLInM_TTwAFfwlocal
deleted file mode 100644
index 8a745d07f..000000000
Binary files a/tests/external-legacy-data/file/temp/datajoint.migrate/djtest_blob_migrate/e46pnXQW9GaCKbL3WxV1crGHeGqcE0OLInM_TTwAFfwlocal and /dev/null differ
diff --git a/tests/external-legacy-data/s3/datajoint.migrate/maps/djtest_blob_migrate/FoRROa2LWM6_wx0RIQ0J-LVvgm256cqDQfJa066HoTEshared b/tests/external-legacy-data/s3/datajoint.migrate/maps/djtest_blob_migrate/FoRROa2LWM6_wx0RIQ0J-LVvgm256cqDQfJa066HoTEshared
deleted file mode 100644
index 38da73099..000000000
Binary files a/tests/external-legacy-data/s3/datajoint.migrate/maps/djtest_blob_migrate/FoRROa2LWM6_wx0RIQ0J-LVvgm256cqDQfJa066HoTEshared and /dev/null differ
diff --git a/tests/external-legacy-data/s3/datajoint.migrate/maps/djtest_blob_migrate/NmWj002gtKUkt9GIBwzn6Iw3x6h7ovlX_FfELbfjwRQshared b/tests/external-legacy-data/s3/datajoint.migrate/maps/djtest_blob_migrate/NmWj002gtKUkt9GIBwzn6Iw3x6h7ovlX_FfELbfjwRQshared
deleted file mode 100644
index 8acc341af..000000000
Binary files a/tests/external-legacy-data/s3/datajoint.migrate/maps/djtest_blob_migrate/NmWj002gtKUkt9GIBwzn6Iw3x6h7ovlX_FfELbfjwRQshared and /dev/null differ
diff --git a/tests/external-legacy-data/s3/datajoint.migrate/maps/djtest_blob_migrate/Ue9c89gKVZD7xPOcHd5Lz6mARJQ50xT1G5cTTX4h0L0shared b/tests/external-legacy-data/s3/datajoint.migrate/maps/djtest_blob_migrate/Ue9c89gKVZD7xPOcHd5Lz6mARJQ50xT1G5cTTX4h0L0shared
deleted file mode 100644
index cfba570e4..000000000
Binary files a/tests/external-legacy-data/s3/datajoint.migrate/maps/djtest_blob_migrate/Ue9c89gKVZD7xPOcHd5Lz6mARJQ50xT1G5cTTX4h0L0shared and /dev/null differ
diff --git a/tests/external-legacy-data/s3/datajoint.migrate/store/djtest_blob_migrate/_3A03zPqfVhbn0rhlOJYGNivFJ4uqYuHaeQBA-V8PKA b/tests/external-legacy-data/s3/datajoint.migrate/store/djtest_blob_migrate/_3A03zPqfVhbn0rhlOJYGNivFJ4uqYuHaeQBA-V8PKA
deleted file mode 100644
index d21049aa6..000000000
Binary files a/tests/external-legacy-data/s3/datajoint.migrate/store/djtest_blob_migrate/_3A03zPqfVhbn0rhlOJYGNivFJ4uqYuHaeQBA-V8PKA and /dev/null differ
diff --git a/tests/external-legacy-data/s3/datajoint.migrate/store/djtest_blob_migrate/_Fhi2GUBB0fgxcSP2q-isgncIUTdgGK7ivHiySAU_94 b/tests/external-legacy-data/s3/datajoint.migrate/store/djtest_blob_migrate/_Fhi2GUBB0fgxcSP2q-isgncIUTdgGK7ivHiySAU_94
deleted file mode 100644
index 11a25ad89..000000000
Binary files a/tests/external-legacy-data/s3/datajoint.migrate/store/djtest_blob_migrate/_Fhi2GUBB0fgxcSP2q-isgncIUTdgGK7ivHiySAU_94 and /dev/null differ
diff --git a/tests/external-legacy-data/v0_11.sql b/tests/external-legacy-data/v0_11.sql
deleted file mode 100644
index a666ec484..000000000
--- a/tests/external-legacy-data/v0_11.sql
+++ /dev/null
@@ -1,138 +0,0 @@
-USE djtest_blob_migrate;
--- MySQL dump 10.13  Distrib 5.7.26, for Linux (x86_64)
---
--- Host: localhost    Database: djtest_blob_migrate
--- ------------------------------------------------------
--- Server version	5.7.26
-
-/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
-/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
-/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
-/*!40101 SET NAMES utf8 */;
-/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
-/*!40103 SET TIME_ZONE='+00:00' */;
-/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
-/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
-/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
-/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
-
---
--- Table structure for table `~external`
---
-
-DROP TABLE IF EXISTS `~external`;
-/*!40101 SET @saved_cs_client     = @@character_set_client */;
-/*!40101 SET character_set_client = utf8 */;
-CREATE TABLE `~external` (
-  `hash` char(51) NOT NULL COMMENT 'the hash of stored object + store name',
-  `size` bigint(20) unsigned NOT NULL COMMENT 'size of object in bytes',
-  `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'automatic timestamp',
-  PRIMARY KEY (`hash`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='external storage tracking';
-/*!40101 SET character_set_client = @saved_cs_client */;
-
---
--- Dumping data for table `~external`
---
-
-LOCK TABLES `~external` WRITE;
-/*!40000 ALTER TABLE `~external` DISABLE KEYS */;
-INSERT INTO `~external` VALUES ('e46pnXQW9GaCKbL3WxV1crGHeGqcE0OLInM_TTwAFfwlocal',237,'2019-07-31 17:55:01'),('FoRROa2LWM6_wx0RIQ0J-LVvgm256cqDQfJa066HoTEshared',37,'2019-07-31 17:55:01'),('NmWj002gtKUkt9GIBwzn6Iw3x6h7ovlX_FfELbfjwRQshared',53,'2019-07-31 17:55:01'),('Ue9c89gKVZD7xPOcHd5Lz6mARJQ50xT1G5cTTX4h0L0shared',53,'2019-07-31 17:55:01'),('_3A03zPqfVhbn0rhlOJYGNivFJ4uqYuHaeQBA-V8PKA',237,'2019-07-31 17:55:01'),('_Fhi2GUBB0fgxcSP2q-isgncIUTdgGK7ivHiySAU_94',40,'2019-07-31 17:55:01'),('_Fhi2GUBB0fgxcSP2q-isgncIUTdgGK7ivHiySAU_94local',40,'2019-07-31 17:55:01');
-/*!40000 ALTER TABLE `~external` ENABLE KEYS */;
-UNLOCK TABLES;
-
---
--- Table structure for table `~log`
---
-
-DROP TABLE IF EXISTS `~log`;
-/*!40101 SET @saved_cs_client     = @@character_set_client */;
-/*!40101 SET character_set_client = utf8 */;
-CREATE TABLE `~log` (
-  `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-  `version` varchar(12) NOT NULL COMMENT 'datajoint version',
-  `user` varchar(255) NOT NULL COMMENT 'user@host',
-  `host` varchar(255) NOT NULL DEFAULT '' COMMENT 'system hostname',
-  `event` varchar(255) NOT NULL DEFAULT '' COMMENT 'custom message',
-  PRIMARY KEY (`timestamp`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='event logging table for `djtest_blob_migrate`';
-/*!40101 SET character_set_client = @saved_cs_client */;
-
---
--- Dumping data for table `~log`
---
-
-LOCK TABLES `~log` WRITE;
-/*!40000 ALTER TABLE `~log` DISABLE KEYS */;
-INSERT INTO `~log` VALUES ('2019-07-31 17:54:49','0.11.1py','root@172.168.1.4','297df05ab17c','Declared `djtest_blob_migrate`.`~log`'),('2019-07-31 17:54:54','0.11.1py','root@172.168.1.4','297df05ab17c','Declared `djtest_blob_migrate`.`~external`'),('2019-07-31 17:54:55','0.11.1py','root@172.168.1.4','297df05ab17c','Declared `djtest_blob_migrate`.`b`');
-/*!40000 ALTER TABLE `~log` ENABLE KEYS */;
-UNLOCK TABLES;
-
---
--- Table structure for table `a`
---
-
-DROP TABLE IF EXISTS `a`;
-/*!40101 SET @saved_cs_client     = @@character_set_client */;
-/*!40101 SET character_set_client = utf8 */;
-CREATE TABLE `a` (
-  `id` int(11) NOT NULL,
-  `blob_external` char(51) NOT NULL COMMENT ':external:uses S3',
-  `blob_share` char(51) NOT NULL COMMENT ':external-shared:uses S3',
-  PRIMARY KEY (`id`),
-  KEY `blob_external` (`blob_external`),
-  KEY `blob_share` (`blob_share`),
-  CONSTRAINT `a_ibfk_1` FOREIGN KEY (`blob_external`) REFERENCES `~external` (`hash`),
-  CONSTRAINT `a_ibfk_2` FOREIGN KEY (`blob_share`) REFERENCES `~external` (`hash`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-/*!40101 SET character_set_client = @saved_cs_client */;
-
---
--- Dumping data for table `a`
---
-
-LOCK TABLES `a` WRITE;
-/*!40000 ALTER TABLE `a` DISABLE KEYS */;
-INSERT INTO `a` VALUES (0,'_3A03zPqfVhbn0rhlOJYGNivFJ4uqYuHaeQBA-V8PKA','NmWj002gtKUkt9GIBwzn6Iw3x6h7ovlX_FfELbfjwRQshared'),(1,'_Fhi2GUBB0fgxcSP2q-isgncIUTdgGK7ivHiySAU_94','FoRROa2LWM6_wx0RIQ0J-LVvgm256cqDQfJa066HoTEshared');
-/*!40000 ALTER TABLE `a` ENABLE KEYS */;
-UNLOCK TABLES;
-
---
--- Table structure for table `b`
---
-
-DROP TABLE IF EXISTS `b`;
-/*!40101 SET @saved_cs_client     = @@character_set_client */;
-/*!40101 SET character_set_client = utf8 */;
-CREATE TABLE `b` (
-  `id` int(11) NOT NULL,
-  `blob_local` char(51) NOT NULL COMMENT ':external-local:uses files',
-  `blob_share` char(51) NOT NULL COMMENT ':external-shared:uses S3',
-  PRIMARY KEY (`id`),
-  KEY `blob_local` (`blob_local`),
-  KEY `blob_share` (`blob_share`),
-  CONSTRAINT `b_ibfk_1` FOREIGN KEY (`blob_local`) REFERENCES `~external` (`hash`),
-  CONSTRAINT `b_ibfk_2` FOREIGN KEY (`blob_share`) REFERENCES `~external` (`hash`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-/*!40101 SET character_set_client = @saved_cs_client */;
-
---
--- Dumping data for table `b`
---
-
-LOCK TABLES `b` WRITE;
-/*!40000 ALTER TABLE `b` DISABLE KEYS */;
-INSERT INTO `b` VALUES (0,'e46pnXQW9GaCKbL3WxV1crGHeGqcE0OLInM_TTwAFfwlocal','Ue9c89gKVZD7xPOcHd5Lz6mARJQ50xT1G5cTTX4h0L0shared'),(1,'_Fhi2GUBB0fgxcSP2q-isgncIUTdgGK7ivHiySAU_94local','FoRROa2LWM6_wx0RIQ0J-LVvgm256cqDQfJa066HoTEshared');
-/*!40000 ALTER TABLE `b` ENABLE KEYS */;
-UNLOCK TABLES;
-/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
-
-/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
-/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
-/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
-/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
-/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
-/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
-/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-
--- Dump completed on 2019-07-31 18:16:40
diff --git a/tests/schema_adapted.py b/tests/schema_adapted.py
index 8f966b876..ecb7b2ba8 100644
--- a/tests/schema_adapted.py
+++ b/tests/schema_adapted.py
@@ -23,7 +23,6 @@
 
 
 class GraphAdapter(dj.AttributeAdapter):
-
     attribute_type = "longblob"  # this is how the attribute will be declared
 
     @staticmethod
diff --git a/tests/schema_advanced.py b/tests/schema_advanced.py
index a3c2d0d64..7580611e2 100644
--- a/tests/schema_advanced.py
+++ b/tests/schema_advanced.py
@@ -45,7 +45,7 @@ class Parent(dj.Manual):
     -> Person
     parent_sex  : enum('M','F')
     ---
-    (parent) -> Person
+    -> Person.proj(parent='person_id')
     """
 
     def fill(self):
@@ -132,8 +132,8 @@ class InputCell(dj.Manual):
 @schema
 class LocalSynapse(dj.Manual):
     definition = """  # a synapse within the slice
-    (presynaptic) -> Cell(cell)
-    (postsynaptic)-> Cell
+    -> Cell.proj(presynaptic='cell')
+    -> Cell.proj(postsynaptic='cell')
     """
 
 
@@ -143,5 +143,5 @@ class GlobalSynapse(dj.Manual):
     definition = """
     # a synapse within the slice
     -> Cell.proj(pre_slice="slice", pre_cell="cell")
-    (post_slice, post_cell)-> Cell(slice, cell)
+    -> Cell.proj(post_slice="slice", post_cell="cell")
     """
diff --git a/tests/schema_privileges.py b/tests/schema_privileges.py
new file mode 100644
index 000000000..8b39e4aa1
--- /dev/null
+++ b/tests/schema_privileges.py
@@ -0,0 +1,35 @@
+import datajoint as dj
+
+schema = dj.Schema()
+
+
+@schema
+class Parent(dj.Lookup):
+    definition = """
+    id: int
+    """
+    contents = [(1,)]
+
+
+@schema
+class Child(dj.Computed):
+    definition = """
+    -> Parent
+    """
+
+    def make(self, key):
+        self.insert1(key)
+
+
+@schema
+class NoAccess(dj.Lookup):
+    definition = """
+    string: varchar(10)
+    """
+
+
+@schema
+class NoAccessAgain(dj.Manual):
+    definition = """
+    -> NoAccess
+    """
diff --git a/tests/schema_simple.py b/tests/schema_simple.py
index 4b5a751b4..78f64d036 100644
--- a/tests/schema_simple.py
+++ b/tests/schema_simple.py
@@ -7,8 +7,6 @@
 import hashlib
 import uuid
 import faker
-
-
 from . import PREFIX, CONN_INFO
 import numpy as np
 from datetime import date, timedelta
diff --git a/tests/test_alter.py b/tests/test_alter.py
index c234d2bb2..a785ffa28 100644
--- a/tests/test_alter.py
+++ b/tests/test_alter.py
@@ -4,7 +4,6 @@
 
 @schema
 class Experiment(dj.Imported):
-
     original_definition = """  # information about experiments
     -> Subject
     experiment_id  :smallint  # experiment number for this subject
diff --git a/tests/test_autopopulate.py b/tests/test_autopopulate.py
index 7683e4dd0..bc0c9bb18 100644
--- a/tests/test_autopopulate.py
+++ b/tests/test_autopopulate.py
@@ -53,6 +53,24 @@ def test_populate(self):
         assert_true(self.ephys)
         assert_true(self.channel)
 
+    def test_populate_exclude_error_and_ignore_jobs(self):
+        # test simple populate
+        assert_true(self.subject, "root tables are empty")
+        assert_false(self.experiment, "table already filled?")
+
+        keys = self.experiment.key_source.fetch("KEY", limit=2)
+        for idx, key in enumerate(keys):
+            if idx == 0:
+                schema.schema.jobs.ignore(self.experiment.table_name, key)
+            else:
+                schema.schema.jobs.error(self.experiment.table_name, key, "")
+
+        self.experiment.populate(reserve_jobs=True)
+        assert_equal(
+            len(self.experiment.key_source & self.experiment),
+            len(self.experiment.key_source) - 2,
+        )
+
     def test_allow_direct_insert(self):
         assert_true(self.subject, "root tables are empty")
         key = self.subject.fetch("KEY", limit=1)[0]
diff --git a/tests/test_blob.py b/tests/test_blob.py
index 91b2ce131..35904e4f8 100644
--- a/tests/test_blob.py
+++ b/tests/test_blob.py
@@ -19,7 +19,6 @@
 
 
 def test_pack():
-
     for x in (
         32,
         -3.7e-2,
@@ -166,7 +165,7 @@ def test_recarrays():
 
 
 def test_object_arrays():
-    x = np.array(((1, 2, 3), True))
+    x = np.array(((1, 2, 3), True), dtype="object")
     assert_array_equal(x, unpack(pack(x)), "Object array did not serialize correctly")
 
 
@@ -230,7 +229,7 @@ def test_insert_longblob():
 
 def test_datetime_serialization_speed():
     # If this fails that means for some reason deserializing/serializing
-    # np arrays of np.datetime64 types is now slower than regular arrays of datetime64
+    # np arrays of np.datetime64 types is now slower than regular arrays of datetime
 
     optimized_exe_time = timeit.timeit(
         setup="myarr=pack(np.array([np.datetime64('2022-10-13 03:03:13') for _ in range(0, 10000)]))",
@@ -247,4 +246,4 @@ def test_datetime_serialization_speed():
     )
     print(f"python time {baseline_exe_time}")
 
-    assert optimized_exe_time * 1000 < baseline_exe_time
+    assert optimized_exe_time * 900 < baseline_exe_time
diff --git a/tests/test_blob_migrate.py b/tests/test_blob_migrate.py
deleted file mode 100644
index fa806c557..000000000
--- a/tests/test_blob_migrate.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from nose.tools import assert_equal, raises
-
-import datajoint as dj
-import os
-from pathlib import Path
-from . import S3_CONN_INFO, S3_MIGRATE_BUCKET
-from . import CONN_INFO
-from datajoint.migrate import _migrate_dj011_blob
-
-
-class TestBlobMigrate:
-    @staticmethod
-    def test_convert():
-        # Configure stores
-        default_store = "external"  # naming the unnamed external store
-        dj.config["stores"] = {
-            default_store: dict(
-                protocol="s3",
-                endpoint=S3_CONN_INFO["endpoint"],
-                bucket=S3_MIGRATE_BUCKET,
-                location="store",
-                access_key=S3_CONN_INFO["access_key"],
-                secret_key=S3_CONN_INFO["secret_key"],
-            ),
-            "shared": dict(
-                protocol="s3",
-                endpoint=S3_CONN_INFO["endpoint"],
-                bucket=S3_MIGRATE_BUCKET,
-                location="maps",
-                access_key=S3_CONN_INFO["access_key"],
-                secret_key=S3_CONN_INFO["secret_key"],
-            ),
-            "local": dict(
-                protocol="file",
-                location=str(Path(os.path.expanduser("~"), "temp", S3_MIGRATE_BUCKET)),
-            ),
-        }
-        dj.config["cache"] = str(Path(os.path.expanduser("~"), "temp", "dj-cache"))
-
-        dj.config["database.password"] = CONN_INFO["password"]
-        dj.config["database.user"] = CONN_INFO["user"]
-        dj.config["database.host"] = CONN_INFO["host"]
-        schema = dj.Schema("djtest_blob_migrate")
-
-        # Test if migration throws unexpected exceptions
-        _migrate_dj011_blob(schema, default_store)
-
-        # Test Fetch
-        test_mod = dj.create_virtual_module("test_mod", "djtest_blob_migrate")
-        r1 = test_mod.A.fetch("blob_share", order_by="id")
-        assert_equal(r1[1][1], 2)
-
-        # Test Insert
-        test_mod.A.insert1(
-            {"id": 3, "blob_external": [9, 8, 7, 6], "blob_share": {"number": 5}}
-        )
-        r2 = (test_mod.A & "id=3").fetch1()
-        assert_equal(r2["blob_share"]["number"], 5)
-
-    @staticmethod
-    @raises(ValueError)
-    def test_type_check():
-        dj.migrate_dj011_external_blob_storage_to_dj012(10, "store")
diff --git a/tests/test_connection.py b/tests/test_connection.py
index 3d2861a95..8ac63fb15 100644
--- a/tests/test_connection.py
+++ b/tests/test_connection.py
@@ -67,11 +67,11 @@ class Subjects(dj.Manual):
 
     @classmethod
     def setup_class(cls):
-        cls.relation = cls.Subjects()
+        cls.table = cls.Subjects()
         cls.conn = dj.conn(**CONN_INFO)
 
     def teardown(self):
-        self.relation.delete_quick()
+        self.table.delete_quick()
 
     def test_active(self):
         with self.conn.transaction as conn:
@@ -81,25 +81,25 @@ def test_transaction_rollback(self):
         """Test transaction cancellation using a with statement"""
         tmp = np.array(
             [(1, "Peter", "mouse"), (2, "Klara", "monkey")],
-            self.relation.heading.as_dtype,
+            self.table.heading.as_dtype,
         )
 
-        self.relation.delete()
+        self.table.delete()
         with self.conn.transaction:
-            self.relation.insert1(tmp[0])
+            self.table.insert1(tmp[0])
         try:
             with self.conn.transaction:
-                self.relation.insert1(tmp[1])
+                self.table.insert1(tmp[1])
                 raise DataJointError("Testing rollback")
         except DataJointError:
             pass
         assert_equal(
-            len(self.relation),
+            len(self.table),
             1,
             "Length is not 1. Expected because rollback should have happened.",
         )
         assert_equal(
-            len(self.relation & "subject_id = 2"),
+            len(self.table & "subject_id = 2"),
             0,
             "Length is not 0. Expected because rollback should have happened.",
         )
@@ -108,20 +108,20 @@ def test_cancel(self):
         """Tests cancelling a transaction explicitly"""
         tmp = np.array(
             [(1, "Peter", "mouse"), (2, "Klara", "monkey")],
-            self.relation.heading.as_dtype,
+            self.table.heading.as_dtype,
         )
-        self.relation.delete_quick()
-        self.relation.insert1(tmp[0])
+        self.table.delete_quick()
+        self.table.insert1(tmp[0])
         self.conn.start_transaction()
-        self.relation.insert1(tmp[1])
+        self.table.insert1(tmp[1])
         self.conn.cancel_transaction()
         assert_equal(
-            len(self.relation),
+            len(self.table),
             1,
             "Length is not 1. Expected because rollback should have happened.",
         )
         assert_equal(
-            len(self.relation & "subject_id = 2"),
+            len(self.table & "subject_id = 2"),
             0,
             "Length is not 0. Expected because rollback should have happened.",
         )
diff --git a/tests/test_fetch.py b/tests/test_fetch.py
index 309cac77b..684cd4846 100644
--- a/tests/test_fetch.py
+++ b/tests/test_fetch.py
@@ -380,3 +380,11 @@ def test_dj_u_distinct(self):
         fetched_result = result.fetch(as_dict=True, order_by=("contrast", "brightness"))
         Stimulus.delete_quick()
         assert fetched_result == expected_result
+
+    def test_backslash(self):
+        # https://github.com/datajoint/datajoint-python/issues/999
+        expected = "She\Hulk"
+        Parent.insert([(2, expected)])
+        q = Parent & dict(name=expected)
+        assert q.fetch1("name") == expected
+        q.delete()
diff --git a/tests/test_fetch_same.py b/tests/test_fetch_same.py
index 1cd7e461b..d42d88b1a 100644
--- a/tests/test_fetch_same.py
+++ b/tests/test_fetch_same.py
@@ -6,59 +6,57 @@
 schema = dj.Schema(PREFIX + "_fetch_same", connection=dj.conn(**CONN_INFO))
 
 
-@schema
-class ProjData(dj.Manual):
-    definition = """
-    id : int
-    ---
-    resp : float
-    sim  : float
-    big : longblob
-    blah : varchar(10)
-    """
-
-
-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:
-    @staticmethod
-    def test_object_conversion_one():
-        new = ProjData.proj(sub="resp").fetch("sub")
+    @classmethod
+    def setup_class(cls):
+        @schema
+        class ProjData(dj.Manual):
+            definition = """
+            id : int
+            ---
+            resp : float
+            sim  : float
+            big : longblob
+            blah : varchar(10)
+            """
+
+        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",
+                },
+            ]
+        )
+
+        cls.projdata = ProjData()
+
+    def test_object_conversion_one(self):
+        new = self.projdata.proj(sub="resp").fetch("sub")
         assert_equal(new.dtype, np.float64)
 
-    @staticmethod
-    def test_object_conversion_two():
-        [sub, add] = ProjData.proj(sub="resp", add="sim").fetch("sub", "add")
+    def test_object_conversion_two(self):
+        [sub, add] = self.projdata.proj(sub="resp", add="sim").fetch("sub", "add")
         assert_equal(sub.dtype, np.float64)
         assert_equal(add.dtype, np.float64)
 
-    @staticmethod
-    def test_object_conversion_all():
-        new = ProjData.proj(sub="resp", add="sim").fetch()
+    def test_object_conversion_all(self):
+        new = self.projdata.proj(sub="resp", add="sim").fetch()
         assert_equal(new["sub"].dtype, np.float64)
         assert_equal(new["add"].dtype, np.float64)
 
-    @staticmethod
-    def test_object_no_convert():
-        new = ProjData.fetch()
+    def test_object_no_convert(self):
+        new = self.projdata.fetch()
         assert_equal(new["big"].dtype, "object")
         assert_equal(new["blah"].dtype, "object")
diff --git a/tests/test_jobs.py b/tests/test_jobs.py
index 371b21a2f..21fdef940 100644
--- a/tests/test_jobs.py
+++ b/tests/test_jobs.py
@@ -9,7 +9,6 @@
 
 
 def test_reserve_job():
-
     schema.schema.jobs.delete()
     assert_true(subjects)
     table_name = "fake_table"
diff --git a/tests/test_json.py b/tests/test_json.py
new file mode 100644
index 000000000..b9b13e4ee
--- /dev/null
+++ b/tests/test_json.py
@@ -0,0 +1,219 @@
+import inspect
+from datajoint.declare import declare
+import datajoint as dj
+import numpy as np
+from distutils.version import LooseVersion
+from . import PREFIX
+
+if LooseVersion(dj.conn().query("select @@version;").fetchone()[0]) >= LooseVersion(
+    "8.0.0"
+):
+    schema = dj.Schema(PREFIX + "_json")
+    Team = None
+
+    def setup():
+        global Team
+
+        @schema
+        class Team(dj.Lookup):
+            definition = """
+            name: varchar(40)
+            ---
+            car=null: json
+            unique index(car.name:char(20))
+            uniQue inDex ( name, car.name:char(20), (json_value(`car`, _utf8mb4'$.length' returning decimal(4, 1))) )
+            """
+            contents = [
+                (
+                    "engineering",
+                    {
+                        "name": "Rever",
+                        "length": 20.5,
+                        "inspected": True,
+                        "tire_pressure": [32, 31, 33, 34],
+                        "headlights": [
+                            {
+                                "side": "left",
+                                "hyper_white": None,
+                            },
+                            {
+                                "side": "right",
+                                "hyper_white": None,
+                            },
+                        ],
+                    },
+                ),
+                (
+                    "business",
+                    {
+                        "name": "Chaching",
+                        "length": 100,
+                        "safety_inspected": False,
+                        "tire_pressure": [34, 30, 27, 32],
+                        "headlights": [
+                            {
+                                "side": "left",
+                                "hyper_white": True,
+                            },
+                            {
+                                "side": "right",
+                                "hyper_white": True,
+                            },
+                        ],
+                    },
+                ),
+                (
+                    "marketing",
+                    None,
+                ),
+            ]
+
+    def teardown():
+        schema.drop()
+
+    def test_insert_update():
+        car = {
+            "name": "Discovery",
+            "length": 22.9,
+            "inspected": None,
+            "tire_pressure": [35, 36, 34, 37],
+            "headlights": [
+                {
+                    "side": "left",
+                    "hyper_white": True,
+                },
+                {
+                    "side": "right",
+                    "hyper_white": True,
+                },
+            ],
+        }
+
+        Team.insert1({"name": "research", "car": car})
+        q = Team & {"name": "research"}
+        assert q.fetch1("car") == car
+
+        car.update({"length": 23})
+        Team.update1({"name": "research", "car": car})
+        assert q.fetch1("car") == car
+
+        try:
+            Team.insert1({"name": "hr", "car": car})
+            raise Exception("Inserted non-unique car name.")
+        except dj.DataJointError:
+            pass
+
+        q.delete_quick()
+        assert not q
+
+    def test_describe():
+        rel = Team()
+        context = inspect.currentframe().f_globals
+        s1 = declare(rel.full_table_name, rel.definition, context)
+        s2 = declare(rel.full_table_name, rel.describe(), context)
+        assert s1 == s2
+
+    def test_restrict():
+        # dict
+        assert (Team & {"car.name": "Chaching"}).fetch1("name") == "business"
+
+        assert (Team & {"car.length": 20.5}).fetch1("name") == "engineering"
+
+        assert (Team & {"car.inspected": "true"}).fetch1("name") == "engineering"
+
+        assert (Team & {"car.inspected:unsigned": True}).fetch1("name") == "engineering"
+
+        assert (Team & {"car.safety_inspected": "false"}).fetch1("name") == "business"
+
+        assert (Team & {"car.safety_inspected:unsigned": False}).fetch1(
+            "name"
+        ) == "business"
+
+        assert (Team & {"car.headlights[0].hyper_white": None}).fetch(
+            "name", order_by="name", as_dict=True
+        ) == [
+            {"name": "engineering"},
+            {"name": "marketing"},
+        ]  # if entire record missing, JSON key is missing, or value set to JSON null
+
+        assert (Team & {"car": None}).fetch1("name") == "marketing"
+
+        assert (Team & {"car.tire_pressure": [34, 30, 27, 32]}).fetch1(
+            "name"
+        ) == "business"
+
+        assert (
+            Team & {"car.headlights[1]": {"side": "right", "hyper_white": True}}
+        ).fetch1("name") == "business"
+
+        # sql operators
+        assert (Team & "`car`->>'$.name' LIKE '%ching%'").fetch1(
+            "name"
+        ) == "business", "Missing substring"
+
+        assert (Team & "`car`->>'$.length' > 30").fetch1("name") == "business", "<= 30"
+
+        assert (
+            Team & "JSON_VALUE(`car`, '$.safety_inspected' RETURNING UNSIGNED) = 0"
+        ).fetch1("name") == "business", "Has `safety_inspected` set to `true`"
+
+        assert (Team & "`car`->>'$.headlights[0].hyper_white' = 'null'").fetch1(
+            "name"
+        ) == "engineering", "Has 1st `headlight` with `hyper_white` not set to `null`"
+
+        assert (Team & "`car`->>'$.inspected' IS NOT NULL").fetch1(
+            "name"
+        ) == "engineering", "Missing `inspected` key"
+
+        assert (Team & "`car`->>'$.tire_pressure' = '[34, 30, 27, 32]'").fetch1(
+            "name"
+        ) == "business", "`tire_pressure` array did not match"
+
+        assert (
+            Team
+            & """`car`->>'$.headlights[1]' = '{"side": "right", "hyper_white": true}'"""
+        ).fetch1("name") == "business", "2nd `headlight` object did not match"
+
+    def test_proj():
+        # proj necessary since we need to rename indexed value into a proper attribute name
+        assert Team.proj(car_length="car.length").fetch(
+            as_dict=True, order_by="car_length"
+        ) == [
+            {"name": "marketing", "car_length": None},
+            {"name": "business", "car_length": "100"},
+            {"name": "engineering", "car_length": "20.5"},
+        ]
+
+        assert Team.proj(car_length="car.length:decimal(4, 1)").fetch(
+            as_dict=True, order_by="car_length"
+        ) == [
+            {"name": "marketing", "car_length": None},
+            {"name": "engineering", "car_length": 20.5},
+            {"name": "business", "car_length": 100.0},
+        ]
+
+        assert Team.proj(
+            car_width="JSON_VALUE(`car`, '$.length' RETURNING float) - 15"
+        ).fetch(as_dict=True, order_by="car_width") == [
+            {"name": "marketing", "car_width": None},
+            {"name": "engineering", "car_width": 5.5},
+            {"name": "business", "car_width": 85.0},
+        ]
+
+        assert (
+            (Team & {"name": "engineering"}).proj(car_tire_pressure="car.tire_pressure")
+        ).fetch1("car_tire_pressure") == "[32, 31, 33, 34]"
+
+        assert np.array_equal(
+            Team.proj(car_inspected="car.inspected").fetch(
+                "car_inspected", order_by="name"
+            ),
+            np.array([None, "true", None]),
+        )
+
+        assert np.array_equal(
+            Team.proj(car_inspected="car.inspected:unsigned").fetch(
+                "car_inspected", order_by="name"
+            ),
+            np.array([None, 1, None]),
+        )
diff --git a/tests/test_privileges.py b/tests/test_privileges.py
index 9332a56b7..f32a1103f 100644
--- a/tests/test_privileges.py
+++ b/tests/test_privileges.py
@@ -1,6 +1,8 @@
-from nose.tools import assert_true, raises
+import importlib
 import datajoint as dj
-from . import schema, CONN_INFO
+from . import schema, CONN_INFO_ROOT, PREFIX
+from . import schema_privileges as pipeline
+from nose.tools import assert_true, raises
 
 namespace = locals()
 
@@ -10,7 +12,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
+            host=CONN_INFO_ROOT["host"], user="djview", password="djview", reset=True
         )
 
     @raises(dj.DataJointError)
@@ -46,3 +48,62 @@ class Try(dj.Manual):
             """
 
         Try().insert1((1, 1.5))
+
+
+class TestSubset:
+    USER = "djsubset"
+
+    @classmethod
+    def setup_class(cls):
+        conn = dj.conn(
+            host=CONN_INFO_ROOT["host"],
+            user=CONN_INFO_ROOT["user"],
+            password=CONN_INFO_ROOT["password"],
+            reset=True,
+        )
+        pipeline.schema.activate(f"{PREFIX}_pipeline")
+        conn.query(
+            f"""
+            CREATE USER IF NOT EXISTS '{cls.USER}'@'%%'
+            IDENTIFIED BY '{cls.USER}'
+            """
+        )
+        conn.query(
+            f"""
+            GRANT SELECT, INSERT, UPDATE, DELETE
+            ON `{PREFIX}_pipeline`.`#parent`
+            TO '{cls.USER}'@'%%'
+            """
+        )
+        conn.query(
+            f"""
+            GRANT SELECT, INSERT, UPDATE, DELETE
+            ON `{PREFIX}_pipeline`.`__child`
+            TO '{cls.USER}'@'%%'
+            """
+        )
+        cls.connection = dj.conn(
+            host=CONN_INFO_ROOT["host"],
+            user=cls.USER,
+            password=cls.USER,
+            reset=True,
+        )
+
+    @classmethod
+    def teardown_class(cls):
+        conn = dj.conn(
+            host=CONN_INFO_ROOT["host"],
+            user=CONN_INFO_ROOT["user"],
+            password=CONN_INFO_ROOT["password"],
+            reset=True,
+        )
+        conn.query(f"DROP USER {cls.USER}")
+        conn.query(f"DROP DATABASE {PREFIX}_pipeline")
+
+    def test_populate_activate(self):
+        importlib.reload(pipeline)
+        pipeline.schema.activate(
+            f"{PREFIX}_pipeline", create_schema=True, create_tables=False
+        )
+        pipeline.Child.populate()
+        assert pipeline.Child.progress(display=False)[0] == 0
diff --git a/tests/test_relation.py b/tests/test_relation.py
index 53dbf957b..a5f5da3af 100644
--- a/tests/test_relation.py
+++ b/tests/test_relation.py
@@ -175,7 +175,7 @@ def test_insert_select_ignore_extra_fields3(self):
         )
 
     def test_skip_duplicates(self):
-        """test that skip_dublicates works when inserting from another relation"""
+        """test that skip_duplicates works when inserting from another table"""
         self.test_no_extra.delete()
         self.test_no_extra.insert(
             self.test, ignore_extra_fields=True, skip_duplicates=True
@@ -272,18 +272,18 @@ def test_blob_insert(self):
         Y = self.img.fetch()[0]["img"]
         assert_true(np.all(X == Y), "Inserted and retrieved image are not identical")
 
-    @raises(dj.DataJointError)
     def test_drop(self):
         """Tests dropping tables"""
         dj.config["safemode"] = True
+        with patch.object(dj.utils, "input", create=True, return_value="yes"):
+            self.trash.drop()
         try:
-            with patch.object(dj.utils, "input", create=True, return_value="yes"):
-                self.trash.drop()
-        except:
+            self.trash.fetch()
+            raise Exception("Fetched after table dropped.")
+        except dj.DataJointError:
             pass
         finally:
             dj.config["safemode"] = False
-        self.trash.fetch()
 
     def test_table_regexp(self):
         """Test whether table names are matched by regular expressions"""
diff --git a/tests/test_relational_operand.py b/tests/test_relational_operand.py
index 48be3bdf6..0611ab267 100644
--- a/tests/test_relational_operand.py
+++ b/tests/test_relational_operand.py
@@ -504,14 +504,14 @@ def test_date():
         F.insert1((2, "2019-09-25"))
 
         new_value = None
-        (F & "id=2")._update("date", new_value)
+        F.update1(dict((F & "id=2").fetch1("KEY"), date=new_value))
         assert_equal((F & "id=2").fetch1("date"), new_value)
 
         new_value = datetime.date(2019, 10, 25)
-        (F & "id=2")._update("date", new_value)
+        F.update1(dict((F & "id=2").fetch1("KEY"), date=new_value))
         assert_equal((F & "id=2").fetch1("date"), new_value)
 
-        (F & "id=2")._update("date")
+        F.update1(dict((F & "id=2").fetch1("KEY"), date=None))
         assert_equal((F & "id=2").fetch1("date"), None)
 
     @staticmethod
@@ -532,19 +532,21 @@ def test_ellipsis():
     @raises(dj.DataJointError)
     def test_update_single_key():
         """Test that only one row can be updated"""
-        TTestUpdate()._update("string_attr", "my new string")
+        TTestUpdate.update1(
+            dict(TTestUpdate.fetch1("KEY"), string_attr="my new string")
+        )
 
     @staticmethod
     @raises(dj.DataJointError)
     def test_update_no_primary():
         """Test that no primary key can be updated"""
-        TTestUpdate()._update("primary_key", 2)
+        TTestUpdate.update1(dict(TTestUpdate.fetch1("KEY"), primary_key=2))
 
     @staticmethod
     @raises(dj.DataJointError)
     def test_update_missing_attribute():
         """Test that attribute is in table"""
-        TTestUpdate()._update("not_existing", 2)
+        TTestUpdate.update1(dict(TTestUpdate.fetch1("KEY"), not_existing=2))
 
     @staticmethod
     def test_update_string_attribute():
@@ -553,7 +555,7 @@ def test_update_string_attribute():
         s = "".join(
             random.choice(string.ascii_uppercase + string.digits) for _ in range(10)
         )
-        rel._update("string_attr", s)
+        TTestUpdate.update1(dict(rel.fetch1("KEY"), string_attr=s))
         assert_equal(s, rel.fetch1("string_attr"), "Updated string does not match")
 
     @staticmethod
@@ -561,9 +563,9 @@ def test_update_numeric_attribute():
         """Test replacing a string value"""
         rel = TTestUpdate() & dict(primary_key=0)
         s = random.randint(0, 10)
-        rel._update("num_attr", s)
+        TTestUpdate.update1(dict(rel.fetch1("KEY"), num_attr=s))
         assert_equal(s, rel.fetch1("num_attr"), "Updated integer does not match")
-        rel._update("num_attr", None)
+        TTestUpdate.update1(dict(rel.fetch1("KEY"), num_attr=None))
         assert_true(np.isnan(rel.fetch1("num_attr")), "Numeric value is not NaN")
 
     @staticmethod
@@ -571,7 +573,7 @@ def test_update_blob_attribute():
         """Test replacing a string value"""
         rel = TTestUpdate() & dict(primary_key=0)
         s = rel.fetch1("blob_attr")
-        rel._update("blob_attr", s.T)
+        TTestUpdate.update1(dict(rel.fetch1("KEY"), blob_attr=s.T))
         assert_equal(
             s.T.shape, rel.fetch1("blob_attr").shape, "Array dimensions do not match"
         )
diff --git a/tests/test_s3.py b/tests/test_s3.py
index 90449f64f..6f15628e8 100644
--- a/tests/test_s3.py
+++ b/tests/test_s3.py
@@ -12,7 +12,6 @@
 class TestS3:
     @staticmethod
     def test_connection():
-
         # Initialize httpClient with relevant timeout.
         http_client = urllib3.PoolManager(
             timeout=30,
@@ -36,7 +35,6 @@ def test_connection():
 
     @staticmethod
     def test_connection_secure():
-
         # Initialize httpClient with relevant timeout.
         http_client = urllib3.PoolManager(
             timeout=30,
diff --git a/tests/test_schema.py b/tests/test_schema.py
index ff4c0ebec..8ec24fc49 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -59,7 +59,7 @@ def test_namespace_population():
 @raises(dj.DataJointError)
 def test_undecorated_table():
     """
-    Undecorated user relation classes should raise an informative exception upon first use
+    Undecorated user table classes should raise an informative exception upon first use
     """
 
     class UndecoratedClass(dj.Manual):
diff --git a/tests/test_university.py b/tests/test_university.py
index f0575e41e..2d87e3f86 100644
--- a/tests/test_university.py
+++ b/tests/test_university.py
@@ -33,11 +33,9 @@ def test_activate():
         Enroll,
         Grade,
     ):
-        import csv
+        from pathlib import Path
 
-        with open("./data/" + table.__name__ + ".csv") as f:
-            reader = csv.DictReader(f)
-            table().insert(reader)
+        table().insert(Path("./data/" + table.__name__ + ".csv"))
 
 
 def test_fill():