Repository: invokermain/pymssql-utils (language: python)

Issue:
Move to poetry + pyproject.toml

Explore memory related optimisations
Currently querying and cleaning large SQL result sets uses excessive memory.

Potential improvements in this area:
- Make `_execute` call `cursor.fetchmany(10000)` instead of `cursor.fetch_all()`. Move cleaning from DatabaseResult to here and clean in batches.
- Optimise cleaning by checking first non-null value in a column and figuring out it's format.
- Make the data attribute it's own class which can act as a iterator of dicts but internally stores a tuple of tuple

Could be useful for profiling: https://pypi.org/project/memory-profiler/


Discussion / hints:

According to DB-API specs cursor should have a [description attribute](https://www.python.org/dev/peps/pep-0249/#description) which should to some extent clue (data types)[https://www.python.org/dev/peps/pep-0249/#type-objects].
This could help with cleaning optimisations.

Fix patch:
diff --git a/.github/workflows/python-lint.yml b/.github/workflows/python-lint.yml
new file mode 100644
index 0000000..ea2cb4d
--- /dev/null
+++ b/.github/workflows/python-lint.yml
@@ -0,0 +1,24 @@
+  
+name: Lint
+
+on: [push, pull_request]
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up Python 3.9
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.9
+      - name: Setup Poetry
+        uses: abatilo/actions-poetry@v2.1.0
+      - name: Install requirements
+        run: pip install black isort flake8
+      - name: Black
+        run: black . --check
+      - name: Flake8
+        run: flake8 --max-line-length 120 --extend-ignore E203
+      - name: Isort
+        run: isort . -c
diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
index affc6a5..6d26de1 100644
--- a/.github/workflows/python-publish.yml
+++ b/.github/workflows/python-publish.yml
@@ -1,6 +1,3 @@
-# This workflow will upload a Python Package using Twine when a release is created
-# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
-
 name: Upload Python Package
 
 on:
@@ -17,15 +14,12 @@ jobs:
     - name: Set up Python
       uses: actions/setup-python@v2
       with:
-        python-version: '3.x'
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install setuptools wheel twine
-    - name: Build and publish
+        python-version: '3.9'
+    - name: Setup Poetry
+      uses: abatilo/actions-poetry@v2.1.0
+    - name: Build
+      run: poetry build
+    - name: Publish
+      run: poetry publish
       env:
-        TWINE_USERNAME: __token__
-        TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
-      run: |
-        python setup.py sdist bdist_wheel
-        twine upload dist/*
+        POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..39f6121
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,707 @@
+[[package]]
+name = "appdirs"
+version = "1.4.4"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "atomicwrites"
+version = "1.4.0"
+description = "Atomic file writes."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "attrs"
+version = "21.2.0"
+description = "Classes Without Boilerplate"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.extras]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
+docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
+
+[[package]]
+name = "black"
+version = "21.6b0"
+description = "The uncompromising code formatter."
+category = "dev"
+optional = false
+python-versions = ">=3.6.2"
+
+[package.dependencies]
+appdirs = "*"
+click = ">=7.1.2"
+mypy-extensions = ">=0.4.3"
+pathspec = ">=0.8.1,<1"
+regex = ">=2020.1.8"
+toml = ">=0.10.1"
+typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\""}
+typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"]
+python2 = ["typed-ast (>=1.4.2)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "click"
+version = "8.0.1"
+description = "Composable command line interface toolkit"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.4"
+description = "Cross-platform colored terminal text."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "flake8"
+version = "3.9.2"
+description = "the modular source code checker: pep8 pyflakes and co"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[package.dependencies]
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+mccabe = ">=0.6.0,<0.7.0"
+pycodestyle = ">=2.7.0,<2.8.0"
+pyflakes = ">=2.3.0,<2.4.0"
+
+[[package]]
+name = "importlib-metadata"
+version = "4.6.1"
+description = "Read metadata from Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
+zipp = ">=0.5"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+perf = ["ipython"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
+
+[[package]]
+name = "iniconfig"
+version = "1.1.1"
+description = "iniconfig: brain-dead simple config-ini parsing"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "isort"
+version = "5.9.2"
+description = "A Python utility / library to sort Python imports."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1,<4.0"
+
+[package.extras]
+pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
+requirements_deprecated_finder = ["pipreqs", "pip-api"]
+colors = ["colorama (>=0.4.3,<0.5.0)"]
+plugins = ["setuptools"]
+
+[[package]]
+name = "mccabe"
+version = "0.6.1"
+description = "McCabe checker, plugin for flake8"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "mypy-extensions"
+version = "0.4.3"
+description = "Experimental type system extensions for programs checked with the mypy typechecker."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "numpy"
+version = "1.21.0"
+description = "NumPy is the fundamental package for array computing with Python."
+category = "main"
+optional = true
+python-versions = ">=3.7"
+
+[[package]]
+name = "orjson"
+version = "3.5.4"
+description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[[package]]
+name = "packaging"
+version = "21.0"
+description = "Core utilities for Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pyparsing = ">=2.0.2"
+
+[[package]]
+name = "pandas"
+version = "1.1.5"
+description = "Powerful data structures for data analysis, time series, and statistics"
+category = "main"
+optional = true
+python-versions = ">=3.6.1"
+
+[package.dependencies]
+numpy = ">=1.15.4"
+python-dateutil = ">=2.7.3"
+pytz = ">=2017.2"
+
+[package.extras]
+test = ["pytest (>=4.0.2)", "pytest-xdist", "hypothesis (>=3.58)"]
+
+[[package]]
+name = "pathspec"
+version = "0.8.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "pluggy"
+version = "0.13.1"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+
+[[package]]
+name = "py"
+version = "1.10.0"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pycodestyle"
+version = "2.7.0"
+description = "Python style guide checker"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pyflakes"
+version = "2.3.1"
+description = "passive checker of Python programs"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pymssql"
+version = "2.2.1"
+description = "DB-API interface to Microsoft SQL Server for Python. (new Cython-based version)"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pyparsing"
+version = "2.4.7"
+description = "Python parsing module"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "pytest"
+version = "6.2.4"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
+attrs = ">=19.2.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<1.0.0a1"
+py = ">=1.8.2"
+toml = "*"
+
+[package.extras]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+
+[[package]]
+name = "pytest-dotenv"
+version = "0.5.2"
+description = "A py.test plugin that parses environment files before running tests"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pytest = ">=5.0.0"
+python-dotenv = ">=0.9.1"
+
+[[package]]
+name = "pytest-mock"
+version = "3.6.1"
+description = "Thin-wrapper around the mock package for easier use with pytest"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pytest = ">=5.0"
+
+[package.extras]
+dev = ["pre-commit", "tox", "pytest-asyncio"]
+
+[[package]]
+name = "python-dateutil"
+version = "2.8.1"
+description = "Extensions to the standard Python datetime module"
+category = "main"
+optional = true
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "python-dotenv"
+version = "0.18.0"
+description = "Read key-value pairs from a .env file and set them as environment variables"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.extras]
+cli = ["click (>=5.0)"]
+
+[[package]]
+name = "pytz"
+version = "2021.1"
+description = "World timezone definitions, modern and historical"
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "regex"
+version = "2021.7.6"
+description = "Alternative regular expression module, to replace re."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "main"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "typed-ast"
+version = "1.4.3"
+description = "a fork of Python 2 and 3 ast modules with type comment support"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "typing-extensions"
+version = "3.10.0.0"
+description = "Backported and Experimental Type Hints for Python 3.5+"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "zipp"
+version = "3.5.0"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
+
+[extras]
+all = ["orjson", "pandas"]
+json = ["orjson"]
+pandas = ["pandas"]
+
+[metadata]
+lock-version = "1.1"
+python-versions = ">=3.7,<=3.10"
+content-hash = "acb1add77cec37cc280d9c698776ae3d6a691cd7c79c9d27f55a630a996b61c9"
+
+[metadata.files]
+appdirs = [
+    {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
+    {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
+]
+atomicwrites = [
+    {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
+    {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
+]
+attrs = [
+    {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
+    {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
+]
+black = [
+    {file = "black-21.6b0-py3-none-any.whl", hash = "sha256:dfb8c5a069012b2ab1e972e7b908f5fb42b6bbabcba0a788b86dc05067c7d9c7"},
+    {file = "black-21.6b0.tar.gz", hash = "sha256:dc132348a88d103016726fe360cb9ede02cecf99b76e3660ce6c596be132ce04"},
+]
+click = [
+    {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
+    {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"},
+]
+colorama = [
+    {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
+    {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
+]
+flake8 = [
+    {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
+    {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
+]
+importlib-metadata = [
+    {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"},
+    {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"},
+]
+iniconfig = [
+    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
+    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+]
+isort = [
+    {file = "isort-5.9.2-py3-none-any.whl", hash = "sha256:eed17b53c3e7912425579853d078a0832820f023191561fcee9d7cae424e0813"},
+    {file = "isort-5.9.2.tar.gz", hash = "sha256:f65ce5bd4cbc6abdfbe29afc2f0245538ab358c14590912df638033f157d555e"},
+]
+mccabe = [
+    {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
+    {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
+]
+mypy-extensions = [
+    {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
+    {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
+]
+numpy = [
+    {file = "numpy-1.21.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d5caa946a9f55511e76446e170bdad1d12d6b54e17a2afe7b189112ed4412bb8"},
+    {file = "numpy-1.21.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ac4fd578322842dbda8d968e3962e9f22e862b6ec6e3378e7415625915e2da4d"},
+    {file = "numpy-1.21.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:598fe100b2948465cf3ed64b1a326424b5e4be2670552066e17dfaa67246011d"},
+    {file = "numpy-1.21.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c55407f739f0bfcec67d0df49103f9333edc870061358ac8a8c9e37ea02fcd2"},
+    {file = "numpy-1.21.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:75579acbadbf74e3afd1153da6177f846212ea2a0cc77de53523ae02c9256513"},
+    {file = "numpy-1.21.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cc367c86eb87e5b7c9592935620f22d13b090c609f1b27e49600cd033b529f54"},
+    {file = "numpy-1.21.0-cp37-cp37m-win32.whl", hash = "sha256:d89b0dc7f005090e32bb4f9bf796e1dcca6b52243caf1803fdd2b748d8561f63"},
+    {file = "numpy-1.21.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eda2829af498946c59d8585a9fd74da3f810866e05f8df03a86f70079c7531dd"},
+    {file = "numpy-1.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1a784e8ff7ea2a32e393cc53eb0003eca1597c7ca628227e34ce34eb11645a0e"},
+    {file = "numpy-1.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bba474a87496d96e61461f7306fba2ebba127bed7836212c360f144d1e72ac54"},
+    {file = "numpy-1.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fd0a359c1c17f00cb37de2969984a74320970e0ceef4808c32e00773b06649d9"},
+    {file = "numpy-1.21.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e4d5a86a5257843a18fb1220c5f1c199532bc5d24e849ed4b0289fb59fbd4d8f"},
+    {file = "numpy-1.21.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:620732f42259eb2c4642761bd324462a01cdd13dd111740ce3d344992dd8492f"},
+    {file = "numpy-1.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9205711e5440954f861ceeea8f1b415d7dd15214add2e878b4d1cf2bcb1a914"},
+    {file = "numpy-1.21.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ad09f55cc95ed8d80d8ab2052f78cc21cb231764de73e229140d81ff49d8145e"},
+    {file = "numpy-1.21.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a1f2fb2da242568af0271455b89aee0f71e4e032086ee2b4c5098945d0e11cf6"},
+    {file = "numpy-1.21.0-cp38-cp38-win32.whl", hash = "sha256:e58ddb53a7b4959932f5582ac455ff90dcb05fac3f8dcc8079498d43afbbde6c"},
+    {file = "numpy-1.21.0-cp38-cp38-win_amd64.whl", hash = "sha256:d2910d0a075caed95de1a605df00ee03b599de5419d0b95d55342e9a33ad1fb3"},
+    {file = "numpy-1.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a290989cd671cd0605e9c91a70e6df660f73ae87484218e8285c6522d29f6e38"},
+    {file = "numpy-1.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3537b967b350ad17633b35c2f4b1a1bbd258c018910b518c30b48c8e41272717"},
+    {file = "numpy-1.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc6c650f8700ce1e3a77668bb7c43e45c20ac06ae00d22bdf6760b38958c883"},
+    {file = "numpy-1.21.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:709884863def34d72b183d074d8ba5cfe042bc3ff8898f1ffad0209161caaa99"},
+    {file = "numpy-1.21.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bebab3eaf0641bba26039fb0b2c5bf9b99407924b53b1ea86e03c32c64ef5aef"},
+    {file = "numpy-1.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf680682ad0a3bef56dae200dbcbac2d57294a73e5b0f9864955e7dd7c2c2491"},
+    {file = "numpy-1.21.0-cp39-cp39-win32.whl", hash = "sha256:d95d16204cd51ff1a1c8d5f9958ce90ae190be81d348b514f9be39f878b8044a"},
+    {file = "numpy-1.21.0-cp39-cp39-win_amd64.whl", hash = "sha256:2ba579dde0563f47021dcd652253103d6fd66165b18011dce1a0609215b2791e"},
+    {file = "numpy-1.21.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3c40e6b860220ed862e8097b8f81c9af6d7405b723f4a7af24a267b46f90e461"},
+    {file = "numpy-1.21.0.zip", hash = "sha256:e80fe25cba41c124d04c662f33f6364909b985f2eb5998aaa5ae4b9587242cce"},
+]
+orjson = [
+    {file = "orjson-3.5.4-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:cc687744ee2707ac68467273c4bf371b4c73c50c412bd0053ae8357ad380884e"},
+    {file = "orjson-3.5.4-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:12f45867b0de52487ce2d739cb7f0d7a912ddec897a9fd1781173285e66334d0"},
+    {file = "orjson-3.5.4-cp36-cp36m-macosx_10_7_x86_64.whl", hash = "sha256:50e97976f6a94076c0f99efb05782ea102c64e4d392160ba44bd519d5324185e"},
+    {file = "orjson-3.5.4-cp36-cp36m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:66dba60d015396391012beeb1543cb78b16b96e7ceb0045cddac03c08cdea6fa"},
+    {file = "orjson-3.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2e5b550981843d5737e76b773e0ab0a8f10c6a519aadd0f1edc66b3362afd9c"},
+    {file = "orjson-3.5.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48e93a1297f5021457c50cbeca72ef763fb481509c8d10b1eae41e6aa7350173"},
+    {file = "orjson-3.5.4-cp36-none-win_amd64.whl", hash = "sha256:2ab6607a104efba1ed8994095c417555712a727290426249961bb75deef80d7e"},
+    {file = "orjson-3.5.4-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:486cf365bae0a0b6a3a7d0920519be4c0c293d8ddaa3882eb2a06253c427c1fa"},
+    {file = "orjson-3.5.4-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:ea9657b3662105180a959b25368b7309827133aef3df7ef2bdd18aebdc1edec2"},
+    {file = "orjson-3.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b2a0f926a05ebe3f90da6aaff406f0ab1507d6fc6c5e2202a84fc64d2d0f167"},
+    {file = "orjson-3.5.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57d38172b3b010efa5d2bd83df612353028570fc3fc5cecba743df98624c43bf"},
+    {file = "orjson-3.5.4-cp37-none-win_amd64.whl", hash = "sha256:945143f8e88c57cf105418c882c8dd998bac24a4425dc17b7ea2fcf3c8edeedc"},
+    {file = "orjson-3.5.4-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:432cd966bae77956e26ecc8f6c6ac9bbd2d108593c70f388305c3cb1990a1614"},
+    {file = "orjson-3.5.4-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:7ab65d949318c13111432d222f2bad7e1990f482fb80c0704edf3b5c419d3a8b"},
+    {file = "orjson-3.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b76528ae585c7de70f466f8cc60798507c7b2ce1f15a6bb127de68b5ebfb8e42"},
+    {file = "orjson-3.5.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab65e7f1f5fa3bf45cac52579e481cc5f67af70539b1f2d806ce58e8907bee8b"},
+    {file = "orjson-3.5.4-cp38-none-win_amd64.whl", hash = "sha256:6844fb152d9449405fb4f9f930d1ae98a893539025b22f3b22b8a85b6c86edce"},
+    {file = "orjson-3.5.4-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:f4ef393053ef9d928def45468f84b8a850624c25e6960285b97ab5cfe03d5e45"},
+    {file = "orjson-3.5.4-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:4c91dcc78a1e9022f8b08a20dca7e3b517582173e468a04193f0309025910496"},
+    {file = "orjson-3.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:751858f4b22e43d2a68df876b414ec2a988ceef326f520b372f5695b3937b533"},
+    {file = "orjson-3.5.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d39eea5bb3387e0dda3035bc7befca9e54cd707c636e9831b8814db1569d3c3"},
+    {file = "orjson-3.5.4-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:872eae46544f47fd94ee8f433496a428bf170fb41fbacfe72cd3a15af55ecfff"},
+    {file = "orjson-3.5.4-cp39-none-win_amd64.whl", hash = "sha256:d94f490da4e2f2f31e21acd1df8d6b2a8ee37e9872ef81b5a50e94c35d8f8c25"},
+    {file = "orjson-3.5.4.tar.gz", hash = "sha256:ff518ad10adf5fdefe20e1098b55710d73ac6774bd6840e6edb2a3b55d640240"},
+]
+packaging = [
+    {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
+    {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
+]
+pandas = [
+    {file = "pandas-1.1.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:bf23a3b54d128b50f4f9d4675b3c1857a688cc6731a32f931837d72effb2698d"},
+    {file = "pandas-1.1.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5a780260afc88268a9d3ac3511d8f494fdcf637eece62fb9eb656a63d53eb7ca"},
+    {file = "pandas-1.1.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:b61080750d19a0122469ab59b087380721d6b72a4e7d962e4d7e63e0c4504814"},
+    {file = "pandas-1.1.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:0de3ddb414d30798cbf56e642d82cac30a80223ad6fe484d66c0ce01a84d6f2f"},
+    {file = "pandas-1.1.5-cp36-cp36m-win32.whl", hash = "sha256:70865f96bb38fec46f7ebd66d4b5cfd0aa6b842073f298d621385ae3898d28b5"},
+    {file = "pandas-1.1.5-cp36-cp36m-win_amd64.whl", hash = "sha256:19a2148a1d02791352e9fa637899a78e371a3516ac6da5c4edc718f60cbae648"},
+    {file = "pandas-1.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26fa92d3ac743a149a31b21d6f4337b0594b6302ea5575b37af9ca9611e8981a"},
+    {file = "pandas-1.1.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c16d59c15d946111d2716856dd5479221c9e4f2f5c7bc2d617f39d870031e086"},
+    {file = "pandas-1.1.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3be7a7a0ca71a2640e81d9276f526bca63505850add10206d0da2e8a0a325dae"},
+    {file = "pandas-1.1.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:573fba5b05bf2c69271a32e52399c8de599e4a15ab7cec47d3b9c904125ab788"},
+    {file = "pandas-1.1.5-cp37-cp37m-win32.whl", hash = "sha256:21b5a2b033380adbdd36b3116faaf9a4663e375325831dac1b519a44f9e439bb"},
+    {file = "pandas-1.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:24c7f8d4aee71bfa6401faeba367dd654f696a77151a8a28bc2013f7ced4af98"},
+    {file = "pandas-1.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2860a97cbb25444ffc0088b457da0a79dc79f9c601238a3e0644312fcc14bf11"},
+    {file = "pandas-1.1.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5008374ebb990dad9ed48b0f5d0038124c73748f5384cc8c46904dace27082d9"},
+    {file = "pandas-1.1.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2c2f7c670ea4e60318e4b7e474d56447cf0c7d83b3c2a5405a0dbb2600b9c48e"},
+    {file = "pandas-1.1.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0a643bae4283a37732ddfcecab3f62dd082996021b980f580903f4e8e01b3c5b"},
+    {file = "pandas-1.1.5-cp38-cp38-win32.whl", hash = "sha256:5447ea7af4005b0daf695a316a423b96374c9c73ffbd4533209c5ddc369e644b"},
+    {file = "pandas-1.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:4c62e94d5d49db116bef1bd5c2486723a292d79409fc9abd51adf9e05329101d"},
+    {file = "pandas-1.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:731568be71fba1e13cae212c362f3d2ca8932e83cb1b85e3f1b4dd77d019254a"},
+    {file = "pandas-1.1.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c61c043aafb69329d0f961b19faa30b1dab709dd34c9388143fc55680059e55a"},
+    {file = "pandas-1.1.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2b1c6cd28a0dfda75c7b5957363333f01d370936e4c6276b7b8e696dd500582a"},
+    {file = "pandas-1.1.5-cp39-cp39-win32.whl", hash = "sha256:c94ff2780a1fd89f190390130d6d36173ca59fcfb3fe0ff596f9a56518191ccb"},
+    {file = "pandas-1.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:edda9bacc3843dfbeebaf7a701763e68e741b08fccb889c003b0a52f0ee95782"},
+    {file = "pandas-1.1.5.tar.gz", hash = "sha256:f10fc41ee3c75a474d3bdf68d396f10782d013d7f67db99c0efbfd0acb99701b"},
+]
+pathspec = [
+    {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
+    {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
+]
+pluggy = [
+    {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
+    {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
+]
+py = [
+    {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
+    {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
+]
+pycodestyle = [
+    {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
+    {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
+]
+pyflakes = [
+    {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
+    {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
+]
+pymssql = [
+    {file = "pymssql-2.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6fc382f253365f22286372a32cfeb5d380ad955045ae6f88cf372f758635557a"},
+    {file = "pymssql-2.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1032cc2bafb9edcb309ac526c359387b5d1e4c06c62a92872f8c28681883fe25"},
+    {file = "pymssql-2.2.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ecb95a2301375b4a48ba73fcfd664981d511904e552cfd2c17530ec03a5f502d"},
+    {file = "pymssql-2.2.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:0dafb4b2cf4a1deace503656b917cc5f4dad7d600f7cfc5e16f10918727e369f"},
+    {file = "pymssql-2.2.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:f293d4cb9c03e6d859c8f3afb6fa7a99b30fe8f42eafedfa21358b1d8a420165"},
+    {file = "pymssql-2.2.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9cbad4cd57ef67a32e55ead549da73ad5730ae5da0ace2a2206451897716ccfa"},
+    {file = "pymssql-2.2.1-cp36-cp36m-manylinux_2_24_i686.whl", hash = "sha256:8ec909a71b239243d55fe59cbf8e25c8ef5dce3e3bac58c3aa0eaddaeffcd790"},
+    {file = "pymssql-2.2.1-cp36-cp36m-manylinux_2_24_x86_64.whl", hash = "sha256:5a23654683698a26a8d45b239e19820ab9e4239aa52293e81a9ba9ddddf3006d"},
+    {file = "pymssql-2.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:8bc05f632641e51d5782761622eee2538444fdbb39c9c0750ad76d8f650e6898"},
+    {file = "pymssql-2.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e8deccee711d1cdc8b6010622ced9d064bc943e5a0540616348768d4adc6e95e"},
+    {file = "pymssql-2.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:379fcd412cf34a493b49cefd689f5f9f0878af57ce130dc47bd5dead8bb67f80"},
+    {file = "pymssql-2.2.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:a579ae07dc7528385bf339261308e396bd483e68e770f84feb31032e18c72d2b"},
+    {file = "pymssql-2.2.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d4ff41cc76c3851e0410feaf96f7760c2eeb9fb84597baf47d7431124e66d6f8"},
+    {file = "pymssql-2.2.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:f3201b11dafd073183b6237667d2224a3a1f1d1ef41c4f351b48a0779127ac40"},
+    {file = "pymssql-2.2.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:7301d2da53b436e07f23f847172d9da8c1f506390b0ad4d32ad73527b67728ce"},
+    {file = "pymssql-2.2.1-cp37-cp37m-manylinux_2_24_i686.whl", hash = "sha256:39c61b8104fbd4d9b1e74c7f2c90c3871d0f1ef82e1be177d544b207e44f1b4c"},
+    {file = "pymssql-2.2.1-cp37-cp37m-manylinux_2_24_x86_64.whl", hash = "sha256:40d355603dd31258dd6de28351f504fcc4d70b715637c01ffb384fd9ba0189df"},
+    {file = "pymssql-2.2.1-cp37-cp37m-win32.whl", hash = "sha256:3638ca82915d663acc7f898290bcc3e94a236ba56530b6959f821334977bf56e"},
+    {file = "pymssql-2.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:42cbfc756c9b29f3b974a3352c744c90c280c905f7cadda2dfcb6e46ca96b3f6"},
+    {file = "pymssql-2.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8319bbbcaa8b3ca89f897e808ca5b48d9c64d3231408b1f0e1ce4400bcd7d6ae"},
+    {file = "pymssql-2.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:dbfc4b97e6e6ce776461570d79c738116c20e2c2c01ed5f3afe03ec6740dacce"},
+    {file = "pymssql-2.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c841921a1729035f8fe056c76b1202985f203b4ff589904d26cb33885271cdad"},
+    {file = "pymssql-2.2.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:2b865ba3644e08d2ce10e2b06256dadb64677e1c81651224c4b1cabea2be2fd8"},
+    {file = "pymssql-2.2.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4465d1776d3cf36d17b3058d155ec8a581b1bc9ecf29740ea07254ea72248b73"},
+    {file = "pymssql-2.2.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0b4c2b4b3b13f866a037c905220c6b3691c20d19a18ff6f6457351f15620db46"},
+    {file = "pymssql-2.2.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:cead77d3ac65c1e2910c3c082760148fd275815d5df4b13549e485fc305f4768"},
+    {file = "pymssql-2.2.1-cp38-cp38-manylinux_2_24_i686.whl", hash = "sha256:c956c93f57d69c4551b442dec2b6eed1e4a0e185ae913ec52305e4b66d4903b7"},
+    {file = "pymssql-2.2.1-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:fd2c537c798d2e0166cdebd2dd29f1a676fc508d664cf1fefe69ae51c3004a82"},
+    {file = "pymssql-2.2.1-cp38-cp38-win32.whl", hash = "sha256:a82ba1f66f6537a42c54704d0da171a1541a3d88225d6e9ba6a17712968b0980"},
+    {file = "pymssql-2.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:a9adf3ca9f143dace3d4d52bc056e8a70e5f956c4555a9e204ddf4937d98ba66"},
+    {file = "pymssql-2.2.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:724432260900d1a4dff51da52f276741e143756712fbf285d8b228b2340403a9"},
+    {file = "pymssql-2.2.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:0acfb137c385b14a7b27c6e9c6c2a047e5575c45b85239503086895d8d71819e"},
+    {file = "pymssql-2.2.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:a67b45a91203a35251d427cd35a316e878a27d275f20a6ae815a0db6bb0769de"},
+    {file = "pymssql-2.2.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:844883d3543d8da5037ef8a925a75d0c1de3972d2f9754f063c87c7d7f580b68"},
+    {file = "pymssql-2.2.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:d28cec093e53f7a7e6242683f46418a2c0348eb6d22c5008d442028b6207e204"},
+    {file = "pymssql-2.2.1-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:fe53b55ef9572ac3d5f34f2acd3adf1ec9ee7d1f637bc59bdd58dc06c4ef4ed7"},
+    {file = "pymssql-2.2.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:6ef017731bd6e1ffaed4637e767c1983750f8b63de6f8e6ed1e700578a73b4a3"},
+    {file = "pymssql-2.2.1-cp39-cp39-manylinux_2_24_i686.whl", hash = "sha256:c788534ff389d31742f71a8fae2922b6a498bcde8e3c6c650f15495f33e50e44"},
+    {file = "pymssql-2.2.1-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:6b845d1c940521d3ec344906208fbc001bef4913e780ae799ad06f4d9e197f8b"},
+    {file = "pymssql-2.2.1-cp39-cp39-win32.whl", hash = "sha256:13d0cce2716d34e8486cdd83602f72e13a1261de1f50a98ddf161d207e0f3ec0"},
+    {file = "pymssql-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:637432b07f9b6e5f0b1d77cec8b32b6d5ee182033c6c7b47ef2d5a3d9233ee4c"},
+    {file = "pymssql-2.2.1.tar.gz", hash = "sha256:7238530b45955f793d235664cc6f104d8b8fbcdeb6fa394dd22d8fe43748bab0"},
+]
+pyparsing = [
+    {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
+    {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
+]
+pytest = [
+    {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"},
+    {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"},
+]
+pytest-dotenv = [
+    {file = "pytest-dotenv-0.5.2.tar.gz", hash = "sha256:2dc6c3ac6d8764c71c6d2804e902d0ff810fa19692e95fe138aefc9b1aa73732"},
+    {file = "pytest_dotenv-0.5.2-py3-none-any.whl", hash = "sha256:40a2cece120a213898afaa5407673f6bd924b1fa7eafce6bda0e8abffe2f710f"},
+]
+pytest-mock = [
+    {file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"},
+    {file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"},
+]
+python-dateutil = [
+    {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
+    {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
+]
+python-dotenv = [
+    {file = "python-dotenv-0.18.0.tar.gz", hash = "sha256:effaac3c1e58d89b3ccb4d04a40dc7ad6e0275fda25fd75ae9d323e2465e202d"},
+    {file = "python_dotenv-0.18.0-py2.py3-none-any.whl", hash = "sha256:dd8fe852847f4fbfadabf6183ddd4c824a9651f02d51714fa075c95561959c7d"},
+]
+pytz = [
+    {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
+    {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"},
+]
+regex = [
+    {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"},
+    {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"},
+    {file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"},
+    {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"},
+    {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"},
+    {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"},
+    {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"},
+    {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"},
+    {file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"},
+    {file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"},
+    {file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"},
+    {file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"},
+    {file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"},
+    {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"},
+    {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"},
+    {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"},
+    {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"},
+    {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"},
+    {file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"},
+    {file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"},
+    {file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"},
+    {file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"},
+    {file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"},
+    {file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"},
+    {file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"},
+    {file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"},
+    {file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"},
+    {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"},
+    {file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"},
+    {file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"},
+    {file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"},
+    {file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"},
+    {file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"},
+    {file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"},
+    {file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"},
+    {file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"},
+    {file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"},
+    {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"},
+    {file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"},
+    {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"},
+    {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"},
+]
+six = [
+    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+toml = [
+    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+typed-ast = [
+    {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
+    {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"},
+    {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"},
+    {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"},
+    {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"},
+    {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"},
+    {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"},
+    {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"},
+    {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"},
+    {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"},
+    {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"},
+    {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"},
+    {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"},
+    {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"},
+    {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"},
+    {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"},
+    {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
+    {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
+]
+typing-extensions = [
+    {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
+    {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"},
+    {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"},
+]
+zipp = [
+    {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"},
+    {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"},
+]
diff --git a/pymssqlutils/__init__.py b/pymssqlutils/__init__.py
index 3d850dc..671e303 100644
--- a/pymssqlutils/__init__.py
+++ b/pymssqlutils/__init__.py
@@ -1,11 +1,11 @@
-from .databaseresult import DatabaseResult, DatabaseError
+from .databaseresult import DatabaseError, DatabaseResult
 from .methods import (
     execute,
-    query,
-    to_sql_list,
     model_to_values,
-    substitute_parameters,
+    query,
     set_connection_details,
+    substitute_parameters,
+    to_sql_list,
 )
 
 __all__ = [
diff --git a/pymssqlutils/databaseresult.py b/pymssqlutils/databaseresult.py
index 2421dfc..3bbabca 100644
--- a/pymssqlutils/databaseresult.py
+++ b/pymssqlutils/databaseresult.py
@@ -1,79 +1,69 @@
 import logging
-import re
 import struct
-from datetime import date, time, datetime, timedelta, timezone
+from datetime import date, datetime, time, timedelta, timezone
 from decimal import Decimal
-from typing import Dict, Any, List, Union
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
 
 import pymssql as sql
-from dateutil.parser import isoparse
-
-from pymssqlutils.helpers import SQLParameter
 
 logger = logging.getLogger(__name__)
 
-date_regex = re.compile(r"(\d{4})-(\d{2})-(\d{2})")
-time_regex = re.compile(r"(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6})\d?)?")
-datetime2_regex = re.compile(r"\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}.\d{7}")
-datetimeoffset_regex = re.compile(
-    r"(\d{4}-\d{2}-\d{2})\s(\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?)\d?\s([+-]\d{2}:\d{2})"
-)
-
-
-def _is_time(x):
-    return re.fullmatch(time_regex, x)
 
+def _parse_datetimeoffset_from_bytes(item: bytes) -> datetime:
+    # fix for certain versions of FreeTDS driver returning Datetimeoffset as bytes
+    microseconds, days, tz, _ = struct.unpack("QIhH", item)
+    return datetime(
+        1900, 1, 1, 0, 0, 0, tzinfo=timezone(timedelta(minutes=tz))
+    ) + timedelta(days=days, minutes=tz, microseconds=microseconds / 10)
 
-def _is_date(x):
-    return re.fullmatch(date_regex, x)
 
+def identity(x):
+    return x
 
-def _is_datetimeoffset(x):
-    return re.fullmatch(datetimeoffset_regex, x)
 
+def cursor_generator(cursor_):
+    while True:
+        out = cursor_.fetchmany(size=10000)
+        if out:
+            yield out
+        return
 
-def _is_datetime2(x):
-    return re.fullmatch(datetime2_regex, x)
 
+def _get_data_mapper(sql_type_hint: int, item: Any) -> Callable[[Any], Any]:
+    if sql_type_hint == 1:  # STRING: str
+        if isinstance(item, str):
+            return identity
 
-def _clean(item: Any) -> Any:
-    if isinstance(item, str):
-        # handle various date/time returns that pymssql doesn't handle
-        match = _is_date(item)
-        if match:
-            return date(int(match[1]), int(match[2]), int(match[3]))
-
-        match = _is_time(item)
-        if match:
-            return time(
-                int(match[1]),
-                int(match[2]),
-                int(match[3]),
-                microsecond=int(match[4]) if match[4] else 0,
-            )
-
-        match = _is_datetime2(item)
-        if match:
-            return isoparse(item[:-1])
-
-        match = _is_datetimeoffset(item)
-        if match:
-            return isoparse(f"{match[1]}T{match[2]}" + (match[3] or ""))
-
-    if isinstance(item, bytes):
+    if sql_type_hint == 2:  # BINARY: bytes, datetime, time, date
+        if isinstance(item, datetime):
+            return identity
+        if isinstance(item, date):
+            return identity
+        if isinstance(item, time):
+            return identity
         try:
-            # fix for certain versions of FreeTDS driver returning Datetimeoffset as bytes
-            microseconds, days, tz, _ = struct.unpack("QIhH", item)
-            return datetime(
-                1900, 1, 1, 0, 0, 0, tzinfo=timezone(timedelta(minutes=tz))
-            ) + timedelta(days=days, minutes=tz, microseconds=microseconds / 10)
+            _parse_datetimeoffset_from_bytes(item)
+            return _parse_datetimeoffset_from_bytes
         except struct.error:
-            return item
+            # it's not a datetime
+            return identity
+
+    if sql_type_hint == 3:  # NUMBER: int, float
+        if isinstance(item, int):
+            return identity
+        if isinstance(item, float):
+            return identity
+
+    if sql_type_hint == 4:  # DATETIME: datetime
+        if isinstance(item, datetime):
+            return identity
 
-    if isinstance(item, Decimal):
-        return float(item)
+    if sql_type_hint == 5:  # DECIMAL: float (forced), int
+        # warning: bigint is returned as Decimal type
+        if isinstance(item, Decimal):
+            return float
 
-    return item
+    raise ValueError(f"unhandled type ({sql_type_hint}, {item}, {type(item)})")
 
 
 class DatabaseError(Exception):
@@ -84,31 +74,75 @@ class DatabaseResult:
     ok: bool
     fetch: bool
     commit: bool
-    columns: List[str] = None
-    data: List[Dict[str, SQLParameter]] = None
-    error: sql.Error = None
+    columns: Optional[Tuple[str, ...]]
+    error: sql.Error
+    source_types: Optional[Tuple[int, ...]]
 
     def __init__(
         self,
         ok: bool,
         fetch: bool,
         commit: bool,
-        data: List[Dict] = None,
+        cursor: sql.Cursor = None,
         error: sql.Error = None,
     ):
         self.ok = ok
         self.fetch = fetch
         self.commit = commit
+        self.error = error
+        self.columns = None
+        self.source_types = None
+        self._data_mappers = None
+        self._data = None
+
+        if self.error:
+            return
+
+        if fetch:
+            self.columns = tuple(x[0] for x in cursor.description)
+            self.source_types = tuple(x[1] for x in cursor.description)
+            self._data_mappers = [None] * len(self.source_types)
+            self._data = [
+                item
+                for items in cursor_generator(cursor)
+                for item in self._clean_batch(items)
+            ]
+
+    def _clean_batch(self, items):
+        return tuple(
+            tuple(self._clean_item(e, item) for e, item in enumerate(row))
+            for row in items
+        )
 
-        if error:
-            self.error = error
-        else:
-            if data:
-                self.columns = [*data[0]]
-                self.data = [{k: _clean(v) for k, v in row.items()} for row in data]
-            elif self.fetch:
-                self.data = []
-                self.columns = []
+    def _clean_item(self, idx: int, item: Any):
+        if item is None:
+            return None
+
+        if self._data_mappers[idx] is not None:
+            return self._data_mappers[idx](item)
+
+        data_mapper = _get_data_mapper(self.source_types[idx], item)
+
+        if data_mapper is None:
+            return item
+
+        self._data_mappers[idx] = data_mapper
+        return self._data_mappers[idx](item)
+
+    @property
+    def data(self) -> Optional[List[Dict[str, Tuple[Any, ...]]]]:
+        if self._data is not None:
+            return [
+                {self.columns[e]: item for e, item in enumerate(row)}
+                for row in self._data
+            ]
+        return None
+
+    @property
+    def raw_data(self) -> Optional[List[Tuple[Any, ...]]]:
+        if self._data is not None:
+            return self._data
+        return None
 
     def write_error_to_logger(self, name: str = "unknown") -> None:
         """
@@ -163,7 +197,7 @@ def to_json(self, as_bytes=False) -> Union[bytes, str]:
         :return: Union[bytes, str]
         """
 
-        if self.data is None:
+        if not self._data:
             raise ValueError("DatabaseResult class has no data to cast to DataFrame")
 
         # noinspection PyUnresolvedReferences
diff --git a/pymssqlutils/helpers.py b/pymssqlutils/helpers.py
index e54960f..fcbaf72 100644
--- a/pymssqlutils/helpers.py
+++ b/pymssqlutils/helpers.py
@@ -1,5 +1,5 @@
 from datetime import date, datetime, time
-from typing import Union, Tuple, Dict, Optional
+from typing import Dict, Optional, Tuple, Union
 
 SQLParameter = Optional[Union[str, int, float, date, datetime, time, bool, bytes]]
 SQLParameters = Union[Tuple[SQLParameter, ...], Dict[str, SQLParameter], SQLParameter]
diff --git a/pymssqlutils/methods.py b/pymssqlutils/methods.py
index afddf9b..7e47b4b 100644
--- a/pymssqlutils/methods.py
+++ b/pymssqlutils/methods.py
@@ -1,20 +1,12 @@
 import logging
 import os
 from itertools import zip_longest
-from typing import (
-    Dict,
-    List,
-    Tuple,
-    Union,
-    Iterable,
-    Mapping,
-    Any,
-)
+from typing import Any, Dict, Iterable, List, Tuple, Union
 
 import pymssql as sql
 
 from .databaseresult import DatabaseResult
-from .helpers import SQLParameters, SQLParameter
+from .helpers import SQLParameter, SQLParameters
 
 logger = logging.getLogger(__name__)
 
@@ -216,7 +208,7 @@ def _execute_batched(
     **kwargs,
 ) -> DatabaseResult:
     """
-    This is an internal method and you should call execute() instead
+    This is an internal method, you should call execute() instead
     """
     if parameters:
         fillvalue = (
@@ -239,13 +231,13 @@ def _execute_batched(
             for i in range(0, len(operations), batch_size)
         ]
 
-    with sql.connect(as_dict=True, **_with_conn_details(kwargs)) as cnxn:
+    with sql.connect(**_with_conn_details(kwargs)) as cnxn:
         with cnxn.cursor() as cur:
             for batch in batched:
                 cur.execute(batch)
-            data = cur.fetchall() if fetch else None
+            result = DatabaseResult(ok=True, fetch=fetch, commit=True, cursor=cur)
         cnxn.commit()
-    return DatabaseResult(data=data, ok=True, fetch=fetch, commit=True)
+    return result
 
 
 def _execute(
@@ -256,9 +248,9 @@ def _execute(
     **kwargs,
 ) -> DatabaseResult:
     """
-    This is an internal method and you should call execute() instead
+    This is an internal method, you should call execute() instead
     """
-    with sql.connect(as_dict=True, **kwargs) as cnxn:
+    with sql.connect(**kwargs) as cnxn:
         with cnxn.cursor() as cur:
             if parameters:
                 fillvalue = (
@@ -274,12 +266,12 @@ def _execute(
                 for operation in operations:
                     cur.execute(operation)
 
-            data = cur.fetchall() if fetch else None
+            result = DatabaseResult(ok=True, fetch=fetch, commit=commit, cursor=cur)
 
         if commit:
             cnxn.commit()
 
-    return DatabaseResult(data=data, ok=True, fetch=fetch, commit=commit)
+    return result
 
 
 def to_sql_list(listlike: Iterable[SQLParameter]) -> str:
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..9fb67b5
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,53 @@
+[tool.poetry]
+name = "pymssql-utils"
+version = "0.1.0"
+description = "pymssql-utils is a small library that wraps pymssql to make your life easier."
+authors = ["Tim OSullivan <tim@lanster.dev>"]
+license = "GNU LGPLv2.1"
+homepage = "https://github.com/invokermain/pymssql-utils"
+readme = "README.md"
+classifiers = [
+    "License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)",
+    "Programming Language :: Python :: 3.7",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Topic :: Database",
+    "Typing :: Typed",
+]
+packages = [
+    { include = "pymssqlutils" },
+]
+
+[tool.poetry.dependencies]
+python = ">=3.7,<=3.10"
+pymssql = "~2"
+orjson = { version = ">=3.4.0,<4.0.0", optional = true }
+pandas = { version = ">=1.0.0,<2.0.0", optional = true }
+
+[tool.poetry.dev-dependencies]
+pytest = "~6"
+pytest-mock = ">=3.6.0,<4.0.0"
+pytest-dotenv = ">=0.5.0,<1.0.0"
+black = "^21.6b0"
+flake8 = "^3.9.2"
+isort = "^5.9.2"
+
+[tool.poetry.extras]
+json = ["orjson"]
+pandas = ["pandas"]
+all = ["orjson", "pandas"]
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.isort]
+multi_line_output = 3
+include_trailing_comma = true
+force_grid_wrap = 0
+use_parentheses = true
+ensure_newline_before_comments = true
+line_length = 88
+
+[tool.black]
+line-length = 88
\ No newline at end of file
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 9c4faed..0000000
--- a/setup.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import pathlib
-from setuptools import setup, find_packages
-
-extra_json = ["orjson>=3.4.0,<4.0.0"]
-extra_test = [
-    "pytest-mock>=3.6.0,<4.0.0",
-    "pandas>=1.0.0,<2.0.0",
-    "pytest-dotenv>=0.5.0,<1.0.0",
-]
-extra_all = extra_json + extra_test
-
-setup(
-    name="pymssql-utils",
-    version="0.0.16",
-    description="pymssql-utils is a small library that wraps pymssql to make your life easier.",
-    long_description=(pathlib.Path(__file__).parent / "README.md").read_text(),
-    long_description_content_type="text/markdown",
-    url="https://github.com/invokermain/pymssql-utils",
-    author="Tim OSullivan",
-    author_email="tim@lanster.dev",
-    license="GNU LGPLv2.1",
-    classifiers=[
-        "License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)",
-        "Programming Language :: Python :: 3.6",
-        "Programming Language :: Python :: 3.7",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Topic :: Database",
-        "Typing :: Typed",
-    ],
-    packages=find_packages(),
-    python_requires=">=3.6",
-    include_package_data=True,
-    install_requires=[
-        "pymssql>=2.1.0,<3.0.0",
-        "python-dateutil>=2.8.0,<3.0.0",
-    ],
-    extras_require={
-        "all": extra_all,
-        "json": extra_json,
-        "test": extra_test,
-    },
-)


Test patch:
diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml
index cb7f353..9b91cff 100644
--- a/.github/workflows/python-test.yml
+++ b/.github/workflows/python-test.yml
@@ -1,27 +1,24 @@
-name: Python package
 
-on: [push]
+name: Test
 
-jobs:
-  build:
+on: [push, pull_request]
 
-    runs-on: ubuntu-latest
+jobs:
+  test:
     strategy:
       matrix:
-        python-version: [3.6, 3.7, 3.8, 3.9]
-
+        python-version: [3.7, 3.8, 3.9]
+        os: [ubuntu-latest, macos-latest, windows-latest]
+    runs-on: ${{ matrix.os }}
     steps:
       - uses: actions/checkout@v2
       - name: Set up Python ${{ matrix.python-version }}
         uses: actions/setup-python@v2
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install dependencies
-        run: |
-          python -m pip install --upgrade pip
-          pip3 install pytest
-          # install local package with all extras
-          pip3 install -e .[all]
-      - name: Test with pytest
-        run: |
-          pytest
+      - name: Setup Poetry
+        uses: abatilo/actions-poetry@v2.1.0
+      - name: Install venv
+        run: poetry install --no-root -E all
+      - name: Pytest
+        run: poetry run python -m pytest
diff --git a/tests/helpers.py b/tests/helpers.py
index 8f2a723..d718685 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -1,10 +1,108 @@
-import datetime as dt
+from datetime import date, datetime, time
 from decimal import Decimal
+from typing import Any, Dict, List, Tuple
 
+from pymssql import Cursor
 
-class MockCursor:
-    def __init__(self):
+cursor_description = (
+    ("Col_Int", 3, None, None, None, None, None),
+    ("Col_BigInt", 5, None, None, None, None, None),
+    ("Col_SmallInt", 3, None, None, None, None, None),
+    ("Col_TinyInt", 3, None, None, None, None, None),
+    ("Col_Numeric", 5, None, None, None, None, None),
+    ("Col_Decimal", 5, None, None, None, None, None),
+    ("Col_Float", 3, None, None, None, None, None),
+    ("Col_Real", 3, None, None, None, None, None),
+    ("Col_Varbinary", 2, None, None, None, None, None),
+    ("Col_Binary", 2, None, None, None, None, None),
+    ("Col_Text", 1, None, None, None, None, None),
+    ("Col_Varchar", 1, None, None, None, None, None),
+    ("Col_Char", 1, None, None, None, None, None),
+    ("Col_Ntext", 1, None, None, None, None, None),
+    ("Col_Nvarchar", 1, None, None, None, None, None),
+    ("Col_Nchar", 1, None, None, None, None, None),
+    ("Col_Date", 2, None, None, None, None, None),
+    ("Col_Time1", 2, None, None, None, None, None),
+    ("Col_Time2", 2, None, None, None, None, None),
+    ("Col_Time3", 2, None, None, None, None, None),
+    ("Col_Time4", 2, None, None, None, None, None),
+    ("Col_Time5", 2, None, None, None, None, None),
+    ("Col_Time6", 2, None, None, None, None, None),
+    ("Col_Time7", 2, None, None, None, None, None),
+    ("Col_Smalldatetime", 4, None, None, None, None, None),
+    ("Col_Datetime", 4, None, None, None, None, None),
+    ("Col_Datetime2", 2, None, None, None, None, None),
+    ("Col_Datetimeoffset0", 2, None, None, None, None, None),
+    ("Col_Datetimeoffset1", 2, None, None, None, None, None),
+    ("Col_Datetimeoffset2", 2, None, None, None, None, None),
+    ("Col_Datetimeoffset3", 2, None, None, None, None, None),
+    ("Col_Datetimeoffset4", 2, None, None, None, None, None),
+    ("Col_Datetimeoffset5", 2, None, None, None, None, None),
+    ("Col_Datetimeoffset6", 2, None, None, None, None, None),
+    ("Col_Datetimeoffset7", 2, None, None, None, None, None),
+    ("Col_Null", 3, None, None, None, None, None),
+)
+
+cursor_row = [
+    (
+        1,
+        Decimal("2147483648"),
+        -123,
+        123,
+        Decimal("124"),
+        Decimal("124"),
+        123.55555,
+        123.55554962158203,
+        b"BinaryText",
+        (
+            b"ABCDEF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+            + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+        ),
+        "abc",
+        "abc",
+        "a                             ",
+        "abc",
+        "abc",
+        "a                             ",
+        date(2021, 7, 7),
+        time(9, 49, 17, 900000),
+        time(9, 49, 17, 890000),
+        time(9, 49, 17, 887000),
+        time(9, 49, 17, 887000),
+        time(9, 49, 17, 887000),
+        time(9, 49, 17, 887000),
+        time(9, 49, 17, 887000),
+        datetime(2021, 7, 7, 9, 49),
+        datetime(2021, 7, 7, 9, 49, 17, 887000),
+        datetime(2021, 7, 7, 9, 49, 17, 887000),
+        b"\x00;>\xf1I\x00\x00\x00^\xad\x00\x00<\x00\x00\xe0",
+        b"\xc0\xf8.\xf1I\x00\x00\x00^\xad\x00\x00<\x00\x01\xe0",
+        b" r-\xf1I\x00\x00\x00^\xad\x00\x00<\x00\x02\xe0",
+        b"\xf0\xfc,\xf1I\x00\x00\x00^\xad\x00\x00<\x00\x03\xe0",
+        b"\xf0\xfc,\xf1I\x00\x00\x00^\xad\x00\x00<\x00\x04\xe0",
+        b"T\xfd,\xf1I\x00\x00\x00^\xad\x00\x00<\x00\x05\xe0",
+        b"T\xfd,\xf1I\x00\x00\x00^\xad\x00\x00<\x00\x06\xe0",
+        b"Q\xfd,\xf1I\x00\x00\x00^\xad\x00\x00<\x00\x07\xe0",
+        None,
+    )
+]
+
+
+class MockCursor(Cursor):
+    def __init__(
+        self,
+        row_count: int = 100,
+        description: Tuple[Tuple, ...] = None,
+        row: List[Tuple[Any, ...]] = None,
+    ):
         self.executions = []
+        self.row_count = row_count
+        self.remaining_rows = row_count
+        self.description_ = description
+        self.example_row_ = row
+
+        if self.description_:
+            assert len(self.description_) == len(self.example_row_[0])
 
     def __enter__(self):
         return self
@@ -15,32 +113,23 @@ def __exit__(self, exc_type, exc_val, exc_tb):
     def execute(self, operations, parameters=None):
         self.executions.append((operations, parameters))
 
-    def fetchall(self):
-        return [
-            {
-                "Col_Date": "2021-01-18",
-                "Col_Time0": "22:07:47",
-                "Col_Time1": "22:07:47.1",
-                "Col_Time2": "22:07:47.11",
-                "Col_Time3": "22:07:47.113",
-                "Col_Time4": "22:07:47.1128",
-                "Col_Time5": "22:07:47.11285",
-                "Col_Time6": "22:07:47.112848",
-                "Col_Time7": "22:07:47.1128480",
-                "Col_Smalldatetime": dt.datetime(2021, 1, 18, 22, 8),
-                "Col_Datetime": dt.datetime(2021, 1, 18, 22, 7, 47, 113000),
-                "Col_Datetime2": "2021-01-18 22:07:47.1128480",
-                "Col_Datetimeoffset0": "2021-01-18 22:07:47 +00:00",
-                "Col_Datetimeoffset1": "2021-01-18 22:07:47.1 +00:00",
-                "Col_Datetimeoffset2": "2021-01-18 22:07:47.11 +00:00",
-                "Col_Datetimeoffset3": "2021-01-18 22:07:47.113 +00:00",
-                "Col_Datetimeoffset4": "2021-01-18 22:07:47.1128 +00:00",
-                "Col_Datetimeoffset5": "2021-01-18 22:07:47.11285 +00:00",
-                "Col_Datetimeoffset6": "2021-01-18 22:07:47.112848 +00:00",
-                "Col_Datetimeoffset7": "2021-01-18 22:07:47.1128480 +00:00",
-                "Col_Numeric": Decimal("1.250"),
-            }
-        ]
+    @property
+    def description(self):
+        if self.description_:
+            return self.description_
+        return cursor_description
+
+    def fetchmany(self, size=10000):
+        if self.row_count - size <= 0:
+            return self._get_rows(self.row_count)
+        else:
+            self.row_count -= size
+            return self._get_rows(size)
+
+    def _get_rows(self, size: int):
+        if not self.example_row_:
+            return cursor_row * size
+        return self.example_row_ * size
 
 
 class MockConnection:
@@ -54,3 +143,204 @@ def cursor(self):
     @staticmethod
     def commit():
         pass
+
+
+def generate_fake_data_query(rows=1000, null_percentage=0) -> str:
+    return f"""
+        DECLARE @NullRatio FLOAT = {null_percentage};
+
+        SELECT
+            1 Col_Int,
+            2147483648 Col_BigInt,
+            CAST(-123 AS SMALLINT) Col_SmallInt,
+            CAST(123 AS TINYINT) Col_TinyInt,
+            CAST(123.55555 AS NUMERIC) Col_Numeric,
+            CAST(123.55555 AS DECIMAL) Col_Decimal,
+            CAST(123.55555 AS FLOAT) Col_Float,
+            CAST(123.55555 AS REAL) Col_Real,
+            CONVERT(VARBINARY(MAX), 'BinaryText') Col_Varbinary,
+            CONVERT(BINARY(32), 'ABCDEF') Col_Binary,
+            CAST('abc' AS TEXT) Col_Text,
+            CAST('abc' AS VARCHAR(100)) Col_Varchar,
+            CAST('a' AS CHAR) Col_Char,
+            CAST('abc' AS NTEXT) Col_Ntext,
+            CAST('abc' AS NVARCHAR(100)) Col_Nvarchar,
+            CAST('a' AS NCHAR) Col_Nchar,
+            CAST(GETDATE() AS DATE) Col_Date,
+            CAST(SYSDATETIMEOFFSET() AS time(1)) Col_Time1,
+            CAST(SYSDATETIMEOFFSET() AS time(2)) Col_Time2,
+            CAST(SYSDATETIMEOFFSET() AS time(3)) Col_Time3,
+            CAST(SYSDATETIMEOFFSET() AS time(4)) Col_Time4,
+            CAST(SYSDATETIMEOFFSET() AS time(5)) Col_Time5,
+            CAST(SYSDATETIMEOFFSET() AS time(6)) Col_Time6,
+            CAST(SYSDATETIMEOFFSET() AS time(7)) Col_Time7,
+            CAST(SYSDATETIMEOFFSET() AS Smalldatetime) Col_Smalldatetime,
+            CAST(SYSDATETIMEOFFSET() AS Datetime) Col_Datetime,
+            CAST(SYSDATETIMEOFFSET() AS Datetime2) Col_Datetime2,
+            CAST(SYSDATETIMEOFFSET() AS Datetimeoffset(0)) Col_Datetimeoffset0,
+            CAST(SYSDATETIMEOFFSET() AS Datetimeoffset(1)) Col_Datetimeoffset1,
+            CAST(SYSDATETIMEOFFSET() AS Datetimeoffset(2)) Col_Datetimeoffset2,
+            CAST(SYSDATETIMEOFFSET() AS Datetimeoffset(3)) Col_Datetimeoffset3,
+            CAST(SYSDATETIMEOFFSET() AS Datetimeoffset(4)) Col_Datetimeoffset4,
+            CAST(SYSDATETIMEOFFSET() AS Datetimeoffset(5)) Col_Datetimeoffset5,
+            CAST(SYSDATETIMEOFFSET() AS Datetimeoffset(6)) Col_Datetimeoffset6,
+            CAST(SYSDATETIMEOFFSET() AS Datetimeoffset(7)) Col_Datetimeoffset7,
+            NULL Col_Null
+        INTO
+            #row;
+
+        WITH BaseData AS (
+            SELECT * FROM #row
+            UNION ALL
+            SELECT
+                Col_Int + 1
+                , Col_BigInt
+                , Col_SmallInt
+                , Col_TinyInt
+                , Col_Numeric
+                , Col_Decimal
+                , Col_Float
+                , Col_Real
+                , Col_Varbinary
+                , Col_Binary
+                , Col_Text
+                , Col_Varchar
+                , Col_Char
+                , Col_Ntext
+                , Col_Nvarchar
+                , Col_Nchar
+                , Col_Date
+                , Col_Time1
+                , Col_Time2
+                , Col_Time3
+                , Col_Time4
+                , Col_Time5
+                , Col_Time6
+                , Col_Time7
+                , Col_Smalldatetime
+                , Col_Datetime
+                , Col_Datetime2
+                , Col_Datetimeoffset0
+                , Col_Datetimeoffset1
+                , Col_Datetimeoffset2
+                , Col_Datetimeoffset3
+                , Col_Datetimeoffset4
+                , Col_Datetimeoffset5
+                , Col_Datetimeoffset6
+                , Col_Datetimeoffset7
+                , Col_Null
+            FROM
+                BaseData
+            WHERE
+                Col_Int < {rows}
+        )
+        SELECT
+            *
+        INTO
+            #generated
+        FROM
+            BaseData
+        OPTION (MAXRECURSION 0);
+
+        DECLARE @Id INT = 1
+        WHILE @Id <= {rows}
+        BEGIN
+            UPDATE #generated SET
+                Col_Int = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Int)
+                , Col_BigInt = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_BigInt)
+                , Col_SmallInt = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_SmallInt)
+                , Col_TinyInt = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_TinyInt)
+                , Col_Numeric = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Numeric)
+                , Col_Decimal = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Decimal)
+                , Col_Float = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Float)
+                , Col_Real = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Real)
+                , Col_Varbinary = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Varbinary)
+                , Col_Binary = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Binary)
+                , Col_Text = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Text)
+                , Col_Varchar = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Varchar)
+                , Col_Char = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Char)
+                , Col_Ntext = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Ntext)
+                , Col_Nvarchar = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Nvarchar)
+                , Col_Nchar = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Nchar)
+                , Col_Date = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Date)
+                , Col_Time1 = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Time1)
+                , Col_Time2 = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Time2)
+                , Col_Time3 = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Time3)
+                , Col_Time4 = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Time4)
+                , Col_Time5 = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Time5)
+                , Col_Time6 = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Time6)
+                , Col_Time7 = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Time7)
+                , Col_Smalldatetime = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Smalldatetime)
+                , Col_Datetime = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Datetime)
+                , Col_Datetime2 = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Datetime2)
+                , Col_Datetimeoffset0 = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Datetimeoffset0)
+                , Col_Datetimeoffset1 = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Datetimeoffset1)
+                , Col_Datetimeoffset2 = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Datetimeoffset2)
+                , Col_Datetimeoffset3 = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Datetimeoffset3)
+                , Col_Datetimeoffset4 = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Datetimeoffset4)
+                , Col_Datetimeoffset5 = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Datetimeoffset5)
+                , Col_Datetimeoffset6 = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Datetimeoffset6)
+                , Col_Datetimeoffset7 = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Datetimeoffset7)
+                , Col_Null = IIF(RAND(CHECKSUM(NEWID())) <= @NullRatio, Null, Col_Null)
+            WHERE Col_Int = @Id
+
+            SET @Id = @Id + 1
+
+        END
+
+        SELECT * FROM #generated
+    """
+
+
+def check_correct_types(data: Dict[str, Any], allow_null=False):
+    def check_is_none(item):
+        if allow_null:
+            return item is None
+        return False
+
+    def do_assert(item, intended_type) -> bool:
+        return isinstance(item, intended_type) or check_is_none(item)
+
+    columns = [
+        ("Col_Int", int),
+        ("Col_BigInt", float),
+        ("Col_SmallInt", int),
+        ("Col_TinyInt", int),
+        ("Col_Numeric", float),
+        ("Col_Decimal", float),
+        ("Col_Float", float),
+        ("Col_Real", float),
+        ("Col_Varbinary", bytes),
+        ("Col_Binary", bytes),
+        ("Col_Text", str),
+        ("Col_Varchar", str),
+        ("Col_Char", str),
+        ("Col_Ntext", str),
+        ("Col_Nvarchar", str),
+        ("Col_Nchar", str),
+        ("Col_Date", date),
+        ("Col_Time1", time),
+        ("Col_Time2", time),
+        ("Col_Time3", time),
+        ("Col_Time4", time),
+        ("Col_Time5", time),
+        ("Col_Time6", time),
+        ("Col_Time7", time),
+        ("Col_Smalldatetime", datetime),
+        ("Col_Datetime", datetime),
+        ("Col_Datetime2", datetime),
+        ("Col_Datetimeoffset0", datetime),
+        ("Col_Datetimeoffset1", datetime),
+        ("Col_Datetimeoffset2", datetime),
+        ("Col_Datetimeoffset3", datetime),
+        ("Col_Datetimeoffset4", datetime),
+        ("Col_Datetimeoffset5", datetime),
+        ("Col_Datetimeoffset6", datetime),
+        ("Col_Datetimeoffset7", datetime),
+        ("Col_Null", type(None)),
+    ]
+
+    for key, target_type in columns:
+        assert do_assert(data[key], target_type)
+
+    assert data["Col_Binary"].decode("UTF8") if data["Col_Binary"] is not None else True
diff --git a/tests/test_database.py b/tests/test_database.py
index fc9fbec..f58f6c8 100644
--- a/tests/test_database.py
+++ b/tests/test_database.py
@@ -1,10 +1,11 @@
 import os
-from datetime import date, time, datetime, timezone, timedelta
+from datetime import datetime, timedelta, timezone
 
 import pytest
 
 import pymssqlutils as sql
 from pymssqlutils import model_to_values
+from tests.helpers import check_correct_types, generate_fake_data_query
 
 SKIP_FILE = not os.environ.get("TEST_ON_DATABASE")
 SKIP_REASON = "TEST_ON_DATABASE is not set in environment"
@@ -16,56 +17,51 @@ def test_can_connect():
 
 
 @pytest.mark.skipif(SKIP_FILE, reason=SKIP_REASON)
-def test_type_parsing():
-    data = sql.query(
-        """
-            SELECT
-                CAST(GETDATE() AS DATE) Col_Date,
-                CONVERT(VARBINARY(MAX), 'BinaryText') Col_Binary,
-                CAST(SYSDATETIMEOFFSET() AS time(1)) Col_Time1,
-                CAST(SYSDATETIMEOFFSET() AS time(2)) Col_Time2,
-                CAST(SYSDATETIMEOFFSET() AS time(3)) Col_Time3,
-                CAST(SYSDATETIMEOFFSET() AS time(4)) Col_Time4,
-                CAST(SYSDATETIMEOFFSET() AS time(5)) Col_Time5,
-                CAST(SYSDATETIMEOFFSET() AS time(6)) Col_Time6,
-                CAST(SYSDATETIMEOFFSET() AS time(7)) Col_Time7,
-                CAST(SYSDATETIMEOFFSET() AS Smalldatetime) Col_Smalldatetime,
-                CAST(SYSDATETIMEOFFSET() AS Datetime) Col_Datetime,
-                CAST(SYSDATETIMEOFFSET() AS Datetime2) Col_Datetime2,
-                CAST(SYSDATETIMEOFFSET() AS Datetimeoffset(0)) Col_Datetimeoffset0,
-                CAST(SYSDATETIMEOFFSET() AS Datetimeoffset(1)) Col_Datetimeoffset1,
-                CAST(SYSDATETIMEOFFSET() AS Datetimeoffset(2)) Col_Datetimeoffset2,
-                CAST(SYSDATETIMEOFFSET() AS Datetimeoffset(3)) Col_Datetimeoffset3,
-                CAST(SYSDATETIMEOFFSET() AS Datetimeoffset(4)) Col_Datetimeoffset4,
-                CAST(SYSDATETIMEOFFSET() AS Datetimeoffset(5)) Col_Datetimeoffset5,
-                CAST(SYSDATETIMEOFFSET() AS Datetimeoffset(6)) Col_Datetimeoffset6,
-                CAST(SYSDATETIMEOFFSET() AS Datetimeoffset(7)) Col_Datetimeoffset7,
-                CAST(1.2344566 AS NUMERIC) Col_Numeric
-        """
-    ).data[0]
-
-    assert isinstance(data["Col_Binary"], bytes)
-    assert data["Col_Binary"].decode("UTF8") == "BinaryText"
-    assert isinstance(data["Col_Date"], date)
-    assert isinstance(data["Col_Time1"], time)
-    assert isinstance(data["Col_Time2"], time)
-    assert isinstance(data["Col_Time3"], time)
-    assert isinstance(data["Col_Time4"], time)
-    assert isinstance(data["Col_Time5"], time)
-    assert isinstance(data["Col_Time6"], time)
-    assert isinstance(data["Col_Time7"], time)
-    assert isinstance(data["Col_Smalldatetime"], datetime)
-    assert isinstance(data["Col_Datetime"], datetime)
-    assert isinstance(data["Col_Datetime2"], datetime)
-    assert isinstance(data["Col_Datetimeoffset0"], datetime)
-    assert isinstance(data["Col_Datetimeoffset1"], datetime)
-    assert isinstance(data["Col_Datetimeoffset2"], datetime)
-    assert isinstance(data["Col_Datetimeoffset3"], datetime)
-    assert isinstance(data["Col_Datetimeoffset4"], datetime)
-    assert isinstance(data["Col_Datetimeoffset5"], datetime)
-    assert isinstance(data["Col_Datetimeoffset6"], datetime)
-    assert isinstance(data["Col_Datetimeoffset7"], datetime)
-    assert isinstance(data["Col_Numeric"], float)
+def test_type_parsing_single_row():
+    data = sql.query(generate_fake_data_query(1, 0)).data[0]
+
+    check_correct_types(data)
+
+
+@pytest.mark.skipif(SKIP_FILE, reason=SKIP_REASON)
+def test_type_parsing_multi_row():
+    rows = sql.query(generate_fake_data_query(50, 0)).data
+
+    for data in rows:
+        check_correct_types(data)
+
+
+@pytest.mark.skipif(SKIP_FILE, reason=SKIP_REASON)
+def test_type_parsing_large_multi_row():
+    rows = sql.query(generate_fake_data_query(15000, 0)).data
+
+    for data in rows:
+        check_correct_types(data)
+
+
+@pytest.mark.skipif(SKIP_FILE, reason=SKIP_REASON)
+def test_type_parsing_single_row_all_null():
+    data = sql.query(generate_fake_data_query(1, 1)).data[0]
+
+    for item in data.values():
+        assert item is None
+
+
+@pytest.mark.skipif(SKIP_FILE, reason=SKIP_REASON)
+def test_type_parsing_multi_row_all_null():
+    rows = sql.query(generate_fake_data_query(5, 1)).data
+
+    for data in rows:
+        for item in data.values():
+            assert item is None
+
+
+@pytest.mark.skipif(SKIP_FILE, reason=SKIP_REASON)
+def test_type_parsing_multi_row_some_null():
+    rows = sql.query(generate_fake_data_query(15, 0.5)).data
+
+    for data in rows:
+        check_correct_types(data, True)
 
 
 @pytest.mark.skipif(SKIP_FILE, reason=SKIP_REASON)
@@ -95,8 +91,8 @@ def test_execute_batched():
 
 @pytest.mark.skipif(SKIP_FILE, reason=SKIP_REASON)
 def test_execute_many_operations():
-    result = sql.execute([f"SELECT {val} val" for val in range(1000)], fetch=True)
-    assert result.data[0]["val"] == 999
+    result = sql.execute([f"SELECT {val} val" for val in range(100)], fetch=True)
+    assert result.data[0]["val"] == 99
 
 
 @pytest.mark.skipif(SKIP_FILE, reason=SKIP_REASON)
diff --git a/tests/test_main.py b/tests/test_main.py
index 3061c61..a734242 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -1,4 +1,4 @@
-from datetime import date, time, datetime, timezone, timedelta
+from datetime import datetime, timedelta, timezone
 
 import pymssql
 import pytest
@@ -7,14 +7,18 @@
 import pymssqlutils as sql
 from pymssqlutils import (
     DatabaseResult,
-    set_connection_details,
     model_to_values,
+    set_connection_details,
     substitute_parameters,
     to_sql_list,
 )
 from pymssqlutils.methods import _with_conn_details
-
-from tests.helpers import MockCursor
+from tests.helpers import (
+    MockCursor,
+    check_correct_types,
+    cursor_description,
+    cursor_row,
+)
 
 
 def test_with_conn_details_from_args():
@@ -236,35 +240,29 @@ def test_query(mocker: MockerFixture, monkeypatch):
 
 def test_data_parsing(monkeypatch):
     result = DatabaseResult(
-        ok=True, fetch=True, commit=False, data=MockCursor().fetchall(), error=None
+        ok=True, fetch=True, commit=False, cursor=MockCursor(row_count=1)
     )
-    data = result.data[0]
-
-    assert isinstance(data["Col_Date"], date)
-    assert isinstance(data["Col_Time1"], time)
-    assert isinstance(data["Col_Time2"], time)
-    assert isinstance(data["Col_Time3"], time)
-    assert isinstance(data["Col_Time4"], time)
-    assert isinstance(data["Col_Time5"], time)
-    assert isinstance(data["Col_Time6"], time)
-    assert isinstance(data["Col_Time7"], time)
-    assert isinstance(data["Col_Smalldatetime"], datetime)
-    assert isinstance(data["Col_Datetime"], datetime)
-    assert isinstance(data["Col_Datetime2"], datetime)
-    assert isinstance(data["Col_Datetimeoffset0"], datetime)
-    assert isinstance(data["Col_Datetimeoffset1"], datetime)
-    assert isinstance(data["Col_Datetimeoffset2"], datetime)
-    assert isinstance(data["Col_Datetimeoffset3"], datetime)
-    assert isinstance(data["Col_Datetimeoffset4"], datetime)
-    assert isinstance(data["Col_Datetimeoffset5"], datetime)
-    assert isinstance(data["Col_Datetimeoffset6"], datetime)
-    assert isinstance(data["Col_Datetimeoffset7"], datetime)
-    assert isinstance(data["Col_Numeric"], float)
+    check_correct_types(result.data[0])
 
 
 def test_data_serializable():
+    pytest.importorskip("orjson")
+
+    description = list(cursor_description)
+    example_row = list(cursor_row[0])
+
+    # remove bytes as not JSON Serializable
+    for idx in (9, 8):
+        example_row.pop(idx)
+        description.pop(idx)
+
     result = DatabaseResult(
-        ok=True, fetch=True, commit=False, data=MockCursor().fetchall(), error=None
+        ok=True,
+        fetch=True,
+        commit=False,
+        cursor=MockCursor(
+            row_count=3, row=[tuple(example_row)], description=tuple(description)
+        ),
     )
 
     assert isinstance(result.to_json(), str)
@@ -312,12 +310,12 @@ def test_result_error_handling_on_raise(monkeypatch):
 def test_cast_to_dataframe():
     pandas = pytest.importorskip("pandas")
     result = DatabaseResult(
-        ok=True, fetch=True, commit=False, data=MockCursor().fetchall(), error=None
+        ok=True, fetch=True, commit=False, cursor=MockCursor(row_count=5)
     )
     df = result.to_dataframe()
 
     assert isinstance(df, pandas.DataFrame)
-    assert df.columns.tolist() == result.columns
+    assert df.columns.tolist() == list(result.columns)
 
 
 def test_dict_model_to_values():