diff --git a/pyproject.toml b/pyproject.toml index 87b8af376b..232f8ea19a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ "duckdb>=1.3.2", "filelock>=3.20.3", "orjson>=3.10.0", - "polyglot-sql==0.11.0", + "polyglot-sql==0.12.0", "pydantic-settings>=2,<3", "PyYAML>=6.0.2", "rich>=15.0.0", diff --git a/src/sqlbuild/compiler/compile/_helpers/analysis/dynamic_pivot.py b/src/sqlbuild/compiler/compile/_helpers/analysis/dynamic_pivot.py index fd8385e427..5089e2b14d 100644 --- a/src/sqlbuild/compiler/compile/_helpers/analysis/dynamic_pivot.py +++ b/src/sqlbuild/compiler/compile/_helpers/analysis/dynamic_pivot.py @@ -140,6 +140,7 @@ def _analyze_passthrough( DynamicColumnFamilyProof(name=family.name, inferred_type=family.type) for family in families ), + input_relations=(name,), ) @@ -174,6 +175,13 @@ def _analyze_pivot( ) if source_columns is None: return _failure("dynamic pivot input does not have an authoritative, explicit schema") + input_relation: str | None = _external_relation_name( + node=source_node, + ctes=ctes, + seen=frozenset(), + ) + if input_relation is None: + return _failure("dynamic pivot input relation could not be resolved") pivot_column: str = pivot_columns[0] matched_aggregate_indices: set[int] = set() family_proofs: list[DynamicColumnFamilyProof] = [] @@ -238,6 +246,7 @@ def _analyze_pivot( for name in fixed_names ), families=tuple(family_proofs), + input_relations=(input_relation,), bare_dynamic_pivot=bare_dynamic_pivot, ) @@ -393,6 +402,46 @@ def _relation_schema( ) +def _external_relation_name( + *, + node: object, + ctes: _CteMap, + seen: frozenset[str], +) -> str | None: + key: str = _node_key(node) + payload: object = cast(dict[str, object], node).get(key) if isinstance(node, dict) else None + if key in _QUERY_AST_KINDS and isinstance(payload, dict): + return _external_relation_name( + node=cast(dict[str, object], payload).get("this"), + ctes=ctes, + seen=seen, + ) + if key == _SELECT_AST_KIND and isinstance(payload, dict): + payload_dict: dict[str, object] = cast(dict[str, object], payload) + if payload_dict.get("joins"): + return None + relations: tuple[dict[str, object], ...] = _nested_dict_list( + node=payload_dict.get("from"), key="expressions" + ) + if len(relations) != 1: + return None + return _external_relation_name(node=relations[0], ctes=ctes, seen=seen) + relation_name: str | None = _relation_name(node) + if relation_name is None: + return None + normalized_name: str = relation_name.casefold() + if normalized_name not in ctes: + return relation_name + cte: dict[str, object] | None = ctes[normalized_name] + if cte is None or normalized_name in seen: + return None + return _external_relation_name( + node=cte, + ctes=ctes, + seen=seen | {normalized_name}, + ) + + def _select_schema( *, node: object, diff --git a/src/sqlbuild/compiler/compile/models.py b/src/sqlbuild/compiler/compile/models.py index f6318ad87d..999eabe754 100644 --- a/src/sqlbuild/compiler/compile/models.py +++ b/src/sqlbuild/compiler/compile/models.py @@ -159,6 +159,7 @@ class DynamicColumnContractProof: output_proven: bool fixed_columns: tuple[InferredColumn, ...] = field(default_factory=tuple) families: tuple[DynamicColumnFamilyProof, ...] = field(default_factory=tuple) + input_relations: tuple[str, ...] = field(default_factory=tuple) failure_reason: str | None = None bare_dynamic_pivot: bool = False diff --git a/src/sqlbuild/compiler/discovery/_helpers/filesystem/python_paths.py b/src/sqlbuild/compiler/discovery/_helpers/filesystem/python_paths.py index 48e593c87c..fe43286a0b 100644 --- a/src/sqlbuild/compiler/discovery/_helpers/filesystem/python_paths.py +++ b/src/sqlbuild/compiler/discovery/_helpers/filesystem/python_paths.py @@ -37,7 +37,7 @@ _LANGUAGE_PYTHON_ROOTS: frozenset[tuple[str, str]] = frozenset( {("functions", "python"), ("hooks", "python")} ) -_IGNORED_ROOTS: frozenset[str] = frozenset({"logs", "target", "venv"}) +_IGNORED_ROOTS: frozenset[str] = frozenset({"logs", "target", "tests", "venv"}) _LEGACY_DIAGNOSTIC_ROOTS: frozenset[str] = frozenset({"event_exporters"}) _SUPPORTED_ROOT_FILES: frozenset[Path] = frozenset({Path("adapter.py"), Path("definitions.py")}) _SCOPED_DECLARATION_DIRECTORIES: frozenset[str] = ( diff --git a/src/sqlbuild/compiler/planner/_helpers/fixtures/completion.py b/src/sqlbuild/compiler/planner/_helpers/fixtures/completion.py index ac41fe5775..3d4ebc5b4c 100644 --- a/src/sqlbuild/compiler/planner/_helpers/fixtures/completion.py +++ b/src/sqlbuild/compiler/planner/_helpers/fixtures/completion.py @@ -17,6 +17,7 @@ CompiledModel, CompiledProject, CompileSqlReference, + DynamicColumnContractProof, InferredColumn, ) from sqlbuild.compiler.compile.types import CompiledResourceType @@ -295,6 +296,12 @@ def _star_fixture_keys( for dependency in model.deps if (CompiledResourceType(dependency.resource_type), dependency.name) in fixture_keys ) + dynamic_contract: DynamicColumnContractProof | None = model.dynamic_column_contract + if dynamic_contract is not None and dynamic_contract.output_proven: + input_relations: frozenset[str] = frozenset( + name.casefold() for name in dynamic_contract.input_relations + ) + return frozenset(key for key in dependency_keys if key[1].casefold() in input_relations) schema_tables: list[dict[str, object]] = [] for key in dependency_keys: relation: FixtureRelationMetadata | None = relations.get(key) diff --git a/src/sqlbuild/compiler/sql_analysis/_helpers/polyglot.py b/src/sqlbuild/compiler/sql_analysis/_helpers/polyglot.py index 04900c1457..2252e43013 100644 --- a/src/sqlbuild/compiler/sql_analysis/_helpers/polyglot.py +++ b/src/sqlbuild/compiler/sql_analysis/_helpers/polyglot.py @@ -6,11 +6,76 @@ import polyglot_sql +from sqlbuild.compiler.sql_analysis.constants import ( + POLYGLOT_ANALYZE_QUERY_ENTRY_POINT, + POLYGLOT_ANALYZE_QUERY_OPTIONS_ARGUMENT_COUNT, + POLYGLOT_ANALYZE_QUERY_OPTIONS_ARGUMENT_INDEX, + POLYGLOT_COMPLEXITY_GUARD_OPTION, + POLYGLOT_COMPLEXITY_GUARD_PARAMETER, + POLYGLOT_MAX_FUNCTION_CALL_DEPTH, + POLYGLOT_MAX_FUNCTION_CALL_DEPTH_OPTION, +) + +_COMPLEXITY_GUARD: dict[str, int] = { + POLYGLOT_MAX_FUNCTION_CALL_DEPTH_OPTION: POLYGLOT_MAX_FUNCTION_CALL_DEPTH, +} +_GUARDED_ENTRY_POINTS: frozenset[str] = frozenset( + { + POLYGLOT_ANALYZE_QUERY_ENTRY_POINT, + "parse", + "parse_data_type", + "parse_one", + "transpile", + "validate", + "validate_with_schema", + } +) + + +class _GuardedEntryPoint: + """One Polyglot entry point with SQLBuild's bounded trusted-SQL budget.""" + + def __init__(self, *, name: str, function: Any) -> None: + self._name: str = name + self._function: Any = function + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + if self._has_explicit_options_guard(args=args, kwargs=kwargs): + return self._function(*args, **kwargs) + guarded_kwargs: dict[str, Any] = { + POLYGLOT_COMPLEXITY_GUARD_PARAMETER: dict(_COMPLEXITY_GUARD), + **kwargs, + } + return self._function(*args, **guarded_kwargs) + + def _has_explicit_options_guard(self, *, args: tuple[Any, ...], kwargs: dict[str, Any]) -> bool: + if self._name != POLYGLOT_ANALYZE_QUERY_ENTRY_POINT: + return False + options: object = kwargs.get("options") + if len(args) >= POLYGLOT_ANALYZE_QUERY_OPTIONS_ARGUMENT_COUNT: + options = args[POLYGLOT_ANALYZE_QUERY_OPTIONS_ARGUMENT_INDEX] + return isinstance(options, dict) and POLYGLOT_COMPLEXITY_GUARD_OPTION in options + + +class _PolyglotSqlModule: + """Proxy Polyglot parsing through SQLBuild's trusted-SQL policy.""" + + def __getattr__(self, name: str) -> Any: + attribute: Any = getattr(polyglot_sql, name) + return ( + _GuardedEntryPoint(name=name, function=attribute) + if name in _GUARDED_ENTRY_POINTS + else attribute + ) + + +_POLYGLOT_SQL_MODULE: _PolyglotSqlModule = _PolyglotSqlModule() + def import_polyglot() -> Any: """Return SQLBuild's required Polyglot SQL module.""" - return polyglot_sql + return _POLYGLOT_SQL_MODULE def import_polyglot_sql() -> Any: diff --git a/src/sqlbuild/compiler/sql_analysis/constants.py b/src/sqlbuild/compiler/sql_analysis/constants.py index 8de11714a0..74b456c87a 100644 --- a/src/sqlbuild/compiler/sql_analysis/constants.py +++ b/src/sqlbuild/compiler/sql_analysis/constants.py @@ -14,6 +14,13 @@ SQL_IDENTIFIER_PREFIX: str = "_" SQL_NULL_KEYWORD: str = "NULL" SNOWFLAKE_DIALECT_NAME: str = "snowflake" +POLYGLOT_ANALYZE_QUERY_ENTRY_POINT: str = "analyze_query" +POLYGLOT_ANALYZE_QUERY_OPTIONS_ARGUMENT_COUNT: int = 2 +POLYGLOT_ANALYZE_QUERY_OPTIONS_ARGUMENT_INDEX: int = 1 +POLYGLOT_COMPLEXITY_GUARD_PARAMETER: str = "complexity_guard" +POLYGLOT_COMPLEXITY_GUARD_OPTION: str = "complexityGuard" +POLYGLOT_MAX_FUNCTION_CALL_DEPTH_OPTION: str = "maxFunctionCallDepth" +POLYGLOT_MAX_FUNCTION_CALL_DEPTH: int = 512 BINDING_UNKNOWN_TABLE_INTERNAL_CODE: str = "B000" POLYGLOT_KIND_ALIAS: str = "alias" diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/_test_types.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/_test_types.py index 20a9462961..8aa27f8058 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/_test_types.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/_test_types.py @@ -100,3 +100,10 @@ class CompileSelectionTestCase: selection_args: tuple[str, ...] expected_stdout_fragments: tuple[str, ...] unexpected_stdout_fragments: tuple[str, ...] + + +@dataclass(frozen=True) +class DeepSqlAnalysisCompileTestCase: + description: str + function_depth: int + expected_stdout_fragment: str diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py index 38d4599382..5ab0ddae75 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py @@ -40,6 +40,13 @@ class CompileBenchmarkMeasurement(NamedTuple): summary: dict[str, int] +def nested_coalesce_expression(*, function_depth: int) -> str: + expression: str = "1" + for _ in range(function_depth): + expression = f"COALESCE({expression}, 0)" + return expression + + class LayeredProductionCompileBenchmarkResult(NamedTuple): cold: CompileBenchmarkMeasurement warm: CompileBenchmarkMeasurement diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_compile_complexity_guard.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_compile_complexity_guard.py new file mode 100644 index 0000000000..378ac50db1 --- /dev/null +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_compile_complexity_guard.py @@ -0,0 +1,66 @@ +"""E2E coverage for SQL analysis complexity budgets.""" + +from __future__ import annotations + +import subprocess +from pathlib import Path + +import pytest + +from tests.e2e.src.sqlbuild.cli.commands.main.compile._test_types import ( + DeepSqlAnalysisCompileTestCase, +) +from tests.e2e.src.sqlbuild.cli.commands.main.compile.helpers import ( + nested_coalesce_expression, +) +from tests.e2e.src.sqlbuild.cli.commands.shared.helpers import prepare_inline_project, run_sqb + + +@pytest.mark.parametrize( + "test_case", + ( + DeepSqlAnalysisCompileTestCase( + description="trusted model exceeds the defensive default", + function_depth=65, + expected_stdout_fragment="Project compiled 1 model", + ), + ), + ids=lambda case: case.description, +) +def test_given_deep_trusted_model_when_compiling_then_sql_analysis_completes( + test_case: DeepSqlAnalysisCompileTestCase, + tmp_path: Path, +) -> None: + project_dir: Path = prepare_inline_project( + tmp_path=tmp_path, + project_name="deep_sql_analysis", + repo_files={ + "sqlbuild_project.toml": ( + 'name = "deep_sql_analysis"\n' + 'adapter = "duckdb"\n' + 'default_target = "dev"\n\n' + "[connection]\n" + 'database = ":memory:"\n\n' + "[targets.dev]\n" + 'schema = "dev"\n' + ), + "models/deep_expression.sql": ( + "MODEL (columns (resolved_value (type NUMBER)));\n\n" + f"SELECT {nested_coalesce_expression(function_depth=test_case.function_depth)} " + "AS resolved_value\n" + ), + }, + ) + + result: subprocess.CompletedProcess[str] = run_sqb( + command=("--no-color", "compile"), + project_dir=project_dir, + working_dir=project_dir, + ) + + assert result.returncode == 0, result.stdout + result.stderr + assert test_case.expected_stdout_fragment in result.stdout + + +if __name__ == "__main__": + pytest.main([__file__, "-vv"]) diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_python_project_layout.py b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_python_project_layout.py index 0e63710194..6d20890f98 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_python_project_layout.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/compile/test_python_project_layout.py @@ -50,5 +50,40 @@ def test_given_python_under_unsupported_root_when_compiling_then_command_fails_w assert all(fragment in result.stderr for fragment in test_case.expected_stderr_fragments) +@pytest.mark.parametrize( + "test_case", + ( + PythonProjectLayoutCompileTestCase( + description="custom Rule harness test path", + repo_files={ + "sqlbuild_project.toml": 'name = "rule_harness_layout"\nadapter = "duckdb"\n', + "models/orders.sql": "MODEL ();\nSELECT 1 AS order_id\n", + "tests/rules/test_order_policy.py": "def test_order_policy(): pass\n", + }, + expected_exit_code=0, + expected_stderr_fragments=(), + ), + ), + ids=lambda case: case.description, +) +def test_given_rule_harness_test_when_compiling_then_python_test_path_is_accepted( + test_case: PythonProjectLayoutCompileTestCase, + tmp_path: Path, +) -> None: + project_dir: Path = prepare_inline_project( + tmp_path=tmp_path, + project_name="rule_harness_layout", + repo_files=test_case.repo_files, + ) + + result: subprocess.CompletedProcess[str] = run_sqb( + command=("--no-color", "compile"), + project_dir=project_dir, + ) + + assert result.returncode == test_case.expected_exit_code, result.stdout + result.stderr + assert "Project compiled 1 model" in result.stdout + + if __name__ == "__main__": pytest.main([__file__, "-vv"]) diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/test/helpers.py b/tests/e2e/src/sqlbuild/cli/commands/main/test/helpers.py index 09b323204c..378d464141 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/test/helpers.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/test/helpers.py @@ -3,6 +3,75 @@ from __future__ import annotations +def build_dynamic_pivot_test_project_files() -> dict[str, str]: + """Build a Snowflake dynamic-pivot project for offline test-plan inspection.""" + + return { + "sqlbuild_project.toml": ( + 'name = "dynamic_pivot_test_plan"\n' + 'adapter = "snowflake"\n\n' + "[defaults]\n" + 'contract = "enforced"\n' + ), + "models/stg_order_amounts.sql": ( + "MODEL (\n" + " database analytics,\n" + " schema staging,\n" + " columns (\n" + " customer_id (type INTEGER),\n" + " category (type VARCHAR),\n" + ' amount (type "DECIMAL(12,2)"),\n' + " country (type VARCHAR),\n" + " ),\n" + ");\n" + "SELECT CAST(1 AS INTEGER) AS customer_id, " + "CAST('books' AS VARCHAR) AS category, " + "CAST(10.25 AS DECIMAL(12,2)) AS amount, " + "CAST('US' AS VARCHAR) AS country\n" + ), + "models/customer_category_amounts.sql": ( + "MODEL (\n" + " database analytics,\n" + " schema mart,\n" + " columns (\n" + " customer_id (type INTEGER),\n" + " country (type VARCHAR),\n" + " ),\n" + " dynamic_columns (\n" + " category_amounts (\n" + " pivot_column category,\n" + " value_column amount,\n" + " aggregate MAX,\n" + ' type "DECIMAL(12,2)"\n' + " )\n" + " ),\n" + ");\n" + "WITH pivot_input AS (\n" + " SELECT customer_id, category, amount, country\n" + ' FROM __ref("stg_order_amounts")\n' + "), final AS (\n" + " SELECT *\n" + " FROM pivot_input PIVOT(MAX(amount) FOR category IN (ANY ORDER BY category))\n" + ")\n" + "SELECT * FROM final\n" + ), + "tests/unit/test_customer_category_amounts.sql": ( + 'TEST (name "customer_category_amounts__empty_input");\n\n' + "WITH __ref__stg_order_amounts AS (\n" + " SELECT\n" + " CAST(NULL AS INTEGER) AS customer_id,\n" + " CAST(NULL AS VARCHAR) AS category,\n" + " CAST(NULL AS DECIMAL(12,2)) AS amount,\n" + " CAST(NULL AS VARCHAR) AS country\n" + " WHERE FALSE\n" + "), __assert__empty AS (\n" + ' SELECT 1 AS unexpected_row FROM __ref("customer_category_amounts")\n' + ")\n" + "SELECT 1\n" + ), + } + + def build_chain_test_project_files(*, sql_analysis_enabled: bool) -> dict[str, str]: """Build an inline project with a two-model SQL unit-test chain.""" diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/test/test_test.py b/tests/e2e/src/sqlbuild/cli/commands/main/test/test_test.py index 4e0760859d..c305282c66 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/test/test_test.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/test/test_test.py @@ -27,6 +27,7 @@ build_complex_values_fixture_project_files, build_cte_derived_output_fixture_project_files, build_cte_partial_source_fixture_project_files, + build_dynamic_pivot_test_project_files, build_empty_partial_fixture_project_files, build_explicit_typed_null_fixture_project_files, build_incompatible_fixture_type_project_files, @@ -455,6 +456,15 @@ def test_given_invalid_fixture_when_testing_then_static_diagnostics_prevent_conn "Test plan inspection complete: 1 selected, 0 errors.", ), ), + SqlTestPlanInspectionE2ETestCase( + description="dynamic pivot uses compiler-proven fixture closure", + repo_files=build_dynamic_pivot_test_project_files(), + expected_stdout_fragments=( + "mocked refs: stg_order_amounts", + "real models: customer_category_amounts", + "Test plan inspection complete: 1 selected, 0 errors.", + ), + ), ], ids=lambda case: case.description, ) diff --git a/tests/unit/src/sqlbuild/compiler/compile/_helpers/analysis/_test_types.py b/tests/unit/src/sqlbuild/compiler/compile/_helpers/analysis/_test_types.py index 35719bacd6..f422157cba 100644 --- a/tests/unit/src/sqlbuild/compiler/compile/_helpers/analysis/_test_types.py +++ b/tests/unit/src/sqlbuild/compiler/compile/_helpers/analysis/_test_types.py @@ -12,4 +12,5 @@ class DynamicPivotAnalysisTestCase: expected_fixed_columns: tuple[str, ...] = () expected_failure_fragment: str = "" expected_family_types: tuple[str | None, ...] = ("DECIMAL(12,2)",) + expected_input_relations: tuple[str, ...] = () aggregate: str = "MAX" diff --git a/tests/unit/src/sqlbuild/compiler/compile/_helpers/analysis/test_dynamic_pivot.py b/tests/unit/src/sqlbuild/compiler/compile/_helpers/analysis/test_dynamic_pivot.py index cb323f3930..812f5c5c4a 100644 --- a/tests/unit/src/sqlbuild/compiler/compile/_helpers/analysis/test_dynamic_pivot.py +++ b/tests/unit/src/sqlbuild/compiler/compile/_helpers/analysis/test_dynamic_pivot.py @@ -26,6 +26,20 @@ ), expected_proven=True, expected_fixed_columns=("customer_id", "country"), + expected_input_relations=("order_amounts",), + ), + DynamicPivotAnalysisTestCase( + description="parenthesized pivot input CTE retains its authoritative relation", + dialect="snowflake", + query_sql=( + "WITH pivot_input AS ((" + "SELECT customer_id, category, amount, country FROM order_amounts" + ")) SELECT * FROM pivot_input " + "PIVOT(MAX(amount) FOR category IN (ANY ORDER BY category))" + ), + expected_proven=True, + expected_fixed_columns=("customer_id", "country"), + expected_input_relations=("order_amounts",), ), DynamicPivotAnalysisTestCase( description="static pivot list requires exact columns", @@ -101,6 +115,7 @@ expected_proven=True, expected_fixed_columns=("customer_id", "country"), expected_family_types=("DECIMAL(12, 2)",), + expected_input_relations=("order_amounts",), ), DynamicPivotAnalysisTestCase( description="widening aggregate input cast does not prove output type", @@ -112,6 +127,7 @@ expected_proven=True, expected_fixed_columns=("customer_id",), expected_family_types=(None,), + expected_input_relations=("order_amounts",), aggregate="SUM", ), DynamicPivotAnalysisTestCase( @@ -174,4 +190,5 @@ def test_given_dynamic_family_when_analyzing_output_then_requires_supported_clos assert tuple(family.inferred_type for family in proof.families) == ( test_case.expected_family_types ) + assert proof.input_relations == test_case.expected_input_relations assert test_case.expected_failure_fragment in (proof.failure_reason or "") diff --git a/tests/unit/src/sqlbuild/compiler/compile/_helpers/test_sql_analysis_columns.py b/tests/unit/src/sqlbuild/compiler/compile/_helpers/test_sql_analysis_columns.py index 4cc09a9b9e..c44ee02c33 100644 --- a/tests/unit/src/sqlbuild/compiler/compile/_helpers/test_sql_analysis_columns.py +++ b/tests/unit/src/sqlbuild/compiler/compile/_helpers/test_sql_analysis_columns.py @@ -1106,7 +1106,11 @@ def raise_parse_error(*args: object, **kwargs: object) -> object: expected_columns=( InferredColumn(name="amount", type="INT"), InferredColumn(name="status", type="TEXT"), - InferredColumn(name="nullable_status", type="TEXT"), + InferredColumn( + name="nullable_status", + type="TEXT", + nullability=InferredNullability.NULLABLE, + ), ), expected_lineage_columns=direct_orders_lineage("amount", "status") + ( @@ -1287,7 +1291,11 @@ def test_given_cte_chain_when_analyzing_direct_passthrough_then_type_is_conserva name="source_name", nullability=InferredNullability.NON_NULL, ), - InferredColumn(name="active", type="BOOLEAN"), + InferredColumn( + name="active", + type="BOOLEAN", + nullability=InferredNullability.NON_NULL, + ), InferredColumn(name="selected", type="BOOLEAN"), InferredColumn(name="full_name", type="TEXT"), InferredColumn(name="status", type="VARCHAR(16777216)"), diff --git a/tests/unit/src/sqlbuild/compiler/sql_analysis/main/_test_types.py b/tests/unit/src/sqlbuild/compiler/sql_analysis/main/_test_types.py index 98c2d40ddf..3568e0fe3b 100644 --- a/tests/unit/src/sqlbuild/compiler/sql_analysis/main/_test_types.py +++ b/tests/unit/src/sqlbuild/compiler/sql_analysis/main/_test_types.py @@ -18,3 +18,18 @@ class PolyglotSqlNormalizationTestCase: sql: str dialect: str | None expected_sql: str + + +@dataclass(frozen=True) +class PolyglotDefaultGuardTestCase: + description: str + function_depth: int + expected_kind: str + + +@dataclass(frozen=True) +class PolyglotExplicitGuardTestCase: + description: str + function_depth: int + maximum_function_depth: int + expected_error_pattern: str diff --git a/tests/unit/src/sqlbuild/compiler/sql_analysis/main/helpers.py b/tests/unit/src/sqlbuild/compiler/sql_analysis/main/helpers.py new file mode 100644 index 0000000000..c73c266f3a --- /dev/null +++ b/tests/unit/src/sqlbuild/compiler/sql_analysis/main/helpers.py @@ -0,0 +1,8 @@ +"""Helpers for SQL analysis public-entry tests.""" + + +def nested_coalesce_sql(*, function_depth: int) -> str: + expression: str = "value" + for _ in range(function_depth): + expression = f"COALESCE({expression}, 0)" + return f"SELECT {expression} AS resolved_value FROM records" diff --git a/tests/unit/src/sqlbuild/compiler/sql_analysis/main/test_import_polyglot_sql.py b/tests/unit/src/sqlbuild/compiler/sql_analysis/main/test_import_polyglot_sql.py new file mode 100644 index 0000000000..09551cef1a --- /dev/null +++ b/tests/unit/src/sqlbuild/compiler/sql_analysis/main/test_import_polyglot_sql.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from typing import Any + +import pytest + +from sqlbuild.compiler.sql_analysis.main.import_polyglot_sql import import_polyglot_sql +from tests.unit.src.sqlbuild.compiler.sql_analysis.main._test_types import ( + PolyglotDefaultGuardTestCase, + PolyglotExplicitGuardTestCase, +) +from tests.unit.src.sqlbuild.compiler.sql_analysis.main.helpers import nested_coalesce_sql + + +@pytest.mark.parametrize( + "test_case", + ( + PolyglotDefaultGuardTestCase( + description="trusted SQL exceeds the defensive default", + function_depth=65, + expected_kind="select", + ), + ), + ids=lambda case: case.description, +) +def test_given_deep_trusted_sql_when_parsing_then_sqlbuild_uses_bounded_budget( + test_case: PolyglotDefaultGuardTestCase, +) -> None: + polyglot_module: Any = import_polyglot_sql() + + parsed: Any = polyglot_module.parse_one( + nested_coalesce_sql(function_depth=test_case.function_depth), + dialect="snowflake", + ) + + assert parsed.kind == test_case.expected_kind + + +@pytest.mark.parametrize( + "test_case", + ( + PolyglotExplicitGuardTestCase( + description="caller retains the defensive default", + function_depth=65, + maximum_function_depth=64, + expected_error_pattern="FUNCTION_NESTING_DEPTH_EXCEEDED", + ), + ), + ids=lambda case: case.description, +) +def test_given_explicit_guard_when_parsing_then_caller_budget_is_preserved( + test_case: PolyglotExplicitGuardTestCase, +) -> None: + polyglot_module: Any = import_polyglot_sql() + + with pytest.raises(polyglot_module.ParseError, match=test_case.expected_error_pattern): + polyglot_module.parse_one( + nested_coalesce_sql(function_depth=test_case.function_depth), + dialect="snowflake", + complexity_guard={"maxFunctionCallDepth": test_case.maximum_function_depth}, + ) + + +@pytest.mark.parametrize( + "test_case", + ( + PolyglotExplicitGuardTestCase( + description="analysis options retain the caller guard", + function_depth=65, + maximum_function_depth=64, + expected_error_pattern="FUNCTION_NESTING_DEPTH_EXCEEDED", + ), + ), + ids=lambda case: case.description, +) +def test_given_options_guard_when_analyzing_then_caller_budget_is_preserved( + test_case: PolyglotExplicitGuardTestCase, +) -> None: + polyglot_module: Any = import_polyglot_sql() + + with pytest.raises(polyglot_module.ParseError, match=test_case.expected_error_pattern): + polyglot_module.analyze_query( + nested_coalesce_sql(function_depth=test_case.function_depth), + { + "dialect": "snowflake", + "complexityGuard": { + "maxFunctionCallDepth": test_case.maximum_function_depth, + }, + }, + ) + + +if __name__ == "__main__": + pytest.main([__file__, "-vv"]) diff --git a/uv.lock b/uv.lock index 1b7501cc0d..0955bd77e4 100644 --- a/uv.lock +++ b/uv.lock @@ -2866,29 +2866,29 @@ wheels = [ [[package]] name = "polyglot-sql" -version = "0.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/bb/3e60486d308cd29baa09c214528d8fffa81fc6221ac3ab1f0acb9f79fb40/polyglot_sql-0.11.0.tar.gz", hash = "sha256:e504c00684ee878b25433cdc4b5daeb096606c6025632a57346e4a2398c85a52", size = 1915236, upload-time = "2026-09-16T13:44:57.26Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/89/89818644cd56e7cf0ff41743a95d3a54a1ba28c89acaf6da26c01b720796/polyglot_sql-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:955076f56806c3b566fcb982c26bde62ebe1b879108c9c45eae723fab461d078", size = 11688358, upload-time = "2026-09-16T13:44:01.622Z" }, - { url = "https://files.pythonhosted.org/packages/5a/2e/d54bf5ddbc0d755a444401e497cd7223e566fe3e46478b7db6eeccee903c/polyglot_sql-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a17e06999e0ada81604742819d789433a8911ddf16327d89b8383d8e8625ae9", size = 11022059, upload-time = "2026-09-16T13:44:04.077Z" }, - { url = "https://files.pythonhosted.org/packages/c6/5a/67225199c258add9472acd5cecff8529f1b4bccb0af35fa6d1d71a5f6130/polyglot_sql-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928863c8ce70b17d87a655ffeef598524e64230391a0d70b085b3cec978496b", size = 11849411, upload-time = "2026-09-16T13:44:06.552Z" }, - { url = "https://files.pythonhosted.org/packages/2f/1b/8615412a3f14f32da812866fc134a89b51b70f3aaa4fe1690a538206e856/polyglot_sql-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7d4095a5e0b9c45d397d050e727c71d0f48b787ecc42228777ed10e2fc22578", size = 10869867, upload-time = "2026-09-16T13:44:08.878Z" }, - { url = "https://files.pythonhosted.org/packages/3c/31/23108146b5de65f83955be9cfd4205bee24c0154359ca3e1ab34ab7720b2/polyglot_sql-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:0071af0022d891ac5234383111619c6534dcd5acb6681d251bcdeeb19b654264", size = 10662540, upload-time = "2026-09-16T13:44:11.363Z" }, - { url = "https://files.pythonhosted.org/packages/b6/40/19f9e3f13d588c02a71b1aed462006dc063cac8e334d3db568fc6241717b/polyglot_sql-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:087ea83398f33ba5fad869ca77953141778a607b5d71ff18cb13bbdf77d0f48b", size = 11688451, upload-time = "2026-09-16T13:44:13.547Z" }, - { url = "https://files.pythonhosted.org/packages/59/4d/961019c4e3e8ec7793ee030c170a3d56b5a1d3aba153a27e89990a900153/polyglot_sql-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:245d0c4aebe4f47a2f5f74770f588aaad8e95b60a3dd192b7882acb997aef163", size = 11022291, upload-time = "2026-09-16T13:44:15.829Z" }, - { url = "https://files.pythonhosted.org/packages/ad/af/5186612b8c6293d59dd8ac225250d060d46b08af46bdb4f7c9672b7d9cc6/polyglot_sql-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1610bd0f5fd9399077c1cd174f2bb72508f12b2f6adfaa409e456afe841c1651", size = 11849762, upload-time = "2026-09-16T13:44:18.201Z" }, - { url = "https://files.pythonhosted.org/packages/0a/a3/a99de05374b27a1b67c1d87419569234dd563eff00461cfd209e8e433c96/polyglot_sql-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3e73273aceac69ad924beaec71c3046ae7771d8389f166d2c0c1eeeea9076db", size = 10869177, upload-time = "2026-09-16T13:44:20.696Z" }, - { url = "https://files.pythonhosted.org/packages/8a/7c/bb9ff3da17c0ab5508a0d68e8324dbe0306d74c02acf2fcf5bcbf9801709/polyglot_sql-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f35d50aee210625413ec90ae5fd1cfca9cb552415ae1e984662af795af525db7", size = 10662753, upload-time = "2026-09-16T13:44:22.919Z" }, - { url = "https://files.pythonhosted.org/packages/9a/c2/a53bf3552adb5b35b97a0525d054f3dc45c08627069e379f39ce29115715/polyglot_sql-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:248af7dc342c7ac67357a8b81a99090949bc9a2e5e160c4f8dc0347f81cf7cde", size = 11690665, upload-time = "2026-09-16T13:44:25.236Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b3/ba45e7a2b8267fa8aa94c0df6e9489441e34a04fb40cf282d3ea508ad9f0/polyglot_sql-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b3cc1a6087617544916371323aaca768e4176d161a296f2cf8ac1010de0d79e7", size = 11024851, upload-time = "2026-09-16T13:44:27.663Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a6/54664d1d4fb69980ac898c64b718c26616db339129c69574c681d11224fb/polyglot_sql-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb92979fc9dff12b96a354bb12123a76691d447aa67747047dbf3ea7e21b7ac2", size = 11849921, upload-time = "2026-09-16T13:44:30.131Z" }, - { url = "https://files.pythonhosted.org/packages/7f/4a/0c8678b42932dcc649fa602b12dda58a8e8a27cf4e7db68e5832c8547970/polyglot_sql-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cba73a661835769af6dee21c44aefa969e49cdf0954aba548155afd747e508b9", size = 10870596, upload-time = "2026-09-16T13:44:32.506Z" }, - { url = "https://files.pythonhosted.org/packages/92/6a/580c46b88263f0da637aeaf524ab84810cd5a2c720ececf3ef81c606497c/polyglot_sql-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:54aa8bd89d119ffa264756972a44d548e600e3d8d1d400416aa8924b6bff6e41", size = 10663183, upload-time = "2026-09-16T13:44:35.057Z" }, - { url = "https://files.pythonhosted.org/packages/46/5b/392a00b7a18bfc9af4b790acdbcf45e5c3fbd794060b7d2aa4116dbbc7b4/polyglot_sql-0.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8013bbd5f4ce53980f58919bdede2d397e21f6c04985cc13bf4f510881cc7380", size = 11950739, upload-time = "2026-09-16T13:44:37.367Z" }, - { url = "https://files.pythonhosted.org/packages/e9/55/8c9e2e7e5970dfc66943d9a5671a59c438a7812efc052619b96c9aa1de24/polyglot_sql-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b9b9eb16724e2b96fdcf487e69a2047d91ea309e8d9080125c55598ef336fcf", size = 10944134, upload-time = "2026-09-16T13:44:39.597Z" }, - { url = "https://files.pythonhosted.org/packages/c5/5f/8d47b870e93f69bdf02e0d767b4d722ed6d29f784a6958dac7b8881c380e/polyglot_sql-0.11.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b362bcb815ecb0e46cbd2cf7be6e17caa1424352fcca381c531a53459b14c3f6", size = 10870596, upload-time = "2026-09-16T13:44:41.894Z" }, - { url = "https://files.pythonhosted.org/packages/9e/9c/3cff0f115fc1505124727a7ced3dc8f88900e8dc860d2ec476dcc4679cee/polyglot_sql-0.11.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f1408701b8a867dae2fa1775985f803dab73c71f9dbccfff70bee74696936f", size = 10944136, upload-time = "2026-09-16T13:44:44.388Z" }, +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/1d/9c90e163a8aad8640f5d27758f388cff1eddfd7313689ff5dbb688554a03/polyglot_sql-0.12.0.tar.gz", hash = "sha256:21238b930910c63cbaf98db47d592e1cd011683deceb3e9930e1ed0db2da89ea", size = 1967360, upload-time = "2026-09-18T19:19:35.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/fd/39cc4b735c5cc0553b5e7bf9ead668cd123bb35aaf4cc87ae68c95297e03/polyglot_sql-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e22f72ce05dfcaca3eca5efb20e9880db26877a0fb333c6771614dd8e7f1d61", size = 11982006, upload-time = "2026-09-18T19:18:29.676Z" }, + { url = "https://files.pythonhosted.org/packages/ee/45/6b9d08d7522ae97d07411bfcce78547b3eaeefb4071077218d1c7762d437/polyglot_sql-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fa659f8dc9414b9082b05ba13fecbd4faf736ebdcc9b0d4d8e04400298c0fd5", size = 11285547, upload-time = "2026-09-18T19:18:32.391Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cd/c6843bebc6c5f57c91af917f3483d459835a1adbbe04d908f655126bfb39/polyglot_sql-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16e1b03b6dcf398ed6c7868e271f91eb72d64d434ea05a77ac7c2f81c7e83bce", size = 12141990, upload-time = "2026-09-18T19:18:35.161Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e1/466694e02797332d20c30fa7dcfb2a7ca3d86227aa843c614f398750d2f4/polyglot_sql-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9767cf912231a25a34ca143f3f0a4fc938e74ad3b2463a9366c919699f9f3970", size = 11182872, upload-time = "2026-09-18T19:18:38.276Z" }, + { url = "https://files.pythonhosted.org/packages/26/4c/0b3fcd94edd87c5c3a6371351684c8b8dde5a59bede9cf1ac1d29aa94970/polyglot_sql-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba92a042b4c31ea4e01c4755aa22eebc024396fd3c1d157a7f4d82686bfb6359", size = 10945551, upload-time = "2026-09-18T19:18:41.12Z" }, + { url = "https://files.pythonhosted.org/packages/03/f8/ff0959abc0af4bba637b8fdeb862759c6f9731551081852df38b3f12932a/polyglot_sql-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4cb68d0af5d8ced6b0d496bdf7be72de3c31f8a1a81a773db69b3fe430754352", size = 11982119, upload-time = "2026-09-18T19:18:44.035Z" }, + { url = "https://files.pythonhosted.org/packages/52/37/e42e097167dc7da110c1d12523f73d26f1b49a34797aa462f28e7fa9ff44/polyglot_sql-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:13b33b8b14e96e89301036e6282115708e5b316bb24d20a12d5372efc05f167c", size = 11283832, upload-time = "2026-09-18T19:18:47.172Z" }, + { url = "https://files.pythonhosted.org/packages/79/07/71dfba1fc51432bda284e5394a9ef3dabc5e72112fd7028708db852b2f21/polyglot_sql-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1287fa54dd6657f42aee0e426ccffea99e5db995f60c27b90914db2e0c0655", size = 12141415, upload-time = "2026-09-18T19:18:50.205Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6e/142762fb1cfc4d286bc218f89522570c93269ad844251a3657ee4bc51671/polyglot_sql-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a280a0ad51c015846df7de833527de6f8950118e12b2c27295f0c16ced16a74", size = 11182625, upload-time = "2026-09-18T19:18:52.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/44/5b5b85679d1e0c04a08f6acaa1f6a8c96799b0053c49aeaa772521c3d658/polyglot_sql-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:d0eee8d6d5fd2223dfce59e277cb316efa9a5361bf23f0ee43d3ac9b00ff10af", size = 10945934, upload-time = "2026-09-18T19:18:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/6d/80/95b765a03b00f99c32988c394903a90af67568b98765d54f884cc011194a/polyglot_sql-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5679a366729fd894c7d6e1da071083d633b15f1813b777ecae7b2b151af1c3cf", size = 11985867, upload-time = "2026-09-18T19:18:58.295Z" }, + { url = "https://files.pythonhosted.org/packages/a8/38/25618ec36569241effec75818801c2cb9d2e3927b94a86ae9dd5ac17f32c/polyglot_sql-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:15fda75da8cdd70f64e8129e6c9fea4fa30f6d7300dc0d614b2418fc72078b1c", size = 11286086, upload-time = "2026-09-18T19:19:01.033Z" }, + { url = "https://files.pythonhosted.org/packages/31/ff/6e002c4851c211567fb5d09431f6c356fd13643d7bbb5c26fd7e18c8ab79/polyglot_sql-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21a8afa14df83005224b94604444036aafcacd9d6f24fd38c99d65720828367f", size = 12142441, upload-time = "2026-09-18T19:19:04.168Z" }, + { url = "https://files.pythonhosted.org/packages/76/da/18c232d9956475931840f3440906ae42caf848a7726c33fdb5e31e6d6576/polyglot_sql-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fb4ff099e6ef4bc3d88e8b33bb9e75b35cd62bfda040f2373f2d8de67911af6", size = 11183280, upload-time = "2026-09-18T19:19:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/b7/18/4ea30b0a3eba3fc247af83297965775f01391399f3badac0088c35e7f15a/polyglot_sql-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa2d9aef776efe32c5748cdd4bcb3d1ec83c5462695f4f910ae4ebd109dfbcd3", size = 10946787, upload-time = "2026-09-18T19:19:10.437Z" }, + { url = "https://files.pythonhosted.org/packages/d4/59/b00018ca8657fb522d801616c5cb2e9151ec12ec7e91db92e90f350c59cc/polyglot_sql-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e9b667b92879ba08fd6a3a352e3db91c5b5b322c812389062491138faceaf5b", size = 12240568, upload-time = "2026-09-18T19:19:13.393Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fc/8e07d7d0ebeb4ff4c27b97fd67966c672df05afc106b58c9ac44076f9c48/polyglot_sql-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1416b7ead207ec15f0730dd2be593ea436c2b752c6376c8b844e62a332f0f9c8", size = 11254688, upload-time = "2026-09-18T19:19:16.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/e0/3e1a224bde8db663da684c53e1f5fe28044ecf20a1c584a90851f37937c9/polyglot_sql-0.12.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54764eeb2a6837e074c64e267c1bd6cc1a6abc5732a59d3daed213e636d59024", size = 11183280, upload-time = "2026-09-18T19:19:18.935Z" }, + { url = "https://files.pythonhosted.org/packages/fb/72/7c25bacea13f811be14875cab0a0f01157e5bfa4d8815bc97d2f39198a7b/polyglot_sql-0.12.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd2ae1fe8ef6bbf7164196cf529f57ac7fad4410810fce46b7379e58cac6ebf9", size = 11254691, upload-time = "2026-09-18T19:19:21.606Z" }, ] [[package]] @@ -4232,7 +4232,7 @@ requires-dist = [ { name = "ingestr", marker = "extra == 'ingestr'", specifier = ">=1.0.0" }, { name = "jinja2", marker = "extra == 'dbt'", specifier = ">=3.1" }, { name = "orjson", specifier = ">=3.10.0" }, - { name = "polyglot-sql", specifier = "==0.11.0" }, + { name = "polyglot-sql", specifier = "==0.12.0" }, { name = "psycopg", extras = ["binary"], marker = "extra == 'postgres'", specifier = ">=3.2.0" }, { name = "pydantic-settings", specifier = ">=2,<3" }, { name = "pymssql", marker = "extra == 'sqlserver'", specifier = ">=2.3.0" },