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.
Files changed (279) hide show
  1. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/PKG-INFO +3 -1
  2. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/README.md +2 -0
  3. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/install.md +3 -0
  4. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/pyproject.toml +1 -0
  5. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/_version.py +2 -2
  6. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/load.py +17 -16
  7. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/sql_endpoints.py +88 -8
  8. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/statistics.py +39 -2
  9. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/sql.py +52 -8
  10. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/conftest.py +28 -22
  11. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_load.py +78 -6
  12. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_sql_endpoints.py +233 -11
  13. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_statistics.py +221 -1
  14. fabric_dw-2026.6.0a1.dev184/tests/unit/test_entry_points.py +40 -0
  15. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_sql.py +363 -0
  16. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.devcontainer/devcontainer.json +0 -0
  17. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.dockerignore +0 -0
  18. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.env.example +0 -0
  19. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/CODEOWNERS +0 -0
  20. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  21. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  22. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  23. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/dependabot.yml +0 -0
  24. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/release-drafter.yml +0 -0
  25. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/ci.yml +0 -0
  26. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/codeql.yml +0 -0
  27. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/docker.yml +0 -0
  28. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/integration.yml +0 -0
  29. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/pause-capacity.yml +0 -0
  30. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/publish.yml +0 -0
  31. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/release-drafter.yml +0 -0
  32. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/secret-scan.yml +0 -0
  33. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.github/workflows/security.yml +0 -0
  34. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.gitignore +0 -0
  35. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.gitleaks.toml +0 -0
  36. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.gitleaksignore +0 -0
  37. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/.python-version +0 -0
  38. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/CODE_OF_CONDUCT.md +0 -0
  39. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/CONTRIBUTING.md +0 -0
  40. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/Dockerfile +0 -0
  41. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/LICENSE +0 -0
  42. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/SECURITY.md +0 -0
  43. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/assets/logo.svg +0 -0
  44. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/authentication.md +0 -0
  45. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/cli.md +0 -0
  46. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/code-of-conduct.md +0 -0
  47. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/completion.md +0 -0
  48. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/contributing.md +0 -0
  49. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/index.md +0 -0
  50. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/integration-coverage.md +0 -0
  51. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/license.md +0 -0
  52. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/mcp-tools.md +0 -0
  53. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/mcp.md +0 -0
  54. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/security.md +0 -0
  55. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/telemetry.md +0 -0
  56. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs/troubleshooting.md +0 -0
  57. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/docs_build/cloudflare_pages.sh +0 -0
  58. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/justfile +0 -0
  59. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/overrides/partials/integrations/analytics/custom.html +0 -0
  60. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/__init__.py +0 -0
  61. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/auth.py +0 -0
  62. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cache.py +0 -0
  63. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/__init__.py +0 -0
  64. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/_context.py +0 -0
  65. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/_main.py +0 -0
  66. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/_render.py +0 -0
  67. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/__init__.py +0 -0
  68. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/_utils.py +0 -0
  69. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/audit.py +0 -0
  70. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/cache.py +0 -0
  71. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/completion.py +0 -0
  72. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/config.py +0 -0
  73. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/dbt.py +0 -0
  74. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/functions.py +0 -0
  75. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/procedures.py +0 -0
  76. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/queries.py +0 -0
  77. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/restore_points.py +0 -0
  78. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/schemas.py +0 -0
  79. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/snapshots.py +0 -0
  80. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/sql.py +0 -0
  81. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/sql_endpoints.py +0 -0
  82. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/sql_pools.py +0 -0
  83. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/statistics.py +0 -0
  84. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/tables.py +0 -0
  85. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/views.py +0 -0
  86. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/warehouses.py +0 -0
  87. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/cli/commands/workspaces.py +0 -0
  88. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/config.py +0 -0
  89. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/exceptions.py +0 -0
  90. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/http_client.py +0 -0
  91. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/identifiers.py +0 -0
  92. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/logging.py +0 -0
  93. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/__init__.py +0 -0
  94. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/_context.py +0 -0
  95. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/_guards.py +0 -0
  96. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/_helpers.py +0 -0
  97. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/server.py +0 -0
  98. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/__init__.py +0 -0
  99. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/audit.py +0 -0
  100. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/cache.py +0 -0
  101. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/dbt.py +0 -0
  102. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/functions.py +0 -0
  103. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/load.py +0 -0
  104. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/procedures.py +0 -0
  105. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/queries.py +0 -0
  106. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/restore.py +0 -0
  107. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/schemas.py +0 -0
  108. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/snapshots.py +0 -0
  109. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/sql_endpoints.py +0 -0
  110. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/sql_exec.py +0 -0
  111. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/sql_pools.py +0 -0
  112. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/statistics.py +0 -0
  113. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/tables.py +0 -0
  114. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/views.py +0 -0
  115. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/warehouses.py +0 -0
  116. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/mcp/tools/workspaces.py +0 -0
  117. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/models.py +0 -0
  118. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/py.typed +0 -0
  119. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/resolver.py +0 -0
  120. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/__init__.py +0 -0
  121. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/_concurrency.py +0 -0
  122. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/_helpers.py +0 -0
  123. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/_lro.py +0 -0
  124. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/audit.py +0 -0
  125. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/capacities.py +0 -0
  126. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/dbt_scaffold.py +0 -0
  127. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/functions.py +0 -0
  128. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/ownership.py +0 -0
  129. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/permissions.py +0 -0
  130. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/procedures.py +0 -0
  131. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/queries.py +0 -0
  132. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/query_insights.py +0 -0
  133. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/restore.py +0 -0
  134. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/schemas.py +0 -0
  135. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/snapshots.py +0 -0
  136. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/sql_exec.py +0 -0
  137. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/sql_pools.py +0 -0
  138. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/tables.py +0 -0
  139. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/views.py +0 -0
  140. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/warehouses.py +0 -0
  141. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/services/workspaces.py +0 -0
  142. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/sql_io.py +0 -0
  143. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/telemetry.py +0 -0
  144. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/telemetry_commands.py +0 -0
  145. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/src/fabric_dw/types.py +0 -0
  146. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/__init__.py +0 -0
  147. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/conftest.py +0 -0
  148. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/fixtures/__init__.py +0 -0
  149. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/fixtures/api_payloads.py +0 -0
  150. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/__init__.py +0 -0
  151. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_cli_smoke.py +0 -0
  152. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_cross_cutting.py +0 -0
  153. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_audit.py +0 -0
  154. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_create_and_load.py +0 -0
  155. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_create_empty_table.py +0 -0
  156. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_dbt_scaffold.py +0 -0
  157. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_functions.py +0 -0
  158. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_load.py +0 -0
  159. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_ownership.py +0 -0
  160. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_permissions.py +0 -0
  161. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_procedures.py +0 -0
  162. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_queries.py +0 -0
  163. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_query_insights.py +0 -0
  164. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_restore.py +0 -0
  165. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_schemas.py +0 -0
  166. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_snapshots.py +0 -0
  167. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_sql_endpoints.py +0 -0
  168. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_sql_exec.py +0 -0
  169. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_sql_pools.py +0 -0
  170. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_statistics.py +0 -0
  171. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_tables.py +0 -0
  172. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_views.py +0 -0
  173. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_warehouses.py +0 -0
  174. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_services_workspaces.py +0 -0
  175. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/integration/test_smoke.py +0 -0
  176. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/__init__.py +0 -0
  177. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/__init__.py +0 -0
  178. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/__init__.py +0 -0
  179. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/conftest.py +0 -0
  180. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_audit.py +0 -0
  181. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_audit_respx.py +0 -0
  182. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_completion.py +0 -0
  183. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_config.py +0 -0
  184. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_dbt.py +0 -0
  185. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_functions.py +0 -0
  186. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_procedures.py +0 -0
  187. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_queries.py +0 -0
  188. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_query_insights.py +0 -0
  189. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_restore_points.py +0 -0
  190. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_restore_points_respx.py +0 -0
  191. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_schemas.py +0 -0
  192. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_snapshots.py +0 -0
  193. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_snapshots_respx.py +0 -0
  194. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_sql.py +0 -0
  195. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_sql_endpoints.py +0 -0
  196. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_sql_pools.py +0 -0
  197. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_statistics.py +0 -0
  198. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_tables.py +0 -0
  199. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_utils.py +0 -0
  200. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_views.py +0 -0
  201. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_warehouses.py +0 -0
  202. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_warehouses_respx.py +0 -0
  203. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_workspaces.py +0 -0
  204. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/commands/test_workspaces_respx.py +0 -0
  205. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/test_cache.py +0 -0
  206. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/test_completion.py +0 -0
  207. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/test_main.py +0 -0
  208. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/cli/test_render.py +0 -0
  209. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/conftest.py +0 -0
  210. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/__init__.py +0 -0
  211. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/conftest.py +0 -0
  212. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/test_context.py +0 -0
  213. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/test_contract.py +0 -0
  214. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/test_procedures.py +0 -0
  215. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/test_security.py +0 -0
  216. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/test_server.py +0 -0
  217. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/__init__.py +0 -0
  218. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_audit.py +0 -0
  219. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_dbt.py +0 -0
  220. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_functions.py +0 -0
  221. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_load.py +0 -0
  222. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_queries.py +0 -0
  223. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_restore.py +0 -0
  224. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_snapshots.py +0 -0
  225. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_sql_pools.py +0 -0
  226. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_statistics.py +0 -0
  227. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_tables.py +0 -0
  228. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_tool_quality.py +0 -0
  229. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_views.py +0 -0
  230. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_warehouses.py +0 -0
  231. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/mcp/tools/test_workspaces.py +0 -0
  232. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/__init__.py +0 -0
  233. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/_helpers.py +0 -0
  234. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/conftest.py +0 -0
  235. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_audit.py +0 -0
  236. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_capacity_skip.py +0 -0
  237. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_concurrency.py +0 -0
  238. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_create_and_load.py +0 -0
  239. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_dbt_scaffold.py +0 -0
  240. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_functions.py +0 -0
  241. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_helpers.py +0 -0
  242. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_lro.py +0 -0
  243. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_ownership.py +0 -0
  244. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_permissions.py +0 -0
  245. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_procedures.py +0 -0
  246. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_queries.py +0 -0
  247. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_query_insights.py +0 -0
  248. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_restore.py +0 -0
  249. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_schemas.py +0 -0
  250. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_snapshots.py +0 -0
  251. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_sql_exec.py +0 -0
  252. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_sql_pools.py +0 -0
  253. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_tables.py +0 -0
  254. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_views.py +0 -0
  255. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_warehouses.py +0 -0
  256. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/services/test_workspaces.py +0 -0
  257. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_auth.py +0 -0
  258. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_cache.py +0 -0
  259. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_cli_help_alias.py +0 -0
  260. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_cli_help_width.py +0 -0
  261. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_config.py +0 -0
  262. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_exceptions.py +0 -0
  263. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_global_options_anywhere.py +0 -0
  264. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_http_client.py +0 -0
  265. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_identifiers.py +0 -0
  266. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_integration_conftest_readiness.py +0 -0
  267. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_logging.py +0 -0
  268. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_models.py +0 -0
  269. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_resolver.py +0 -0
  270. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_services_w_series.py +0 -0
  271. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_shutdown_clean.py +0 -0
  272. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_smoke.py +0 -0
  273. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_sql_io.py +0 -0
  274. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_telemetry.py +0 -0
  275. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_telemetry_commands.py +0 -0
  276. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/tests/unit/test_types.py +0 -0
  277. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/uv.lock +0 -0
  278. {fabric_dw-2026.6.0a1.dev182 → fabric_dw-2026.6.0a1.dev184}/wrangler.toml +0 -0
  279. {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.dev182
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
@@ -34,6 +34,8 @@ pip install fabric-dw
34
34
  uvx fabric-dw --help
35
35
  ```
36
36
 
37
+ After installation, the `fdw` command is a short alias for `fabric-dw` — both invoke the same entry point.
38
+
37
39
  ## Quick Start
38
40
 
39
41
  ### 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.
@@ -75,6 +75,7 @@ Issues = "https://github.com/sdebruyn/fabric-dw-mcp-cli/issues"
75
75
  [project.scripts]
76
76
  fabric-dw = "fabric_dw.cli:main"
77
77
  fabric-dw-mcp = "fabric_dw.mcp.server:run"
78
+ fdw = "fabric_dw.cli:main"
78
79
 
79
80
  [dependency-groups]
80
81
  dev = [
@@ -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.dev182'
22
- __version_tuple__ = version_tuple = (2026, 6, 0, 'a1', 'dev182')
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 returns a result set with (rows_loaded, rows_rejected, …)
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
- cols, rows = run_query(target, sql, mode=_mode, commit=True)
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
- if rows:
792
- row = rows[0]
793
- # The result set columns vary slightly by Fabric version.
794
- # Columns: rows_loaded, rows_rejected [, rejected_file_location]
795
- try:
796
- col_map = {c.lower(): i for i, c in enumerate(cols)}
797
- loaded = int(row[col_map.get("rows_loaded", 0)] or 0)
798
- rejected_idx = col_map.get("rows_rejected")
799
- rejected = int(row[rejected_idx] or 0) if rejected_idx is not None else 0
800
- except (IndexError, TypeError, ValueError):
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(
@@ -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
- _CONN_STRING_POLL_TIMEOUT: float = 600.0 # 10 min Fabric preview eventual-consistency window
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
- return Warehouse.from_api(resp.json(), kind=WarehouseKind.SQL_ENDPOINT)
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 polls
148
- ``GET /workspaces/{ws}/sqlEndpoints/{id}`` until the connection string
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 600.0, i.e. 10 min).
236
+ timeout: Maximum wall-clock seconds to wait (default 120.0).
157
237
 
158
238
  Returns:
159
239
  The non-empty connection string.
@@ -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 _run() -> StatisticDetails:
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
- return await asyncio.to_thread(_run)
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
- cols = [col[0] for col in (cur.description or [])]
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
- return cols, ([row] if row is not None else [])
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
- rows: list[tuple[Any, ...]] = cur.fetchall()
993
- return cols, rows
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
- _SQL_ENDPOINT_PROVISION_TIMEOUT_S = 600 # 10 min Fabric preview provisioning can be slow
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
- _SQL_READINESS_TIMEOUT_S = 600 # 10 min — warm-up window for Fabric preview SQL engine
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
- # Even after provisioningStatus=Success, the connectionString on the
669
- # SQL endpoint resource itself can be empty due to eventual consistency.
670
- # Poll GET /sqlEndpoints/{id} until connection_string is non-empty so
671
- # that tests fetching the endpoint directly get a populated value.
672
- from fabric_dw.services.sql_endpoints import ( # noqa: PLC0415
673
- get_endpoint_connection_string,
674
- )
675
-
676
- try:
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),
@@ -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
- mock_run.return_value = (
285
- ["rows_loaded", "rows_rejected"],
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 == 2
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 = (["rows_loaded"], [(5,)])
386
+ mock_run.return_value = ([], [(5,)])
315
387
  await copy_into_from_url(
316
388
  target,
317
389
  "dbo",