Skip to content
Merged

Mssql #110

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
c0897d4
Update to ignore opiates
myyong Apr 10, 2026
834a20b
Merge branch 'main' of https://github.com/SAFEHR-data/datafaker
myyong Apr 10, 2026
a053742
Template omop orm.yaml, config.yaml and df.py files
myyong Apr 13, 2026
675dd92
Fix ordered list formatting
myyong Apr 13, 2026
54d13b5
Fix pre-commit hooks.
myyong Apr 13, 2026
1c42c49
Fix headers that broke pre-commit hook
myyong Apr 13, 2026
b311776
Organised UI commands
myyong Apr 13, 2026
b2f14ab
Add initial MS-SQL support: driver deps, async DSN rewriting, schema …
myyong May 19, 2026
76fec75
Extend type parser for MS-SQL column types (issue #96)
myyong May 19, 2026
da4a69b
Strip SERIAL/IDENTITY for MS-SQL target databases (issue #97)
myyong May 19, 2026
fabb0cc
Add commit link to challenge 5 in MS-SQL README
myyong May 19, 2026
dfb698f
Add issue #98 link for challenge 6 (UUID type mapping) to MS-SQL README
myyong May 19, 2026
39709d8
Add tests for challenge 7 dialect-agnostic error handling
myyong May 19, 2026
0925a93
Add commit links for challenge 7 to MS-SQL README
myyong May 19, 2026
7183d05
Document challenge 8 as deferred in README and issue #99
myyong May 19, 2026
d872fd9
Document macOS ODBC driver setup and add .env.example for MS-SQL
myyong May 19, 2026
3c3c20a
Merge branch 'ui-update' into mssql
myyong May 19, 2026
db2e48b
Fix Steps paths in MS-SQL README to run from example directory
myyong May 20, 2026
fc97b2c
Add TrustServerCertificate=yes to MS-SQL DSN examples
myyong May 20, 2026
81a65eb
Fix schema-qualified FK resolution and MS-SQL multiple cascade paths …
myyong May 20, 2026
84a209f
Let database generate integer primary keys on MS-SQL (closes #104)
myyong May 21, 2026
40fc121
Rename mimic_omop example to omop-mssql
myyong May 21, 2026
1857e76
Add omop-postgresql example and missing test fixture
myyong May 21, 2026
3637258
Fix dialect-specific SQL in generator commands (closes #105)
myyong May 21, 2026
ad98acf
Update omop-postgresql README to use relative paths
myyong May 21, 2026
7c0add6
Fix RANDOM()/LIMIT dialect incompatibility in ChoiceGeneratorFactory …
myyong May 21, 2026
2bcea2b
Fix schema-qualified table missing from live queries in ChoiceGenerat…
myyong May 21, 2026
41b96f6
Fix schema-missing FROM clause in Buckets queries
myyong May 21, 2026
29e7889
Fix remaining RANDOM()/LIMIT dialect incompatibilities (closes #107)
myyong May 22, 2026
91578da
Fix missing schema qualification in interactive shell SELECT statements
myyong May 22, 2026
4d03aa3
Fix unqualified table names in raw SQL during configure-generators
myyong May 22, 2026
173f6da
Fix unqualified table names in src-stats queries written by configure…
myyong May 22, 2026
1132dc6
Fix schema-qualified table name in ChoiceGenerator stored queries (cl…
myyong May 22, 2026
3aefaa2
Config files for omop-mssql and omop-postgresl
myyong May 27, 2026
086e2d2
Merge branch 'main' of https://github.com/SAFEHR-data/datafaker
myyong Jun 19, 2026
3b09619
Add example source stats file
myyong Jun 29, 2026
cb4df26
Merge branch 'main' of https://github.com/SAFEHR-data/datafaker
myyong Jun 29, 2026
6ed6f97
Merge branch 'main' into mssql
myyong Jun 29, 2026
530ed2f
Add MS-SQL end-to-end test infrastructure
myyong Jun 29, 2026
f67c61e
Fix stale datafaker.generators imports in test_generators_dialect.py
myyong Jun 29, 2026
39d4fc5
Fix AttributeError in ContinuousLogDistributionProposerFactory
myyong Jun 30, 2026
5ce81ff
Changed name of dodgy test person
myyong Jun 30, 2026
2f1cb91
Merge branch 'main' of https://github.com/SAFEHR-data/datafaker
myyong Jun 30, 2026
a6efbc1
Merge branch 'main' into mssql
myyong Jun 30, 2026
78d2504
Fix MSSQL end-to-end tests: connection handling and type parsing
myyong Jun 30, 2026
c67b6cc
Remove debug logging of INSERT statements from create.py
myyong Jul 1, 2026
75731c7
Remove test_duckdb_serial_hook_still_works: remove_serial does not exist
myyong Jul 1, 2026
7a98a1c
Quick fixes
Jul 1, 2026
476fb42
Fix intermittent TCP reset errors in MSSQL tests
myyong Jul 1, 2026
f7ff564
Dispose engines before closing database in RequiresDBTestCase.tearDown
myyong Jul 1, 2026
7150945
Removed non-existing file
myyong Jul 1, 2026
178a394
Fix test_float: use assertIsInstance after FLOAT changed to numeric_type
myyong Jul 1, 2026
2b34a54
Fix schema handling, FK resolution for dotted table names, and test r…
myyong Jul 2, 2026
9d2bc36
Fixed peek column headings
Jul 2, 2026
586be1d
Fix test_end_to_end_parquet: two bugs blocking the test on mssql branch
myyong Jul 2, 2026
5b63eb8
Merge branch 'mssql' of https://github.com/SAFEHR-data/datafaker into…
myyong Jul 2, 2026
c1920b2
Fix schema routing regression: restore search_path for non-MSSQL dial…
myyong Jul 2, 2026
c13e104
Environment variables to set so that mssql and postgres tests can be …
myyong Jul 2, 2026
4382149
Fix NoReferencedTableError in reinstate_vocab_foreign_key_constraints
myyong Jul 2, 2026
e8e504b
Generate primary keys explicitly via increment() on all dialects
myyong Jul 2, 2026
94d4d14
Set autoincrement=False on all destination columns
myyong Jul 2, 2026
3c656ae
Extend MSSQL dialect support to continuous and intervals proposers
myyong Jul 2, 2026
8787862
Fix flaky test_print_data: rename bulk person name to length < 13
myyong Jul 2, 2026
1a3ea27
remove env.template
Jul 3, 2026
758496f
Fixed lots of test failures, added more MSSQL tests
Jul 8, 2026
e3e3490
Merge branch 'mssql' of github:SAFEHR-data/datafaker into mssql
Jul 8, 2026
9d11af9
Fixed a load of tests. Also:
Jul 9, 2026
29cb2eb
Fixed MsSql pre-test drop database
Jul 10, 2026
87a8b39
Fixed DuckDB workaround and made it implicit
Jul 10, 2026
22a932f
Removed some explicit tests for "mssql" dialect
Jul 10, 2026
f316c6a
pre-commit clean
Jul 11, 2026
63e463d
Fixed tests
Jul 11, 2026
da69c12
bump version to 0.5.0
Jul 13, 2026
520cf9e
Attempt to install MS SQL Engine in github actions tests
Jul 13, 2026
2388ba1
attempt #2 for mssql tests
Jul 13, 2026
a76934f
attempt #3
Jul 13, 2026
99176a6
attempt #4
Jul 13, 2026
6be9ddc
fix test.yml syntax
Jul 15, 2026
dc58788
CI should use docker compose and pytest
Jul 15, 2026
b6ca1ca
CI installs MSSQL tools
Jul 15, 2026
7d7f3e4
Remove old test-mssql config
Jul 15, 2026
0f998eb
Make psycopg2 import optional in db_utils
myyong Jul 15, 2026
4fb279f
Fix isort import ordering in test files
myyong Jul 15, 2026
ee3918a
Fixed pytest warnings.
Jul 15, 2026
54fdeea
Fixed pre-commit
Jul 15, 2026
b278aba
Merge pull request #123 from SAFEHR-data/mssql-ci-tests
tim-band Jul 15, 2026
6557d8e
CovariateQuery._inner_query SQL -> SqlAlchemy ORM
Jul 16, 2026
08cb12c
CovariateQuery._middle_query now returns a SQLAlchemy expression
Jul 16, 2026
b24dec5
Corrected install location
Jul 17, 2026
2935fad
Partition count queries made SQLAlchemy expressions
Jul 17, 2026
ed224cc
CovariateQuery.get returns a SQLAlchemy Select object
Jul 17, 2026
21f2629
Fix for Python 3.10
Jul 18, 2026
f85493f
Fixes for manual testing
Jul 20, 2026
0de9b89
Fix help text
Jul 21, 2026
4562ce0
Tidy up dependency failed error
Jul 21, 2026
c3c3fc0
pre-commit clear
Jul 21, 2026
42f5b08
Simplified installation
Jul 22, 2026
79faf2e
Message improved for ODBC driver adapter exception
Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,34 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: Start PostgreSQL
- name: Install prerequisites
id: install-prereqs
shell: bash
run: |
sudo systemctl start postgresql.service
- name: Install poetry
sudo apt install python3-poetry mssql-tools18
background: true
- name: Start PostgreSQL
shell: bash
run: |
sudo apt install python3-poetry
sudo systemctl start postgresql.service
- name: Wait for poetry to be installed
wait: install-prereqs
- name: Configure poetry
shell: bash
run: |
python -m poetry config virtualenvs.in-project true
run: python -m poetry config virtualenvs.in-project true
- name: Install dependencies
id: install-deps
shell: bash
if: steps.poetry-cache.outputs.cache-hit != 'true'
run: |
python -m poetry install --all-extras
run: python -m poetry install --all-extras
background: true
- name: Set up MS SQL Server
uses: hoverkraft-tech/compose-action@11beaa1c2dae4e8ed7b1665aa074723b6cecb0e4 # v3.0.0
with:
compose-file: ./docker-compose.yml
- name: Wait for the dependencies
wait:
- install-deps
- name: Run tests
shell: bash
run: |
poetry run python -m unittest
run: poetry run python -m pytest -rs
10 changes: 8 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,16 @@ Please install the following software on your workstation:

## Running unit tests

Executing unit tests is straightforward:
Executing unit tests is straightforward. For example inside a Poetry shell you can run:

```bash
python -m unittest discover --verbose tests/
pytest -rs
```

or outside:

```bash
poetry run pytest -rs
```

## Building documentation locally
Expand Down
33 changes: 3 additions & 30 deletions datafaker/create.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Functions and classes to create and populate the target database."""
import re
from collections import Counter
from pathlib import Path
from typing import Any, Generator, Iterable, Iterator, Mapping, Sequence, Tuple
Expand All @@ -8,9 +7,8 @@
import yaml
from sqlalchemy import Connection, insert, inspect
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.orm import Session
from sqlalchemy.schema import CreateSchema, CreateTable, MetaData, Table
from sqlalchemy.schema import CreateSchema, MetaData, Table

from datafaker.base import FileUploader
from datafaker.db_utils import (
Expand All @@ -33,31 +31,6 @@
Story = Generator[Tuple[str, dict[str, Any]], dict[str, Any], None]
RowCounts = Counter[str]

serial_re = re.compile(r"\bSERIAL\b")


@compiles(CreateTable, "duckdb")
def remove_on_delete_cascade(element: CreateTable, compiler: Any, **kw: Any) -> str:
"""
Intercede in compilation for column creation.

DuckDB does not understand cascades, and we don't care about
that in datafaker so we remove ``ON DELETE CASCASE``.

DuckDB does not understand ``SERIAL`` and we don't care
about autoincrementing, so we will replace it simply with
``INTEGER``.

Ideally ``duckdb_engine`` would remove these for us.
:param element: The CreateTable being executed.
:param compiler: Actually a DDLCompiler, but that type is not exported.
:param kw: Further arguments.
:return: Corrected SQL.
"""
text: str = compiler.visit_create_table(element, **kw)
t2 = serial_re.sub("INTEGER", text)
return t2.replace(" ON DELETE CASCADE", "")


def create_db_tables(metadata: MetaData) -> None:
"""Create tables described by the sqlalchemy metadata object."""
Expand Down Expand Up @@ -320,7 +293,7 @@ def insert(self) -> None:
table = self._table_dict[self._table_name]
if table.name in self._table_generator_dict:
table_generator = self._table_generator_dict[table.name]
default_values = table_generator(self._dst_conn)
default_values = table_generator.generate_row(self._dst_conn)
else:
default_values = {}
insert_values = {**default_values, **self._provided_values}
Expand Down Expand Up @@ -403,7 +376,7 @@ def populate(
with dst_conn.begin():
try:
for _ in range(table_generator.num_rows_per_pass):
stmt = insert(table).values(table_generator(dst_conn))
stmt = insert(table).values(table_generator.generate_row(dst_conn))
dst_conn.execute(stmt)
row_counts[table.name] = row_counts.get(table.name, 0) + 1
dst_conn.commit()
Expand Down
37 changes: 29 additions & 8 deletions datafaker/db_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,16 @@
import sqlalchemy.dialects
import yaml

# pylint: disable=no-name-in-module
from psycopg2.errors import UndefinedObject # ty: ignore[unresolved-import]
try:
# pylint: disable=no-name-in-module
from psycopg2.errors import UndefinedObject # ty: ignore[unresolved-import]
except ImportError:
# psycopg2 is only installed with the "postgres" extra; this error can
# only be raised by the psycopg2 driver, so it never matches otherwise.
class UndefinedObject: # type: ignore[no-redef]
"""Placeholder when psycopg2 is not installed."""


from sqlalchemy import Connection, Engine, ForeignKey, create_engine, event, select
from sqlalchemy.engine.interfaces import DBAPIConnection
from sqlalchemy.exc import (
Expand All @@ -38,7 +46,9 @@
get_ignored_table_names,
get_vocabulary_table_names,
logger,
make_async_dsn,
make_foreign_key_name,
unqualify_fk_target,
)

# Define some types used repeatedly in the code base
Expand Down Expand Up @@ -139,23 +149,31 @@ def create_db_engine(
**kwargs: Any,
) -> MaybeAsyncEngine:
"""Create a SQLAlchemy Engine."""
kwargs.setdefault("pool_pre_ping", True)
try:
if use_asyncio:
async_dsn = db_dsn.replace("postgresql://", "postgresql+asyncpg://")
engine: MaybeAsyncEngine = create_async_engine(async_dsn, **kwargs)
engine: MaybeAsyncEngine = create_async_engine(
make_async_dsn(db_dsn), **kwargs
)
else:
engine = create_engine(db_dsn, **kwargs)
except NoSuchModuleError as exc:
logger.error("Failed to connect to the database: %s", exc)
logger.error("Perhaps the dialect '%s' is invalid.", db_dsn.split(":")[0])
raise Exit(1) from exc
except ModuleNotFoundError as exc:
logger.error("Failed to connect to the database: %s", exc)
raise Exit(1) from exc
except ValueError as exc:
logger.error("DSN %s is malformed: %s", db_dsn, exc)
raise Exit(1) from exc

settings = {}
if schema_name is not None:
settings["search_path"] = schema_name
if get_sync_engine(engine).dialect.name == "mssql":
engine = engine.execution_options(schema_translate_map={None: schema_name})
else:
settings["search_path"] = schema_name
if parquet_dir is not None:
joined = ",".join(_find_parquet_directories(parquet_dir))
# double up single quotes
Expand Down Expand Up @@ -202,11 +220,11 @@ def create_db_engine_dst(
return create_db_engine(db_dsn, schema_name, use_asyncio)


def get_metadata(engine: Engine) -> MetaData:
def get_metadata(engine: Engine, schema_name: Optional[str] = None) -> MetaData:
"""Get the MetaData object associated with the engine passed."""
md = MetaData()
try:
md.reflect(engine)
md.reflect(engine, schema=schema_name)
except OperationalError as exc:
logger.error("Cannot connect to database: %s", exc)
raise Exit(1) from exc
Expand Down Expand Up @@ -417,10 +435,13 @@ def reinstate_vocab_foreign_key_constraints(
].items():
fk_targets = column_dict.get("foreign_keys", [])
if fk_targets:
table_names = frozenset(meta_dict.get("tables", {}).keys())
fk = ForeignKeyConstraint(
columns=[column_name],
name=make_foreign_key_name(vocab_table_name, column_name),
refcolumns=fk_targets,
refcolumns=[
unqualify_fk_target(t, table_names) for t in fk_targets
],
)
logger.debug("Restoring foreign key constraint %s", fk.name)
with Session(dst_engine) as session:
Expand Down
Loading