fabric-dw 2026.6.0a1.dev182__tar.gz → 2026.6.0a1.dev184__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.
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/PKG-INFO +3 -1
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/README.md +2 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/install.md +3 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/pyproject.toml +1 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/_version.py +2 -2
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/load.py +17 -16
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/sql_endpoints.py +88 -8
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/statistics.py +39 -2
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/sql.py +52 -8
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/conftest.py +28 -22
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_load.py +78 -6
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_sql_endpoints.py +233 -11
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_statistics.py +221 -1
- fabric_dw-2026.6.0a1.dev184/tests/unit/test_entry_points.py +40 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_sql.py +363 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.devcontainer/devcontainer.json +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.dockerignore +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.env.example +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/CODEOWNERS +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/dependabot.yml +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/release-drafter.yml +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/ci.yml +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/codeql.yml +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/docker.yml +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/integration.yml +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/pause-capacity.yml +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/publish.yml +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/release-drafter.yml +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/secret-scan.yml +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/security.yml +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.gitignore +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.gitleaks.toml +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.gitleaksignore +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.python-version +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/CODE_OF_CONDUCT.md +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/CONTRIBUTING.md +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/Dockerfile +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/LICENSE +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/SECURITY.md +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/assets/logo.svg +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/authentication.md +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/cli.md +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/code-of-conduct.md +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/completion.md +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/contributing.md +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/index.md +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/integration-coverage.md +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/license.md +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/mcp-tools.md +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/mcp.md +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/security.md +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/telemetry.md +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/troubleshooting.md +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs_build/cloudflare_pages.sh +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/justfile +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/overrides/partials/integrations/analytics/custom.html +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/__init__.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/auth.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cache.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/__init__.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/_context.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/_main.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/_render.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/__init__.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/_utils.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/audit.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/cache.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/completion.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/config.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/dbt.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/functions.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/procedures.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/queries.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/restore_points.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/schemas.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/snapshots.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/sql.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/sql_endpoints.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/sql_pools.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/statistics.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/tables.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/views.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/warehouses.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/workspaces.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/config.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/exceptions.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/http_client.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/identifiers.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/logging.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/__init__.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/_context.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/_guards.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/_helpers.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/server.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/__init__.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/audit.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/cache.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/dbt.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/functions.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/load.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/procedures.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/queries.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/restore.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/schemas.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/snapshots.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/sql_endpoints.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/sql_exec.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/sql_pools.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/statistics.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/tables.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/views.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/warehouses.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/workspaces.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/models.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/py.typed +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/resolver.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/__init__.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/_concurrency.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/_helpers.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/_lro.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/audit.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/capacities.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/dbt_scaffold.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/functions.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/ownership.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/permissions.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/procedures.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/queries.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/query_insights.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/restore.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/schemas.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/snapshots.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/sql_exec.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/sql_pools.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/tables.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/views.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/warehouses.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/workspaces.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/sql_io.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/telemetry.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/telemetry_commands.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/types.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/__init__.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/conftest.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/fixtures/__init__.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/fixtures/api_payloads.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/__init__.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_cli_smoke.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_cross_cutting.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_audit.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_create_and_load.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_create_empty_table.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_dbt_scaffold.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_functions.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_load.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_ownership.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_permissions.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_procedures.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_queries.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_query_insights.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_restore.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_schemas.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_snapshots.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_sql_endpoints.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_sql_exec.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_sql_pools.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_statistics.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_tables.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_views.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_warehouses.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_workspaces.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_smoke.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/__init__.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/__init__.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/__init__.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/conftest.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_audit.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_audit_respx.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_completion.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_config.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_dbt.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_functions.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_procedures.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_queries.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_query_insights.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_restore_points.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_restore_points_respx.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_schemas.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_snapshots.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_snapshots_respx.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_sql.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_sql_endpoints.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_sql_pools.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_statistics.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_tables.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_utils.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_views.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_warehouses.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_warehouses_respx.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_workspaces.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_workspaces_respx.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/test_cache.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/test_completion.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/test_main.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/test_render.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/conftest.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/__init__.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/conftest.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/test_context.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/test_contract.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/test_procedures.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/test_security.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/test_server.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/__init__.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_audit.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_dbt.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_functions.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_load.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_queries.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_restore.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_snapshots.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_sql_pools.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_statistics.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_tables.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_tool_quality.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_views.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_warehouses.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_workspaces.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/__init__.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/_helpers.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/conftest.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_audit.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_capacity_skip.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_concurrency.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_create_and_load.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_dbt_scaffold.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_functions.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_helpers.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_lro.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_ownership.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_permissions.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_procedures.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_queries.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_query_insights.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_restore.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_schemas.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_snapshots.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_sql_exec.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_sql_pools.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_tables.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_views.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_warehouses.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_workspaces.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_auth.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_cache.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_cli_help_alias.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_cli_help_width.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_config.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_exceptions.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_global_options_anywhere.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_http_client.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_identifiers.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_integration_conftest_readiness.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_logging.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_models.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_resolver.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_services_w_series.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_shutdown_clean.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_smoke.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_sql_io.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_telemetry.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_telemetry_commands.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_types.py +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/uv.lock +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/wrangler.toml +0 -0
- {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/zensical.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fabric-dw
|
|
3
|
-
Version: 2026.6.0a1.
|
|
3
|
+
Version: 2026.6.0a1.dev184
|
|
4
4
|
Summary: Python CLI + MCP server for administering Microsoft Fabric Data Warehouses and SQL Analytics Endpoints
|
|
5
5
|
Project-URL: Homepage, https://fdw.debruyn.dev
|
|
6
6
|
Project-URL: Documentation, https://fdw.debruyn.dev
|
|
@@ -89,6 +89,8 @@ pip install fabric-dw
|
|
|
89
89
|
uvx fabric-dw --help
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
After installation, the `fdw` command is a short alias for `fabric-dw` — both invoke the same entry point.
|
|
93
|
+
|
|
92
94
|
## Quick Start
|
|
93
95
|
|
|
94
96
|
### CLI
|
|
@@ -39,6 +39,9 @@ fabric-dw --help
|
|
|
39
39
|
|
|
40
40
|
You should see the top-level help output listing available commands.
|
|
41
41
|
|
|
42
|
+
!!! tip "Short alias"
|
|
43
|
+
`fdw` is an equivalent short alias for `fabric-dw` — both commands invoke the same entry point.
|
|
44
|
+
|
|
42
45
|
---
|
|
43
46
|
|
|
44
47
|
See also the [Authentication](authentication.md) page for the full credential chain and the [Troubleshooting](troubleshooting.md) page for common failure modes such as expired tokens and 403 permission errors.
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2026.6.0a1.
|
|
22
|
-
__version_tuple__ = version_tuple = (2026, 6, 0, 'a1', '
|
|
21
|
+
__version__ = version = '2026.6.0a1.dev184'
|
|
22
|
+
__version_tuple__ = version_tuple = (2026, 6, 0, 'a1', 'dev184')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -762,11 +762,16 @@ async def copy_into_from_url(
|
|
|
762
762
|
_logger.debug("copy_into_from_url: schema=%s table=%s file_type=%s", schema, table, file_type)
|
|
763
763
|
|
|
764
764
|
def _run() -> tuple[int, int]:
|
|
765
|
-
# COPY INTO
|
|
765
|
+
# mssql-python ≥ 1.9.0: COPY INTO produces NO result set.
|
|
766
|
+
# cursor.description is None and fetchall() raises
|
|
767
|
+
# ProgrammingError: Invalid cursor state.
|
|
768
|
+
# cursor.rowcount correctly equals the number of rows loaded.
|
|
769
|
+
# rows_rejected is not exposed via the ODBC rowcount API; it is
|
|
770
|
+
# always reported as 0 here (known limitation).
|
|
766
771
|
|
|
767
772
|
_mode = mode if isinstance(mode, CredentialMode) else CredentialMode.DEFAULT
|
|
768
773
|
try:
|
|
769
|
-
|
|
774
|
+
_cols, rows = run_query(target, sql, mode=_mode, commit=True, fetch="rowcount")
|
|
770
775
|
except FabricError:
|
|
771
776
|
# Already a mapped high-level error — re-raise as-is; it does not
|
|
772
777
|
# contain the raw SQL statement text.
|
|
@@ -788,20 +793,16 @@ async def copy_into_from_url(
|
|
|
788
793
|
f"{type(exc).__name__} (details suppressed to protect credentials)"
|
|
789
794
|
)
|
|
790
795
|
raise FabricError(safe_msg) from exc
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
loaded = int(row[0] or 0) if row else 0
|
|
802
|
-
rejected = 0
|
|
803
|
-
return loaded, rejected
|
|
804
|
-
return 0, 0
|
|
796
|
+
# rows[0][0] is cursor.rowcount from run_query fetch="rowcount".
|
|
797
|
+
# ODBC rowcount is -1 when the driver cannot determine the count;
|
|
798
|
+
# treat any negative value as 0.
|
|
799
|
+
rows_loaded = (
|
|
800
|
+
int(rows[0][0])
|
|
801
|
+
if rows and rows[0] and rows[0][0] is not None and rows[0][0] >= 0
|
|
802
|
+
else 0
|
|
803
|
+
)
|
|
804
|
+
rows_rejected = 0
|
|
805
|
+
return rows_loaded, rows_rejected
|
|
805
806
|
|
|
806
807
|
rows_loaded, rows_rejected = await asyncio.to_thread(_run)
|
|
807
808
|
return CopyIntoResult(
|
{fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/sql_endpoints.py
RENAMED
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import logging
|
|
7
|
+
from typing import Any, cast
|
|
7
8
|
from uuid import UUID
|
|
8
9
|
|
|
9
10
|
from fabric_dw.exceptions import FabricServerError, NotFoundError, PermissionDeniedError
|
|
@@ -17,7 +18,13 @@ _logger = logging.getLogger("fabric_dw.sql_endpoints")
|
|
|
17
18
|
|
|
18
19
|
# Bounded polling for eventual-consistency fields (e.g. connection_string).
|
|
19
20
|
_CONN_STRING_POLL_INTERVAL: float = 5.0
|
|
20
|
-
|
|
21
|
+
# For lakehouse-derived endpoints the connection string lives on the *Lakehouse*
|
|
22
|
+
# body (sqlEndpointProperties.connectionString) and is available within ~20s of
|
|
23
|
+
# provisioning. The GET /sqlEndpoints/{id} resource always returns an empty
|
|
24
|
+
# connectionString for these endpoints — it never populates. The fallback reads
|
|
25
|
+
# from the parent Lakehouse instead, so the window needed is just the
|
|
26
|
+
# provisioning time (≈20s), not the original 10-minute guess.
|
|
27
|
+
_CONN_STRING_POLL_TIMEOUT: float = 120.0
|
|
21
28
|
|
|
22
29
|
__all__ = [
|
|
23
30
|
"get_endpoint",
|
|
@@ -107,10 +114,58 @@ async def list_all_workspaces(http: FabricHttpClient) -> list[Warehouse]:
|
|
|
107
114
|
)
|
|
108
115
|
|
|
109
116
|
|
|
117
|
+
async def _resolve_lakehouse_connection_string(
|
|
118
|
+
http: FabricHttpClient,
|
|
119
|
+
workspace_id: UUID,
|
|
120
|
+
endpoint_id: UUID,
|
|
121
|
+
) -> str | None:
|
|
122
|
+
"""Find the connection string for a lakehouse-derived SQL endpoint via the parent Lakehouse.
|
|
123
|
+
|
|
124
|
+
For lakehouse-derived SQL analytics endpoints, ``GET /sqlEndpoints/{id}``
|
|
125
|
+
permanently returns an empty ``connectionString`` — the value lives only on
|
|
126
|
+
the parent Lakehouse at
|
|
127
|
+
``properties.sqlEndpointProperties.connectionString``.
|
|
128
|
+
|
|
129
|
+
This helper pages ``GET /workspaces/{ws}/lakehouses``, locates the lakehouse
|
|
130
|
+
whose ``properties.sqlEndpointProperties.id`` matches *endpoint_id*, and
|
|
131
|
+
returns that lakehouse's ``connectionString``. Returns ``None`` when no
|
|
132
|
+
matching lakehouse is found (e.g. the endpoint belongs to a Warehouse, not a
|
|
133
|
+
Lakehouse).
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
http: An authenticated :class:`~fabric_dw.http_client.FabricHttpClient`.
|
|
137
|
+
workspace_id: The UUID of the workspace to search.
|
|
138
|
+
endpoint_id: The UUID of the SQL analytics endpoint whose connection
|
|
139
|
+
string we need.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
The non-empty connection string from the matching lakehouse, or ``None``
|
|
143
|
+
if no lakehouse in the workspace has a paired endpoint with this ID.
|
|
144
|
+
"""
|
|
145
|
+
# str(UUID) is always lowercase; lowercase the API value too so the match is
|
|
146
|
+
# robust against Fabric returning an uppercase/mixed-case UUID string.
|
|
147
|
+
endpoint_id_str = str(endpoint_id).lower()
|
|
148
|
+
async for lh in http.iter_paginated(HttpBase.FABRIC, f"/workspaces/{workspace_id}/lakehouses"):
|
|
149
|
+
props = lh.get("properties")
|
|
150
|
+
props_dict = cast("dict[str, Any]", props) if isinstance(props, dict) else {}
|
|
151
|
+
sql_ep = props_dict.get("sqlEndpointProperties")
|
|
152
|
+
sql_ep_dict = cast("dict[str, Any]", sql_ep) if isinstance(sql_ep, dict) else {}
|
|
153
|
+
if str(sql_ep_dict.get("id", "")).lower() == endpoint_id_str:
|
|
154
|
+
conn = str(sql_ep_dict.get("connectionString", ""))
|
|
155
|
+
return conn or None
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
110
159
|
async def get_endpoint(http: FabricHttpClient, workspace_id: UUID, endpoint_id: UUID) -> Warehouse:
|
|
111
160
|
"""Fetch a single SQL analytics endpoint by ID.
|
|
112
161
|
|
|
113
|
-
Uses ``GET /workspaces/{ws}/sqlEndpoints/{id}``.
|
|
162
|
+
Uses ``GET /workspaces/{ws}/sqlEndpoints/{id}``. When the endpoint's own
|
|
163
|
+
``connectionString`` is empty (which is permanent for lakehouse-derived
|
|
164
|
+
endpoints), falls back to scanning ``GET /workspaces/{ws}/lakehouses`` for
|
|
165
|
+
the parent Lakehouse whose ``properties.sqlEndpointProperties.id`` matches
|
|
166
|
+
*endpoint_id* and reads the connection string from there. No extra
|
|
167
|
+
lakehouse call is made when the endpoint resource already carries a
|
|
168
|
+
connection string.
|
|
114
169
|
|
|
115
170
|
Args:
|
|
116
171
|
http: An authenticated :class:`~fabric_dw.http_client.FabricHttpClient`.
|
|
@@ -119,7 +174,9 @@ async def get_endpoint(http: FabricHttpClient, workspace_id: UUID, endpoint_id:
|
|
|
119
174
|
|
|
120
175
|
Returns:
|
|
121
176
|
A populated :class:`~fabric_dw.models.Warehouse` instance with
|
|
122
|
-
``kind == WarehouseKind.SQL_ENDPOINT``.
|
|
177
|
+
``kind == WarehouseKind.SQL_ENDPOINT``. The ``connection_string``
|
|
178
|
+
field is populated whenever the parent Lakehouse exposes it (i.e. after
|
|
179
|
+
``provisioningStatus`` reaches ``"Success"``).
|
|
123
180
|
|
|
124
181
|
Raises:
|
|
125
182
|
NotFoundError: If the endpoint does not exist (404).
|
|
@@ -129,7 +186,27 @@ async def get_endpoint(http: FabricHttpClient, workspace_id: UUID, endpoint_id:
|
|
|
129
186
|
HttpBase.FABRIC,
|
|
130
187
|
f"/workspaces/{workspace_id}/sqlEndpoints/{endpoint_id}",
|
|
131
188
|
)
|
|
132
|
-
|
|
189
|
+
wh = Warehouse.from_api(resp.json(), kind=WarehouseKind.SQL_ENDPOINT)
|
|
190
|
+
|
|
191
|
+
if wh.connection_string:
|
|
192
|
+
# Fast path: endpoint resource already carries the connection string.
|
|
193
|
+
return wh
|
|
194
|
+
|
|
195
|
+
# Slow path: lakehouse-derived endpoints never populate connectionString on
|
|
196
|
+
# the /sqlEndpoints/{id} resource. Look it up via the parent Lakehouse.
|
|
197
|
+
_logger.debug(
|
|
198
|
+
"endpoint %s has empty connectionString on /sqlEndpoints resource; "
|
|
199
|
+
"falling back to lakehouse scan for workspace %s",
|
|
200
|
+
endpoint_id,
|
|
201
|
+
workspace_id,
|
|
202
|
+
)
|
|
203
|
+
lh_conn = await _resolve_lakehouse_connection_string(http, workspace_id, endpoint_id)
|
|
204
|
+
if lh_conn:
|
|
205
|
+
# Return a copy with the connection string resolved from the lakehouse,
|
|
206
|
+
# preserving every other field (description, collation, created_date, …).
|
|
207
|
+
return wh.model_copy(update={"connection_string": lh_conn})
|
|
208
|
+
|
|
209
|
+
return wh
|
|
133
210
|
|
|
134
211
|
|
|
135
212
|
async def get_endpoint_connection_string(
|
|
@@ -144,16 +221,19 @@ async def get_endpoint_connection_string(
|
|
|
144
221
|
|
|
145
222
|
SQL analytics endpoints are provisioned with eventual consistency: the
|
|
146
223
|
``connectionString`` field may be empty or absent immediately after
|
|
147
|
-
the endpoint is created. This function
|
|
148
|
-
|
|
149
|
-
is non-empty, up to *timeout* seconds.
|
|
224
|
+
the endpoint is created. This function calls :func:`get_endpoint`
|
|
225
|
+
(which includes the lakehouse-fallback for lakehouse-derived endpoints)
|
|
226
|
+
until the connection string is non-empty, up to *timeout* seconds.
|
|
227
|
+
For lakehouse-derived endpoints the value is available within ~20s of
|
|
228
|
+
``provisioningStatus`` reaching ``"Success"``; the default timeout is
|
|
229
|
+
120 s, well above that window.
|
|
150
230
|
|
|
151
231
|
Args:
|
|
152
232
|
http: An authenticated :class:`~fabric_dw.http_client.FabricHttpClient`.
|
|
153
233
|
workspace_id: The UUID of the workspace containing the endpoint.
|
|
154
234
|
endpoint_id: The UUID of the SQL analytics endpoint.
|
|
155
235
|
poll_interval: Seconds between polls (default 5.0).
|
|
156
|
-
timeout: Maximum wall-clock seconds to wait (default
|
|
236
|
+
timeout: Maximum wall-clock seconds to wait (default 120.0).
|
|
157
237
|
|
|
158
238
|
Returns:
|
|
159
239
|
The non-empty connection string.
|
{fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/statistics.py
RENAMED
|
@@ -45,6 +45,7 @@ reject SQL Analytics Endpoint items client-side with a clear
|
|
|
45
45
|
from __future__ import annotations
|
|
46
46
|
|
|
47
47
|
import asyncio
|
|
48
|
+
import time
|
|
48
49
|
from datetime import UTC, datetime
|
|
49
50
|
from typing import cast
|
|
50
51
|
|
|
@@ -80,6 +81,17 @@ _SQL_ENDPOINT_READONLY_MSG = (
|
|
|
80
81
|
_SAMPLE_PERCENT_MIN = 1
|
|
81
82
|
_SAMPLE_PERCENT_MAX = 100
|
|
82
83
|
|
|
84
|
+
# Bounded retry for Fabric DW eventual-consistency: DBCC SHOW_STATISTICS may
|
|
85
|
+
# return "Could not locate statistics" for a short window after CREATE STATISTICS
|
|
86
|
+
# even though sys.stats already reflects the new statistic. Poll until the
|
|
87
|
+
# statistic becomes visible to DBCC, up to _DBCC_STAT_TIMEOUT seconds total.
|
|
88
|
+
_DBCC_STAT_POLL_INTERVAL: float = 3.0
|
|
89
|
+
_DBCC_STAT_TIMEOUT: float = 60.0
|
|
90
|
+
|
|
91
|
+
# Substring matched (case-insensitive) against the exception message to
|
|
92
|
+
# distinguish Fabric's eventual-consistency transient error from other errors.
|
|
93
|
+
_DBCC_NOT_FOUND_MSG = "could not locate statistics"
|
|
94
|
+
|
|
83
95
|
|
|
84
96
|
def _assert_not_sql_endpoint(kind: WarehouseKind) -> None:
|
|
85
97
|
"""Raise :class:`~fabric_dw.exceptions.ItemKindError` for SQL Endpoint items.
|
|
@@ -431,7 +443,8 @@ async def show_statistics(
|
|
|
431
443
|
density_sql = _DBCC_DENSITY_SQL.format(table_s=table_s, stat_literal=stat_literal)
|
|
432
444
|
histogram_sql = _DBCC_HISTOGRAM_SQL.format(table_s=table_s, stat_literal=stat_literal)
|
|
433
445
|
|
|
434
|
-
def
|
|
446
|
+
def _run_once() -> StatisticDetails:
|
|
447
|
+
"""Execute DBCC SHOW_STATISTICS queries once (no retry)."""
|
|
435
448
|
if histogram_only:
|
|
436
449
|
h_cols, h_rows = run_query(target, histogram_sql, mode=mode)
|
|
437
450
|
return StatisticDetails(
|
|
@@ -455,7 +468,31 @@ async def show_statistics(
|
|
|
455
468
|
histogram=[_row_to_histogram(h_cols, r) for r in h_rows],
|
|
456
469
|
)
|
|
457
470
|
|
|
458
|
-
|
|
471
|
+
# Fabric DW eventual-consistency retry: DBCC SHOW_STATISTICS may raise
|
|
472
|
+
# "Could not locate statistics" for a short window after CREATE STATISTICS
|
|
473
|
+
# (the statistic is visible in sys.stats but not yet to DBCC). Retry
|
|
474
|
+
# until visible or until the timeout expires, then raise NotFoundError.
|
|
475
|
+
# NOTE: the effective timeout can exceed _DBCC_STAT_TIMEOUT by up to one
|
|
476
|
+
# TDS round-trip (the time asyncio.to_thread(_run_once) takes to return).
|
|
477
|
+
deadline = time.monotonic() + _DBCC_STAT_TIMEOUT
|
|
478
|
+
while True:
|
|
479
|
+
try:
|
|
480
|
+
return await asyncio.to_thread(_run_once)
|
|
481
|
+
except Exception as exc:
|
|
482
|
+
# map_driver_error() inside run_query may promote the raw DBCC
|
|
483
|
+
# driver error to NotFoundError before it reaches us. Check the
|
|
484
|
+
# message on ANY exception so that promoted NotFoundErrors are
|
|
485
|
+
# still retried. The empty-rows NotFoundError has the message
|
|
486
|
+
# "Statistic [..] on [..] not found" (no "could not locate
|
|
487
|
+
# statistics"), so it correctly falls through to the re-raise below.
|
|
488
|
+
if _DBCC_NOT_FOUND_MSG not in str(exc).lower():
|
|
489
|
+
# Not the transient DBCC error — propagate immediately.
|
|
490
|
+
raise
|
|
491
|
+
remaining = deadline - time.monotonic()
|
|
492
|
+
if remaining <= 0:
|
|
493
|
+
msg = f"Statistic [{stat_name}] on [{schema}].[{table}] not found"
|
|
494
|
+
raise NotFoundError(msg) from exc
|
|
495
|
+
await asyncio.sleep(min(_DBCC_STAT_POLL_INTERVAL, remaining))
|
|
459
496
|
|
|
460
497
|
|
|
461
498
|
async def create_statistics(
|
|
@@ -902,7 +902,7 @@ def run_query( # noqa: PLR0913
|
|
|
902
902
|
params: Sequence[object] | None = None,
|
|
903
903
|
mode: CredentialMode = CredentialMode.DEFAULT,
|
|
904
904
|
commit: bool = False,
|
|
905
|
-
fetch: Literal["all", "none", "one"] = "all",
|
|
905
|
+
fetch: Literal["all", "none", "one", "rowcount"] = "all",
|
|
906
906
|
autocommit: bool = False,
|
|
907
907
|
) -> tuple[list[str], list[tuple[Any, ...]]]:
|
|
908
908
|
"""Open a connection, execute *statement*, fetch rows, close, and map errors.
|
|
@@ -945,9 +945,21 @@ def run_query( # noqa: PLR0913
|
|
|
945
945
|
fetch: One of:
|
|
946
946
|
- ``"all"`` (default) — call ``fetchall()`` and return
|
|
947
947
|
``(columns, rows)``; columns are derived from ``cursor.description``.
|
|
948
|
+
If ``cursor.description`` is ``None`` (no result set), returns
|
|
949
|
+
``([], [])`` without calling ``fetchall()`` (defensive guard against
|
|
950
|
+
``ProgrammingError: Invalid cursor state``).
|
|
948
951
|
- ``"one"`` — call ``fetchone()``; returns ``(columns, [row])``
|
|
949
|
-
or ``(columns, [])`` when no row is found.
|
|
952
|
+
or ``(columns, [])`` when no row is found. Same ``description``
|
|
953
|
+
guard applies.
|
|
950
954
|
- ``"none"`` — do not fetch; returns ``([], [])``.
|
|
955
|
+
- ``"rowcount"`` — read ``cursor.rowcount`` instead of fetching a
|
|
956
|
+
result set. Returns ``([], [(rowcount,)])`` so callers read
|
|
957
|
+
``rows[0][0]``. Commits (when requested) AFTER reading rowcount.
|
|
958
|
+
Use for statements that produce no result set but report an affected-
|
|
959
|
+
row count via the ODBC ``SQLRowCount`` API (e.g. ``COPY INTO`` on
|
|
960
|
+
mssql-python ≥ 1.9.0 — ``cursor.description`` is ``None`` and
|
|
961
|
+
``fetchall()`` raises ``ProgrammingError: Invalid cursor state``,
|
|
962
|
+
but ``cursor.rowcount`` correctly equals the rows loaded).
|
|
951
963
|
|
|
952
964
|
autocommit: When ``True``, open the connection with ODBC-level autocommit
|
|
953
965
|
so the driver does not wrap the statement in an explicit transaction.
|
|
@@ -977,20 +989,52 @@ def run_query( # noqa: PLR0913
|
|
|
977
989
|
cur.execute(statement, params)
|
|
978
990
|
else:
|
|
979
991
|
cur.execute(statement)
|
|
980
|
-
if commit and not autocommit:
|
|
981
|
-
c.commit()
|
|
982
992
|
|
|
983
993
|
if fetch == "none":
|
|
994
|
+
# No result set to read; commit now (if requested) and return.
|
|
995
|
+
if commit and not autocommit:
|
|
996
|
+
c.commit()
|
|
997
|
+
return [], []
|
|
998
|
+
|
|
999
|
+
if fetch == "rowcount":
|
|
1000
|
+
# Read cursor.rowcount immediately — COPY INTO (and similar
|
|
1001
|
+
# statements) produce no result set on mssql-python ≥ 1.9.0:
|
|
1002
|
+
# cursor.description is None and fetchall() raises
|
|
1003
|
+
# ProgrammingError: Invalid cursor state.
|
|
1004
|
+
# cursor.rowcount is correctly set to the number of affected rows.
|
|
1005
|
+
rc = cur.rowcount
|
|
1006
|
+
if commit and not autocommit:
|
|
1007
|
+
c.commit()
|
|
1008
|
+
return [], [(rc,)]
|
|
1009
|
+
|
|
1010
|
+
# Defensive guard: if the driver returns no result set (description is
|
|
1011
|
+
# None) for an "all" or "one" fetch, commit (when requested) and return
|
|
1012
|
+
# empty results instead of calling fetchall()/fetchone() which would
|
|
1013
|
+
# raise "ProgrammingError: Invalid cursor state".
|
|
1014
|
+
if cur.description is None:
|
|
1015
|
+
if commit and not autocommit:
|
|
1016
|
+
c.commit()
|
|
984
1017
|
return [], []
|
|
985
1018
|
|
|
986
|
-
|
|
1019
|
+
# Fetch the result set BEFORE committing. Committing first invalidates
|
|
1020
|
+
# the cursor's prepared statement on mssql-python, which causes
|
|
1021
|
+
# "Associated statement is not prepared" on the subsequent fetchall/
|
|
1022
|
+
# fetchone call.
|
|
1023
|
+
cols = [col[0] for col in cur.description]
|
|
987
1024
|
|
|
988
1025
|
if fetch == "one":
|
|
989
1026
|
row = cur.fetchone()
|
|
990
|
-
|
|
1027
|
+
result: tuple[list[str], list[tuple[Any, ...]]] = (
|
|
1028
|
+
cols,
|
|
1029
|
+
([row] if row is not None else []),
|
|
1030
|
+
)
|
|
1031
|
+
else:
|
|
1032
|
+
rows: list[tuple[Any, ...]] = cur.fetchall()
|
|
1033
|
+
result = cols, rows
|
|
991
1034
|
|
|
992
|
-
|
|
993
|
-
|
|
1035
|
+
if commit and not autocommit:
|
|
1036
|
+
c.commit()
|
|
1037
|
+
return result
|
|
994
1038
|
|
|
995
1039
|
conn, attempt, max_attempts, _ = _with_connect_retry(target, mode, autocommit)
|
|
996
1040
|
try:
|
|
@@ -26,13 +26,16 @@ from fabric_dw.sql import (
|
|
|
26
26
|
logger = logging.getLogger(__name__)
|
|
27
27
|
|
|
28
28
|
# Maximum time to wait for a SQL analytics endpoint to provision on a new lakehouse.
|
|
29
|
-
|
|
29
|
+
# Live probes show provisioning completes in ~18s; 120s gives a generous margin.
|
|
30
|
+
_SQL_ENDPOINT_PROVISION_TIMEOUT_S = 120 # 2 min — provisioning observed at ~18s
|
|
30
31
|
# Polling interval between provisioning status checks.
|
|
31
32
|
_SQL_ENDPOINT_POLL_INTERVAL_S = 5
|
|
32
33
|
|
|
33
34
|
# Maximum time (seconds) to wait for a fresh warehouse/endpoint SQL database
|
|
34
35
|
# to become connectable after creation.
|
|
35
|
-
|
|
36
|
+
# 10 min — warm-up window for Fabric preview SQL engine
|
|
37
|
+
# (DB provisioning + SP permission propagation)
|
|
38
|
+
_SQL_READINESS_TIMEOUT_S = 600
|
|
36
39
|
# Starting backoff delay (seconds) between readiness probe attempts.
|
|
37
40
|
_SQL_READINESS_BACKOFF_INITIAL_S = 2.0
|
|
38
41
|
# Maximum backoff delay cap (seconds).
|
|
@@ -665,28 +668,31 @@ async def ephemeral_sql_endpoint(
|
|
|
665
668
|
f"for lakehouse {lh_id}: {ep_id_raw!r}"
|
|
666
669
|
)
|
|
667
670
|
|
|
668
|
-
#
|
|
669
|
-
#
|
|
670
|
-
#
|
|
671
|
-
#
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
ep_conn = await get_endpoint_connection_string(
|
|
678
|
-
http,
|
|
679
|
-
workspace_id,
|
|
680
|
-
ep_uuid,
|
|
681
|
-
poll_interval=_SQL_ENDPOINT_POLL_INTERVAL_S,
|
|
682
|
-
timeout=max(1.0, deadline - time.monotonic()),
|
|
683
|
-
)
|
|
684
|
-
except Exception as exc:
|
|
685
|
-
pytest.skip(
|
|
686
|
-
f"SQL endpoint {ep_uuid} connection_string did not populate "
|
|
687
|
-
f"within timeout: {exc}"
|
|
671
|
+
# The Lakehouse body already has connectionString at
|
|
672
|
+
# properties.sqlEndpointProperties.connectionString when
|
|
673
|
+
# provisioningStatus=Success. Use it directly — it is available
|
|
674
|
+
# immediately at that point. Only fall back to polling via
|
|
675
|
+
# get_endpoint_connection_string if the value happens to be absent
|
|
676
|
+
# (defensive guard; should not occur in practice).
|
|
677
|
+
if not ep_conn:
|
|
678
|
+
from fabric_dw.services.sql_endpoints import ( # noqa: PLC0415
|
|
679
|
+
get_endpoint_connection_string,
|
|
688
680
|
)
|
|
689
681
|
|
|
682
|
+
try:
|
|
683
|
+
ep_conn = await get_endpoint_connection_string(
|
|
684
|
+
http,
|
|
685
|
+
workspace_id,
|
|
686
|
+
ep_uuid,
|
|
687
|
+
poll_interval=_SQL_ENDPOINT_POLL_INTERVAL_S,
|
|
688
|
+
timeout=max(1.0, deadline - time.monotonic()),
|
|
689
|
+
)
|
|
690
|
+
except Exception as exc:
|
|
691
|
+
pytest.skip(
|
|
692
|
+
f"SQL endpoint {ep_uuid} connection_string did not populate "
|
|
693
|
+
f"within timeout: {exc}"
|
|
694
|
+
)
|
|
695
|
+
|
|
690
696
|
wh = Warehouse.model_validate(
|
|
691
697
|
{
|
|
692
698
|
"id": str(ep_uuid),
|
{fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_load.py
RENAMED
|
@@ -274,6 +274,12 @@ class TestCopyIntoFromUrlGuard:
|
|
|
274
274
|
|
|
275
275
|
class TestCopyIntoFromUrl:
|
|
276
276
|
async def test_returns_copy_into_result(self) -> None:
|
|
277
|
+
"""copy_into_from_url must return rows_loaded from cursor.rowcount (fetch='rowcount').
|
|
278
|
+
|
|
279
|
+
mssql-python ≥ 1.9.0: COPY INTO returns no result set; run_query with
|
|
280
|
+
fetch='rowcount' returns ([], [(N,)]) where N is cursor.rowcount.
|
|
281
|
+
rows_rejected is always 0 (not exposed via ODBC rowcount).
|
|
282
|
+
"""
|
|
277
283
|
from fabric_dw.sql import SqlTarget # noqa: PLC0415
|
|
278
284
|
|
|
279
285
|
target = SqlTarget(
|
|
@@ -281,10 +287,8 @@ class TestCopyIntoFromUrl:
|
|
|
281
287
|
)
|
|
282
288
|
|
|
283
289
|
with patch("fabric_dw.services.load.run_query") as mock_run:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
[(100, 2)],
|
|
287
|
-
)
|
|
290
|
+
# Simulate fetch="rowcount" return: ([], [(N,)])
|
|
291
|
+
mock_run.return_value = ([], [(100,)])
|
|
288
292
|
result = await copy_into_from_url(
|
|
289
293
|
target,
|
|
290
294
|
"dbo",
|
|
@@ -295,8 +299,76 @@ class TestCopyIntoFromUrl:
|
|
|
295
299
|
|
|
296
300
|
assert isinstance(result, CopyIntoResult)
|
|
297
301
|
assert result.rows_loaded == 100
|
|
298
|
-
assert result.rows_rejected ==
|
|
302
|
+
assert result.rows_rejected == 0 # not available via rowcount
|
|
299
303
|
assert result.target == "dbo.sales"
|
|
304
|
+
# Verify fetch="rowcount" was requested
|
|
305
|
+
_call_kwargs = mock_run.call_args.kwargs
|
|
306
|
+
assert _call_kwargs.get("fetch") == "rowcount"
|
|
307
|
+
assert _call_kwargs.get("commit") is True
|
|
308
|
+
|
|
309
|
+
async def test_rowcount_negative_treated_as_zero(self) -> None:
|
|
310
|
+
"""ODBC rowcount=-1 (unknown) must be treated as 0 rows loaded."""
|
|
311
|
+
from fabric_dw.sql import SqlTarget # noqa: PLC0415
|
|
312
|
+
|
|
313
|
+
target = SqlTarget(
|
|
314
|
+
workspace_id="ws-id", database="db", connection_string="server=x;database=y"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
with patch("fabric_dw.services.load.run_query") as mock_run:
|
|
318
|
+
# Simulate ODBC rowcount=-1 (driver cannot determine count)
|
|
319
|
+
mock_run.return_value = ([], [(-1,)])
|
|
320
|
+
result = await copy_into_from_url(
|
|
321
|
+
target,
|
|
322
|
+
"dbo",
|
|
323
|
+
"t",
|
|
324
|
+
"https://example.com/f.parquet",
|
|
325
|
+
file_type="PARQUET",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
assert result.rows_loaded == 0
|
|
329
|
+
assert result.rows_rejected == 0
|
|
330
|
+
|
|
331
|
+
async def test_rowcount_none_treated_as_zero(self) -> None:
|
|
332
|
+
"""rowcount=None in result rows must be treated as 0 rows loaded."""
|
|
333
|
+
from fabric_dw.sql import SqlTarget # noqa: PLC0415
|
|
334
|
+
|
|
335
|
+
target = SqlTarget(
|
|
336
|
+
workspace_id="ws-id", database="db", connection_string="server=x;database=y"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
with patch("fabric_dw.services.load.run_query") as mock_run:
|
|
340
|
+
mock_run.return_value = ([], [(None,)])
|
|
341
|
+
result = await copy_into_from_url(
|
|
342
|
+
target,
|
|
343
|
+
"dbo",
|
|
344
|
+
"t",
|
|
345
|
+
"https://example.com/f.parquet",
|
|
346
|
+
file_type="PARQUET",
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
assert result.rows_loaded == 0
|
|
350
|
+
assert result.rows_rejected == 0
|
|
351
|
+
|
|
352
|
+
async def test_empty_rows_treated_as_zero(self) -> None:
|
|
353
|
+
"""Empty rows list from run_query must yield rows_loaded=0."""
|
|
354
|
+
from fabric_dw.sql import SqlTarget # noqa: PLC0415
|
|
355
|
+
|
|
356
|
+
target = SqlTarget(
|
|
357
|
+
workspace_id="ws-id", database="db", connection_string="server=x;database=y"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
with patch("fabric_dw.services.load.run_query") as mock_run:
|
|
361
|
+
mock_run.return_value = ([], [])
|
|
362
|
+
result = await copy_into_from_url(
|
|
363
|
+
target,
|
|
364
|
+
"dbo",
|
|
365
|
+
"t",
|
|
366
|
+
"https://example.com/f.parquet",
|
|
367
|
+
file_type="PARQUET",
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
assert result.rows_loaded == 0
|
|
371
|
+
assert result.rows_rejected == 0
|
|
300
372
|
|
|
301
373
|
async def test_secret_not_logged(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
302
374
|
"""Secret values must never appear in log output (happy path)."""
|
|
@@ -311,7 +383,7 @@ class TestCopyIntoFromUrl:
|
|
|
311
383
|
caplog.at_level(logging.DEBUG, logger="fabric_dw"),
|
|
312
384
|
patch("fabric_dw.services.load.run_query") as mock_run,
|
|
313
385
|
):
|
|
314
|
-
mock_run.return_value = ([
|
|
386
|
+
mock_run.return_value = ([], [(5,)])
|
|
315
387
|
await copy_into_from_url(
|
|
316
388
|
target,
|
|
317
389
|
"dbo",
|