sql-testing-library 0.12.0__tar.gz → 0.13.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/CHANGELOG.md +10 -0
  2. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/PKG-INFO +3 -6
  3. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/README.md +2 -5
  4. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/pyproject.toml +2 -1
  5. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/src/sql_testing_library/_adapters/snowflake.py +2 -2
  6. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/src/sql_testing_library/_core.py +23 -2
  7. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/src/sql_testing_library/_pytest_plugin.py +7 -1
  8. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/src/sql_testing_library/_sql_logger.py +45 -7
  9. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/LICENSE +0 -0
  10. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/src/sql_testing_library/__init__.py +0 -0
  11. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/src/sql_testing_library/_adapters/__init__.py +0 -0
  12. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/src/sql_testing_library/_adapters/athena.py +0 -0
  13. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/src/sql_testing_library/_adapters/base.py +0 -0
  14. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/src/sql_testing_library/_adapters/bigquery.py +0 -0
  15. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/src/sql_testing_library/_adapters/presto.py +0 -0
  16. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/src/sql_testing_library/_adapters/redshift.py +0 -0
  17. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/src/sql_testing_library/_adapters/trino.py +0 -0
  18. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/src/sql_testing_library/_exceptions.py +0 -0
  19. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/src/sql_testing_library/_mock_table.py +0 -0
  20. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/src/sql_testing_library/_sql_utils.py +0 -0
  21. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/src/sql_testing_library/_types.py +0 -0
  22. {sql_testing_library-0.12.0 → sql_testing_library-0.13.0}/src/sql_testing_library/py.typed +0 -0
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## 0.13.0 (2025-06-27)
9
+
10
+ ### Feat
11
+
12
+ - add pytest-xdist support for parallel test execution (#105)
13
+
14
+ ### Fix
15
+
16
+ - **snowflake**: fix issue related to physical view for snowflake (#104)
17
+
8
18
  ## 0.12.0 (2025-06-25)
9
19
 
10
20
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sql-testing-library
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  Summary: A powerful Python framework for unit testing SQL queries across BigQuery, Snowflake, Redshift, Athena, and Trino with mock data
5
5
  License: MIT
6
6
  Keywords: sql,testing,unit-testing,mock-data,database-testing,bigquery,snowflake,redshift,athena,trino,data-engineering,etl-testing,sql-validation,query-testing
@@ -155,7 +155,7 @@ The library supports two execution modes for mock data injection. **CTE Mode is
155
155
  | Execution Mode | Description | BigQuery | Athena | Redshift | Trino | Snowflake |
156
156
  |----------------|-------------|----------|--------|----------|-------|-----------|
157
157
  | **CTE Mode** | Mock data injected as Common Table Expressions | ✅ | ✅ | ✅ | ✅ | ✅ |
158
- | **Physical Tables** | Mock data created as temporary tables | ✅ | ✅ | ✅ | ✅ | ⚠️* |
158
+ | **Physical Tables** | Mock data created as temporary tables | ✅ | ✅ | ✅ | ✅ | |
159
159
 
160
160
  ### Execution Mode Details
161
161
 
@@ -306,7 +306,7 @@ def test_physical_tables():
306
306
  **Notes:**
307
307
  - **CTE Mode**: Default mode, works with all database engines, suitable for most use cases
308
308
  - **Physical Tables**: Used automatically when CTE queries exceed database size limits or when explicitly requested
309
- - **⚠️ Snowflake Physical Tables**: Currently disabled in test environment due to temporary table visibility limitations. CTE mode works perfectly for all use cases.
309
+ - **Snowflake**: Full support for both CTE and physical table modes
310
310
 
311
311
  ## Installation
312
312
 
@@ -1172,9 +1172,6 @@ For detailed usage and configuration options, see the example files included.
1172
1172
 
1173
1173
  The library has a few known limitations that are planned to be addressed in future updates:
1174
1174
 
1175
- ### Snowflake Support
1176
- - Physical table tests for Snowflake are currently skipped due to complex mocking requirements
1177
-
1178
1175
  ### General Improvements
1179
1176
  - Add support for more SQL dialects
1180
1177
  - Improve error handling for malformed SQL
@@ -98,7 +98,7 @@ The library supports two execution modes for mock data injection. **CTE Mode is
98
98
  | Execution Mode | Description | BigQuery | Athena | Redshift | Trino | Snowflake |
99
99
  |----------------|-------------|----------|--------|----------|-------|-----------|
100
100
  | **CTE Mode** | Mock data injected as Common Table Expressions | ✅ | ✅ | ✅ | ✅ | ✅ |
101
- | **Physical Tables** | Mock data created as temporary tables | ✅ | ✅ | ✅ | ✅ | ⚠️* |
101
+ | **Physical Tables** | Mock data created as temporary tables | ✅ | ✅ | ✅ | ✅ | |
102
102
 
103
103
  ### Execution Mode Details
104
104
 
@@ -249,7 +249,7 @@ def test_physical_tables():
249
249
  **Notes:**
250
250
  - **CTE Mode**: Default mode, works with all database engines, suitable for most use cases
251
251
  - **Physical Tables**: Used automatically when CTE queries exceed database size limits or when explicitly requested
252
- - **⚠️ Snowflake Physical Tables**: Currently disabled in test environment due to temporary table visibility limitations. CTE mode works perfectly for all use cases.
252
+ - **Snowflake**: Full support for both CTE and physical table modes
253
253
 
254
254
  ## Installation
255
255
 
@@ -1115,9 +1115,6 @@ For detailed usage and configuration options, see the example files included.
1115
1115
 
1116
1116
  The library has a few known limitations that are planned to be addressed in future updates:
1117
1117
 
1118
- ### Snowflake Support
1119
- - Physical table tests for Snowflake are currently skipped due to complex mocking requirements
1120
-
1121
1118
  ### General Improvements
1122
1119
  - Add support for more SQL dialects
1123
1120
  - Improve error handling for malformed SQL
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "sql-testing-library"
7
- version = "0.12.0"
7
+ version = "0.13.0"
8
8
  description = "A powerful Python framework for unit testing SQL queries across BigQuery, Snowflake, Redshift, Athena, and Trino with mock data"
9
9
  authors = ["Gurmeet Saran <gurmeetx@gmail.com>", "Kushal Thakkar <kushal.thakkar@gmail.com>"]
10
10
  maintainers = ["Gurmeet Saran <gurmeetx@gmail.com>", "Kushal Thakkar <kushal.thakkar@gmail.com>"]
@@ -104,6 +104,7 @@ pytest = ">=7.0.0"
104
104
  pytest-asyncio = ">=0.21.0"
105
105
  pytest-mock = ">=3.10.0"
106
106
  pytest-cov = ">=4.1.0"
107
+ pytest-xdist = ">=3.0.0"
107
108
  pytest-rerunfailures = ">=12.0.0"
108
109
  black = ">=22.0.0"
109
110
  flake8 = ">=4.0.0"
@@ -369,7 +369,7 @@ class SnowflakeAdapter(DatabaseAdapter):
369
369
  snowflake_type = "VARIANT"
370
370
  else:
371
371
  snowflake_type = type_mapping.get(col_type, "VARCHAR")
372
- column_defs.append(f'"{col_name}" {snowflake_type}')
372
+ column_defs.append(f"{col_name} {snowflake_type}")
373
373
 
374
374
  columns_sql = ",\n ".join(column_defs)
375
375
 
@@ -390,7 +390,7 @@ class SnowflakeAdapter(DatabaseAdapter):
390
390
  col_type = column_types.get(col_name, str)
391
391
  value = first_row[col_name]
392
392
  formatted_value = self.format_value_for_cte(value, col_type)
393
- select_expressions.append(f'{formatted_value} AS "{col_name}"')
393
+ select_expressions.append(f"{formatted_value} AS {col_name}")
394
394
 
395
395
  # Start with the first row in the SELECT
396
396
  select_sql = f"SELECT {', '.join(select_expressions)}"
@@ -556,8 +556,29 @@ class SQLTestFramework:
556
556
  break
557
557
 
558
558
  if replacement_name:
559
- # Create a new Table node with the replacement name
560
- new_table = exp.Table(this=exp.Identifier(this=replacement_name))
559
+ # Parse the replacement name to handle schema qualification
560
+ parts = replacement_name.split(".")
561
+ # Only quote identifiers for Snowflake physical tables (which contain TEMP_)
562
+ should_quote = "TEMP_" in replacement_name and dialect == "snowflake"
563
+
564
+ if len(parts) == 3:
565
+ # catalog.schema.table format
566
+ new_table = exp.Table(
567
+ this=exp.Identifier(this=parts[2], quoted=should_quote),
568
+ db=exp.Identifier(this=parts[1], quoted=should_quote),
569
+ catalog=exp.Identifier(this=parts[0], quoted=should_quote),
570
+ )
571
+ elif len(parts) == 2:
572
+ # schema.table format
573
+ new_table = exp.Table(
574
+ this=exp.Identifier(this=parts[1], quoted=should_quote),
575
+ db=exp.Identifier(this=parts[0], quoted=should_quote),
576
+ )
577
+ else:
578
+ # Just table name
579
+ new_table = exp.Table(
580
+ this=exp.Identifier(this=replacement_name, quoted=should_quote)
581
+ )
561
582
 
562
583
  # Preserve the table alias if it exists
563
584
  if hasattr(node, "alias") and node.alias:
@@ -489,9 +489,15 @@ def pytest_collection_modifyitems(config: pytest.Config, items: List[Item]) -> N
489
489
 
490
490
 
491
491
  def pytest_configure(config: pytest.Config) -> None:
492
- """Register custom markers."""
492
+ """Register custom markers and configure xdist worker detection."""
493
493
  config.addinivalue_line("markers", "sql_test: mark test as a SQL test")
494
494
 
495
+ # Set up pytest-xdist worker ID if running in parallel
496
+ if hasattr(config, "workerinput"):
497
+ # We're running with pytest-xdist
498
+ worker_id = config.workerinput["workerid"] # type: ignore[attr-defined]
499
+ os.environ["PYTEST_XDIST_WORKER"] = worker_id
500
+
495
501
 
496
502
  def pytest_runtest_call(item: Item) -> None:
497
503
  """Custom test execution for SQL tests."""
@@ -2,6 +2,8 @@
2
2
 
3
3
  import os
4
4
  import re
5
+ import threading
6
+ import uuid
5
7
  from datetime import datetime
6
8
  from pathlib import Path
7
9
  from typing import Any, Dict, List, Optional
@@ -17,6 +19,7 @@ class SQLLogger:
17
19
  # Class variable to store the run directory for the current test session
18
20
  _run_directory: Optional[Path] = None
19
21
  _run_id: Optional[str] = None
22
+ _lock = threading.Lock() # Thread lock for concurrent access
20
23
 
21
24
  def __init__(self, log_dir: Optional[str] = None) -> None:
22
25
  """Initialize SQL logger.
@@ -66,19 +69,44 @@ class SQLLogger:
66
69
  self.log_dir.mkdir(parents=True, exist_ok=True)
67
70
  self._logged_files: List[str] = []
68
71
 
72
+ def _get_worker_id(self) -> Optional[str]:
73
+ """Get pytest-xdist worker ID if running in parallel.
74
+
75
+ Returns:
76
+ Worker ID (e.g., 'gw0', 'gw1') or None if running serially
77
+ """
78
+ # Check for pytest-xdist worker ID in environment
79
+ worker_id = os.environ.get("PYTEST_XDIST_WORKER")
80
+ if worker_id:
81
+ return worker_id
82
+
83
+ # Alternative: Check for pytest-xdist worker input (for older versions)
84
+ # This would be set in conftest.py if needed
85
+ return os.environ.get("PYTEST_CURRENT_TEST_WORKER")
86
+
69
87
  def _ensure_run_directory(self) -> Path:
70
88
  """Ensure run directory exists, creating it if necessary.
71
89
 
72
90
  Returns:
73
91
  Path to the run directory
74
92
  """
75
- # Create run directory if not already created for this session
76
- if SQLLogger._run_directory is None:
77
- # Generate run ID with timestamp
78
- timestamp = datetime.now().strftime("%Y%m%dT%H%M%S")
79
- SQLLogger._run_id = f"runid_{timestamp}"
80
- SQLLogger._run_directory = self.log_dir / SQLLogger._run_id
81
- SQLLogger._run_directory.mkdir(parents=True, exist_ok=True)
93
+ with SQLLogger._lock:
94
+ # Create run directory if not already created for this session
95
+ if SQLLogger._run_directory is None:
96
+ # Generate run ID with timestamp
97
+ timestamp = datetime.now().strftime("%Y%m%dT%H%M%S")
98
+
99
+ # Check if running in parallel mode
100
+ worker_id = self._get_worker_id()
101
+ if worker_id:
102
+ # Include worker ID in run directory name for parallel execution
103
+ SQLLogger._run_id = f"runid_{timestamp}_{worker_id}"
104
+ else:
105
+ # Serial execution - use standard run ID
106
+ SQLLogger._run_id = f"runid_{timestamp}"
107
+
108
+ SQLLogger._run_directory = self.log_dir / SQLLogger._run_id
109
+ SQLLogger._run_directory.mkdir(parents=True, exist_ok=True)
82
110
  return SQLLogger._run_directory
83
111
 
84
112
  def should_log(self, log_sql: Optional[bool] = None) -> bool:
@@ -138,10 +166,20 @@ class SQLLogger:
138
166
  if failed:
139
167
  components.append("FAILED")
140
168
 
169
+ # Add worker ID if running in parallel
170
+ worker_id = self._get_worker_id()
171
+ if worker_id:
172
+ components.append(f"w{worker_id}")
173
+
141
174
  # Add timestamp for uniqueness
142
175
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] # Milliseconds
143
176
  components.append(timestamp)
144
177
 
178
+ # Add a short UUID suffix to absolutely guarantee uniqueness
179
+ # This handles edge cases where tests might start at exactly the same millisecond
180
+ unique_suffix = str(uuid.uuid4())[:8]
181
+ components.append(unique_suffix)
182
+
145
183
  # Join with double underscore for clarity
146
184
  filename = "__".join(components) + ".sql"
147
185