Repository: lycantropos/hypothesis_sqlalchemy (language: python)

Issue:
uuid factory
I've noticed that with hypothesis v6.39.6 hypothesis_sqlalchemy will fail to produce examples of tables with UUID columns. It seems to fail because hypothesis adopted a f`unc(*, keyword_argument=default)` pattern for their function signatures and hypothesis_sqlalchemy attempts to call `strategies.uuids(version)`, when the function signature is `uuids(*, version: Optional[int] = None)`.

I understand that this is no the version of hypothesis listed in the requirements, however, this is the version required by Pydantic, so at the moment users are forced to choose between generating examples for SQLAlchemy or Pydantic, or abandoning UUID fields. It seems like a simple fix and would be backwards compatible.


Discussion / hints:

Fix patch:
diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml
deleted file mode 100644
index 98351e4..0000000
--- a/.azure-pipelines.yml
+++ /dev/null
@@ -1,94 +0,0 @@
-jobs:
-  - job: 'Windows'
-    pool:
-      vmImage: 'windows-latest'
-    strategy:
-      matrix:
-        Python35_x86:
-          python.version: '3.5'
-          python.architecture: 'x86'
-        Python35_x64:
-          python.version: '3.5'
-          python.architecture: 'x64'
-        Python36_x86:
-          python.version: '3.6'
-          python.architecture: 'x86'
-        Python36_x64:
-          python.version: '3.6'
-          python.architecture: 'x64'
-        Python37_x86:
-          python.version: '3.7'
-          python.architecture: 'x86'
-        Python37_x64:
-          python.version: '3.7'
-          python.architecture: 'x64'
-        Python38_x86:
-          python.version: '3.8'
-          python.architecture: 'x86'
-        Python38_x64:
-          python.version: '3.8'
-          python.architecture: 'x64'
-        PyPy3:
-          python.version: 'pypy3'
-          python.architecture: 'x86'
-
-    steps:
-      - task: UsePythonVersion@0
-        inputs:
-          versionSpec: '$(python.version)'
-          architecture: '$(python.architecture)'
-      - script: python -m pip install --upgrade pip setuptools
-        displayName: 'Install packaging tools'
-      - script: python -m pip install --upgrade codecov
-        displayName: 'Install codecov'
-      - script: python -m pip install --force-reinstall -r requirements.txt
-        displayName: 'Install requirements'
-      - script: python -m pip install --force-reinstall -r requirements-tests.txt
-        displayName: 'Install tests requirements'
-      - script: pytest
-        displayName: 'Run tests'
-      - script: python -m doctest README.md
-        displayName: 'Run doctests'
-      - script: python -m codecov -f coverage.xml -X gcov
-        condition: succeeded()
-        displayName: 'Upload coverage'
-        env:
-          CODECOV_TOKEN: $(CODECOV_TOKEN)
-
-  - job: 'MacOS'
-    pool:
-      vmImage: 'macOS-latest'
-    strategy:
-      matrix:
-        Python35:
-          python.version: '3.5'
-        Python36:
-          python.version: '3.6'
-        Python37:
-          python.version: '3.7'
-        Python38:
-          python.version: '3.8'
-        PyPy3:
-          python.version: 'pypy3'
-
-    steps:
-      - task: UsePythonVersion@0
-        inputs:
-          versionSpec: '$(python.version)'
-      - script: python -m pip install --upgrade pip setuptools
-        displayName: 'Install packaging tools'
-      - script: python -m pip install --upgrade codecov
-        displayName: 'Install codecov'
-      - script: python -m pip install --force-reinstall -r requirements.txt
-        displayName: 'Install requirements'
-      - script: python -m pip install --force-reinstall -r requirements-tests.txt
-        displayName: 'Install tests requirements'
-      - script: pytest
-        displayName: 'Run tests'
-      - script: python -m doctest README.md
-        displayName: 'Run doctests'
-      - script: python -m codecov -f coverage.xml -X gcov
-        condition: succeeded()
-        displayName: 'Upload coverage'
-        env:
-          CODECOV_TOKEN: $(CODECOV_TOKEN)
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 804d751..63b55ee 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,20 +1,19 @@
-[bumpversion]
-current_version = 0.3.2
-commit = True
-parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-(?P<release>.*))?
-serialize = 
-	{major}.{minor}.{patch}-{release}
-	{major}.{minor}.{patch}
-
-[bumpversion:part:release]
-optional_value = beta
-values = 
-	alpha
-	beta
-
-[bumpversion:file:docker-compose.cpython.yml]
-
-[bumpversion:file:docker-compose.pypy.yml]
-
-[bumpversion:file:hypothesis_sqlalchemy/__init__.py]
-
+[bumpversion]
+current_version = 1.0.0
+commit = True
+parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-(?P<release>.*))?
+serialize = 
+	{major}.{minor}.{patch}-{release}
+	{major}.{minor}.{patch}
+
+[bumpversion:part:release]
+optional_value = beta
+values = 
+	alpha
+	beta
+
+[bumpversion:file:docker-compose.cpython.yml]
+
+[bumpversion:file:docker-compose.pypy.yml]
+
+[bumpversion:file:hypothesis_sqlalchemy/__init__.py]
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..9f9fbd9
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,2 @@
+[run]
+source = hypothesis_sqlalchemy
diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index 7e693e5..0000000
--- a/.dockerignore
+++ /dev/null
@@ -1,33 +0,0 @@
-# logs
-*.log
-
-# Byte-compiled / optimized / DLL files
-.cache/
-**/__pycache__/
-*.py[cod]
-
-# C extensions
-*.so
-
-# Distribution / packaging
-bin/
-build/
-develop-eggs/
-dist/
-eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-*.egg-info/
-.installed.cfg
-*.eggs
-
-# tests
-.hypothesis/
-.coverage
-result.xml
-
-# IntelliJ IDEA
-.idea/
diff --git a/.env b/.env
index 4d04558..aed5940 100644
--- a/.env
+++ b/.env
@@ -1,4 +1,4 @@
 CPYTHON_IMAGE_NAME=python
-CPYTHON_IMAGE_VERSION=3.5
+CPYTHON_IMAGE_VERSION=3.6
 PYPY_IMAGE_NAME=pypy
-PYPY_IMAGE_VERSION=3-6
+PYPY_IMAGE_VERSION=3.6
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..2acd18e
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,105 @@
+name: CI
+
+on:
+  push:
+    branches:
+      - 'master'
+    tags: [ 'v*' ]
+  pull_request:
+    branches:
+      - 'master'
+
+jobs:
+  test:
+    name: 'Test'
+    strategy:
+      matrix:
+        python_version: ['3.6', '3.7', '3.8', '3.9', '3.10', 'pypy-3.7']
+        os: ['ubuntu', 'macos', 'windows']
+      fail-fast: true
+    runs-on: ${{ matrix.os }}-latest
+    steps:
+      - name: 'Checkout'
+        uses: actions/checkout@v3
+        with:
+          submodules: true
+      - name: 'Setup Python ${{ matrix.python_version }}'
+        uses: actions/setup-python@v3
+        with:
+          python-version: ${{ matrix.python_version }}
+      - name: 'Install packaging tools'
+        run: python -m pip install -U pip setuptools
+      - name: 'Install coverage dependencies'
+        run: python -m pip install -U coverage
+      - name: 'Install dependencies'
+        run: python -m pip install -r requirements.txt
+      - name: 'Install tests dependencies'
+        run: python -m pip install -r requirements-tests.txt
+      - name: 'Run doctests'
+        run: >
+          coverage run -m doctest README.md
+          && coverage run --append -m pytest --doctest-modules --ignore=tests
+      - name: 'Run tests'
+        run: coverage run --append -m pytest
+      - name: 'Collect coverage'
+        run: coverage xml
+      - name: 'Upload coverage'
+        with:
+          fail_ci_if_error: false
+        uses: codecov/codecov-action@v2.1.0
+        continue-on-error: true
+
+  pre-deploy:
+    name: 'Pre-deploy'
+    runs-on: ubuntu-latest
+    needs: test
+    if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
+    steps:
+      - name: '_'
+        run: echo "Predeploy step"
+
+  build:
+    name: 'Build'
+    runs-on: ubuntu-latest
+    needs: pre-deploy
+    steps:
+      - name: 'Checkout'
+        uses: actions/checkout@v3
+        with:
+          submodules: true
+      - name: 'Setup Python'
+        uses: actions/setup-python@v3
+      - name: 'Install packaging tools'
+        run: python -m pip install -U build pip setuptools wheel
+      - name: 'Install dependencies'
+        run: python -m pip install -r requirements.txt
+      - name: 'Build'
+        run: python -m build --sdist --wheel
+      - name: 'Upload'
+        uses: actions/upload-artifact@v2
+        with:
+          name: dist
+          path: dist
+
+  deploy:
+    name: 'Deploy'
+    environment: release
+    needs: build
+    runs-on: ubuntu-latest
+    steps:
+      - name: 'Checkout'
+        uses: actions/checkout@v3
+        with:
+          submodules: true
+      - name: 'Download distributions'
+        uses: actions/download-artifact@v2
+        with:
+          name: dist
+          path: dist
+      - name: 'Collected dists'
+        run: tree dist
+      - name: 'PyPI upload'
+        uses: pypa/gh-action-pypi-publish@v1.5.0
+        with:
+          packages_dir: dist
+          password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.gitignore b/.gitignore
index efb4ac6..c7b289b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@ __pycache__/
 # Distribution / packaging
 .Python
 build/
+_build/
 develop-eggs/
 dist/
 downloads/
@@ -43,3 +44,6 @@ venv*/
 
 ### JetBrains
 .idea/
+
+### MacOS
+.DS_Store
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index b5edd0b..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-dist: xenial
-language: python
-
-matrix:
-  include:
-    - python: pypy3.5-6.0.0
-    - python: pypy3.6-7.0.0
-    - python: 3.5
-    - python: 3.6
-    - python: 3.7
-    - python: 3.8
-
-install:
-  - python -m pip install --upgrade pip setuptools
-  - python -m pip install --upgrade codecov
-  - python -m pip install --force-reinstall -r requirements.txt
-  - python -m pip install --force-reinstall -r requirements-tests.txt
-
-script:
-  - pytest
-  - python -m doctest README.md
-
-after_success:
-  - python -m codecov -f coverage.xml -X gcov
-
-deploy:
-  provider: pypi
-  on:
-    branch: master
-    python: 3.5
-  user: __token__
-  password:
-    secure: NMHZ+2HBImxiEodaMi1V3oebZTZT1x7Njqennkt6FmauylCq9Zk/02vPivCPe7OZxu9KrlXTit7ZpmLWMRDrZYGWHTYxhPyMs62hyEFyzdKYr15GSJkHgWBAH3mWL9FdDhnSvmKmRtJNFa18REmpElqSEH0MUnDsMqr+PWuJlmgYC/LTs55Au7Cy9lfAY6fatyZbLQ+3cRY1oChH4eVPhcWsUIG4RSsPqiZfFuhKKLy8goHNYEd38TdMcbTHO/VK/NMnhZ8rMTvV5rNzU6+tXQgJDKcg5tr84SvwJJnGfNS8WIs1GoRbXMZGDu8YD1D5Xd2F4Upy0yTjz3qVuWEPegCk9mLAV6mPGv0cikz1QGrzF8IREzYkAS3lLfpdXR4EIYa21lfOoMkUHhFRcIU53GlAr8W7iHnD9YzRhsKkRfvuYtrCGRWvI5SM2XER01Hb3rVTbhb98cOzETeGemx/FeuNYoBrgXMua/0nyBJyb2lApay3RLSsWTgOaQEKA9P6O4vx3thK+NNYScKxLFnU30RrN7iX6VUClONIVugNtw7rfJzl8Tm/C5Sv95qRIChEJXGqaaUttt/GzW3sBdmCHXRvGvxlyVneM3uzUrROYCi1IsZv7K7WQKIVFwZjNnp4A8MPhmQ31Pykfg1hK/2mlu0lbkOzl0xqBGyijBH/mMM=
-  distributions: sdist bdist_wheel
-  skip_existing: true
diff --git a/Dockerfile b/Dockerfile
index f677673..a821fc6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,20 +1,20 @@
-ARG PYTHON_IMAGE
-ARG PYTHON_IMAGE_VERSION
-
-FROM ${PYTHON_IMAGE}:${PYTHON_IMAGE_VERSION}
-
-RUN pip install --upgrade pip setuptools
-
-WORKDIR /opt/hypothesis_sqlalchemy
-
-COPY requirements.txt .
-RUN pip install --force-reinstall -r requirements.txt
-
-COPY requirements-tests.txt .
-RUN pip install --force-reinstall -r requirements-tests.txt
-
-COPY README.md .
-COPY pytest.ini .
-COPY setup.py .
-COPY hypothesis_sqlalchemy hypothesis_sqlalchemy/
-COPY tests/ tests/
+ARG IMAGE_NAME
+ARG IMAGE_VERSION
+
+FROM ${IMAGE_NAME}:${IMAGE_VERSION}
+
+RUN pip install --upgrade pip setuptools
+
+WORKDIR /opt/hypothesis_sqlalchemy
+
+COPY requirements.txt .
+RUN pip install -r requirements.txt
+
+COPY requirements-tests.txt .
+RUN pip install -r requirements-tests.txt
+
+COPY README.md .
+COPY pytest.ini .
+COPY setup.py .
+COPY hypothesis_sqlalchemy hypothesis_sqlalchemy
+COPY tests tests
diff --git a/MANIFEST.in b/MANIFEST.in
index 5ee0522..096dd50 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,1 +1,2 @@
-include requirements.txt
+include LICENSE
+include requirements.txt
diff --git a/README.md b/README.md
index a4e9da9..84dc948 100644
--- a/README.md
+++ b/README.md
@@ -1,42 +1,28 @@
-`hypothesis` strategies for `SQLAlchemy`
-========================================
+hypothesis_sqlalchemy
+=====================
 
-[![](https://travis-ci.org/lycantropos/hypothesis_sqlalchemy.svg?branch=master)](https://travis-ci.org/lycantropos/hypothesis_sqlalchemy "Travis CI")
-[![](https://dev.azure.com/lycantropos/hypothesis_sqlalchemy/_apis/build/status/lycantropos.hypothesis_sqlalchemy?branchName=master)](https://dev.azure.com/lycantropos/hypothesis_sqlalchemy/_build/latest?definitionId=7&branchName=master "Azure Pipelines")
+[![](https://github.com/lycantropos/hypothesis_sqlalchemy/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/lycantropos/hypothesis_sqlalchemy/actions/workflows/ci.yml "Github Actions")
 [![](https://codecov.io/gh/lycantropos/hypothesis_sqlalchemy/branch/master/graph/badge.svg)](https://codecov.io/gh/lycantropos/hypothesis_sqlalchemy "Codecov")
 [![](https://img.shields.io/github/license/lycantropos/hypothesis_sqlalchemy.svg)](https://github.com/lycantropos/hypothesis_sqlalchemy/blob/master/LICENSE "License")
 [![](https://badge.fury.io/py/hypothesis-sqlalchemy.svg)](https://badge.fury.io/py/hypothesis-sqlalchemy "PyPI")
 
-In what follows
-- `python` is an alias for `python3.5` or any later
-version (`python3.6` and so on),
-- `pypy` is an alias for `pypy3.5` or any later
-version (`pypy3.6` and so on).
+In what follows `python` is an alias for `python3.6` or `pypy3.6`
+or any later version (`python3.7`, `pypy3.7` and so on).
 
 Installation
 ------------
 
-Install the latest `pip` & `setuptools` packages versions:
-- with `CPython`
-  ```bash
-  python -m pip install --upgrade pip setuptools
-  ```
-- with `PyPy`
-  ```bash
-  pypy -m pip install --upgrade pip setuptools
-  ```
+Install the latest `pip` & `setuptools` packages versions
+```bash
+python -m pip install --upgrade pip setuptools
+```
 
 ### User
 
-Download and install the latest stable version from `PyPI` repository:
-- with `CPython`
-  ```bash
-  python -m pip install --upgrade hypothesis_sqlalchemy
-  ```
-- with `PyPy`
-  ```bash
-  pypy -m pip install --upgrade hypothesis_sqlalchemy
-  ```
+Download and install the latest stable version from `PyPI` repository
+```bash
+python -m pip install --upgrade hypothesis_sqlalchemy
+```
 
 ### Developer
 
@@ -46,40 +32,39 @@ git clone https://github.com/lycantropos/hypothesis_sqlalchemy.git
 cd hypothesis_sqlalchemy
 ```
 
-Install dependencies:
-- with `CPython`
-  ```bash
-  python -m pip install --force-reinstall -r requirements.txt
-  ```
-- with `PyPy`
-  ```bash
-  pypy -m pip install --force-reinstall -r requirements.txt
-  ```
+Install dependencies
+```bash
+python -m pip install -r requirements.txt
+```
 
-Install:
-- with `CPython`
-  ```bash
-  python setup.py install
-  ```
-- with `PyPy`
-  ```bash
-  pypy setup.py install
-  ```
+Install
+```bash
+python setup.py install
+```
 
 Usage
 -----
 
-Let's take a look at what can be generated and how.
+With setup
+```python
+>>> import warnings
+>>> from hypothesis.errors import NonInteractiveExampleWarning
+>>> # ignore hypothesis warnings caused by `example` method call
+... warnings.filterwarnings('ignore', category=NonInteractiveExampleWarning)
+
+```
+let's take a look at what can be generated and how.
 
 ### Tables
 
 We can write a strategy that produces tables
 ```python
->>> from hypothesis_sqlalchemy import tabular
->>> from sqlalchemy.schema import MetaData
->>> tables = tabular.factory(metadata=MetaData(),
-...                          min_size=3,
-...                          max_size=10)
+>>> from hypothesis_sqlalchemy import scheme
+>>> from sqlalchemy.engine.default import DefaultDialect
+>>> dialect = DefaultDialect()
+>>> tables = scheme.tables(dialect,
+...                        min_size=3,
+...                        max_size=10)
 >>> table = tables.example()
 >>> from sqlalchemy.schema import Table
 >>> isinstance(table, Table)
@@ -116,9 +101,9 @@ and we can write strategy that
 * produces single records (as `tuple`s)
     ```python
     >>> from hypothesis import strategies
-    >>> from hypothesis_sqlalchemy import tabular
-    >>> records = tabular.records.factory(user_table, 
-    ...                                   email_address=strategies.emails())
+    >>> from hypothesis_sqlalchemy.sample import table_records
+    >>> records = table_records(user_table, 
+    ...                         email_address=strategies.emails())
     >>> record = records.example()
     >>> isinstance(record, tuple)
     True
@@ -132,11 +117,11 @@ and we can write strategy that
     ```
 * produces records `list`s (with configurable `list` size bounds)
     ```python
-    >>> from hypothesis_sqlalchemy import tabular
-    >>> records_lists = tabular.records.lists_factory(user_table,
-    ...                                               min_size=2,
-    ...                                               max_size=5, 
-    ...                                               email_address=strategies.emails())
+    >>> from hypothesis_sqlalchemy.sample import table_records_lists
+    >>> records_lists = table_records_lists(user_table,
+    ...                                     min_size=2,
+    ...                                     max_size=5, 
+    ...                                     email_address=strategies.emails())
     >>> records_list = records_lists.example()
     >>> isinstance(records_list, list)
     True
@@ -195,15 +180,10 @@ This will set version to `major.minor.patch`.
 
 ### Running tests
 
-Install dependencies:
-- with `CPython`
-  ```bash
-  python -m pip install --force-reinstall -r requirements-tests.txt
-  ```
-- with `PyPy`
-  ```bash
-  pypy -m pip install --force-reinstall -r requirements-tests.txt
-  ```
+Install dependencies
+```bash
+python -m pip install -r requirements-tests.txt
+```
 
 Plain
 ```bash
@@ -220,7 +200,7 @@ Inside `Docker` container:
   docker-compose --file docker-compose.pypy.yml up
   ```
 
-`Bash` script (e.g. can be used in `Git` hooks):
+`Bash` script:
 - with `CPython`
   ```bash
   ./run-tests.sh
@@ -235,7 +215,7 @@ Inside `Docker` container:
   ./run-tests.sh pypy
   ```
 
-`PowerShell` script (e.g. can be used in `Git` hooks):
+`PowerShell` script:
 - with `CPython`
   ```powershell
   .\run-tests.ps1
diff --git a/docker-compose.cpython.yml b/docker-compose.cpython.yml
index 569285c..3a4bfb2 100644
--- a/docker-compose.cpython.yml
+++ b/docker-compose.cpython.yml
@@ -1,19 +1,19 @@
-version: '3'
-
-services:
-  hypothesis_sqlalchemy-cpython:
-    build:
-      context: .
-      args:
-        - PYTHON_IMAGE=${CPYTHON_IMAGE_NAME}
-        - PYTHON_IMAGE_VERSION=${CPYTHON_IMAGE_VERSION}
-    image: lycantropos/hypothesis_sqlalchemy-cpython:0.3.2
-    volumes:
-      - ./hypothesis_sqlalchemy/:/opt/hypothesis_sqlalchemy/hypothesis_sqlalchemy/
-      - ./tests/:/opt/hypothesis_sqlalchemy/tests/
-      - ./README.md:/opt/hypothesis_sqlalchemy/README.md
-      - ./requirements.txt:/opt/hypothesis_sqlalchemy/requirements.txt
-      - ./requirements-tests.txt:/opt/hypothesis_sqlalchemy/requirements-tests.txt
-      - ./setup.py:/opt/hypothesis_sqlalchemy/setup.py
-      - ./pytest.ini:/opt/hypothesis_sqlalchemy/pytest.ini
-    entrypoint: pytest
+version: '3'
+
+services:
+  hypothesis_sqlalchemy-cpython:
+    build:
+      context: .
+      args:
+        - IMAGE_NAME=${CPYTHON_IMAGE_NAME}
+        - IMAGE_VERSION=${CPYTHON_IMAGE_VERSION}
+    image: lycantropos/hypothesis_sqlalchemy-cpython:1.0.0
+    volumes:
+      - ./hypothesis_sqlalchemy:/opt/hypothesis_sqlalchemy/hypothesis_sqlalchemy
+      - ./pytest.ini:/opt/hypothesis_sqlalchemy/pytest.ini
+      - ./README.md:/opt/hypothesis_sqlalchemy/README.md
+      - ./requirements-tests.txt:/opt/hypothesis_sqlalchemy/requirements-tests.txt
+      - ./requirements.txt:/opt/hypothesis_sqlalchemy/requirements.txt
+      - ./setup.py:/opt/hypothesis_sqlalchemy/setup.py
+      - ./tests:/opt/hypothesis_sqlalchemy/tests
+    entrypoint: pytest
diff --git a/docker-compose.pypy.yml b/docker-compose.pypy.yml
index 21ebd17..92493f9 100644
--- a/docker-compose.pypy.yml
+++ b/docker-compose.pypy.yml
@@ -1,19 +1,19 @@
-version: '3'
-
-services:
-  hypothesis_sqlalchemy-pypy:
-    build:
-      context: .
-      args:
-        - PYTHON_IMAGE=${PYPY_IMAGE_NAME}
-        - PYTHON_IMAGE_VERSION=${PYPY_IMAGE_VERSION}
-    image: lycantropos/hypothesis_sqlalchemy-pypy:0.3.2
-    volumes:
-      - ./hypothesis_sqlalchemy/:/opt/hypothesis_sqlalchemy/hypothesis_sqlalchemy/
-      - ./tests/:/opt/hypothesis_sqlalchemy/tests/
-      - ./README.md:/opt/hypothesis_sqlalchemy/README.md
-      - ./requirements.txt:/opt/hypothesis_sqlalchemy/requirements.txt
-      - ./requirements-tests.txt:/opt/hypothesis_sqlalchemy/requirements-tests.txt
-      - ./setup.py:/opt/hypothesis_sqlalchemy/setup.py
-      - ./pytest.ini:/opt/hypothesis_sqlalchemy/pytest.ini
-    entrypoint: pytest
+version: '3'
+
+services:
+  hypothesis_sqlalchemy-pypy:
+    build:
+      context: .
+      args:
+        - IMAGE_NAME=${PYPY_IMAGE_NAME}
+        - IMAGE_VERSION=${PYPY_IMAGE_VERSION}
+    image: lycantropos/hypothesis_sqlalchemy-pypy:1.0.0
+    volumes:
+      - ./hypothesis_sqlalchemy:/opt/hypothesis_sqlalchemy/hypothesis_sqlalchemy
+      - ./pytest.ini:/opt/hypothesis_sqlalchemy/pytest.ini
+      - ./README.md:/opt/hypothesis_sqlalchemy/README.md
+      - ./requirements-tests.txt:/opt/hypothesis_sqlalchemy/requirements-tests.txt
+      - ./requirements.txt:/opt/hypothesis_sqlalchemy/requirements.txt
+      - ./setup.py:/opt/hypothesis_sqlalchemy/setup.py
+      - ./tests:/opt/hypothesis_sqlalchemy/tests
+    entrypoint: pytest
diff --git a/hypothesis_sqlalchemy/__init__.py b/hypothesis_sqlalchemy/__init__.py
index 36c8bf7..63fc67e 100644
--- a/hypothesis_sqlalchemy/__init__.py
+++ b/hypothesis_sqlalchemy/__init__.py
@@ -1,3 +1,3 @@
-"""``hypothesis`` strategies for generating ``SQLAlchemy`` objects."""
+"""``hypothesis`` strategies for ``SQLAlchemy`` objects and data from them."""
 
-__version__ = '0.3.2'
+__version__ = '1.0.0'
diff --git a/hypothesis_sqlalchemy/columnar/__init__.py b/hypothesis_sqlalchemy/columnar/__init__.py
deleted file mode 100644
index 4ad9ae5..0000000
--- a/hypothesis_sqlalchemy/columnar/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from . import records
-from .base import (factory,
-                   lists_factory)
diff --git a/hypothesis_sqlalchemy/columnar/base.py b/hypothesis_sqlalchemy/columnar/base.py
deleted file mode 100644
index 82643f1..0000000
--- a/hypothesis_sqlalchemy/columnar/base.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from operator import attrgetter
-from typing import (List,
-                    Optional)
-
-from hypothesis import strategies
-from sqlalchemy.engine import Dialect
-from sqlalchemy.schema import Column
-
-from hypothesis_sqlalchemy.hints import Strategy
-from hypothesis_sqlalchemy.utils import to_sql_identifiers
-from . import types as _types
-
-
-def factory(dialect: Dialect,
-            *,
-            names: Optional[Strategy[str]] = None,
-            types: Optional[Strategy[_types.TypeOrInstance]] = None,
-            are_unique: Strategy[Optional[bool]] = strategies.none(),
-            are_primary_keys: Strategy[Optional[bool]] = strategies.none(),
-            are_auto_incremented: Strategy[Optional[bool]] = strategies.none(),
-            are_nullable: Strategy[Optional[bool]] = strategies.booleans(),
-            are_indexed: Strategy[Optional[bool]] = strategies.booleans()
-            ) -> Strategy[Column]:
-    names = to_sql_identifiers(dialect) if names is None else names
-    types = _types.factory(dialect) if types is None else types
-    return strategies.builds(Column,
-                             name=names,
-                             type_=types,
-                             autoincrement=are_auto_incremented,
-                             unique=are_unique,
-                             nullable=are_nullable,
-                             primary_key=are_primary_keys,
-                             index=are_indexed)
-
-
-def lists_factory(columns: Strategy[Column],
-                  *,
-                  min_size: int = 0,
-                  max_size: Optional[int] = None) -> Strategy[List[Column]]:
-    return strategies.lists(columns,
-                            min_size=min_size,
-                            max_size=max_size,
-                            unique_by=attrgetter('name'))
diff --git a/hypothesis_sqlalchemy/columnar/records.py b/hypothesis_sqlalchemy/columnar/records.py
deleted file mode 100644
index eb621a3..0000000
--- a/hypothesis_sqlalchemy/columnar/records.py
+++ /dev/null
@@ -1,67 +0,0 @@
-from itertools import starmap
-from operator import itemgetter
-from typing import (AbstractSet,
-                    Any,
-                    List,
-                    Optional)
-
-from hypothesis import strategies
-from sqlalchemy.schema import (Column,
-                               Constraint)
-
-from hypothesis_sqlalchemy import constrained
-from hypothesis_sqlalchemy.hints import (RecordType,
-                                         Strategy)
-from . import values
-
-
-def factory(columns: List[Column],
-            **fixed_columns_values: Strategy) -> Strategy[RecordType]:
-    def to_plain_values_strategy(column: Column) -> Strategy[Any]:
-        result = values.factory(column)
-        if column.nullable:
-            # putting simpler strategies first
-            # more info at
-            # https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.strategies.one_of
-            result = strategies.none() | result
-        return result
-
-    if fixed_columns_values:
-        def to_values_strategy(column: Column) -> Strategy[Any]:
-            column_name = column.name
-            if column_name in fixed_columns_values:
-                return fixed_columns_values[column_name]
-            else:
-                return to_plain_values_strategy(column)
-    else:
-        to_values_strategy = to_plain_values_strategy
-    return strategies.tuples(*map(to_values_strategy, columns))
-
-
-def lists_factory(columns: List[Column],
-                  constraints: AbstractSet[Constraint],
-                  *,
-                  min_size: int = 0,
-                  max_size: Optional[int] = None,
-                  **fixed_columns_values: Strategy
-                  ) -> Strategy[List[RecordType]]:
-    values_tuples = factory(columns,
-                            **fixed_columns_values)
-    columns_indices = {column: index for index, column in enumerate(columns)}
-    unique_indices = [[columns_indices[column]
-                       for column in constraint.columns]
-                      for constraint in constraints
-                      if isinstance(constraint, constrained.UNIQUE_TYPES)
-                      and constraint.columns]
-
-    if unique_indices:
-        # Create a tuple of functions, each function asserting the uniqueness
-        # of a single column value
-        unique_by = tuple(starmap(itemgetter, unique_indices))
-    else:
-        unique_by = None
-
-    return strategies.lists(values_tuples,
-                            min_size=min_size,
-                            max_size=max_size,
-                            unique_by=unique_by)
diff --git a/hypothesis_sqlalchemy/columnar/types.py b/hypothesis_sqlalchemy/columnar/types.py
deleted file mode 100644
index 38dd2a4..0000000
--- a/hypothesis_sqlalchemy/columnar/types.py
+++ /dev/null
@@ -1,118 +0,0 @@
-from functools import partial
-from typing import (Iterable,
-                    Optional,
-                    Type,
-                    Union)
-
-from hypothesis import strategies
-from sqlalchemy import exc
-from sqlalchemy.dialects import postgresql
-from sqlalchemy.engine import Dialect
-from sqlalchemy.sql.sqltypes import (BigInteger,
-                                     Boolean,
-                                     Date,
-                                     DateTime,
-                                     Enum,
-                                     Float,
-                                     Integer,
-                                     Interval,
-                                     LargeBinary,
-                                     Numeric,
-                                     SmallInteger,
-                                     String,
-                                     Time)
-from sqlalchemy.sql.type_api import (TypeEngine,
-                                     to_instance)
-
-from hypothesis_sqlalchemy import enumerable
-from hypothesis_sqlalchemy.hints import Strategy
-from hypothesis_sqlalchemy.utils import to_sql_identifiers
-
-TypeOrInstance = Union[TypeEngine, Type[TypeEngine]]
-
-
-def strings_factory(dialect: Dialect,
-                    *,
-                    lengths: Strategy[int] = strategies.none()
-                    ) -> Strategy[TypeEngine]:
-    return strategies.builds(String,
-                             length=lengths)
-
-
-def binary_strings_factory(dialect: Dialect,
-                           *,
-                           lengths: Strategy[int] = strategies.none()
-                           ) -> Strategy[TypeEngine]:
-    return strategies.builds(LargeBinary,
-                             length=lengths)
-
-
-def filter_unsupported_types(types_or_instances: Iterable[TypeOrInstance],
-                             *,
-                             dialect: Dialect) -> Iterable[TypeOrInstance]:
-    return filter(partial(is_type_supported,
-                          dialect=dialect),
-                  types_or_instances)
-
-
-def is_type_supported(type_or_instance: Type[TypeOrInstance],
-                      *,
-                      dialect: Dialect) -> bool:
-    instance = to_instance(type_or_instance)
-    try:
-        instance.compile(dialect)
-    except exc.UnsupportedCompilationError:
-        return False
-    else:
-        return True
-
-
-def primary_keys_factory(dialect: Dialect) -> Strategy[TypeEngine]:
-    types = list(filter_unsupported_types([SmallInteger, Integer, BigInteger,
-                                           postgresql.UUID],
-                                          dialect=dialect))
-    return strategies.sampled_from(types).map(to_instance)
-
-
-def enums_factory(dialect: Dialect,
-                  *,
-                  values: Optional[Strategy[str]] = None,
-                  min_size: int = 1,
-                  max_size: Optional[int] = None) -> Strategy[TypeEngine]:
-    if values is None:
-        values = to_sql_identifiers(dialect)
-    enums_keys = values.filter(enumerable.is_valid_key)
-    return ((strategies.tuples(enumerable.factory(keys=enums_keys,
-                                                  min_size=min_size,
-                                                  max_size=max_size))
-             | strategies.lists(values,
-                                min_size=min_size,
-                                max_size=max_size))
-            .map(lambda type_values: Enum(*type_values)))
-
-
-EXTRA_COLUMNS_TYPES = [Numeric, Float, Boolean, Date, DateTime, Interval, Time]
-
-
-def factory(dialect: Dialect,
-            *,
-            primary_keys_types: Optional[Strategy[TypeEngine]] = None,
-            string_types: Optional[Strategy[TypeEngine]] = None,
-            binary_string_types: Optional[Strategy[TypeEngine]] = None,
-            enum_types: Optional[Strategy[TypeEngine]] = None
-            ) -> Strategy[TypeEngine]:
-    if primary_keys_types is None:
-        primary_keys_types = primary_keys_factory(dialect)
-    if string_types is None:
-        string_types = strings_factory(dialect)
-    if binary_string_types is None:
-        binary_string_types = binary_strings_factory(dialect)
-    if enum_types is None:
-        enum_types = enums_factory(dialect)
-    extra_types = list(filter_unsupported_types(EXTRA_COLUMNS_TYPES,
-                                                dialect=dialect))
-    return strategies.one_of(primary_keys_types,
-                             strategies.sampled_from(extra_types),
-                             string_types,
-                             binary_string_types,
-                             enum_types)
diff --git a/hypothesis_sqlalchemy/columnar/values.py b/hypothesis_sqlalchemy/columnar/values.py
deleted file mode 100644
index 3812dfb..0000000
--- a/hypothesis_sqlalchemy/columnar/values.py
+++ /dev/null
@@ -1,114 +0,0 @@
-from datetime import (date,
-                      datetime,
-                      time,
-                      timedelta)
-from decimal import Decimal
-from enum import Enum
-from functools import (partial,
-                       singledispatch)
-from typing import (Any,
-                    Optional,
-                    Union)
-from uuid import UUID
-
-from hypothesis import strategies
-from sqlalchemy import (Column,
-                        Enum as EnumType,
-                        LargeBinary,
-                        String)
-from sqlalchemy.dialects import postgresql
-from sqlalchemy.sql.type_api import TypeEngine
-
-from hypothesis_sqlalchemy.hints import Strategy
-
-# we're using integers as primary key values
-# which are usually positive
-MIN_POSITIVE_INTEGER_VALUE = 1
-MAX_SMALLINT_VALUE = 32767
-MIN_DATE_TIME = datetime.utcfromtimestamp(0)
-
-
-def factory(column: Column) -> Strategy:
-    return from_type(column.type)
-
-
-to_booleans = strategies.booleans
-to_integers = strategies.integers
-to_floats = strategies.floats
-to_decimals = strategies.decimals
-
-
-def to_date_times(*,
-                  min_value: datetime = datetime.min,
-                  max_value: datetime = datetime.max,
-                  timezones: Strategy = strategies.none()) -> Strategy:
-    date_times = strategies.datetimes(min_value=min_value,
-                                      max_value=max_value,
-                                      timezones=timezones)
-    return date_times.map(partial(datetime.replace,
-                                  microsecond=0))
-
-
-to_dates = strategies.dates
-
-values_by_python_types = {
-    bool: to_booleans(),
-    int: to_integers(min_value=MIN_POSITIVE_INTEGER_VALUE,
-                     max_value=MAX_SMALLINT_VALUE),
-    float: to_floats(allow_nan=False,
-                     allow_infinity=False),
-    Decimal: to_decimals(allow_nan=False,
-                         allow_infinity=False),
-    datetime: to_date_times(min_value=MIN_DATE_TIME),
-    date: to_dates(min_value=MIN_DATE_TIME.date()),
-    time: strategies.times(),
-    timedelta: strategies.timedeltas(),
-}
-
-
-@singledispatch
-def from_type(type_: TypeEngine) -> Strategy[Any]:
-    return values_by_python_types[type_.python_type]
-
-
-ascii_not_null_characters = strategies.characters(min_codepoint=1,
-                                                  max_codepoint=127)
-
-
-@from_type.register(String)
-def string_type_values_factory(type_: String,
-                               *,
-                               alphabet: Strategy = ascii_not_null_characters
-                               ) -> Strategy[str]:
-    return strategies.text(alphabet=alphabet,
-                           max_size=type_.length)
-
-
-@from_type.register(LargeBinary)
-def binary_string_type_values_factory(type_: LargeBinary) -> Strategy[bytes]:
-    return strategies.binary(max_size=type_.length)
-
-
-@from_type.register(EnumType)
-def enum_type_values_factory(type_: EnumType) -> Strategy[Union[str, Enum]]:
-    enum_class = type_.enum_class
-    if enum_class is None:
-        # The source of enumerated values may be a list of string values
-        values = type_.enums
-    else:
-        # ... or a PEP-435-compliant enumerated class.
-        # More info at
-        # http://docs.sqlalchemy.org/en/latest/core/type_basics.html#sqlalchemy.types.Enum
-        values = list(enum_class)
-    return strategies.sampled_from(values)
-
-
-@from_type.register(postgresql.UUID)
-def uuid_type_values_factory(type_: postgresql.UUID,
-                             *,
-                             version: Optional[int] = None
-                             ) -> Strategy[Union[str, UUID]]:
-    result = strategies.uuids(version)
-    if not type_.as_uuid:
-        result = result.map(str)
-    return result
diff --git a/hypothesis_sqlalchemy/constrained/__init__.py b/hypothesis_sqlalchemy/constrained/__init__.py
deleted file mode 100644
index 20a3d0b..0000000
--- a/hypothesis_sqlalchemy/constrained/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from .base import (UNIQUE_TYPES,
-                   lists_factory,
-                   primary_keys_factory,
-                   unique_factory)
diff --git a/hypothesis_sqlalchemy/constrained/base.py b/hypothesis_sqlalchemy/constrained/base.py
deleted file mode 100644
index 9897c8a..0000000
--- a/hypothesis_sqlalchemy/constrained/base.py
+++ /dev/null
@@ -1,70 +0,0 @@
-from operator import add
-from typing import (List,
-                    Optional)
-
-from hypothesis import strategies
-from sqlalchemy import (Constraint,
-                        PrimaryKeyConstraint,
-                        UniqueConstraint)
-from sqlalchemy.schema import Column
-
-from hypothesis_sqlalchemy.hints import Strategy
-
-UNIQUE_TYPES = (PrimaryKeyConstraint, UniqueConstraint)
-
-
-def lists_factory(columns: List[Column],
-                  *,
-                  min_size: int = 0,
-                  max_size: Optional[int] = None,
-                  primary_key_min_size: int = 0,
-                  primary_key_max_size: Optional[int] = None,
-                  unique_min_size: int = 0,
-                  unique_max_size: Optional[int] = None
-                  ) -> Strategy[List[Constraint]]:
-    if not columns:
-        return strategies.builds(list)
-    primary_keys_lists = (primary_keys_factory(columns,
-                                               min_size=primary_key_min_size,
-                                               max_size=primary_key_max_size)
-                          .map(lambda key: [key]))
-    unique_lists = strategies.lists(unique_factory(columns,
-                                                   min_size=unique_min_size,
-                                                   max_size=unique_max_size),
-                                    min_size=max(min_size - 1, 0),
-                                    max_size=max_size - 1
-                                    if max_size is not None
-                                    else max_size)
-    return (strategies.tuples(primary_keys_lists, unique_lists)
-            .map(lambda lists_pair: add(*lists_pair)))
-
-
-def unique_factory(columns: List[Column],
-                   *,
-                   min_size: int = 0,
-                   max_size: Optional[int] = None
-                   ) -> Strategy[UniqueConstraint]:
-    if max_size is None:
-        max_size = len(columns)
-    return (strategies.sets(strategies.sampled_from(columns) if columns
-                            else strategies.nothing(),
-                            min_size=min_size,
-                            max_size=max_size)
-            .map(lambda constraint_columns:
-                 UniqueConstraint(*constraint_columns)))
-
-
-def primary_keys_factory(columns: List[Column],
-                         *,
-                         min_size: int = 0,
-                         max_size: Optional[int] = None
-                         ) -> Strategy[PrimaryKeyConstraint]:
-    existing_columns = [column for column in columns if column.primary_key]
-    if existing_columns:
-        return strategies.just(PrimaryKeyConstraint(*existing_columns))
-    return (strategies.sets(strategies.sampled_from(columns) if columns
-                            else strategies.nothing(),
-                            min_size=min_size,
-                            max_size=max_size)
-            .map(lambda constraint_columns:
-                 PrimaryKeyConstraint(*constraint_columns)))
diff --git a/tests/columnar_tests/__init__.py b/hypothesis_sqlalchemy/core/__init__.py
similarity index 100%
rename from tests/columnar_tests/__init__.py
rename to hypothesis_sqlalchemy/core/__init__.py
diff --git a/hypothesis_sqlalchemy/core/column.py b/hypothesis_sqlalchemy/core/column.py
new file mode 100644
index 0000000..059c99d
--- /dev/null
+++ b/hypothesis_sqlalchemy/core/column.py
@@ -0,0 +1,38 @@
+from typing import Optional
+
+from hypothesis import strategies
+from sqlalchemy.engine import Dialect
+from sqlalchemy.schema import Column
+
+from . import column_type
+from .hints import (Scalar,
+                    Strategy)
+from .utils import to_sql_identifiers
+
+
+def instances(dialect: Dialect,
+              *,
+              names: Optional[Strategy[str]] = None,
+              types: Optional[Strategy[column_type.TypeOrInstance]] = None,
+              are_unique: Strategy[Optional[bool]] = strategies.none(),
+              are_primary_keys: Strategy[Optional[bool]] = strategies.none(),
+              are_auto_incremented: Strategy[Optional[bool]]
+              = strategies.none(),
+              are_nullable: Strategy[Optional[bool]] = strategies.booleans(),
+              are_indexed: Strategy[Optional[bool]] = strategies.booleans()
+              ) -> Strategy[Column]:
+    names = to_sql_identifiers(dialect) if names is None else names
+    types = column_type.instances(dialect) if types is None else types
+    return strategies.builds(Column,
+                             name=names,
+                             type_=types,
+                             autoincrement=are_auto_incremented,
+                             unique=are_unique,
+                             nullable=are_nullable,
+                             primary_key=are_primary_keys,
+                             index=are_indexed)
+
+
+def scalars(column: Column) -> Strategy[Scalar]:
+    result = column_type.scalars(column.type)
+    return strategies.none() | result if column.nullable else result
diff --git a/hypothesis_sqlalchemy/core/column_type.py b/hypothesis_sqlalchemy/core/column_type.py
new file mode 100644
index 0000000..64d82f8
--- /dev/null
+++ b/hypothesis_sqlalchemy/core/column_type.py
@@ -0,0 +1,204 @@
+from datetime import (date,
+                      datetime,
+                      time,
+                      timedelta)
+from decimal import Decimal
+from enum import Enum
+from functools import (partial,
+                       singledispatch)
+from typing import (Iterable,
+                    Optional,
+                    Type,
+                    Union)
+from uuid import UUID
+
+from hypothesis import strategies
+from sqlalchemy.dialects import postgresql
+from sqlalchemy.engine import Dialect
+from sqlalchemy.sql.sqltypes import (BigInteger,
+                                     Boolean,
+                                     Date,
+                                     DateTime,
+                                     Enum as EnumType,
+                                     Float,
+                                     Integer,
+                                     Interval,
+                                     LargeBinary,
+                                     Numeric,
+                                     SmallInteger,
+                                     String,
+                                     Text,
+                                     Time,
+                                     exc)
+from sqlalchemy.sql.type_api import (TypeEngine,
+                                     to_instance)
+
+from . import enum
+from .hints import (Scalar,
+                    Strategy)
+from .utils import to_sql_identifiers
+
+TypeOrInstance = Union[TypeEngine, Type[TypeEngine]]
+EXTRA = [Numeric, Float, Boolean, Date, DateTime, Interval, Time]
+
+
+def instances(dialect: Dialect,
+              *,
+              primary_key_types: Optional[Strategy[TypeEngine]] = None,
+              string_types: Optional[Strategy[TypeEngine]] = None,
+              binary_string_types: Optional[Strategy[TypeEngine]] = None,
+              enum_types: Optional[Strategy[TypeEngine]] = None
+              ) -> Strategy[TypeEngine]:
+    if primary_key_types is None:
+        primary_key_types = primary_keys(dialect)
+    if string_types is None:
+        string_types = (strings(dialect)
+                        if _is_type_supported(Text,
+                                              dialect=dialect)
+                        else strategies.nothing())
+    if binary_string_types is None:
+        binary_string_types = (binary_strings(dialect)
+                               if _is_type_supported(LargeBinary,
+                                                     dialect=dialect)
+                               else strategies.nothing())
+    if enum_types is None:
+        enum_types = (enums(dialect)
+                      if _is_type_supported(EnumType(name='_'),
+                                            dialect=dialect)
+                      else strategies.nothing())
+    extra_types = list(_filter_unsupported_types(EXTRA,
+                                                 dialect=dialect))
+    return strategies.one_of(primary_key_types,
+                             (strategies.sampled_from(extra_types)
+                              .map(to_instance)),
+                             string_types,
+                             binary_string_types,
+                             enum_types)
+
+
+def binary_strings(dialect: Dialect,
+                   *,
+                   lengths: Strategy[int] = strategies.none()
+                   ) -> Strategy[TypeEngine]:
+    return strategies.builds(LargeBinary,
+                             length=lengths)
+
+
+def enums(dialect: Dialect,
+          *,
+          names: Optional[Strategy[str]] = None,
+          values: Optional[Strategy[str]] = None,
+          min_size: int = 1,
+          max_size: Optional[int] = None) -> Strategy[TypeEngine]:
+    if names is None:
+        names = to_sql_identifiers(dialect)
+    if values is None:
+        values = to_sql_identifiers(dialect)
+    enums_keys = values.filter(enum.is_valid_key)
+    args = (strategies.tuples(enum.types(keys=enums_keys,
+                                         min_size=min_size,
+                                         max_size=max_size))
+            | strategies.lists(values,
+                               min_size=min_size,
+                               max_size=max_size))
+    return (strategies.tuples(names, args)
+            .map(lambda name_with_args: EnumType(*name_with_args[1],
+                                                 name=name_with_args[0])))
+
+
+def primary_keys(dialect: Dialect) -> Strategy[TypeEngine]:
+    types = list(_filter_unsupported_types([SmallInteger, Integer, BigInteger,
+                                            postgresql.UUID],
+                                           dialect=dialect))
+    return strategies.sampled_from(types).map(to_instance)
+
+
+def strings(dialect: Dialect,
+            *,
+            lengths: Strategy[int] = strategies.none()
+            ) -> Strategy[TypeEngine]:
+    return strategies.builds(Text,
+                             length=lengths)
+
+
+@singledispatch
+def scalars(type_: TypeEngine) -> Strategy[Scalar]:
+    return _values_by_python_types[type_.python_type]
+
+
+# we're using integers as primary key values
+# which are usually positive
+_MIN_POSITIVE_INTEGER_VALUE = 1
+_MAX_SMALLINT_VALUE = 32767
+_ascii_not_null_characters = strategies.characters(min_codepoint=1,
+                                                   max_codepoint=127)
+_values_by_python_types = {
+    bool: strategies.booleans(),
+    int: strategies.integers(min_value=_MIN_POSITIVE_INTEGER_VALUE,
+                             max_value=_MAX_SMALLINT_VALUE),
+    float: strategies.floats(allow_nan=False,
+                             allow_infinity=False),
+    Decimal: strategies.decimals(allow_nan=False,
+                                 allow_infinity=False),
+    datetime: strategies.datetimes(),
+    date: strategies.dates(),
+    time: strategies.times(),
+    timedelta: strategies.timedeltas(),
+}
+
+
+@scalars.register(String)
+def _(type_: String,
+      *,
+      alphabet: Strategy = _ascii_not_null_characters) -> Strategy[str]:
+    return strategies.text(alphabet=alphabet,
+                           max_size=type_.length)
+
+
+@scalars.register(LargeBinary)
+def _(type_: LargeBinary) -> Strategy[bytes]:
+    return strategies.binary(max_size=type_.length)
+
+
+@scalars.register(EnumType)
+def _(type_: EnumType) -> Strategy[Union[str, Enum]]:
+    enum_class = type_.enum_class
+    if enum_class is None:
+        # The source of enumerated values may be a list of string values
+        values = type_.enums
+    else:
+        # ... or a PEP-435-compliant enumerated class.
+        # More info at
+        # http://docs.sqlalchemy.org/en/latest/core/type_basics.html#sqlalchemy.types.Enum
+        values = list(enum_class)
+    return strategies.sampled_from(values)
+
+
+@scalars.register(postgresql.UUID)
+def _(type_: postgresql.UUID,
+      *,
+      version: Optional[int] = None) -> Strategy[Union[str, UUID]]:
+    result = strategies.uuids(version=version)
+    if not type_.as_uuid:
+        result = result.map(str)
+    return result
+
+
+def _filter_unsupported_types(types_or_instances: Iterable[TypeOrInstance],
+                              *,
+                              dialect: Dialect) -> Iterable[TypeOrInstance]:
+    return filter(partial(_is_type_supported,
+                          dialect=dialect),
+                  types_or_instances)
+
+
+def _is_type_supported(type_or_instance: Type[TypeOrInstance],
+                       *,
+                       dialect: Dialect) -> bool:
+    instance = to_instance(type_or_instance)
+    try:
+        instance.compile(dialect)
+    except exc.UnsupportedCompilationError:
+        return False
+    else:
+        return True
diff --git a/hypothesis_sqlalchemy/core/columns_records.py b/hypothesis_sqlalchemy/core/columns_records.py
new file mode 100644
index 0000000..3aefe92
--- /dev/null
+++ b/hypothesis_sqlalchemy/core/columns_records.py
@@ -0,0 +1,50 @@
+from itertools import starmap
+from operator import itemgetter
+from typing import (AbstractSet,
+                    Any,
+                    List,
+                    Optional)
+
+from hypothesis import strategies
+from sqlalchemy.schema import (Column,
+                               Constraint)
+
+from . import table_constraints
+from .column import scalars
+from .hints import (Record,
+                    Scalar,
+                    Strategy)
+
+
+def instances(columns: List[Column],
+              **fixed_columns_values: Strategy[Scalar]) -> Strategy[Record]:
+    if fixed_columns_values:
+        def column_scalars(column: Column) -> Strategy[Scalar]:
+            column_name = column.name
+            return (fixed_columns_values[column_name]
+                    if column_name in fixed_columns_values
+                    else scalars(column))
+    else:
+        column_scalars = scalars
+    return strategies.tuples(*map(column_scalars, columns))
+
+
+def lists(columns: List[Column],
+          constraints: AbstractSet[Constraint],
+          *,
+          min_size: int = 0,
+          max_size: Optional[int] = None,
+          **fixed_columns_values: Strategy[Any]) -> Strategy[List[Record]]:
+    columns_indices = {column: index for index, column in enumerate(columns)}
+    unique_indices = [[columns_indices[column]
+                       for column in constraint.columns]
+                      for constraint in constraints
+                      if isinstance(constraint, table_constraints.UNIQUE_TYPES)
+                      and constraint.columns]
+    unique_by = (tuple(starmap(itemgetter, unique_indices))
+                 if unique_indices
+                 else None)
+    return strategies.lists(instances(columns, **fixed_columns_values),
+                            min_size=min_size,
+                            max_size=max_size,
+                            unique_by=unique_by)
diff --git a/hypothesis_sqlalchemy/enumerable.py b/hypothesis_sqlalchemy/core/enum.py
similarity index 82%
rename from hypothesis_sqlalchemy/enumerable.py
rename to hypothesis_sqlalchemy/core/enum.py
index 4169184..92c2484 100644
--- a/hypothesis_sqlalchemy/enumerable.py
+++ b/hypothesis_sqlalchemy/core/enum.py
@@ -18,15 +18,15 @@
 UniqueBy = Optional[Callable[[Any], Hashable]]
 
 
-def factory(*,
-            names: Strategy[str] = python_identifiers,
-            bases: Strategy[Bases] =
-            strategies.tuples(strategies.just(Enum)),
-            keys: Strategy[str],
-            values: Strategy[Any] = strategies.integers(),
-            unique_by: UniqueBy = None,
-            min_size: int = 0,
-            max_size: Optional[int] = None) -> Strategy:
+def types(*,
+          names: Strategy[str] = python_identifiers,
+          bases: Strategy[Bases]
+          = strategies.tuples(strategies.just(Enum)),
+          keys: Strategy[str],
+          values: Strategy[Any] = strategies.integers(),
+          unique_by: UniqueBy = None,
+          min_size: int = 0,
+          max_size: Optional[int] = None) -> Strategy[EnumMeta]:
     contents = (strategies.tuples(strategies.lists(keys,
                                                    min_size=min_size,
                                                    max_size=max_size,
@@ -40,7 +40,7 @@ def factory(*,
             .map(lambda args: _to_enum(*args)))
 
 
-def _to_enum(name: str, bases: Bases, contents: Dict[str, Any]) -> type:
+def _to_enum(name: str, bases: Bases, contents: Dict[str, Any]) -> EnumMeta:
     contents = _to_enum_contents(name, bases, contents)
     return EnumMeta(name, bases, contents)
 
diff --git a/hypothesis_sqlalchemy/core/hints.py b/hypothesis_sqlalchemy/core/hints.py
new file mode 100644
index 0000000..19a6f1c
--- /dev/null
+++ b/hypothesis_sqlalchemy/core/hints.py
@@ -0,0 +1,12 @@
+from datetime import (date,
+                      datetime,
+                      timedelta)
+from decimal import Decimal
+from typing import (Tuple,
+                    Union)
+
+from hypothesis.strategies import SearchStrategy
+
+Scalar = Union[int, bool, float, Decimal, str, None, date, timedelta, datetime]
+Record = Tuple[Scalar, ...]
+Strategy = SearchStrategy
diff --git a/hypothesis_sqlalchemy/core/table.py b/hypothesis_sqlalchemy/core/table.py
new file mode 100644
index 0000000..2a45ed7
--- /dev/null
+++ b/hypothesis_sqlalchemy/core/table.py
@@ -0,0 +1,57 @@
+from operator import attrgetter
+from typing import (Any,
+                    Callable,
+                    Optional)
+
+from hypothesis import strategies
+from sqlalchemy.engine import Dialect
+from sqlalchemy.schema import (Column,
+                               MetaData,
+                               Table)
+
+from . import (column,
+               table_constraints)
+from .hints import Strategy
+from .utils import to_sql_identifiers
+
+
+def instances(dialect: Dialect,
+              *,
+              metadatas: Strategy[MetaData] = strategies.builds(MetaData),
+              names: Optional[Strategy[str]] = None,
+              columns: Optional[Strategy[Column]] = None,
+              min_size: int = 0,
+              max_size: Optional[int] = None,
+              extend_existing: Strategy[bool] = strategies.booleans()
+              ) -> Strategy[Table]:
+    tables_names = to_sql_identifiers(dialect) if names is None else names
+    tables_columns = column.instances(dialect) if columns is None else columns
+    tables_columns_lists = strategies.lists(tables_columns,
+                                            min_size=min_size,
+                                            max_size=max_size,
+                                            unique_by=attrgetter('name'))
+
+    def draw_table(draw: Callable[[Strategy], Any]) -> Table:
+        metadata = draw(metadatas)
+        extends_existing = draw(extend_existing)
+        table_names = tables_names
+        if not extends_existing:
+            table_names = (table_names
+                           .filter(lambda identifier:
+                                   identifier not in metadata.tables))
+        table_name = draw(table_names)
+        columns_list = draw(tables_columns_lists)
+        if extends_existing and table_name in metadata.tables:
+            # preserving constraints, especially primary key one
+            existing_table = metadata.tables[table_name]
+            columns_list = [existing_table.c.get(column.name, column)
+                            for column in columns_list]
+        result = Table(table_name, metadata, *columns_list,
+                       extend_existing=extends_existing)
+        constraints = draw(table_constraints.lists(result,
+                                                   primary_key_min_size=1))
+        for constraint in constraints:
+            result.append_constraint(constraint)
+        return result
+
+    return strategies.composite(draw_table)()
diff --git a/hypothesis_sqlalchemy/core/table_constraints.py b/hypothesis_sqlalchemy/core/table_constraints.py
new file mode 100644
index 0000000..4d3b29a
--- /dev/null
+++ b/hypothesis_sqlalchemy/core/table_constraints.py
@@ -0,0 +1,72 @@
+from operator import add
+from typing import (List,
+                    Optional)
+
+from hypothesis import strategies
+from sqlalchemy.schema import (Constraint,
+                               PrimaryKeyConstraint,
+                               Table,
+                               UniqueConstraint)
+
+from .hints import Strategy
+
+UNIQUE_TYPES = (PrimaryKeyConstraint, UniqueConstraint)
+
+
+def unique(table: Table,
+           *,
+           min_size: int = 0,
+           max_size: Optional[int] = None) -> Strategy[UniqueConstraint]:
+    columns = list(table.columns)
+    if max_size is None:
+        max_size = len(columns)
+    return (strategies.sets(strategies.sampled_from(columns)
+                            if columns
+                            else strategies.nothing(),
+                            min_size=min_size,
+                            max_size=max_size)
+            .map(lambda constraint_columns:
+                 UniqueConstraint(*constraint_columns)))
+
+
+def primary_keys(table: Table,
+                 *,
+                 min_size: int = 0,
+                 max_size: Optional[int] = None
+                 ) -> Strategy[PrimaryKeyConstraint]:
+    columns = list(table.columns)
+    existing_columns = [column for column in columns if column.primary_key]
+    if existing_columns:
+        return strategies.just(PrimaryKeyConstraint(*existing_columns))
+    return (strategies.sets(strategies.sampled_from(columns)
+                            if columns
+                            else strategies.nothing(),
+                            min_size=min_size,
+                            max_size=max_size)
+            .map(lambda constraint_columns:
+                 PrimaryKeyConstraint(*constraint_columns)))
+
+
+def lists(table: Table,
+          *,
+          min_size: int = 0,
+          max_size: Optional[int] = None,
+          primary_key_min_size: int = 0,
+          primary_key_max_size: Optional[int] = None,
+          unique_min_size: int = 0,
+          unique_max_size: Optional[int] = None) -> Strategy[List[Constraint]]:
+    if not table.columns:
+        return strategies.builds(list)
+    primary_keys_lists = (primary_keys(table,
+                                       min_size=primary_key_min_size,
+                                       max_size=primary_key_max_size)
+                          .map(lambda key: [key]))
+    unique_lists = strategies.lists(unique(table,
+                                           min_size=unique_min_size,
+                                           max_size=unique_max_size),
+                                    min_size=max(min_size - 1, 0),
+                                    max_size=max_size - 1
+                                    if max_size is not None
+                                    else max_size)
+    return (strategies.tuples(primary_keys_lists, unique_lists)
+            .map(lambda lists_pair: add(*lists_pair)))
diff --git a/hypothesis_sqlalchemy/core/table_records.py b/hypothesis_sqlalchemy/core/table_records.py
new file mode 100644
index 0000000..0bf8c0a
--- /dev/null
+++ b/hypothesis_sqlalchemy/core/table_records.py
@@ -0,0 +1,26 @@
+from typing import (Any,
+                    List,
+                    Optional)
+
+from sqlalchemy.schema import Table
+
+from . import columns_records
+from .hints import (Record,
+                    Strategy)
+
+
+def instances(table: Table,
+              **fixed_columns_values: Strategy) -> Strategy[Record]:
+    return columns_records.instances(table.columns, **fixed_columns_values)
+
+
+def lists(table: Table,
+          *,
+          min_size: int = 0,
+          max_size: Optional[int] = None,
+          **fixed_columns_values: Strategy[Any]
+          ) -> Strategy[List[Record]]:
+    return columns_records.lists(table.columns, table.constraints,
+                                 min_size=min_size,
+                                 max_size=max_size,
+                                 **fixed_columns_values)
diff --git a/hypothesis_sqlalchemy/utils.py b/hypothesis_sqlalchemy/core/utils.py
similarity index 68%
rename from hypothesis_sqlalchemy/utils.py
rename to hypothesis_sqlalchemy/core/utils.py
index 4bca9c2..e69acaf 100644
--- a/hypothesis_sqlalchemy/utils.py
+++ b/hypothesis_sqlalchemy/core/utils.py
@@ -3,19 +3,15 @@
 from hypothesis import strategies
 from sqlalchemy.engine import Dialect
 
-from hypothesis_sqlalchemy.hints import Strategy
+from .hints import Strategy
 
 identifiers_characters = strategies.sampled_from('_' + ascii_letters)
+python_identifiers = (strategies.text(alphabet=identifiers_characters,
+                                      min_size=1)
+                      .filter(str.isidentifier))
 
 
 def to_sql_identifiers(dialect: Dialect) -> Strategy[str]:
-    max_size = dialect.max_identifier_length
-    min_size = min(8, max_size)
     return strategies.text(alphabet=identifiers_characters,
-                           min_size=min_size,
-                           max_size=max_size)
-
-
-python_identifiers = (strategies.text(alphabet=identifiers_characters,
-                                      min_size=1)
-                      .filter(str.isidentifier))
+                           min_size=1,
+                           max_size=dialect.max_identifier_length)
diff --git a/hypothesis_sqlalchemy/dialectic.py b/hypothesis_sqlalchemy/dialectic.py
deleted file mode 100644
index 868dd08..0000000
--- a/hypothesis_sqlalchemy/dialectic.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from sqlalchemy.engine.default import DefaultDialect
-
-default = DefaultDialect()
diff --git a/hypothesis_sqlalchemy/hints.py b/hypothesis_sqlalchemy/hints.py
index 00d2325..26338f9 100644
--- a/hypothesis_sqlalchemy/hints.py
+++ b/hypothesis_sqlalchemy/hints.py
@@ -1,14 +1,5 @@
-from datetime import (date,
-                      datetime,
-                      timedelta)
-from decimal import Decimal
-from typing import (Tuple,
-                    Union)
+from .core import hints as _hints
 
-from hypothesis.strategies import SearchStrategy
-
-ColumnValueType = Union[int, bool,
-                        float, Decimal, str, None,
-                        date, timedelta, datetime]
-RecordType = Tuple[ColumnValueType, ...]
-Strategy = SearchStrategy
+Record = _hints.Record
+Scalar = _hints.Scalar
+Strategy = _hints.Strategy
diff --git a/hypothesis_sqlalchemy/sample.py b/hypothesis_sqlalchemy/sample.py
new file mode 100644
index 0000000..2ca206f
--- /dev/null
+++ b/hypothesis_sqlalchemy/sample.py
@@ -0,0 +1,13 @@
+"""Strategies for generating data from ``SQLAlchemy`` objects."""
+
+from .core import (column as _column,
+                   column_type as _column_type,
+                   columns_records as _columns_records,
+                   table_records as _table_records)
+
+column_type_scalars = _column_type.scalars
+column_scalars = _column.scalars
+columns_records = _columns_records.instances
+columns_records_lists = _columns_records.lists
+table_records = _table_records.instances
+table_records_lists = _table_records.lists
diff --git a/hypothesis_sqlalchemy/scheme.py b/hypothesis_sqlalchemy/scheme.py
new file mode 100644
index 0000000..2bd9075
--- /dev/null
+++ b/hypothesis_sqlalchemy/scheme.py
@@ -0,0 +1,9 @@
+"""Strategies for generating ``SQLAlchemy`` objects."""
+
+from .core import (column as _column,
+                   column_type as _column_type,
+                   table as _table)
+
+column_types = _column_type.instances
+columns = _column.instances
+tables = _table.instances
diff --git a/hypothesis_sqlalchemy/tabular/__init__.py b/hypothesis_sqlalchemy/tabular/__init__.py
deleted file mode 100644
index 4a7b249..0000000
--- a/hypothesis_sqlalchemy/tabular/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from . import records
-from .base import factory
diff --git a/hypothesis_sqlalchemy/tabular/base.py b/hypothesis_sqlalchemy/tabular/base.py
deleted file mode 100644
index a7d2d42..0000000
--- a/hypothesis_sqlalchemy/tabular/base.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from typing import (Any,
-                    Callable,
-                    Optional)
-
-from hypothesis import strategies
-from sqlalchemy.engine import Dialect
-from sqlalchemy.schema import (Column,
-                               MetaData,
-                               Table)
-
-from hypothesis_sqlalchemy import (columnar,
-                                   constrained,
-                                   dialectic)
-from hypothesis_sqlalchemy.hints import Strategy
-from hypothesis_sqlalchemy.utils import to_sql_identifiers
-
-
-def factory(*,
-            dialect: Dialect = dialectic.default,
-            metadata: MetaData,
-            names: Optional[Strategy[str]] = None,
-            columns: Strategy[Column] = None,
-            min_size: int = 0,
-            max_size: Optional[int] = None,
-            extend_existing: Strategy[bool] = strategies.booleans()
-            ) -> Strategy[Table]:
-    names = to_sql_identifiers(dialect) if names is None else names
-    columns = (columnar.factory(dialect)
-               if columns is None else columns)
-    columns_lists = columnar.lists_factory(columns,
-                                           min_size=min_size,
-                                           max_size=max_size)
-
-    def table_factory(draw: Callable[[Strategy], Any]) -> Table:
-        extends_existing = draw(extend_existing)
-        table_names = names
-        if not extends_existing:
-            table_names = (table_names
-                           .filter(lambda identifier:
-                                   identifier not in metadata.tables))
-        table_name = draw(table_names)
-        columns_list = draw(columns_lists)
-        if extends_existing and table_name in metadata.tables:
-            # preserving constraints, especially primary key one
-            existing_table = metadata.tables[table_name]
-            columns_list = [existing_table.c.get(column.name, column)
-                            for column in columns_list]
-        result = Table(table_name,
-                       metadata,
-                       *columns_list,
-                       extend_existing=extends_existing)
-        constraints = draw(constrained.lists_factory(columns_list,
-                                                     primary_key_min_size=1))
-        for constraint in constraints:
-            result.append_constraint(constraint)
-        return result
-
-    return strategies.composite(table_factory)()
diff --git a/hypothesis_sqlalchemy/tabular/records.py b/hypothesis_sqlalchemy/tabular/records.py
deleted file mode 100644
index 2b48e16..0000000
--- a/hypothesis_sqlalchemy/tabular/records.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from typing import Optional
-
-from sqlalchemy import Table
-
-from hypothesis_sqlalchemy import columnar
-from hypothesis_sqlalchemy.hints import Strategy
-
-
-def factory(table: Table,
-            **fixed_columns_values: Strategy) -> Strategy:
-    return columnar.records.factory(table.columns,
-                                    **fixed_columns_values)
-
-
-def lists_factory(table: Table,
-                  *,
-                  min_size: int = 0,
-                  max_size: Optional[int] = None,
-                  **fixed_columns_values: Strategy) -> Strategy:
-    return columnar.records.lists_factory(table.columns,
-                                          table.constraints,
-                                          min_size=min_size,
-                                          max_size=max_size,
-                                          **fixed_columns_values)
diff --git a/requirements.txt b/requirements.txt
index f5235e7..e17c40b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
-sqlalchemy>=1.1.0
-hypothesis>=4.22.0
+sqlalchemy>=1.4.32,<2.0
+hypothesis>=6.31.6,<7.0
diff --git a/setup.py b/setup.py
index e347a43..1949fa9 100644
--- a/setup.py
+++ b/setup.py
@@ -7,25 +7,25 @@
 
 project_base_url = 'https://github.com/lycantropos/hypothesis_sqlalchemy/'
 
-setup(name='hypothesis_sqlalchemy',
+setup(name=hypothesis_sqlalchemy.__name__,
+      packages=find_packages(exclude=('tests', 'tests.*')),
       version=hypothesis_sqlalchemy.__version__,
-      author='Azat Ibrakov',
-      author_email='azatibrakov@gmail.com',
-      url=project_base_url,
-      download_url=project_base_url + 'archive/master.tar.gz',
       description=hypothesis_sqlalchemy.__doc__,
-      long_description=Path('README.md').read_text(),
+      long_description=Path('README.md').read_text(encoding='utf-8'),
       long_description_content_type='text/markdown',
-      license='MIT',
+      author='Azat Ibrakov',
+      author_email='azatibrakov@gmail.com',
       classifiers=[
           'Development Status :: 3 - Alpha',
           'Environment :: Web Environment',
           'Framework :: Hypothesis',
           'Intended Audience :: Developers',
           'License :: OSI Approved :: MIT License',
-          'Programming Language :: Python :: 3.5',
           'Programming Language :: Python :: 3.6',
           'Programming Language :: Python :: 3.7',
+          'Programming Language :: Python :: 3.8',
+          'Programming Language :: Python :: 3.9',
+          'Programming Language :: Python :: 3.10',
           'Programming Language :: Python :: Implementation :: CPython',
           'Programming Language :: Python :: Implementation :: PyPy',
           'Operating System :: POSIX',
@@ -33,7 +33,9 @@
           'Operating System :: Microsoft :: Windows',
           'Topic :: Database :: Database Engines/Servers',
       ],
-      packages=find_packages(exclude=('tests', 'tests.*')),
       keywords=['SQLAlchemy', 'hypothesis'],
-      python_requires='>=3.5',
-      install_requires=Path('requirements.txt').read_text())
+      license='MIT License',
+      url=project_base_url,
+      download_url=project_base_url + 'archive/master.zip',
+      python_requires='>=3.6',
+      install_requires=Path('requirements.txt').read_text(encoding='utf-8'))


Test patch:
diff --git a/pytest.ini b/pytest.ini
index 5d52886..8bb8501 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,7 +1,4 @@
-[pytest]
-addopts = --verbose
-          -s
-          --hypothesis-profile=default
-          --cov-report term
-          --cov-report xml
-          --cov=hypothesis_sqlalchemy
+[pytest]
+addopts = --verbose
+          -s
+          --hypothesis-profile=default
diff --git a/requirements-tests.txt b/requirements-tests.txt
index d87b1c3..757e27a 100644
--- a/requirements-tests.txt
+++ b/requirements-tests.txt
@@ -1,2 +1,1 @@
-pytest>=4.6.1
-pytest-cov>=2.7.1
+pytest>=7.0.1,<8.0
diff --git a/run-tests.ps1 b/run-tests.ps1
index ccc96bd..a2a7910 100644
--- a/run-tests.ps1
+++ b/run-tests.ps1
@@ -14,7 +14,7 @@ if ($STATUS -eq 0)
 }
 else
 {
-    echo "${implementation} tests failed to pass"
+    echo "${implementation} tests failed"
 }
 
 exit $STATUS
diff --git a/run-tests.sh b/run-tests.sh
old mode 100755
new mode 100644
index 477595e..bd5e09e
--- a/run-tests.sh
+++ b/run-tests.sh
@@ -14,7 +14,7 @@ docker-compose --file ${COMPOSE_FILE_NAME} down --remove-orphans
 if [[ "$STATUS" -eq "0" ]]; then
 	echo "${IMPLEMENTATION} tests passed";
 else
-	echo "${IMPLEMENTATION} tests failed to pass"
+	echo "${IMPLEMENTATION} tests failed"
 fi
 
 exit ${STATUS}
diff --git a/tests/columnar_tests/types_tests/test_enums_factory.py b/tests/columnar_tests/types_tests/test_enums_factory.py
deleted file mode 100644
index 560d496..0000000
--- a/tests/columnar_tests/types_tests/test_enums_factory.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from typing import Optional
-
-from hypothesis import given
-from sqlalchemy.engine import Dialect
-from sqlalchemy.sql.sqltypes import Enum as EnumType
-
-from hypothesis_sqlalchemy.columnar.types import enums_factory
-from hypothesis_sqlalchemy.hints import Strategy
-from tests import strategies
-from tests.utils import DataObject
-
-
-@given(strategies.dialects, strategies.min_sizes, strategies.max_sizes)
-def test_basic(dialect: Dialect,
-               min_size: int,
-               max_size: Optional[int]) -> None:
-    result = enums_factory(dialect,
-                           min_size=min_size,
-                           max_size=max_size)
-
-    assert isinstance(result, Strategy)
-
-
-@given(strategies.data, strategies.dialects,
-       strategies.min_sizes, strategies.max_sizes)
-def test_enum_types_factory(data: DataObject,
-                            dialect: Dialect,
-                            min_size: int,
-                            max_size: Optional[int]) -> None:
-    strategy = enums_factory(dialect,
-                             min_size=min_size,
-                             max_size=max_size)
-
-    result = data.draw(strategy)
-
-    assert isinstance(result, EnumType)
-    assert min_size <= len(result.enums)
-    assert max_size is None or len(result.enums) <= max_size
diff --git a/tests/conftest.py b/tests/conftest.py
index 4786106..3fe9fef 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,7 +1,27 @@
+import os
+import platform
+from datetime import timedelta
+
+import pytest
 from hypothesis import (HealthCheck,
                         settings)
 
+is_pypy = platform.python_implementation() == 'PyPy'
+on_ci = bool(os.getenv('CI', False))
+max_examples = settings.default.max_examples
 settings.register_profile('default',
-                          deadline=None,
+                          deadline=(timedelta(hours=1) / max_examples
+                                    if on_ci
+                                    else None),
+                          max_examples=(10
+                                        if is_pypy and on_ci
+                                        else max_examples),
                           suppress_health_check=[HealthCheck.too_slow,
                                                  HealthCheck.filter_too_much])
+
+
+@pytest.hookimpl(trylast=True)
+def pytest_sessionfinish(session: pytest.Session,
+                         exitstatus: pytest.ExitCode) -> None:
+    if exitstatus == pytest.ExitCode.NO_TESTS_COLLECTED:
+        session.exitstatus = pytest.ExitCode.OK
diff --git a/tests/constrained_tests/strategies.py b/tests/constrained_tests/strategies.py
deleted file mode 100644
index 16213c9..0000000
--- a/tests/constrained_tests/strategies.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from operator import attrgetter
-from typing import (List,
-                    Tuple)
-
-from hypothesis import strategies
-from sqlalchemy.schema import (Column,
-                               MetaData)
-
-from hypothesis_sqlalchemy import tabular
-from hypothesis_sqlalchemy.hints import Strategy
-from tests.strategies import (data,
-                              dialects)
-from tests.utils import Bounds
-
-data = data
-columns_lists = (dialects
-                 .flatmap(lambda dialect:
-                          tabular.factory(dialect=dialect,
-                                          metadata=MetaData()))
-                 .map(attrgetter('columns'))
-                 .map(list))
-
-
-def to_columns_lists_with_bounds(columns: List[Column]
-                                 ) -> Strategy[Tuple[List[Column],
-                                                     Bounds]]:
-    def min_size_to_bounds(min_size: int) -> Strategy[Bounds]:
-        return strategies.tuples(strategies.just(min_size),
-                                 strategies.none()
-                                 | strategies.integers(min_size, len(columns)))
-
-    return strategies.tuples(strategies.just(columns),
-                             strategies.integers(0, len(columns))
-                             .flatmap(min_size_to_bounds))
-
-
-columns_lists_with_bounds = columns_lists.flatmap(to_columns_lists_with_bounds)
diff --git a/tests/constrained_tests/test_primary_key_factory.py b/tests/constrained_tests/test_primary_key_factory.py
deleted file mode 100644
index 0d47aa7..0000000
--- a/tests/constrained_tests/test_primary_key_factory.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from typing import (List,
-                    Tuple)
-
-from hypothesis import given
-from sqlalchemy.schema import (Column,
-                               PrimaryKeyConstraint)
-
-from hypothesis_sqlalchemy.constrained import primary_keys_factory
-from hypothesis_sqlalchemy.hints import Strategy
-from tests.utils import (Bounds,
-                         DataObject)
-from . import strategies
-
-
-@given(strategies.columns_lists_with_bounds)
-def test_basic(columns_with_bounds: Tuple[List[Column], Bounds]) -> None:
-    columns, (min_size, max_size) = columns_with_bounds
-
-    result = primary_keys_factory(columns,
-                                  min_size=min_size,
-                                  max_size=max_size)
-
-    assert isinstance(result, Strategy)
-
-
-@given(strategies.data, strategies.columns_lists_with_bounds)
-def test_examples(data: DataObject,
-                  columns_with_bounds: Tuple[List[Column], Bounds]) -> None:
-    columns, (min_size, max_size) = columns_with_bounds
-
-    strategy = primary_keys_factory(columns,
-                                    min_size=min_size,
-                                    max_size=max_size)
-
-    result = data.draw(strategy)
-
-    existing_primary_key_columns = [column
-                                    for column in columns
-                                    if column.primary_key]
-
-    assert isinstance(result, PrimaryKeyConstraint)
-    assert len(result.columns) >= min(len(existing_primary_key_columns),
-                                      min_size)
-    assert (max_size is None
-            or len(result.columns) <= max(len(existing_primary_key_columns),
-                                          max_size))
-    assert all(column in columns
-               for column in result.columns)
diff --git a/tests/constrained_tests/test_unique_factory.py b/tests/constrained_tests/test_unique_factory.py
deleted file mode 100644
index ffa9ee9..0000000
--- a/tests/constrained_tests/test_unique_factory.py
+++ /dev/null
@@ -1,41 +0,0 @@
-from typing import (List,
-                    Tuple)
-
-from hypothesis import given
-from sqlalchemy.schema import (Column,
-                               UniqueConstraint)
-
-from hypothesis_sqlalchemy.constrained import unique_factory
-from hypothesis_sqlalchemy.hints import Strategy
-from tests.utils import (Bounds,
-                         DataObject)
-from . import strategies
-
-
-@given(strategies.columns_lists_with_bounds)
-def test_basic(columns_with_bounds: Tuple[List[Column], Bounds]) -> None:
-    columns, (min_size, max_size) = columns_with_bounds
-
-    result = unique_factory(columns,
-                            min_size=min_size,
-                            max_size=max_size)
-
-    assert isinstance(result, Strategy)
-
-
-@given(strategies.data, strategies.columns_lists_with_bounds)
-def test_examples(data: DataObject,
-                  columns_with_bounds: Tuple[List[Column], Bounds]) -> None:
-    columns, (min_size, max_size) = columns_with_bounds
-
-    strategy = unique_factory(columns,
-                              min_size=min_size,
-                              max_size=max_size)
-
-    result = data.draw(strategy)
-
-    assert isinstance(result, UniqueConstraint)
-    assert len(result.columns) >= min_size
-    assert max_size is None or len(result.columns) <= max_size
-    assert all(column in columns
-               for column in result.columns)
diff --git a/tests/enumerable_tests/__init__.py b/tests/enumerable_tests/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/enumerable_tests/strategies.py b/tests/enumerable_tests/strategies.py
deleted file mode 100644
index d99f047..0000000
--- a/tests/enumerable_tests/strategies.py
+++ /dev/null
@@ -1,114 +0,0 @@
-from enum import (Enum,
-                  EnumMeta,
-                  IntEnum)
-from itertools import filterfalse
-from typing import (Any,
-                    Tuple)
-
-from hypothesis import strategies
-
-from hypothesis_sqlalchemy.enumerable import (Bases,
-                                              UniqueBy,
-                                              is_invalid_key,
-                                              is_valid_key)
-from hypothesis_sqlalchemy.hints import Strategy
-from hypothesis_sqlalchemy.utils import python_identifiers
-from tests.strategies import (data,
-                              max_sizes,
-                              min_sizes)
-
-data = data
-max_sizes = max_sizes
-min_sizes = min_sizes
-integers = strategies.integers()
-floats = strategies.floats()
-strings = strategies.text()
-scalars = (strategies.builds(object)
-           | strategies.none()
-           | integers
-           | strings
-           | floats
-           | strategies.datetimes())
-keys_strategies = strategies.just(python_identifiers.filter(is_valid_key))
-invalid_keys_types_strategies = strategies.just(strategies.none())
-invalid_keys_values_strategies = strategies.just(strings
-                                                 .map('_{}_'.format)
-                                                 .filter(is_invalid_key))
-
-_non_enum_bases = [str, float, int]
-_valid_class_strategies = {str: scalars,
-                           float: floats | floats.map(str),
-                           int: integers | integers.map(str)}
-
-
-class _NoStrConvertible:
-    __str__ = __repr__ = None
-
-
-_invalid_class_strategies = {str: strategies.builds(_NoStrConvertible),
-                             float: strategies.none(),
-                             int: strategies.none()}
-
-
-def is_enum_class(cls: type) -> bool:
-    return isinstance(cls, EnumMeta)
-
-
-def combine_bases_with_values(bases: Bases) -> Tuple[Strategy[Bases],
-                                                     Strategy[Any]]:
-    if any(base is IntEnum for base in bases):
-        values = _valid_class_strategies[int]
-    else:
-        try:
-            non_enum_base = next(filterfalse(is_enum_class, bases))
-        except StopIteration:
-            values = scalars
-        else:
-            values = _valid_class_strategies[non_enum_base]
-    return strategies.just(bases), values
-
-
-def combine_bases_with_unique_values(bases: Bases
-                                     ) -> Tuple[Strategy[Bases],
-                                                Strategy[Any],
-                                                UniqueBy]:
-    if any(base is IntEnum for base in bases):
-        values = _valid_class_strategies[int]
-        unique_by = int
-    else:
-        try:
-            non_enum_base = next(filterfalse(is_enum_class, bases))
-        except StopIteration:
-            values = scalars
-            unique_by = lambda x: x
-        else:
-            values = _valid_class_strategies[non_enum_base]
-            unique_by = non_enum_base
-    return strategies.just(bases), values, unique_by
-
-
-def combine_bases_with_invalid_values(bases: Bases
-                                      ) -> Tuple[Strategy[Bases],
-                                                 Strategy[Any]]:
-    if any(base is IntEnum for base in bases):
-        values = _invalid_class_strategies[int]
-    else:
-        try:
-            non_enum_base = next(filterfalse(is_enum_class, bases))
-        except StopIteration:
-            raise ValueError('Can\'t produce invalid values '
-                             'for generic enum class.')
-        values = _invalid_class_strategies[non_enum_base]
-    return strategies.just(bases), values
-
-
-plain_bases = strategies.tuples(strategies.just(Enum))
-complex_bases = (strategies.tuples(strategies.just(IntEnum)) |
-                 strategies.tuples(strategies.sampled_from(_non_enum_bases),
-                                   strategies.just(Enum)))
-bases_values_strategies = ((plain_bases | complex_bases)
-                           .map(combine_bases_with_values))
-bases_unique_values_strategies = ((plain_bases | complex_bases)
-                                  .map(combine_bases_with_unique_values))
-bases_invalid_values_strategies = complex_bases.map(
-        combine_bases_with_invalid_values)
diff --git a/tests/enumerable_tests/test_factory.py b/tests/enumerable_tests/test_factory.py
deleted file mode 100644
index d9e5d7c..0000000
--- a/tests/enumerable_tests/test_factory.py
+++ /dev/null
@@ -1,115 +0,0 @@
-from enum import EnumMeta
-from typing import (Any,
-                    Optional,
-                    Tuple)
-
-import pytest
-from hypothesis import given
-
-from hypothesis_sqlalchemy.enumerable import (Bases,
-                                              UniqueBy,
-                                              factory)
-from hypothesis_sqlalchemy.hints import Strategy
-from tests.utils import DataObject
-from . import strategies
-
-
-@given(strategies.bases_values_strategies, strategies.keys_strategies,
-       strategies.min_sizes, strategies.max_sizes)
-def test_basic(bases_values: Tuple[Strategy[strategies.Bases], Strategy[Any]],
-               keys: Strategy[str],
-               min_size: int,
-               max_size: Optional[int]) -> None:
-    bases, values = bases_values
-
-    result = factory(bases=bases,
-                     keys=keys,
-                     values=values,
-                     min_size=min_size,
-                     max_size=max_size)
-
-    assert isinstance(result, Strategy)
-
-
-@given(strategies.data, strategies.bases_values_strategies,
-       strategies.keys_strategies, strategies.min_sizes, strategies.max_sizes)
-def test_example(data: DataObject,
-                 bases_values: Tuple[Strategy[strategies.Bases],
-                                     Strategy[Any]],
-                 keys: Strategy[str],
-                 min_size: int,
-                 max_size: Optional[int]) -> None:
-    bases, values = bases_values
-    strategy = factory(bases=bases,
-                       keys=keys,
-                       values=values,
-                       min_size=min_size,
-                       max_size=max_size)
-
-    result = data.draw(strategy)
-
-    assert isinstance(result, EnumMeta)
-    # not checking `len` because of aliases
-    assert min_size <= len(result.__members__)
-    assert max_size is None or len(result.__members__) <= max_size
-
-
-@given(strategies.data, strategies.bases_unique_values_strategies,
-       strategies.keys_strategies, strategies.min_sizes, strategies.max_sizes)
-def test_unique_by(data: DataObject,
-                   bases_values_unique_by: Tuple[Strategy[Bases],
-                                                 Strategy[Any],
-                                                 UniqueBy],
-                   keys: Strategy[str],
-                   min_size: int,
-                   max_size: Optional[int]) -> None:
-    bases, values, unique_by = bases_values_unique_by
-    strategy = factory(bases=bases,
-                       keys=keys,
-                       values=values,
-                       min_size=min_size,
-                       max_size=max_size,
-                       unique_by=unique_by)
-
-    result = data.draw(strategy)
-
-    assert isinstance(result, EnumMeta)
-    assert min_size <= len(result)
-    assert max_size is None or len(result) <= max_size
-
-
-@given(strategies.data, strategies.invalid_keys_types_strategies)
-def test_invalid_keys_types(data: DataObject,
-                            keys: Strategy[Any]) -> None:
-    strategy = factory(keys=keys,
-                       min_size=1)
-
-    with pytest.raises(TypeError):
-        data.draw(strategy)
-
-
-@given(strategies.data, strategies.invalid_keys_values_strategies)
-def test_invalid_keys_values(data: DataObject,
-                             keys: Strategy[str]) -> None:
-    strategy = factory(keys=keys,
-                       min_size=1)
-
-    with pytest.raises(ValueError):
-        data.draw(strategy)
-
-
-@given(strategies.data, strategies.bases_invalid_values_strategies,
-       strategies.keys_strategies)
-def test_invalid_values(data: DataObject,
-                        bases_invalid_values: Tuple[Strategy[Bases],
-                                                    Strategy[Any]],
-                        keys: Strategy[str]) -> None:
-    bases, invalid_values = bases_invalid_values
-
-    result = factory(bases=bases,
-                     keys=keys,
-                     values=invalid_values,
-                     min_size=1)
-
-    with pytest.raises(TypeError):
-        data.draw(result)
diff --git a/tests/columnar_tests/types_tests/__init__.py b/tests/sample_tests/__init__.py
similarity index 100%
rename from tests/columnar_tests/types_tests/__init__.py
rename to tests/sample_tests/__init__.py
diff --git a/tests/sample_tests/strategies.py b/tests/sample_tests/strategies.py
new file mode 100644
index 0000000..044e78b
--- /dev/null
+++ b/tests/sample_tests/strategies.py
@@ -0,0 +1,50 @@
+from functools import partial
+from typing import (Any,
+                    Dict,
+                    Tuple)
+
+from hypothesis import strategies
+from sqlalchemy.dialects import (mysql,
+                                 postgresql,
+                                 sqlite)
+from sqlalchemy.engine.default import DefaultDialect
+from sqlalchemy.schema import (Column,
+                               Table)
+
+from hypothesis_sqlalchemy import scheme
+from hypothesis_sqlalchemy.hints import Strategy
+from hypothesis_sqlalchemy.sample import column_type_scalars
+from tests.utils import (MAX_MIN_SIZE,
+                         MAX_SIZE)
+
+data = strategies.data()
+dialects = (strategies.one_of([strategies.builds(dialect)
+                               for dialect in [DefaultDialect,
+                                               mysql.dialect,
+                                               postgresql.dialect,
+                                               sqlite.dialect]]))
+# for simplest table with single-value enum column
+min_table_records_lists_sizes = strategies.integers(0, 1)
+max_table_records_lists_sizes = (
+        strategies.none() | strategies.integers(MAX_MIN_SIZE + 1, MAX_SIZE)
+)
+tables = dialects.flatmap(scheme.tables)
+
+
+def fix_columns_values(table: Table
+                       ) -> Strategy[Tuple[Table, Dict[str, Strategy[Any]]]]:
+    def to_item(column: Column) -> Tuple[str, Strategy[Any]]:
+        return column.name, column_type_scalars(column.type)
+
+    if table.columns:
+        fixed_columns_values = (
+            (strategies.sets(strategies.sampled_from(list(table.columns)))
+             .map(partial(map, to_item))
+             .map(dict))
+        )
+    else:
+        fixed_columns_values = strategies.builds(dict)
+    return strategies.tuples(strategies.just(table), fixed_columns_values)
+
+
+tables_with_fixed_columns_values = tables.flatmap(fix_columns_values)
diff --git a/tests/tabular_tests/records_tests/test_factory.py b/tests/sample_tests/test_table_records.py
similarity index 86%
rename from tests/tabular_tests/records_tests/test_factory.py
rename to tests/sample_tests/test_table_records.py
index 1df89bb..73751ff 100644
--- a/tests/tabular_tests/records_tests/test_factory.py
+++ b/tests/sample_tests/test_table_records.py
@@ -6,7 +6,7 @@
 from sqlalchemy.schema import Table
 
 from hypothesis_sqlalchemy.hints import Strategy
-from hypothesis_sqlalchemy.tabular.records import factory
+from hypothesis_sqlalchemy.sample import table_records
 from tests.utils import (DataObject,
                          table_record_is_valid)
 from . import strategies
@@ -18,7 +18,7 @@ def test_basic(table_fixed_columns_values: Tuple[Table,
                ) -> None:
     table, fixed_columns_values = table_fixed_columns_values
 
-    result = factory(table, **fixed_columns_values)
+    result = table_records(table, **fixed_columns_values)
 
     assert isinstance(result, Strategy)
 
@@ -30,7 +30,7 @@ def test_examples(data: DataObject,
                   ) -> None:
     table, fixed_columns_values = table_fixed_columns_values
 
-    strategy = factory(table, **fixed_columns_values)
+    strategy = table_records(table, **fixed_columns_values)
 
     result = data.draw(strategy)
 
diff --git a/tests/tabular_tests/records_tests/test_lists_factory.py b/tests/sample_tests/test_table_records_lists.py
similarity index 60%
rename from tests/tabular_tests/records_tests/test_lists_factory.py
rename to tests/sample_tests/test_table_records_lists.py
index 326fa43..84a7643 100644
--- a/tests/tabular_tests/records_tests/test_lists_factory.py
+++ b/tests/sample_tests/test_table_records_lists.py
@@ -8,7 +8,7 @@
 from sqlalchemy.schema import Table
 
 from hypothesis_sqlalchemy.hints import Strategy
-from hypothesis_sqlalchemy.tabular.records import lists_factory
+from hypothesis_sqlalchemy.sample import table_records_lists
 from tests.utils import (DataObject,
                          records_satisfy_table_constraints,
                          table_record_is_valid)
@@ -16,33 +16,35 @@
 
 
 @given(strategies.tables_with_fixed_columns_values,
-       strategies.min_sizes, strategies.max_sizes)
+       strategies.min_table_records_lists_sizes,
+       strategies.max_table_records_lists_sizes)
 def test_basic(table_fixed_columns_values: Tuple[Table,
                                                  Dict[str, Strategy[Any]]],
                min_size: int, max_size: Optional[int]) -> None:
     table, fixed_columns_values = table_fixed_columns_values
 
-    result = lists_factory(table,
-                           min_size=min_size,
-                           max_size=max_size,
-                           **fixed_columns_values)
+    result = table_records_lists(table,
+                                 min_size=min_size,
+                                 max_size=max_size,
+                                 **fixed_columns_values)
 
     assert isinstance(result, Strategy)
 
 
 @given(strategies.data, strategies.tables_with_fixed_columns_values,
-       strategies.min_sizes, strategies.max_sizes)
-def test_lists_factory(
-        data: DataObject,
-        table_fixed_columns_values: Tuple[Table, Dict[str, Strategy[Any]]],
-        min_size: int,
-        max_size: Optional[int]) -> None:
+       strategies.min_table_records_lists_sizes,
+       strategies.max_table_records_lists_sizes)
+def test_lists_factory(data: DataObject,
+                       table_fixed_columns_values
+                       : Tuple[Table, Dict[str, Strategy[Any]]],
+                       min_size: int,
+                       max_size: Optional[int]) -> None:
     table, fixed_columns_values = table_fixed_columns_values
 
-    strategy = lists_factory(table,
-                             min_size=min_size,
-                             max_size=max_size,
-                             **fixed_columns_values)
+    strategy = table_records_lists(table,
+                                   min_size=min_size,
+                                   max_size=max_size,
+                                   **fixed_columns_values)
 
     result = data.draw(strategy)
 
diff --git a/tests/constrained_tests/__init__.py b/tests/scheme_tests/__init__.py
similarity index 100%
rename from tests/constrained_tests/__init__.py
rename to tests/scheme_tests/__init__.py
diff --git a/tests/scheme_tests/strategies.py b/tests/scheme_tests/strategies.py
new file mode 100644
index 0000000..7a05c04
--- /dev/null
+++ b/tests/scheme_tests/strategies.py
@@ -0,0 +1,33 @@
+from typing import (Tuple)
+
+from hypothesis import strategies
+from sqlalchemy import MetaData
+from sqlalchemy.dialects import (mysql,
+                                 postgresql,
+                                 sqlite)
+from sqlalchemy.engine import Dialect
+from sqlalchemy.engine.default import DefaultDialect
+
+from hypothesis_sqlalchemy.core.hints import Strategy
+from hypothesis_sqlalchemy.core.utils import to_sql_identifiers
+from tests.utils import (MAX_MIN_SIZE,
+                         MAX_SIZE)
+
+data = strategies.data()
+dialects = (strategies.one_of([strategies.builds(dialect)
+                               for dialect in [DefaultDialect,
+                                               mysql.dialect,
+                                               postgresql.dialect,
+                                               sqlite.dialect]]))
+just = strategies.just
+
+
+def to_dialects_with_names(dialect: Dialect) -> Strategy[Tuple[Dialect, str]]:
+    return strategies.tuples(just(dialect), to_sql_identifiers(dialect))
+
+
+dialects_with_names = dialects.flatmap(to_dialects_with_names)
+min_tables_sizes = strategies.integers(0, MAX_MIN_SIZE)
+max_tables_sizes = (strategies.none()
+                    | strategies.integers(MAX_MIN_SIZE + 1, MAX_SIZE))
+metadatas = strategies.builds(MetaData)
diff --git a/tests/scheme_tests/test_tables.py b/tests/scheme_tests/test_tables.py
new file mode 100644
index 0000000..c039774
--- /dev/null
+++ b/tests/scheme_tests/test_tables.py
@@ -0,0 +1,66 @@
+from typing import (Optional,
+                    Tuple)
+
+from hypothesis import given
+from sqlalchemy.engine import Dialect
+from sqlalchemy.schema import (MetaData,
+                               Table)
+
+import hypothesis_sqlalchemy.scheme as relational
+from hypothesis_sqlalchemy.core.hints import Strategy
+from tests.utils import DataObject
+from . import strategies
+
+
+@given(strategies.dialects, strategies.min_tables_sizes,
+       strategies.max_tables_sizes)
+def test_basic(dialect: Dialect,
+               min_size: int,
+               max_size: Optional[int]) -> None:
+    result = relational.tables(dialect=dialect,
+                               metadatas=strategies.metadatas,
+                               min_size=min_size,
+                               max_size=max_size)
+
+    assert isinstance(result, Strategy)
+
+
+@given(strategies.data, strategies.dialects, strategies.min_tables_sizes,
+       strategies.max_tables_sizes)
+def test_examples(data: DataObject,
+                  dialect: Dialect,
+                  min_size: int,
+                  max_size: Optional[int]) -> None:
+    strategy = relational.tables(dialect=dialect,
+                                 metadatas=strategies.metadatas,
+                                 min_size=min_size,
+                                 max_size=max_size)
+
+    result = data.draw(strategy)
+
+    assert isinstance(result, Table)
+    assert min_size <= len(result.columns)
+    assert max_size is None or len(result.columns) <= max_size
+    assert not result.columns or result.primary_key
+
+
+@given(strategies.data, strategies.metadatas, strategies.dialects_with_names,
+       strategies.min_tables_sizes, strategies.max_tables_sizes)
+def test_extending(data: DataObject,
+                   metadata: MetaData,
+                   dialect_with_name: Tuple[Dialect, str],
+                   min_size: int,
+                   max_size: Optional[int]) -> None:
+    dialect, name = dialect_with_name
+
+    strategy = relational.tables(dialect=dialect,
+                                 metadatas=strategies.just(metadata),
+                                 names=strategies.just(name),
+                                 min_size=min_size,
+                                 max_size=max_size,
+                                 extend_existing=strategies.just(True))
+
+    first_result = data.draw(strategy)
+    second_result = data.draw(strategy)
+
+    assert first_result == second_result
diff --git a/tests/strategies/__init__.py b/tests/strategies/__init__.py
deleted file mode 100644
index 87ddee9..0000000
--- a/tests/strategies/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from .dialectic import dialects
-from .schema import metadatas
-from .utils import (data,
-                    max_sizes,
-                    min_sizes)
diff --git a/tests/strategies/dialectic.py b/tests/strategies/dialectic.py
deleted file mode 100644
index 44ab95c..0000000
--- a/tests/strategies/dialectic.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from hypothesis import strategies
-from sqlalchemy.dialects import (mysql,
-                                 postgresql,
-                                 sqlite)
-
-from hypothesis_sqlalchemy import dialectic
-
-dialects = (strategies.just(dialectic.default)
-            | strategies.one_of([strategies.builds(dialect)
-                                 for dialect in (mysql.dialect,
-                                                 postgresql.dialect,
-                                                 sqlite.dialect)]))
diff --git a/tests/strategies/schema.py b/tests/strategies/schema.py
deleted file mode 100644
index 10bce11..0000000
--- a/tests/strategies/schema.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from hypothesis import strategies
-from sqlalchemy import MetaData
-
-metadatas = strategies.builds(MetaData)
diff --git a/tests/strategies/utils.py b/tests/strategies/utils.py
deleted file mode 100644
index ea96ea7..0000000
--- a/tests/strategies/utils.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from hypothesis import strategies
-
-data = strategies.data()
-MAX_SIZE = 100
-MAX_MIN_SIZE = MAX_SIZE // 2
-min_sizes = strategies.integers(0, MAX_MIN_SIZE)
-max_sizes = strategies.none() | strategies.integers(MAX_MIN_SIZE + 1, MAX_SIZE)
diff --git a/tests/tabular_tests/__init__.py b/tests/tabular_tests/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/tabular_tests/base_tests/__init__.py b/tests/tabular_tests/base_tests/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/tabular_tests/base_tests/strategies.py b/tests/tabular_tests/base_tests/strategies.py
deleted file mode 100644
index 3e26894..0000000
--- a/tests/tabular_tests/base_tests/strategies.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from typing import Tuple
-
-from hypothesis import strategies
-from sqlalchemy.engine import Dialect
-
-from hypothesis_sqlalchemy.hints import Strategy
-from hypothesis_sqlalchemy.utils import to_sql_identifiers
-from tests.strategies import (data,
-                              dialects,
-                              max_sizes,
-                              metadatas,
-                              min_sizes)
-
-data = data
-dialects = dialects
-just = strategies.just
-
-
-def to_dialects_with_names(dialect: Dialect) -> Strategy[Tuple[Dialect, str]]:
-    return strategies.tuples(just(dialect), to_sql_identifiers(dialect))
-
-
-dialects_with_names = dialects.flatmap(to_dialects_with_names)
-min_sizes = min_sizes
-max_sizes = max_sizes
-metadatas = metadatas
diff --git a/tests/tabular_tests/base_tests/test_factory.py b/tests/tabular_tests/base_tests/test_factory.py
deleted file mode 100644
index 825bb22..0000000
--- a/tests/tabular_tests/base_tests/test_factory.py
+++ /dev/null
@@ -1,70 +0,0 @@
-from typing import (Optional,
-                    Tuple)
-
-from hypothesis import given
-from sqlalchemy.engine import Dialect
-from sqlalchemy.schema import (MetaData,
-                               Table)
-
-from hypothesis_sqlalchemy.hints import Strategy
-from hypothesis_sqlalchemy.tabular import factory
-from tests.utils import DataObject
-from . import strategies
-
-
-@given(strategies.dialects,
-       strategies.metadatas,
-       strategies.min_sizes, strategies.max_sizes)
-def test_basic(dialect: Dialect,
-               metadata: MetaData,
-               min_size: int,
-               max_size: Optional[int]) -> None:
-    result = factory(dialect=dialect,
-                     metadata=metadata,
-                     min_size=min_size,
-                     max_size=max_size)
-
-    assert isinstance(result, Strategy)
-
-
-@given(strategies.data, strategies.dialects,
-       strategies.metadatas,
-       strategies.min_sizes, strategies.max_sizes)
-def test_examples(data: DataObject,
-                  dialect: Dialect,
-                  metadata: MetaData,
-                  min_size: int,
-                  max_size: Optional[int]) -> None:
-    strategy = factory(dialect=dialect,
-                       metadata=metadata,
-                       min_size=min_size,
-                       max_size=max_size)
-
-    result = data.draw(strategy)
-
-    assert isinstance(result, Table)
-    assert min_size <= len(result.columns)
-    assert max_size is None or len(result.columns) <= max_size
-    assert not result.columns or result.primary_key
-
-
-@given(strategies.data, strategies.dialects_with_names,
-       strategies.metadatas, strategies.min_sizes, strategies.max_sizes)
-def test_extending(data: DataObject,
-                   dialect_with_name: Tuple[Dialect, str],
-                   metadata: MetaData,
-                   min_size: int,
-                   max_size: Optional[int]) -> None:
-    dialect, name = dialect_with_name
-
-    strategy = factory(dialect=dialect,
-                       metadata=metadata,
-                       names=strategies.just(name),
-                       min_size=min_size,
-                       max_size=max_size,
-                       extend_existing=strategies.just(True))
-
-    first_result = data.draw(strategy)
-    second_result = data.draw(strategy)
-
-    assert first_result == second_result
diff --git a/tests/tabular_tests/records_tests/__init__.py b/tests/tabular_tests/records_tests/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/tabular_tests/records_tests/strategies.py b/tests/tabular_tests/records_tests/strategies.py
deleted file mode 100644
index 0687b03..0000000
--- a/tests/tabular_tests/records_tests/strategies.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from functools import partial
-from typing import (Any,
-                    Dict,
-                    Tuple)
-
-from hypothesis import strategies
-from sqlalchemy.schema import (Column,
-                               Table)
-
-from hypothesis_sqlalchemy import tabular
-from hypothesis_sqlalchemy.columnar import values
-from hypothesis_sqlalchemy.hints import Strategy
-from tests.strategies import (data,
-                              dialects,
-                              max_sizes,
-                              metadatas,
-                              min_sizes)
-
-data = data
-min_sizes = min_sizes
-max_sizes = max_sizes
-
-tables = strategies.tuples(dialects, metadatas).flatmap(
-        lambda dialect_with_metadata: tabular.factory(
-                dialect=dialect_with_metadata[0],
-                metadata=dialect_with_metadata[1]))
-
-
-def fix_columns_values(table: Table
-                       ) -> Strategy[Tuple[Table, Dict[str, Strategy[Any]]]]:
-    def to_item(column: Column) -> Tuple[str, Strategy[Any]]:
-        return column.name, values.factory(column)
-
-    if table.columns:
-        fixed_columns_values = (strategies.sets(
-                strategies.sampled_from(list(table.columns)))
-                                .map(partial(map, to_item))
-                                .map(dict))
-    else:
-        fixed_columns_values = strategies.builds(dict)
-    return strategies.tuples(strategies.just(table), fixed_columns_values)
-
-
-tables_with_fixed_columns_values = tables.flatmap(fix_columns_values)
diff --git a/tests/utils.py b/tests/utils.py
index c7140bf..70ccd36 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -5,23 +5,22 @@
                     Tuple)
 from uuid import UUID
 
+from hypothesis.strategies import DataObject
 from sqlalchemy import (Column,
                         Table)
 from sqlalchemy.dialects import postgresql
 
-from hypothesis_sqlalchemy import constrained
-from hypothesis_sqlalchemy.hints import RecordType
-
-try:
-    from hypothesis.strategies import DataObject
-except ImportError:
-    from hypothesis._strategies import DataObject
+from hypothesis_sqlalchemy.core import table_constraints
+from hypothesis_sqlalchemy.hints import Record
 
 Bounds = Tuple[int, Optional[int]]
 DataObject = DataObject
 
+MAX_SIZE = 100
+MAX_MIN_SIZE = MAX_SIZE // 2
+
 
-def table_record_is_valid(table_record: RecordType,
+def table_record_is_valid(table_record: Record,
                           *,
                           table: Table) -> bool:
     return all(coordinate is None
@@ -37,7 +36,7 @@ def to_python_type(column: Column) -> type:
     return column_type.python_type
 
 
-def records_satisfy_table_constraints(records: List[RecordType],
+def records_satisfy_table_constraints(records: List[Record],
                                       *,
                                       table: Table) -> bool:
     def all_unique(iterable: Iterable[Hashable]) -> bool:
@@ -54,5 +53,5 @@ def all_unique(iterable: Iterable[Hashable]) -> bool:
                                 for column in constraint.columns)
                           for record in records)
                for constraint in table.constraints
-               if isinstance(constraint, constrained.UNIQUE_TYPES)
+               if isinstance(constraint, table_constraints.UNIQUE_TYPES)
                and constraint.columns)