pax_global_header 0000666 0000000 0000000 00000000064 15253215657 0014525 g ustar 00root root 0000000 0000000 52 comment=171ed6e3a2d1093c8f36d0697c9f3c8a6ddf3871
microsoft-mssql-django-171ed6e/ 0000775 0000000 0000000 00000000000 15253215657 0016545 5 ustar 00root root 0000000 0000000 microsoft-mssql-django-171ed6e/.config/ 0000775 0000000 0000000 00000000000 15253215657 0020070 5 ustar 00root root 0000000 0000000 microsoft-mssql-django-171ed6e/.config/CredScanSuppressions.json 0000664 0000000 0000000 00000000655 15253215657 0025111 0 ustar 00root root 0000000 0000000 {
"tool": "Credential Scanner",
"suppressions": [
{
"file": "CONTRIBUTING.md",
"justification": "Example file showing users how to contribute to our project"
},
{
"file": "testapp/*",
"justification": "Test app contains sample credentials and should be skipped"
},
{
"file": "OneBranchPipelines/*",
"justification": "OneBranch pipeline configuration files"
}
]
}
microsoft-mssql-django-171ed6e/.config/tsaoptions.json 0000664 0000000 0000000 00000000760 15253215657 0023171 0 ustar 00root root 0000000 0000000 {
"instanceUrl": "https://sqlclientdrivers.visualstudio.com",
"projectName": "mssql-django",
"areaPath": "mssql-django",
"iterationPath": "mssql-django",
"notificationAliases": [
"mssqldjango@microsoft.com"
],
"repositoryName": "mssql-django",
"codebaseName": "Django Backend for Microsoft SQL Server",
"allTools": true,
"template": "MSDATA_RevolutionR",
"includePathPatterns": "mssql/*, setup.py",
"excludePathPatterns": "testapp/*, django/*, OneBranchPipelines/*"
}
microsoft-mssql-django-171ed6e/.devcontainer/ 0000775 0000000 0000000 00000000000 15253215657 0021304 5 ustar 00root root 0000000 0000000 microsoft-mssql-django-171ed6e/.devcontainer/Dockerfile 0000664 0000000 0000000 00000002350 15253215657 0023276 0 ustar 00root root 0000000 0000000 # Use Debian Bookworm explicitly - Microsoft packages are signed for Debian 12
FROM mcr.microsoft.com/devcontainers/python:3.14-bookworm
ENV DEBIAN_FRONTEND=noninteractive
# Install system dependencies (including libmemcached-dev for Django's pylibmc tests)
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
gnupg \
unixodbc-dev \
libmemcached-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Install Microsoft ODBC Driver for SQL Server (both 17 and 18)
RUN curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg \
&& echo "deb [arch=amd64,arm64 signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/debian/12/prod bookworm main" > /etc/apt/sources.list.d/mssql-release.list \
&& apt-get update \
&& ACCEPT_EULA=Y apt-get install -y --no-install-recommends msodbcsql17 msodbcsql18 mssql-tools18 \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Add mssql-tools to PATH
ENV PATH="$PATH:/opt/mssql-tools18/bin"
# Pre-install core Python dependencies so post-create is faster
RUN pip install --no-cache-dir \
pyodbc \
coverage
WORKDIR /workspaces/mssql-django
microsoft-mssql-django-171ed6e/.devcontainer/devcontainer.json 0000664 0000000 0000000 00000001667 15253215657 0024672 0 ustar 00root root 0000000 0000000 {
"name": "mssql-django",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/mssql-django",
"postCreateCommand": "bash .devcontainer/scripts/post-create.sh",
"customizations": {
"vscode": {
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true,
"python.testing.unittestArgs": [
"-v", "-s", "-p", "test_*.py", "-t", "."
],
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true
}
},
"extensions": [
"ms-python.python",
"ms-python.debugpy",
"charliermarsh.ruff",
"ms-mssql.mssql",
"github.copilot"
]
}
},
"forwardPorts": [1433],
"remoteEnv": {
"DJANGO_SETTINGS_MODULE": "testapp.settings"
}
}
microsoft-mssql-django-171ed6e/.devcontainer/docker-compose.yml 0000664 0000000 0000000 00000001714 15253215657 0024744 0 ustar 00root root 0000000 0000000 services:
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
volumes:
- ..:/workspaces/mssql-django:cached
command: sleep infinity
environment:
# These env vars are read by testapp/settings.py
- MSSQL_HOST=db
- MSSQL_PORT=1433
- MSSQL_USER=sa
- MSSQL_PASSWORD=MyPassword42
- MSSQL_DRIVER=ODBC Driver 17 for SQL Server
depends_on:
db:
condition: service_healthy
db:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: "MyPassword42"
MSSQL_PID: "Developer"
ports:
- "1433:1433"
healthcheck:
test:
- CMD-SHELL
- /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$$MSSQL_SA_PASSWORD" -C -Q "SELECT 1" || exit 1
interval: 5s
timeout: 5s
retries: 20
start_period: 15s
volumes:
- mssql-data:/var/opt/mssql
volumes:
mssql-data:
microsoft-mssql-django-171ed6e/.devcontainer/scripts/ 0000775 0000000 0000000 00000000000 15253215657 0022773 5 ustar 00root root 0000000 0000000 microsoft-mssql-django-171ed6e/.devcontainer/scripts/post-create.sh 0000775 0000000 0000000 00000004424 15253215657 0025564 0 ustar 00root root 0000000 0000000 #!/usr/bin/env bash
# post-create.sh — runs once after the dev container is built
set -euo pipefail
echo "🚀 Setting up mssql-django development environment..."
echo "📦 Installing mssql-django in editable mode..."
pip install -e .
echo "📦 Installing dev/test dependencies..."
pip install \
pyodbc \
coverage \
unittest-xml-reporting \
tox
# Clone Django source for running Django's own test suite (test.sh)
if [ ! -d "django" ]; then
echo "📥 Cloning Django source for integration tests..."
DJANGO_VERSION="$(python -m django --version)"
git clone --depth 1 --branch "${DJANGO_VERSION}" \
https://github.com/django/django.git django 2>/dev/null || \
git clone --depth 1 https://github.com/django/django.git django
fi
# Set up useful shell aliases
echo "⚡ Setting up aliases..."
cat > ~/.shell_aliases << 'EOF'
# mssql-django development aliases
alias test='python manage.py test testapp'
alias testall='bash test.sh'
alias migrate='python manage.py migrate'
alias makemigrations='python manage.py makemigrations'
alias shell='python manage.py shell'
alias dbshell='python manage.py dbshell'
alias sqlcmd='sqlcmd -S db -U sa -P "${MSSQL_PASSWORD:-MyPassword42}" -C'
EOF
# Ensure aliases are sourced in both shells
grep -qxF 'source ~/.shell_aliases' ~/.bashrc 2>/dev/null || echo 'source ~/.shell_aliases' >> ~/.bashrc
grep -qxF 'source ~/.shell_aliases' ~/.zshrc 2>/dev/null || echo 'source ~/.shell_aliases' >> ~/.zshrc
# Wait for SQL Server to be ready, then verify connectivity
echo "⏳ Waiting for SQL Server..."
bash .devcontainer/scripts/wait-for-sql.sh
echo ""
echo "=============================================="
echo "🎉 mssql-django dev environment is ready!"
echo "=============================================="
echo ""
echo "📦 What's ready:"
echo " ✅ mssql-django installed (editable)"
echo " ✅ SQL Server running (db:1433)"
echo " ✅ Django source cloned for test suite"
echo ""
echo "🚀 Quick start - just type these commands:"
echo " test → Run testapp tests"
echo " testall → Run full Django test suite"
echo " migrate → Run migrations"
echo " shell → Django shell"
echo " sqlcmd → Connect to SQL Server"
echo ""
echo "=============================================="
microsoft-mssql-django-171ed6e/.devcontainer/scripts/run-tests.sh 0000775 0000000 0000000 00000004167 15253215657 0025306 0 ustar 00root root 0000000 0000000 #!/usr/bin/env bash
# run-tests.sh — convenience wrapper for running tests
#
# Usage:
# bash .devcontainer/scripts/run-tests.sh # run mssql-django tests
# bash .devcontainer/scripts/run-tests.sh --django # run Django's test suite (test.sh)
# bash .devcontainer/scripts/run-tests.sh --module queries # run a specific Django test module
# bash .devcontainer/scripts/run-tests.sh --coverage # run with coverage report
set -euo pipefail
cd /workspaces/mssql-django
case "${1:-}" in
--django)
echo "==> Running Django's own test suite via test.sh..."
bash test.sh
;;
--module)
shift
MODULE="${1:?'Provide a module name, e.g.: --module queries'}"
echo "==> Running Django test module: ${MODULE}..."
cd django
DJANGO_VERSION="$(python -m django --version)"
# Ensure checkout matches installed Django version
CURRENT_REF="$(git describe --tags --exact-match 2>/dev/null || echo 'unknown')"
if [ "${CURRENT_REF}" != "${DJANGO_VERSION}" ]; then
echo "==> Switching Django checkout from ${CURRENT_REF} to ${DJANGO_VERSION}..."
git fetch --depth=1 origin +refs/tags/*:refs/tags/* 2>/dev/null || true
git checkout "${DJANGO_VERSION}" 2>/dev/null || true
fi
pip install -q -r tests/requirements/py3.txt 2>/dev/null || true
coverage run tests/runtests.py --settings=testapp.settings --noinput "${MODULE}"
coverage report --include='*mssql*' --omit='*virtualenvs*'
coverage xml --include='*mssql*' --omit='*virtualenvs*' -o coverage.xml
echo "Coverage report written to coverage.xml"
;;
--coverage)
echo "==> Running mssql-django tests with coverage..."
coverage run manage.py test testapp --noinput -v2
coverage report --include='*mssql*' --omit='*virtualenvs*'
coverage xml --include='*mssql*' --omit='*virtualenvs*' -o coverage.xml
echo "Coverage report written to coverage.xml"
;;
*)
echo "==> Running mssql-django tests..."
python manage.py test testapp --noinput -v2
;;
esac
microsoft-mssql-django-171ed6e/.devcontainer/scripts/wait-for-sql.sh 0000775 0000000 0000000 00000001570 15253215657 0025662 0 ustar 00root root 0000000 0000000 #!/usr/bin/env bash
# wait-for-sql.sh — poll SQL Server until it accepts connections
set -euo pipefail
HOST="${MSSQL_HOST:-db}"
PORT="${MSSQL_PORT:-1433}"
USER="${MSSQL_USER:-sa}"
PASSWORD="${MSSQL_PASSWORD:-MyPassword42}"
MAX_RETRIES="${MSSQL_MAX_RETRIES:-30}"
SLEEP_INTERVAL=2
echo "Waiting for SQL Server at ${HOST}:${PORT}..."
for i in $(seq 1 "$MAX_RETRIES"); do
# -C trusts the server certificate (bypasses TLS validation for local dev)
if sqlcmd -S "${HOST},${PORT}" -U "${USER}" -P "${PASSWORD}" -C \
-Q "SELECT 1" -b -o /dev/null 2>/dev/null; then
echo "SQL Server is ready! (attempt ${i}/${MAX_RETRIES})"
exit 0
fi
echo " attempt ${i}/${MAX_RETRIES} — not ready yet, retrying in ${SLEEP_INTERVAL}s..."
sleep "$SLEEP_INTERVAL"
done
echo "ERROR: SQL Server did not become available after ${MAX_RETRIES} attempts."
exit 1
microsoft-mssql-django-171ed6e/.editorconfig 0000664 0000000 0000000 00000000346 15253215657 0021225 0 ustar 00root root 0000000 0000000 # https://editorconfig.org/
root = true
[*]
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
charset = utf-8
max_line_length = 119
[*.{yml,yaml}]
indent_size = 2
microsoft-mssql-django-171ed6e/.github/ 0000775 0000000 0000000 00000000000 15253215657 0020105 5 ustar 00root root 0000000 0000000 microsoft-mssql-django-171ed6e/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15253215657 0022270 5 ustar 00root root 0000000 0000000 microsoft-mssql-django-171ed6e/.github/ISSUE_TEMPLATE/bug_report.md 0000664 0000000 0000000 00000001736 15253215657 0024771 0 ustar 00root root 0000000 0000000 ---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
There are some features which are not supported yet. Please check the [Limitations](https://github.com/microsoft/mssql-django#limitations) first to see if your bug is listed.
**Software versions**
* Django:
* mssql-django:
* python:
* SQL Server:
* OS:
**Table schema and Model**
**Database Connection Settings**
`
// Paste your database settings from Settings.py here.
`
**Problem description and steps to reproduce**
**Expected behavior and actual behavior**
**Error message/stack trace**
**Any other details that can be helpful**
microsoft-mssql-django-171ed6e/.github/ISSUE_TEMPLATE/feature_request.md 0000664 0000000 0000000 00000001470 15253215657 0026017 0 ustar 00root root 0000000 0000000 ---
name: Feature Request
about: Suggest an idea for this project
title: "[FEATURE REQUEST]"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? If so, please give a short summary of the problem and how the feature would resolve it**
**Describe the preferred solution**
**Describe alternatives you've considered**
**Additional context**
**Reference Documentations/Specifications**
microsoft-mssql-django-171ed6e/.github/ISSUE_TEMPLATE/question.md 0000664 0000000 0000000 00000000501 15253215657 0024455 0 ustar 00root root 0000000 0000000 ---
name: Question
about: Ask a question
title: "[QUESTION]"
labels: question
assignees: ''
---
**Question**
**Relevant Issues and Pull Requests**
microsoft-mssql-django-171ed6e/.github/actions/ 0000775 0000000 0000000 00000000000 15253215657 0021545 5 ustar 00root root 0000000 0000000 microsoft-mssql-django-171ed6e/.github/actions/post-coverage-comment/ 0000775 0000000 0000000 00000000000 15253215657 0025763 5 ustar 00root root 0000000 0000000 microsoft-mssql-django-171ed6e/.github/actions/post-coverage-comment/action.yml 0000664 0000000 0000000 00000005063 15253215657 0027767 0 ustar 00root root 0000000 0000000 name: Post Coverage Comment
description: Posts a standardized code coverage comment on a pull request
inputs:
pr_number:
description: 'Pull request number'
required: true
coverage_percentage:
description: 'Overall coverage percentage'
required: true
covered_lines:
description: 'Number of covered lines'
required: true
total_lines:
description: 'Total number of lines'
required: true
patch_coverage_pct:
description: 'Patch/diff coverage percentage'
required: true
low_coverage_files:
description: 'Files with lowest coverage (multiline)'
required: true
patch_coverage_summary:
description: 'Patch coverage summary markdown (multiline)'
required: true
ado_url:
description: 'Azure DevOps build URL'
required: true
runs:
using: composite
steps:
- name: Post coverage comment
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
with:
header: Code Coverage Report
number: ${{ inputs.pr_number }}
message: |
# 📊 Code Coverage Report
### 🔥 Diff Coverage
### **${{ inputs.patch_coverage_pct }}**
|
### 🎯 Overall Coverage
### **${{ inputs.coverage_percentage }}**
|
**📈 Total Lines Covered:** `${{ inputs.covered_lines }}` out of `${{ inputs.total_lines }}`
**📁 Project:** `mssql-django`
|
---
${{ inputs.patch_coverage_summary }}
---
### 📋 Files Needing Attention
📉 Files with overall lowest coverage (click to expand)
```diff
${{ inputs.low_coverage_files }}
```
---
### 🔗 Quick Links
|
⚙️ Build Summary
|
📋 Coverage Details
|
|
[View Azure DevOps Build](${{ inputs.ado_url }})
|
[Browse Full Coverage Report](${{ inputs.ado_url }}&view=codecoverage-tab)
|
microsoft-mssql-django-171ed6e/.github/copilot-instructions.md 0000664 0000000 0000000 00000023340 15253215657 0024644 0 ustar 00root root 0000000 0000000 # GitHub Copilot Instructions for mssql-django
## Project Overview
mssql-django is a Django database backend for Microsoft SQL Server. It enables Django applications to use SQL Server as their database by translating Django's database abstraction layer to SQL Server's T-SQL dialect.
## How these instructions are organized
- **This file** is the repository-wide technical reference: SQL Server limitations, coding patterns, common errors, and testing commands.
- **`AGENTS.md`** holds project facts, contribution conventions, and cross-cutting review judgment.
- **`.github/instructions/*.instructions.md`** hold path-specific review checks, loaded automatically only when a matching file changes.
## Repository Structure
```
mssql/ # Core backend implementation (~4400 lines)
├── schema.py # Schema modifications, _alter_field (~1585 lines, largest file)
├── base.py # DatabaseWrapper, connection/cursor handling (~736 lines)
├── compiler.py # SQL query compilation, pagination, ORDER BY (~697 lines)
├── operations.py # SQL Server-specific operations (~694 lines)
├── functions.py # SQL function overrides via as_microsoft pattern (~673 lines)
├── features.py # SQL Server capability flags
├── introspection.py # Database introspection
├── creation.py # Test database creation/destruction
├── client.py # Command-line client support
└── management/
└── commands/
└── install_regex_clr.py # CLR assembly for REGEXP_LIKE support
testapp/ # Unit tests for the backend
├── tests/ # 214 tests across 15 test files
├── settings.py # Test configuration with EXCLUDED_TESTS
└── models.py # Test models
CHANGELOG.md # User-facing product changes from 1.7.0 onward
django/ # NOT in repo — cloned at runtime by test.sh for full test suite
```
## Key Technical Context
### SQL Server Limitations
| Limitation | Description | Where handled |
|------------|-------------|---------------|
| No tuple/row comparisons | `WHERE (a, b) IN (...)` not supported | Tests excluded |
| ORDER BY uniqueness | Same column can't appear twice | `compiler.py` deduplication |
| No LIMIT/OFFSET natively | Requires `TOP` or `OFFSET...FETCH` | `compiler.py` emulation |
| No boolean in SELECT | `supports_boolean_expr_in_select_clause = False` | CASE WHEN wrapping |
| No subqueries in GROUP BY | `supports_subqueries_in_group_by = False` | `features.py` flag |
| String concatenation | Uses `+` instead of `\|\|` | `compiler.py` |
| RAND() in ORDER BY | Doesn't randomize | Replaced with `NEWID()` in `compiler.py` |
| Identifier quoting | Uses `[brackets]` not `"quotes"` | `operations.py` `quote_name()` |
| ~2100 parameter limit | SQL Server max parameters per query | Temp table splitting in `functions.py` |
### Critical Files
**mssql/schema.py** (~1585 lines) - The **largest** file. Its `_alter_field()` method (~647 lines) handles cascading constraint drop/recreate logic for schema migrations.
**mssql/compiler.py** (~697 lines) - Handles:
- SQL query generation with SQL Server syntax
- OFFSET/LIMIT pagination emulation
- ORDER BY deduplication for composite primary keys
- ROW_NUMBER() window function for offset queries
**mssql/functions.py** (~673 lines) - Uses the `as_microsoft` monkey-patching pattern (see Coding Patterns below). Also handles parameter limit splitting via temp tables for large IN clauses.
**mssql/features.py** - Declares what SQL Server supports/doesn't support. Check here first when a test fails to see if it's a known limitation. On a new Django minor, diff Django's `BaseDatabaseFeatures` and override any newly added `supports_*` flags SQL Server can't honor.
**testapp/settings.py** - Contains `EXCLUDED_TESTS` list for Django tests that cannot pass due to SQL Server limitations (not bugs). Includes version-gated blocks (`if VERSION >= (X, Y):`) spanning Django 3.1 through 6.1.
## Coding Patterns
### The `as_microsoft` Pattern
The primary extension mechanism. Functions in `functions.py` define custom SQL generation and are monkey-patched onto Django expression classes:
```python
def sqlserver_round(self, compiler, connection, **extra_context):
# Custom SQL Server ROUND implementation
return self.as_sql(compiler, connection, template='ROUND(%(expressions)s, %(extra)s)', **extra_context)
# Monkey-patch onto Django's class
Round.as_microsoft = sqlserver_round
```
This pattern is used for: `Cast`, `Ln`, `Log`, `Mod`, `Round`, `Window`, `Now`, `MD5`, `SHA*`, `OrderBy`, `Lookup`, `Random`, and more.
### SQL Generation
When modifying SQL generation in compiler.py:
```python
# Always handle both qualified and unqualified column references
col_ref = '[table].[column]' # qualified
col_ref = '[column]' # unqualified
# Handle multi-column ORDER BY from composite PKs
# Django 5.2+ can emit: "[t].[a] DESC, [t].[b] DESC" as single item
parts = [p.strip() for p in o_sql.split(',')]
```
### Test Exclusions
When a test fails due to SQL Server limitations (not bugs), add to `EXCLUDED_TESTS` in testapp/settings.py:
```python
EXCLUDED_TESTS = [
'app.test_module.TestClass.test_method', # Brief reason
]
```
State whether the reason is a permanent platform limitation or an unimplemented capability — the comment is documentation future maintainers rely on.
### Regex Support
`python manage.py install_regex_clr ` installs a CLR assembly enabling `REGEXP_LIKE` support for regex-based Django tests.
## Testing
### Run mssql-django unit tests (214 tests)
```bash
python manage.py test testapp.tests
```
### Run specific Django test module
```bash
cd django && python tests/runtests.py --settings=testapp.settings
```
### Common test modules for validation
- `composite_pk` - Composite primary key support (Django 5.2+)
- `ordering` - ORDER BY functionality
- `queries` - General query tests
- `aggregation` - Aggregate functions
- `schema` - Schema migration tests
## Version Compatibility
- **Django and Python:** Read the declared ranges from `setup.py`, the executed matrix from
`azure-pipelines.yml`, and environment definitions from `tox.ini`; treat those files as
the source of truth.
- **SQL Server and drivers:** Read the supported platforms from `README.md` and dependency
declarations from `setup.py`; do not duplicate version lists here.
## Common Issues
### "Column specified more than once in ORDER BY"
SQL Server error 169. Check for duplicate columns in ORDER BY, especially with composite primary keys. Fix in `compiler.py` ORDER BY deduplication logic.
### "Tuple lookups not supported"
Add test to `EXCLUDED_TESTS` - this is a SQL Server limitation, not a bug.
### Tests hanging on database creation
The test database may already exist. Answer "yes" to drop it, or use:
```bash
echo "yes" | python tests/runtests.py --settings=testapp.settings
```
## Pull Request Guidelines
1. Run affected Django test modules, not just unit tests
2. Check if failures are bugs or SQL Server limitations
3. Add appropriate test exclusions with comments explaining why
4. Keep compiler.py and schema.py changes focused - they are complex and sensitive
5. When adding SQL Server function overrides, use the `as_microsoft` pattern in `functions.py`
## Development Workflow Rules
For contribution conventions (branch, commit prefixes, history rules) and cross-cutting review judgment, see `AGENTS.md`.
### Git Hygiene
- **Only commit files you intentionally changed.** Untracked files (e.g. `result.xml`, build artifacts) may exist in the workspace but not be in `.gitignore` — do not stage or commit them. Review `git diff` and `git status` before committing.
- Do not modify `testapp/settings.py` database connection settings (ODBC driver version, passwords) as part of a PR — those are local dev environment changes.
### Fix Quality
- Follow the root-cause-over-workaround and node-level-fix principles in `AGENTS.md` (Review judgment).
- Use the existing extension points: the `as_microsoft` monkey-patching pattern in `functions.py`, the `_as_microsoft()` dispatch in `compiler.py`, and compiler method overrides.
### Changelog Maintenance
- Update `CHANGELOG.md` under `Unreleased` in the same PR for user-facing product changes: runtime behavior, supported versions or platforms, installation dependencies, public settings/options/APIs, compatibility, deprecations, removals, and user-visible bug fixes.
- Do not add entries for CI, AI tooling or instructions, DevOps and release pipelines, test-only changes, contributor tooling, internal refactors with no user-visible effect, or documentation-only corrections that do not change the product contract.
- Keep each entry concise, describe the user-visible outcome, and link the PR or issue.
### Test Discipline
- **All tests must be green before submitting.** If a test fails due to a SQL Server limitation (not a bug you introduced), add it to `EXCLUDED_TESTS` in `testapp/settings.py` with a comment explaining why.
- If a failure is outside the scope of your PR, ask whether to fix it or exclude it — don't leave it failing silently.
- Always run the specific Django test modules affected by your change (e.g. `ordering`, `db_functions`, `composite_pk`) in addition to the unit tests (`python manage.py test testapp.tests`).
## Prompt References
Use prompt files via slash-style workspace paths:
- `/.github/prompts/mssql-django-pr-self-check-gate.prompt.md` - Gated PR self-check workflow and merge gate criteria.
- `/.github/prompts/mssql-django-dev-environment-setup.prompt.md` - Development environment setup.
- `/.github/prompts/mssql-django-run-unit-tests.prompt.md` - mssql-django unit test workflow.
- `/.github/prompts/mssql-django-run-django-test-suite.prompt.md` - Upstream Django suite workflow.
microsoft-mssql-django-171ed6e/.github/dependabot.yml 0000664 0000000 0000000 00000000324 15253215657 0022734 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
groups:
github-actions:
patterns: ["*"]
schedule:
interval: "weekly"
cooldown:
default-days: 7
microsoft-mssql-django-171ed6e/.github/instructions/ 0000775 0000000 0000000 00000000000 15253215657 0022651 5 ustar 00root root 0000000 0000000 microsoft-mssql-django-171ed6e/.github/instructions/mssql-base.instructions.md 0000664 0000000 0000000 00000004041 15253215657 0030004 0 ustar 00root root 0000000 0000000 ---
applyTo: "mssql/base.py"
---
# Review guidance: base.py
`base.py` builds the `DatabaseWrapper`, the connection string, and cursor handling. It is
one of only two driver-coupled files (with `introspection.py`) — everything else in
`mssql/` is driver-agnostic. Connection-string changes have broad blast radius; review
them carefully.
## Connection-string defaults
- **Any default the backend injects into the connection string needs a user opt-out.**
The established pattern (see #533, the `Authentication=` / `Trusted_Connection` fix) is:
if the user set the keyword explicitly in `extra_params`, skip our injection. Apply the
same pattern to any newly injected keyword; parse `extra_params` (case-insensitive) to
detect it.
- **MARS defaults on for Microsoft drivers on Windows.** Honor an explicit
`MARS_Connection` in `extra_params` without injecting a duplicate, and keep
`supports_mars` and `can_use_chunked_reads` consistent with that setting. Disabling
MARS must not apply the FreeTDS `fetchone()` result-discard workaround to Microsoft
drivers. See #415.
- **`extra_params` is appended verbatim.** A keyword the backend also sets can appear
twice; dedupe rather than emit duplicates.
## Authentication shape
- `USER` / `PASSWORD` become `UID` / `PWD`, which is **SQL Server authentication**. A
domain `username`/`password` pair is Windows auth (NTLM), which the Microsoft ODBC
driver does not support — passing a domain account as `USER` yields a confusing "login
failed" for a SQL login by that name. Windows auth is process identity via
`Trusted_Connection=yes` (added only when `USER` is empty). See #413, #428.
## Server-property probes
- Detection that runs `SERVERPROPERTY(...)` needs an **already-open connection**, so it
cannot gate connection-string construction (which happens before connect). Don't design
connect-time behavior around a value only available post-connect.
## Testing
- Connection-string builder tests are `SimpleTestCase` (no live DB) — fast to run and the
right home for connstring assertions.
microsoft-mssql-django-171ed6e/.github/instructions/mssql-compiler.instructions.md 0000664 0000000 0000000 00000003626 15253215657 0030714 0 ustar 00root root 0000000 0000000 ---
applyTo: "mssql/compiler.py"
---
# Review guidance: compiler.py
`compiler.py` compiles Django queries into T-SQL: pagination emulation and ORDER BY
handling are the sensitive areas. Keep changes here focused and node-level.
## Deprecated Django API migration
- **Sweep every call site in the file.** When a change moves off a deprecated Django API
(e.g. `quote_name_unless_alias`), grep the full symbol across the module — not just the
path the failing test exercised. The deprecated call typically still works: it warns on
the current Django and only breaks on a future major, so CI and correctness tests stay
green while a missed call site (such as the empty-ordering `ORDER BY pk` fallback)
lingers. See #555/#563.
- **Make the deprecation itself the assertion.** When the query returns correct rows
either way and the only observable change is a `RemovedInDjangoXWarning`, scope a
warnings filter to that specific category and promote it to an error in the regression
test. That reproduces how the upstream warnings-as-errors suite surfaces the problem and
gives an otherwise-untestable fix real coverage. See #559.
## Pagination and ORDER BY
- SQL Server has no native `LIMIT/OFFSET`; pagination is emulated with `TOP` and
`OFFSET ... FETCH` / `ROW_NUMBER()`. Order-dependent changes need coverage on both the
offset and non-offset paths.
- **ORDER BY must not list the same column twice** (SQL Server error 169), which composite
primary keys can trigger when Django emits a multi-column ordering as a single item.
Preserve the deduplication logic and handle both qualified (`[t].[c]`) and unqualified
(`[c]`) column references.
## General
- **Do not do string surgery on compiled SQL.** Solve at the expression/node level
(override the relevant compiler method, use the `as_microsoft` pattern) rather than
regex over generated SQL — the latter is blind to literals, aliases, and escaping.
microsoft-mssql-django-171ed6e/.github/instructions/mssql-features.instructions.md 0000664 0000000 0000000 00000004131 15253215657 0030710 0 ustar 00root root 0000000 0000000 ---
applyTo: "mssql/features.py"
---
# Review guidance: features.py
`features.py` subclasses Django's `BaseDatabaseFeatures` and overrides the `supports_*`
flags SQL Server can't honor. One correct flag both skips the inapplicable Django tests
(`@skipUnlessDBFeature`) and makes runtime raise a clean `NotSupportedError` instead of
emitting SQL the engine rejects.
## First tool on a new Django minor
- **Diff Django's `BaseDatabaseFeatures` between the old and new minor.** New minors add
`supports_*` flags that default to `True`; a backend that doesn't override them inherits
`True`, so Django assumes SQL Server can do the new thing, runs the new tests, and emits
SQL that fails. Override the new flags that SQL Server can't honor.
- Some new-flag failures surface **at test-database creation**, before any test runs — so
a missed override can red the entire suite, not just one test. (Example: Django 6.1's
`supports_on_delete_db_cascade` / `_db_null` / `_db_default` left `True` makes the new
`delete` models create FK graphs SQL Server rejects with error 1785, multiple cascade
paths.) Bitwise-aggregate flags left `True` surface as ODBC error 195.
## Correctness rules
- **Overriding a flag the base class doesn't define yet is an inert no-op** (nothing reads
it on older Django), so a plain `= False` never needs version gating. Version-gate only
when the *reference itself* would fail on an older Django (e.g. importing a symbol that
doesn't exist yet). Follow the existing file: version gates guard imports, plain booleans
are unconditional.
- **Turning a flag off declines the capability — it is not support.** If SQL Server can
actually do the thing with different SQL, that's real (higher-value) work, separate from
making the minor not break.
## Comment discipline
- State whether a `False` reflects a **permanent platform limitation** (a wall — e.g. the
1785 multi-cascade-path restriction) or an **unimplemented capability** (a gap that could
be emulated). Conflating the two turns "we haven't done it" into "it can't be done", and
that misclassification survives for years.
microsoft-mssql-django-171ed6e/.github/instructions/mssql-functions.instructions.md 0000664 0000000 0000000 00000003555 15253215657 0031113 0 ustar 00root root 0000000 0000000 ---
applyTo: "mssql/functions.py"
---
# Review guidance: functions.py
`functions.py` overrides Django SQL functions for SQL Server via the `as_microsoft`
pattern, and handles the parameter-count limit.
## Extension mechanism
- **New function overrides must use the `as_microsoft` pattern** — define a
`sqlserver_` and monkey-patch it onto the Django expression class
(`Round.as_microsoft = sqlserver_round`). Don't special-case functions elsewhere or
post-process compiled SQL.
## String and collation manipulation (historically fragile)
- **`CONVERT(varchar, ...)` without a length truncates to 30 characters.** Any generated
`CONVERT`/`CAST` to a character type that carries an identifier (database name, object
name) must specify an explicit length, or long names are silently cut. See #423.
- **Do not fabricate collations by string substitution.** Deriving a collation with
`.replace('_CI', '_CS')` (or similar) produces invalid collation names for suffixes like
`_CI_AI` or `_SC_UT`, and crashes when the collation is `NULL`. Guard for `NULL` and
don't synthesize collation names from substrings. See #420.
- These live in a small, dense helper (`sqlserver_replace`); a change touching it should
check the whole helper, since multiple reported bugs share it.
## Pattern lookups
- **Escape `[` and `]` in pattern/`LIKE` operands.** SQL Server treats `[...]` as a
character class, so an unescaped `[J]` matches the single character `J`, not the literal
string `[J]` — a silent-wrong-results bug (correct rows, no error). Verify escaping is
applied when a pattern value can come from an `F()` expression or a column, not only from
a literal. See #573.
## Parameter limit
- SQL Server caps parameters per statement (~2100). Large `IN` clauses are split via temp
tables here — preserve that splitting when changing how multi-value lookups are compiled.
microsoft-mssql-django-171ed6e/.github/instructions/mssql-introspection.instructions.md 0000664 0000000 0000000 00000002373 15253215657 0032000 0 ustar 00root root 0000000 0000000 ---
applyTo: "mssql/introspection.py"
---
# Review guidance: introspection.py
`introspection.py` maps SQL Server type codes to Django fields and introspects tables,
columns, and relations. It is one of only two driver-coupled files (with `base.py`) — its
type map depends on driver-exported constants.
## Django-version return shapes
- **Introspection return shapes change across Django minors — write version-aware tests.**
For example, `get_relations()` / foreign-key introspection gained a wider tuple on newer
Django. A change here must be tested against each supported Django version's expected
shape, using `django.VERSION` conditionals in one test rather than assuming a single
shape. See #556/#558.
## Driver type constants
- **Audit every `Database.SQL_*` type constant the map uses.** Driver builds differ in
which constants they export (e.g. `SQL_SS_TIME2` is not exported by every driver), so a
new type mapping should confirm the constant exists on all supported drivers, or provide
a fallback, rather than referencing it unconditionally.
## Schema qualification
- Introspection should behave correctly for tables in **non-default schemas**, not just
`dbo`. Test table/column/relation introspection with a schema-qualified `db_table`.
microsoft-mssql-django-171ed6e/.github/instructions/mssql-operations.instructions.md 0000664 0000000 0000000 00000004066 15253215657 0031264 0 ustar 00root root 0000000 0000000 ---
applyTo: "mssql/operations.py"
---
# Review guidance: operations.py
`operations.py` handles SQL Server quoting, parameter and type handling, and datetime
adaptation. It is the highest-traffic bug surface in the backend — review changes here
against the following checks.
## Type dispatch
- **Use `isinstance`, not exact-type identity.** `type(x) == int` (or `is int`)
silently rejects subclasses such as `IntegerChoices` / `IntEnum`, which reach the
parameter path through raw `GROUP BY` queries and then fall through to
`NotImplementedError`. Classify with `isinstance` (or read the enum's value). See #541.
- **Integer range boundaries.** SQL Server `INT` is `-0x80000000 .. 0x7FFFFFFF`. When
classifying a value as `INT` vs `BIGINT`, verify the exact boundary values are not
misclassified.
## SQL string manipulation
- **Rewriting placeholders on raw SQL is unsafe by construction.** Any `%s`→`{}`-style
substitution is blind to string literals, comments, and escape sequences. A change to
placeholder handling must preserve `%%`-escaped literals; require a test covering
`GROUP BY` + an escaped `%%` literal + a real parameter in the same query. If the fix
only widens a regex, note that it is a band-aid over an unparsed-SQL approach. See #476/#537.
- Prefer solving at Django's expression level over post-processing generated SQL.
## Datetime adaptation
- **Read and write paths must stay symmetric.** `convert_datetimefield_value` (read) and
`adapt_datetimefield_value` (write) must agree on timezone handling. A fix that touches
only one side creates a read/write asymmetry when a per-database `TIME_ZONE` is set
(aware values written as UTC but read back in the connection timezone, or vice versa).
Verify both paths, and test with `USE_TZ=True` plus an explicit non-UTC per-database
`TIME_ZONE`. See #371.
## Identifier quoting
- Identifiers are quoted with `[brackets]` via `quote_name()`. A change that routes more
call sites through quoting is a behavior change for every identifier — review its blast
radius, not just the reported case.
microsoft-mssql-django-171ed6e/.github/instructions/mssql-schema.instructions.md 0000664 0000000 0000000 00000003212 15253215657 0030331 0 ustar 00root root 0000000 0000000 ---
applyTo: "mssql/schema.py"
---
# Review guidance: schema.py
`schema.py` is the schema editor and the largest, most sensitive file in the backend. Its
`_alter_field()` drops and recreates dependent constraints (foreign keys, indexes, unique
constraints) around a column change. Keep changes here focused and well-tested.
## `_alter_field` and cascading constraints
- A change to field alteration must **recreate every constraint it drops**, in the right
order. When reviewing, confirm the set of constraints dropped equals the set recreated —
a missed recreation leaves the schema subtly wrong after a migration.
## Schema-qualified tables (non-default schema)
- **DDL must work for a `db_table` in a non-default schema, not just `dbo`.** A known open
case: adding a non-nullable column (with a default) to a model whose `db_table` uses an
explicit schema fails. Test the matrix explicitly: schema-qualified × nullable/non-nullable
× with/without default. See #402 (reproduced by more than one reporter).
## Blast radius over label
- **A change that reroutes identifier handling is an enhancement, review it as one.** If a
PR (even one labeled `FIX:`) routes every identifier through a new parser or changes a
default of a widely-called helper (e.g. `quote_name()` behavior, `get_table_list()`
defaults), the blast radius is the whole backend, with backward-compatibility and
SemVer implications. Assess it by what it changes, not by its stated intent. See #497.
## General
- Prefer Django's schema-editor hooks over hand-built DDL strings where they exist, and
keep DDL generation consistent with `operations.py` quoting (`[brackets]`).
microsoft-mssql-django-171ed6e/.github/instructions/testapp.instructions.md 0000664 0000000 0000000 00000004417 15253215657 0027424 0 ustar 00root root 0000000 0000000 ---
applyTo: "testapp/**"
---
# Review guidance: testapp/
`testapp/` is the test project we own — the only place we add regression coverage for
`mssql/*.py`. `testapp/settings.py` holds `EXCLUDED_TESTS`; `testapp/models.py` holds the
test models.
## Regression tests are mandatory
- **Every `FIX:` and `FEAT:` ships a test here, in the same PR.** For a `FIX:`, the test
must fail on `dev` without the change and pass with it, and must assert the **exact
trigger combination** from the issue (e.g. GROUP BY + escaped `%%` + a real param), not a
simplified proxy. A one-dimensional test that doesn't reproduce the real combo isn't
coverage.
- **Use the real fields in `testapp/models.py`** — don't invent model fields in a test.
## The `USE_TZ` gap
- `testapp` runs with `USE_TZ=False`, so the timezone / `datetimeoffset` path (a
`DateTimeField` becomes `datetimeoffset` only under `USE_TZ=True`) is exercised **only by
the upstream Django suite**, never by `testapp`. A timezone fix therefore can't be
regression-covered from `testapp` alone — say so explicitly in the PR body. That is the
one accepted "no testapp test" exception.
## Version-aware tests
- **Gate tests by `django.VERSION`, not by skipping.** A fix gated to a new Django minor is
inert outside its target versions; a version-aware test still runs and asserts across the
declared support matrix and automatically starts covering the new minor once it's enabled.
See #558/#559.
- **When the only observable change is a deprecation warning, make the warning the
assertion:** scope a filter to the specific `RemovedInDjangoXWarning` and promote it to an
error in the test. That mirrors Django's own warnings-as-errors suite and gives an
otherwise-untestable fix real coverage.
## `EXCLUDED_TESTS` hygiene
- Exclusions are version-gated blocks (`if VERSION >= (X, Y):`). Add an exclusion only for a
genuine SQL Server limitation, not a bug you introduced.
- **The comment is documentation.** State whether the reason is a permanent platform
limitation or an unimplemented capability — a wrong reason there becomes received wisdom.
- **`@expectedFailure` / `@skip` (or an exclusion) means the bug is NOT fixed** — it
documents a known failure while keeping CI green. Never read "a test exists" as "fixed".
microsoft-mssql-django-171ed6e/.github/prompts/ 0000775 0000000 0000000 00000000000 15253215657 0021611 5 ustar 00root root 0000000 0000000 microsoft-mssql-django-171ed6e/.github/prompts/mssql-django-dev-environment-setup.prompt.md 0000664 0000000 0000000 00000012574 15253215657 0032337 0 ustar 00root root 0000000 0000000 # mssql-django Development Environment Setup
This guide covers setting up a development environment for mssql-django, a Django database backend for Microsoft SQL Server.
## Quick Start
```bash
# 1. Install mssql-django in development mode
pip install -e ".[test]"
# 2. Start SQL Server (Docker)
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=MyPassword42" \
-p 1433:1433 --name sqlserver -d mcr.microsoft.com/mssql/server:2022-latest
# 3. Run mssql-django tests
python manage.py test --noinput
```
## Test Options
| Test Type | Tests | Time | Guide |
|-----------|-------|------|-------|
| mssql-django unit tests | ~42 | ~22s | [mssql-django-run-unit-tests.prompt.md](mssql-django-run-unit-tests.prompt.md) |
| Django full test suite | ~6200 | ~45min | [mssql-django-run-django-test-suite.prompt.md](mssql-django-run-django-test-suite.prompt.md) |
## Project Structure
```
mssql-django/
├── mssql/ # Main backend code
│ ├── base.py # DatabaseWrapper, connection management
│ ├── compiler.py # Query compiler, SQL generation
│ ├── schema.py # Schema editor, migrations
│ ├── operations.py # Database operations, SQL functions
│ ├── features.py # Backend feature flags
│ ├── introspection.py # Database introspection
│ └── creation.py # Test database creation
├── testapp/ # Test application
│ ├── settings.py # Django settings, test exclusions
│ ├── runners.py # Custom test runner
│ ├── models.py # Test models
│ └── tests/ # mssql-django unit tests
├── django/ # NOT in repo — cloned at runtime by test.sh
├── test.sh # Script to run Django's full test suite
├── tox.ini # Test matrix configuration
└── azure-pipelines.yml # CI configuration
```
## Requirements
### System Requirements
- Python 3.8+ (3.12+ recommended; supports up to 3.14)
- SQL Server 2017, 2019, 2022, 2025 or Azure SQL DB / Managed Instance
- ODBC Driver 17 or 18 for SQL Server
### Python Dependencies
```bash
# Development install with test dependencies
pip install -e ".[test]"
# Core dependencies (installed automatically)
# - Django >= 3.2, < 6.1
# - pyodbc >= 3.0
# - tzdata
```
## Configuration
### Database Settings (`testapp/settings.py`)
```python
DATABASES = {
"default": {
"ENGINE": "mssql",
"NAME": "default", # Database name
"USER": "sa", # SQL Server user
"PASSWORD": "MyPassword42", # Password
"HOST": "localhost", # Server host
"PORT": "1433", # Port
"OPTIONS": {
"driver": "ODBC Driver 18 for SQL Server",
"return_rows_bulk_insert": True,
"extra_params": "TrustServerCertificate=yes" # For self-signed certs
},
},
}
```
### Test Exclusions
Tests known to fail due to SQL Server limitations are excluded in `testapp/settings.py`:
```python
EXCLUDED_TESTS = [
# Tuple lookups - SQL Server doesn't support (col1, col2) IN syntax
'foreign_object.test_tuple_lookups.TupleLookupsTests.test_exact',
# ... see settings.py for full list
]
```
## Common Commands
```bash
# Install in development mode
pip install -e ".[test]"
# Run mssql-django tests only
python manage.py test --noinput
# Run Django's full test suite
bash test.sh
# Run specific Django test module
cd django && python tests/runtests.py --settings=testapp.settings composite_pk
# Check Python syntax
python -m py_compile mssql/compiler.py
# View generated SQL
python -c "
from myapp.models import MyModel
print(MyModel.objects.filter(...).query)
"
```
## SQL Server Limitations
Key limitations that affect the backend implementation:
| Limitation | Description | Workaround |
|------------|-------------|------------|
| No LIMIT clause | Must use TOP or OFFSET-FETCH | compiler.py handles this |
| OFFSET requires ORDER BY | Can't paginate without sorting | compiler.py adds default ORDER BY |
| No tuple IN | `(a, b) IN (...)` not supported | Tests excluded |
| No native boolean | Uses BIT type | Automatic conversion |
| Identifier quoting | Must use `[]` not `""` | operations.py quote_name() |
| ORDER BY duplicates | Same column can't appear twice | compiler.py deduplicates |
| ~2100 parameter limit | SQL Server max params per query | Temp table splitting in functions.py |
| No boolean in SELECT | `supports_boolean_expr_in_select_clause = False` | CASE WHEN wrapping |
## Debugging
### Enable SQL Logging
In `testapp/settings.py`:
```python
DEBUG = True # Enables SQL logging to logs/django_sql.log
```
### View Generated SQL
```python
from django.db import connection
qs = MyModel.objects.filter(...)
print(qs.query) # Shows SQL without parameters
print(connection.queries) # Shows executed queries with timing
```
## CI/CD
- **CI Platform:** Azure DevOps
- **Config:** `azure-pipelines.yml`
- **Test Matrix:** `tox.ini` (Python 3.8-3.14 × Django 3.2-6.0)
- **SQL Server:** Windows hosted agents with SQL Server 2019+
## Contributing
1. Create a feature branch
2. Make changes to `mssql/` files
3. Add tests to `testapp/tests/` if needed
4. Run `python manage.py test` to verify
5. For SQL changes, run `bash test.sh` for full validation
6. Submit PR against `dev` branch
microsoft-mssql-django-171ed6e/.github/prompts/mssql-django-pr-self-check-gate.prompt.md 0000664 0000000 0000000 00000011246 15253215657 0031415 0 ustar 00root root 0000000 0000000 # mssql-django Gated PR Self-Check
Use this prompt before merging any backend/compiler/schema PR.
## Objective
Run a **merge gate** that prevents regressions from:
- contract ambiguity,
- double-escaping/double-transforms,
- fast-path branches bypassing shared safety logic,
- stale test exclusions,
- insufficient integration proof.
The output must be a clear **PASS / FAIL / BLOCKED** decision with evidence.
## Inputs
- PR branch name
- Target branch (usually `dev`)
- Changed files (`git diff --name-only ...HEAD`)
- Related Django test modules for changed areas
## Required Checks (in order)
### 1) Contract change declared
For every behavior-changing helper/symbol, declare one explicit contract:
- who returns raw values,
- who escapes/normalizes,
- where transformation happens,
- and whether call sites must change.
### 2) All call sites audited
Audit all consumers of changed symbols and verify:
- contract applied exactly once,
- no mixed old/new behavior,
- no hidden secondary escaping/transform.
### 3) Fast-path invariants preserved
If a fast-path exists (special-case `continue`/`return`/short-circuit), prove it does not bypass shared safety logic:
- ORDER BY dedupe,
- alias handling,
- parameter ordering,
- escaping rules,
- pagination/order requirements.
If it does bypass shared logic, fix by integrating with shared path, not by adding ad-hoc string post-processing.
### 4) Integration test proof
Provide end-to-end proof for each behavior change:
- Add local regression tests under `testapp/tests/` when applicable.
- Run upstream Django tests that exercise the same path.
- Include at least one regression that targets the **risky edge** (duplicate/equivalent path, alias path, mixed type path, etc.).
### 5) Doc/comments synced
Ensure comments and PR description match current behavior:
- remove stale “still failing” language if test now passes,
- add concise reasons for any remaining exclusions.
### 6) Exclusion hygiene (strict)
For any exclusion touched in `testapp/settings.py`:
- Verify candidate removals under **normal settings** (`testapp.settings`), not only fast profile.
- If using `settings_fast`, treat as exploratory only; confirm final decision with `testapp.settings`.
- Keep only exclusions that are proven failing due to true SQL Server limitation or out-of-scope known issue.
### 7) Version matrix evidence
Run targeted tests plus HOT guard matrix and report:
- PASS lanes,
- FAIL lanes,
- BLOCKED lanes with concrete environment reason.
## Merge Gate (must all be true)
- No unresolved contract ambiguity.
- No double-escaping / double-transform pattern in call sites.
- No fast-path branch bypassing shared invariants without explicit handling.
- Integration regression proof exists for each behavior change.
- Exclusion changes are validated under `testapp.settings`.
- Targeted tests green + HOT guard green (or BLOCKED with environment reason).
- PR description includes root cause, fix scope, and exclusions touched.
## Recommended Evidence Commands
```bash
# Diff scope
git --no-pager diff --name-status origin/dev...HEAD
git --no-pager diff --stat origin/dev...HEAD
# Local backend tests
python manage.py test testapp.tests --verbosity 2
# Targeted local module
python manage.py test testapp.tests.test_jsonfield --verbosity 2
# Upstream targeted test(s)
cd django
PYTHONPATH=/workspaces/mssql-django/django python tests/runtests.py --settings=testapp.settings
# HOT matrix / CI guard
# Run or verify the HOT matrix / CI guard workflow for this branch in your CI system
# and record its status (PASS / FAIL / BLOCKED) in the gate summary.
```
## Required Output Format
### Gate Summary
- Overall: PASS / FAIL / BLOCKED
- Scope: changed files + behavior areas
### Checklist Results
- Contract change declared: PASS/FAIL (+ evidence)
- Call-site audit: PASS/FAIL (+ evidence)
- Fast-path invariants: PASS/FAIL (+ evidence)
- Integration test proof: PASS/FAIL (+ tests run)
- Doc/comments sync: PASS/FAIL (+ note)
- Exclusion hygiene: PASS/FAIL (+ list kept/removed)
- Version matrix evidence: PASS/FAIL/BLOCKED (+ lanes)
### Exclusions Decision Table
For each touched exclusion:
- test id
- decision (keep/remove)
- reason
- proof command
- result
### Merge Recommendation
- Merge now / Needs fixes
- If fixes needed, list minimal required changes.
## Notes specific to mssql-django
- `testapp/runners.py` marks excluded tests as expected-failure (`x`) under normal settings. Do not treat `x` alone as proof of backend failure for exclusion decisions.
- For exclusion removal decisions, always re-run candidate tests directly under `testapp.settings` and inspect actual pass/fail outcomes.
- Prefer expression-level/compiler-level fixes over SQL string surgery.
microsoft-mssql-django-171ed6e/.github/prompts/mssql-django-run-django-test-suite.prompt.md 0000664 0000000 0000000 00000014576 15253215657 0032235 0 ustar 00root root 0000000 0000000 # Running Django's Full Test Suite Against SQL Server
This guide covers running Django's comprehensive test suite (~6000+ tests) against SQL Server using mssql-django. This is what CI runs and takes 30-45 minutes.
## Prerequisites
### 1. SQL Server Database (Required)
```bash
# Start SQL Server 2022 in Docker
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=MyPassword42" \
-p 1433:1433 --name sqlserver -d mcr.microsoft.com/mssql/server:2022-latest
# Wait for SQL Server to fully initialize
sleep 25
# Verify it's running
docker logs sqlserver 2>&1 | tail -3
# Should show: "Recovery is complete" and "tempdb database has X data file(s)"
```
### 2. ODBC Driver (Required)
```bash
# Ubuntu 24.04 - Install ODBC Driver 18
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | sudo gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
echo "deb [arch=amd64,arm64,armhf signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/ubuntu/24.04/prod noble main" | sudo tee /etc/apt/sources.list.d/mssql-release.list
sudo apt-get update
sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev
```
### 3. System Dependencies (Required for Django test dependencies)
```bash
# libmemcached is needed for pylibmc (Django test dependency)
sudo apt-get install -y libmemcached-dev
```
### 4. Python Dependencies
```bash
# Install mssql-django in development mode
pip install -e .
# Install coverage (used by test.sh)
pip install coverage
```
### 5. Update Settings for ODBC Driver 18
If using ODBC Driver 18 (Ubuntu 24.04), update `testapp/settings.py`:
```python
DATABASES = {
"default": {
"ENGINE": "mssql",
"NAME": "default",
"USER": "sa",
"PASSWORD": "MyPassword42",
"HOST": "localhost",
"PORT": "1433",
"OPTIONS": {
"driver": "ODBC Driver 18 for SQL Server",
"return_rows_bulk_insert": True,
"extra_params": "TrustServerCertificate=yes"
},
},
'other': {
"ENGINE": "mssql",
"NAME": "other",
"USER": "sa",
"PASSWORD": "MyPassword42",
"HOST": "localhost",
"PORT": "1433",
"OPTIONS": {
"driver": "ODBC Driver 18 for SQL Server",
"return_rows_bulk_insert": True,
"extra_params": "TrustServerCertificate=yes"
},
},
}
```
### 6. Django Repository
The `test.sh` script expects a pre-existing Django clone in the `django/` directory. It uses `git fetch`, not `git clone`:
```bash
cd /workspaces/mssql-django
git clone --depth=1 https://github.com/django/django.git django
```
## Running the Full Test Suite
### Using test.sh (Recommended)
```bash
cd /workspaces/mssql-django
bash test.sh 2>&1 | tee test_output.log
```
This will:
1. Fetch tags and checkout the Django version matching your installed Django
2. Install Django's test requirements
3. Run ~100+ test modules against SQL Server
4. Generate coverage report
**Expected runtime:** 30-45 minutes
### Manual Execution
```bash
cd /workspaces/mssql-django/django
# Fetch Django tags and checkout matching version
DJANGO_VERSION="$(python -m django --version)"
git fetch --depth=1 origin +refs/tags/*:refs/tags/*
git checkout $DJANGO_VERSION
# Install Django test requirements
pip install -r tests/requirements/py3.txt
# Run tests
coverage run tests/runtests.py --settings=testapp.settings --noinput \
aggregation \
annotations \
basic \
composite_pk \
queries \
# ... add more test modules as needed
```
### Run Specific Test Modules
```bash
cd /workspaces/mssql-django/django
python tests/runtests.py --settings=testapp.settings --noinput composite_pk
```
### Run Specific Test
```bash
cd /workspaces/mssql-django/django
python tests/runtests.py --settings=testapp.settings --noinput \
composite_pk.test_filter.CompositePKFilterTests.test_explicit_subquery
```
## Understanding Test Output
Test result symbols:
- `.` = passed
- `x` = expected failure (test is known to fail on SQL Server)
- `s` = skipped (test excluded in settings.py)
- `E` = error
- `F` = failure
## Test Exclusions
Tests that are known to fail on SQL Server are excluded in `testapp/settings.py`:
```python
EXCLUDED_TESTS = [
# Tuple lookups - SQL Server doesn't support (col1, col2) IN syntax
'composite_pk.test_filter.CompositePKFilterTests.test_explicit_subquery',
'foreign_object.test_tuple_lookups.TupleLookupsTests.test_exact',
# ... many more
]
```
## Expected Results
From CI (Django 5.2, Python 3.13):
- **Total tests:** ~6200
- **Passed:** ~5400+
- **Skipped:** ~500+
- **Expected failures:** ~200+
- **Errors:** 0 (if code is working correctly)
## Cleanup
```bash
# Stop SQL Server
docker stop sqlserver && docker rm sqlserver
# Restore settings.py
git checkout testapp/settings.py
# Remove Django clone (optional)
rm -rf django/
```
## Troubleshooting
### "coverage: command not found"
```bash
pip install coverage
```
### "pylibmc build failed"
```bash
sudo apt-get install -y libmemcached-dev
```
### Tests hang or timeout
Check SQL Server is running:
```bash
docker ps | grep sqlserver
docker logs sqlserver | tail -10
```
### "An expression of non-boolean type specified in a context where a condition is expected"
This is the tuple lookup limitation. The test should be in `EXCLUDED_TESTS` in settings.py.
### "A column has been specified more than once in the order by list"
This is the ORDER BY duplicate column issue. If you see this, the deduplication fix in `mssql/compiler.py` may need adjustment.
## CI Configuration
The CI uses `tox` with matrix testing. See:
- `azure-pipelines.yml` - CI configuration
- `tox.ini` - Test matrix (Python versions × Django versions)
- `test.sh` - Test script that CI runs
## Test Modules Covered
The full list of Django test modules run by `test.sh`:
- aggregation, aggregation_regress
- annotations, backends, basic
- bulk_create, composite_pk, constraints
- custom_columns, custom_lookups, custom_managers
- custom_methods, custom_pk, datatypes
- dates, datetimes, db_functions
- defer, delete, expressions
- fixtures, foreign_object, get_or_create
- indexes, inspectdb, introspection
- lookup, m2m_*, many_to_one, many_to_many
- migrations, migrations2, model_fields
- ordering, pagination, prefetch_related
- queries, raw_query, schema
- select_for_update, select_related
- serializers, timezones, transactions
- update, update_only_fields
- ... and more (see test.sh for complete list)
microsoft-mssql-django-171ed6e/.github/prompts/mssql-django-run-unit-tests.prompt.md 0000664 0000000 0000000 00000006050 15253215657 0030772 0 ustar 00root root 0000000 0000000 # Running mssql-django Unit Tests
This guide covers running the mssql-django package's own unit tests (42 tests across 11 test files).
## Prerequisites
### 1. SQL Server Database
You need a running SQL Server instance. Options:
#### Option A: Docker (Recommended for local development)
```bash
# Start SQL Server 2022 in Docker
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=MyPassword42" \
-p 1433:1433 --name sqlserver -d mcr.microsoft.com/mssql/server:2022-latest
# Wait for SQL Server to start (about 20 seconds)
sleep 20
# Verify it's running
docker logs sqlserver 2>&1 | tail -5
```
#### Option B: Use existing SQL Server
Update `testapp/settings.py` with your connection details.
### 2. ODBC Driver
Install Microsoft ODBC Driver for SQL Server:
```bash
# Ubuntu 24.04
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | sudo gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
echo "deb [arch=amd64,arm64,armhf signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/ubuntu/24.04/prod noble main" | sudo tee /etc/apt/sources.list.d/mssql-release.list
sudo apt-get update
sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev
```
**Note:** Ubuntu 24.04 only has ODBC Driver 18 available. The default `testapp/settings.py` uses Driver 17. You may need to update:
```python
# In testapp/settings.py, change:
"OPTIONS": {"driver": "ODBC Driver 17 for SQL Server", ...}
# To:
"OPTIONS": {"driver": "ODBC Driver 18 for SQL Server", "extra_params": "TrustServerCertificate=yes", ...}
```
### 3. Python Dependencies
```bash
# Install mssql-django in development mode with test dependencies
pip install -e ".[test]"
```
The `[test]` extra installs `unittest-xml-reporting` which provides the `xmlrunner` module required by the test runner.
**Warning:** Do NOT install the old `xmlrunner` package directly - it's incompatible with Python 3.12+.
## Running Tests
### Run all mssql-django tests
```bash
python manage.py test --noinput
```
Expected output: `Ran 42 tests in ~22s - OK`
### Run specific test module
```bash
python manage.py test testapp.tests.test_base -v 2
```
### Run specific test class
```bash
python manage.py test testapp.tests.test_base.TestEncodeValue -v 2
```
### Run specific test method
```bash
python manage.py test testapp.tests.test_base.TestEncodeValue.test_simple_value -v 2
```
## Test Structure
- Tests are in `testapp/tests/`
- Test runner: `testapp/runners.py` (ExcludedTestSuiteRunner)
- Settings: `testapp/settings.py`
## Cleanup
```bash
# Stop and remove SQL Server container
docker stop sqlserver && docker rm sqlserver
# Restore settings.py if modified
git checkout testapp/settings.py
```
## Troubleshooting
### "Can't open lib 'ODBC Driver 17 for SQL Server'"
Install ODBC Driver 18 and update settings.py (see Prerequisites section).
### "Login failed for user 'sa'"
Wait longer for SQL Server to start, or check the password matches settings.py.
### ModuleNotFoundError: No module named 'xmlrunner'
```bash
pip install unittest-xml-reporting
# Or: pip install -e ".[test]"
```
microsoft-mssql-django-171ed6e/.github/scripts/ 0000775 0000000 0000000 00000000000 15253215657 0021574 5 ustar 00root root 0000000 0000000 microsoft-mssql-django-171ed6e/.github/scripts/parse_coverage.py 0000664 0000000 0000000 00000002662 15253215657 0025141 0 ustar 00root root 0000000 0000000 """Parse Cobertura coverage.xml and export metrics to GITHUB_ENV."""
import xml.etree.ElementTree as ET
import os
import re
import uuid
root = ET.parse("coverage.xml").getroot()
covered = root.get("lines-covered", "0")
total = root.get("lines-valid", "0")
rate = float(root.get("line-rate", "0"))
pct = f"{rate * 100:.2f}%"
print(f"Overall: {pct} ({covered}/{total} lines)")
env_file = os.environ.get("GITHUB_ENV", "/dev/null")
with open(env_file, "a") as env:
env.write(f"COVERAGE_PERCENTAGE={pct}\n")
env.write(f"COVERED_LINES={covered}\n")
env.write(f"TOTAL_LINES={total}\n")
# Regex to strip ADO agent workspace prefixes
agent_prefix_re = re.compile(
r"^(/mnt/vss/_work/\d+/s/|/home/vsts/work/\d+/s/|[A-Z]:\\\\.*?\\\\s\\\\|\\./)(.*)$"
)
files = []
for cls in root.iter("class"):
fname = cls.get("filename", cls.get("name", "unknown"))
match = agent_prefix_re.match(fname)
if match:
fname = match.group(2)
lr = float(cls.get("line-rate", "0"))
lines_elem = cls.find("lines")
line_count = len(lines_elem.findall("line")) if lines_elem is not None else 0
files.append((fname, lr * 100, line_count))
files.sort(key=lambda x: x[1])
low_cov = "\n".join(
f"- {f}: {p:.1f}% ({l} lines)" for f, p, l in files[:10]
)
print(f"\nLowest coverage files:\n{low_cov}")
delim = f"DELIM_{uuid.uuid4().hex}"
with open(env_file, "a") as env:
env.write(f"LOW_COVERAGE_FILES<<{delim}\n{low_cov}\n{delim}\n")
microsoft-mssql-django-171ed6e/.github/skills/ 0000775 0000000 0000000 00000000000 15253215657 0021406 5 ustar 00root root 0000000 0000000 microsoft-mssql-django-171ed6e/.github/skills/code-review/ 0000775 0000000 0000000 00000000000 15253215657 0023617 5 ustar 00root root 0000000 0000000 microsoft-mssql-django-171ed6e/.github/skills/code-review/SKILL.md 0000664 0000000 0000000 00000036660 15253215657 0025032 0 ustar 00root root 0000000 0000000 ---
name: code-review
description: Review methodology for pull requests to the mssql-django SQL Server backend. Use when reviewing a PR that changes the Django database backend under mssql/ or its tests under testapp/. Adds whole-PR, cross-file, and cross-version review procedure on top of the path-scoped instructions in .github/instructions/.
license: See repository LICENSE.
---
# mssql-django backend review
Guidance for reviewing a change to `mssql-django`, a Django database backend that
translates Django's ORM into SQL Server T-SQL. The path-scoped files in
`.github/instructions/` carry the per-file checks; `AGENTS.md` carries project facts and
review judgment. This skill adds the parts a single-file instruction cannot see: the
**whole-PR procedure**, **cross-file interactions**, and **cross-version safety**.
Reviews are advisory. Prefer a few high-value comments to many low-value ones. Anchor every
significant finding to a concrete query or model operation that behaves differently before
vs. after the change, not to an abstract concern.
## Regression Police: executable review
For every behavior-changing pull request under `mssql/` or `testapp/`, act as **Regression
Police**: run at least one disposable, executable head/base probe before completing the review.
Static reasoning may identify candidates, but it is not evidence when the behavior can be
exercised in `testapp`. If no probe can run, state the concrete reason in the review summary.
Regression Police uses the supported Django configurations declared in
`.github/workflows/test.yml` on the preconfigured review runtime. The ordinary CI matrix owns
Python-version coverage, and legacy-only configurations remain outside these probes.
1. **Freeze the evidence target.** Read the expected head SHA from pull request metadata supplied
to the review, never from the current checkout. Compare it with `git rev-parse HEAD` before
testing so a synthetic merge commit cannot validate itself. Publish evidence only for that
SHA, and stop if the checkout changes during review.
2. **Prove the pull request's claim first.** Prefer one focused test added or changed by the pull
request. Run it on head, then overlay the same test file on the merge base and run the same
test label there. If it cannot run unchanged on base, write a smaller public-behavior probe.
3. **Mine one or two adversarial neighbors.** Trace changed code through its callers. Prioritize
silent wrong results, data loss, broken migrations, and cross-version failures. For schema
changes, exercise forward/reverse migration and inspect catalog state after rename/alter;
include an adjacent filtered, expression, covering, remove/re-add, or repeated-rename shape.
For query changes, exercise filter/exclude plus an annotation, aggregate, or boundary input.
4. **Write minimal proofs.** Put disposable tests under `testapp/tests/`, use existing models and
helpers, and assert observable rows, schema, or errors rather than SQL text when possible.
Do not commit generated tests.
5. **Run the identical test on head and base.** Use `run_differential.py` from this skill
directory with the pull request's base ref, test file, and exact test label. The harness
verifies the reviewed SHA, refuses dependency-metadata differentials, creates isolated
worktrees and dependency environments, uses distinct test databases, confirms failing tests
with a deterministic rerun, runs revisions sequentially, and removes test databases through
a bounded independent admin connection even after timeout or crash.
Set `SKILL_DIR` to the directory containing this loaded `SKILL.md`, `PR_HEAD_SHA` from pull
request metadata, and `PR_BASE_REF` from the pull request's target branch, then run:
```bash
python "$SKILL_DIR/run_differential.py" \
--base-ref "$PR_BASE_REF" \
--head-sha "$PR_HEAD_SHA" \
--test-file testapp/tests/test_regression_police_probe.py \
--test-label testapp.tests.test_regression_police_probe.RegressionProbe.test_behavior
```
Omit `--django` to use the newest configuration from the current test matrix, or pass one
of the exact `django-spec` values from that matrix for a version-specific probe.
6. **Interpret the differential correctly.**
| Base | Head | Verdict |
|------|------|---------|
| Pass | Fail | The pull request introduces a regression. |
| Fail | Pass | The pull request fixes the exercised behavior. |
| Fail | Fail | Pre-existing or an incomplete claimed fix; not a new regression. |
| Pass | Pass | The candidate is disproved only in the tested configuration. |
Setup failures, flaky outcomes, different tests, or a stale SHA are inconclusive and must
not be reported as proof. When the changed code is version-gated, rerun the surviving probe
on the minimum and maximum affected Django versions available in the wheelhouse. Do not
imply Python-matrix coverage; ordinary CI owns that dimension.
7. **Run the Devil's Advocate Rubber Duck.** Assume each candidate is wrong. Challenge the
test's trigger, attribution, determinism, user impact, nearby input shapes, and use of public
behavior. Classify it internally as `KEEP`, `HARDEN`, or `DROP`. Rerun both revisions after
hardening and drop anything that does not survive.
8. **Apply Ponytail.** Shrink every surviving test and recommendation to the smallest case that
still proves the behavior. Reuse existing code and point to the narrowest shared root-cause
correction instead of proposing a new abstraction.
Publish only `KEEP` findings on changed lines. Begin every published Regression Police finding
with the standalone header `**MSSQL-Django Regression Police Agent**`. Start a newly introduced
defect with `Regression Police: Proven regression` and an incomplete claimed fix with
`Regression Police: Proven incomplete fix`. Include concrete user impact, the minimal test, the
exact command, base/head outcomes, and the smallest credible fix direction. Do not treat pending
or absent Azure DevOps runs as evidence, and do not fetch routine successful logs.
Always include a short `Regression Police` review-summary line with the number of probes,
tested revisions/configurations, and outcome, including when all candidates were disproved.
## Review procedure
1. **Restate the change and its blast radius.** What ORM behavior does this alter, and which
callers are affected? A change to a widely-called helper (identifier quoting, JSON path
compilation, connection-string construction, `_alter_field`) has repo-wide reach even if
the diff is small — review it by impact, not by line count or the `FIX:`/`FEAT:` label.
2. **Run the cross-cutting checks below** for the areas the PR touches.
3. **Check the change across the whole support matrix**, not just the author's version.
4. **Check test discipline** (see Testing).
5. **Run Regression Police** for executable behavior candidates.
6. **Calibrate severity** (see Severity) and report.
## Cross-version safety (the most common real defect)
Read the current Django and Python support policy from `setup.py`, the executed matrix from
`azure-pipelines.yml`, and environment definitions from `tox.ini` before reviewing. A
change that is correct on the author's version routinely breaks another.
- **Do not use a Django API newer than the declared minimum without a version guard.**
Accessing an attribute or symbol introduced in a later Django raises
`AttributeError`/`ImportError` on older supported versions, *after* the SELECT compiles —
so it passes the author's local run and fails in the field. Match the version guard already
used by the nearest related branch.
- **Version-gated code for retired versions is dead-code-adjacent.** The tree still contains
compatibility branches for versions below the currently declared floor; those versions
are no longer tested or declared supported, although the compatibility code remains in
the distributed package. A change that adds logic to or depends on a below-floor branch
is working on an untested path — flag it as such and steer the fix onto a supported
branch. Do not ask contributors to *fix* below-floor branches; they exist only as inert
history.
- **New Django minor → check `features.py`.** New minors add `supports_*` flags defaulting to
`True`; if not overridden, Django emits SQL SQL Server rejects, sometimes at test-database
creation (which fails the whole suite before a test runs).
- **Migrating off a deprecated API → sweep every call site.** A deprecated call usually still
works (warns now, breaks on a future major), so CI stays green even if a second call site
was missed. Grep the whole symbol across the module, not just the path the failing test hit.
- **Review the SQL/ORM behavior, not the Python driver.** Keep review at the T-SQL / Django
ORM level. Don't assume a specific DBAPI driver's behavior or bind a fix to one driver's
quirk. Direct DBAPI imports and type-code handling belong in the adapter boundary
(`base.py` and `introspection.py`); capability-based behavior such as MARS may legitimately
appear elsewhere. Flag new direct driver coupling outside that boundary unless the PR
explicitly justifies it.
## Cross-file interactions (single-file review misses these)
- **JSON key/path handling ↔ `operations.py` path semantics.** A change compiling a JSON key
path or array index (`OPENJSON`, key transforms) must not bypass `compile_json_path` in
`operations.py`, which distinguishes array indices from object keys and rejects unsupported
negative indices with `NotSupportedError`. Comparing a raw key against `OPENJSON`'s string
`[key]` column loses that distinction (an integer index and an object key of the same text
collide) and skips the negative-index guard (silently returns no rows instead of raising).
- **Capability flag ↔ actual capability, for every supported config.** A `supports_*` /
`can_*` flag must be `True` only where the operation truly works. Setting it unconditionally
while the implementation declines some configuration (Azure SQL Database, an older SQL Server
build, a permission-limited login) makes `connection.features` advertise a capability that
isn't there and makes the matching flag test wrong for that config.
- **Capability detection must fail safe.** A feature-detection property that opens a
connection or queries the server (for example against `master`) is evaluated by Django
before test-DB creation; if it raises for a restricted login it aborts the whole run.
Catch the connection failure and return `False` so detection degrades to "unavailable"
rather than crashing.
- **Connection-string keyword ↔ runtime flag.** Any keyword the backend injects in `base.py`
needs a user opt-out via `extra_params`, and the corresponding `supports_*` flag must stay
consistent with the effective connection string (the flag is often derived from the driver
name, so the two can silently disagree).
- **Bypassing a shared catalog/safety check.** Emitting DDL (for example `DROP INDEX`) from a
declared/expected name instead of the catalog lookup that previously guarded it can issue
the statement for an object that isn't present and abort the migration. Preserve the
catalog/existence check on the shared path rather than short-circuiting it.
## Correctness traps
- **Test the executable query, not just the generated SQL string.** SQL Server rejects some
text-valid SQL at execution — most notably `OFFSET ... FETCH` without an `ORDER BY`. A
change that only asserts on the compiled SQL can pass while the query fails when run. Verify
the query executes, and that pagination/qualify paths still apply the fallback ordering.
- **Do not assume the shape of Django internals.** Iterating `index.fields_orders` without
unpacking the `(field_name, order)` tuple, or treating every `Q` leaf as a
`(lookup, value)` pair, breaks on the shapes Django also allows (expression nodes, `F()`
references, conditional leaves). Handle expression/node forms, not only the tuple form.
- **Type dispatch by `isinstance`, not exact identity.** `type(x) == int` / `is int` excludes
subclasses such as `IntegerChoices` / `IntEnum`.
- **Prefer node-level fixes over compiled-SQL string surgery.** Regex/`str.format` over raw
SQL is blind to string literals, comments, and escape sequences; solve at Django's
expression level.
## Testing
- **Every `FIX:` / `FEAT:` should ship an owned regression test under `testapp/tests/`** in
the same PR, asserting the exact trigger combination from the issue. Re-enabling upstream
Django tests by removing an `EXCLUDED_TESTS` entry is welcome but is not, by itself, an owned
regression test — flag when a fix relies solely on un-excluding upstream coverage.
- **Assert the promised behavior, not a weaker proxy.** A test whose postcondition still
passes when the behavior is wrong (for example asserting a database merely exists to cover a
`keepdb` no-op that could have dropped and recreated it) does not regression-test the claim.
Assert a marker/row/state that only the correct behavior preserves.
- **Respect feature flags in tests.** A test that calls a capability-gated method regardless of
the flag will fail on the supported configs where the capability is declined. Skip on
connections that don't advertise it.
- **Restore shared connection state.** Tests that pin a `cached_property` (for example the
detected SQL Server version) or mutate class dictionaries must restore both the class state
and `connection.__dict__` entries, or later tests observe stale capability flags.
- **`testapp` runs with `USE_TZ=False`.** The timezone / `datetimeoffset` path is exercised
only by the upstream Django suite; a timezone fix that can't be covered from `testapp` should
say so in the PR body.
- **Version-gate tests with `django.VERSION`**, not by skipping, so a fix gated to a new minor
still runs and asserts on current legs. When the only observable difference is a deprecation
warning, scope a filter to that `RemovedInDjangoXWarning` and promote it to an error so the
deprecation itself is the assertion.
## Severity
- **Silent-wrong-results outrank crashes.** A change that can return incorrect rows with no
error is more serious than one that raises — the crash gets reported, the wrong `filter()`
ships into business logic. Lead such findings with "silent: returns wrong rows".
- **A swallowed exception is a defect, not cleanup.** Catching an error and silently skipping
work (losing an index during restoration) or ignoring a failure that leaks resources
(leaving a data-bearing backup on the server) hides real problems. Surface, handle, or
document — don't swallow.
- **A missing `NotSupportedError` is a defect:** silently returning nothing where the backend
previously raised hides an unsupported operation from the user.
## Exclusion hygiene
- `EXCLUDED_TESTS` comments are documentation. State whether an exclusion is a permanent
platform limitation (a wall) or an unimplemented capability (a gap) — a wrong reason there
becomes received wisdom. `@expectedFailure` / `@skip` documents a known failure; it does not
mean the bug is fixed.
## What NOT to flag
- The `as_microsoft` monkey-patch pattern in `functions.py` is the intended, idiomatic
extension mechanism — don't flag it as a code smell.
- `[bracket]` identifier quoting is correct for SQL Server; don't suggest `"double quotes"`.
- Test exclusions carrying a clear, correct platform-limitation reason are fine as-is.
- Do not ask for a `testapp` test for a change that genuinely can't have one (pure infra, or a
`USE_TZ`-only path) when the PR body already says so.
microsoft-mssql-django-171ed6e/.github/skills/code-review/run_differential.py 0000664 0000000 0000000 00000065141 15253215657 0027520 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
"""Run one unchanged Django test against a pull request head and merge base."""
import argparse
import hashlib
import json
import os
import re
import signal
import stat
import shutil
import subprocess
import sys
import tempfile
import uuid
from pathlib import Path
DEPENDENCY_FILES = (
"Pipfile",
"Pipfile.lock",
"poetry.lock",
"pyproject.toml",
"requirements.txt",
"setup.cfg",
"setup.py",
"tox.ini",
"uv.lock",
)
BASE_PACKAGES = ("pyodbc", "pytz", "unittest-xml-reporting>=3.2.0")
GIT_TIMEOUT = 60
SETUP_TIMEOUT = 300
DJANGO_SPEC_PATTERN = re.compile(
r'django-spec:\s*"(?Pdjango>=\d+\.\d+,<\d+\.\d+)"'
)
def run(command, *, cwd, env=None, capture=False, timeout=None):
return subprocess.run(
command,
cwd=cwd,
env=env,
check=False,
text=True,
stdout=subprocess.PIPE if capture else None,
stderr=subprocess.STDOUT if capture else None,
timeout=timeout,
)
def git(repo, *arguments):
result = run(
["git", *arguments],
cwd=repo,
capture=True,
timeout=GIT_TIMEOUT,
)
if result.returncode:
raise RuntimeError(result.stdout.strip())
return result.stdout.strip()
def validate_test_file(repo, value):
path = Path(value)
if path.is_absolute() or ".." in path.parts:
raise ValueError("test file must be relative to the repository")
resolved = (repo / path).resolve()
try:
resolved.relative_to(repo.resolve())
except ValueError as error:
raise ValueError("test file must be inside the repository") from error
if not resolved.is_file():
raise ValueError(f"test file does not exist: {value}")
return path
def validate_test_label(test_file, test_label):
module = ".".join(test_file.with_suffix("").parts)
if not test_label.startswith(f"{module}."):
raise ValueError(
f"test label must resolve inside copied module {module}: {test_label}"
)
def supported_django(repo):
workflow = repo / ".github" / "workflows" / "test.yml"
specs = list(
dict.fromkeys(
match.group("spec")
for match in DJANGO_SPEC_PATTERN.finditer(
workflow.read_text(encoding="utf-8")
)
)
)
if not specs:
raise RuntimeError(f"no Django matrix specs found in {workflow}")
return specs
def create_environment(python, environment_dir, wheelhouse, django):
subprocess.run(
[python, "-m", "venv", environment_dir],
check=True,
timeout=SETUP_TIMEOUT,
)
executable = "Scripts/python.exe" if os.name == "nt" else "bin/python"
environment_python = environment_dir / executable
install_prefix = [
environment_python,
"-m",
"pip",
"install",
"--quiet",
"--no-index",
"--find-links",
wheelhouse,
]
subprocess.run(
[*install_prefix, "setuptools", "wheel", *BASE_PACKAGES, django],
check=True,
timeout=SETUP_TIMEOUT,
)
return environment_python
def failure_signature(output, test_label):
test_name = test_label.rsplit(".", 1)[-1]
all_headers = re.findall(
r"^(?:FAIL|ERROR)(?: \[[^\]]+\])?: .+$",
output,
re.MULTILINE,
)
headers = [
header for header in all_headers if re.search(re.escape(test_name), header)
]
headers = [re.sub(r" \[[^\]]+\]", "", header) for header in headers]
details = re.findall(
r"^(?:[\w.]+(?:Error|Exception|Failure)|AssertionError)(?:: .+)?$",
output,
re.MULTILINE,
)
frames = [
{
"file": Path(path).name,
"line": int(line),
"function": function,
}
for path, line, function in re.findall(
r'^\s*File "([^"]+)", line (\d+), in (.+)$',
output,
re.MULTILINE,
)
]
if len(all_headers) != 1 or len(headers) != 1 or not details or not frames:
return None
stable = json.dumps(
{"headers": headers, "details": details, "frames": frames},
sort_keys=True,
)
return hashlib.sha256(stable.encode()).hexdigest()
def classify_test(return_code, output, test_label, sentinel):
tests_run = re.search(r"Ran (\d+) tests?", output)
executed_once = bool(tests_run and int(tests_run.group(1)) == 1)
method_passed = f"{sentinel}:PASS" in output
method_failed = f"{sentinel}:FAIL" in output
method_executed = method_passed != method_failed
if not executed_once or not method_executed:
return "inconclusive"
if re.search(
r"(?:skipped|expected failures|unexpected successes)=[1-9]\d*",
output,
):
return "inconclusive"
ok_summary = bool(re.search(r"^OK(?:\s|$)", output, re.MULTILINE))
suite_passed = return_code == 0 and ok_summary
if method_passed and suite_passed:
return "pass"
if method_failed and return_code and failure_signature(output, test_label):
return "fail"
return "inconclusive"
def terminate_process_tree(process):
if os.name == "nt":
subprocess.run(
["taskkill", "/F", "/T", "/PID", str(process.pid)],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return
try:
os.killpg(process.pid, signal.SIGTERM)
except ProcessLookupError:
pass
try:
process.wait(timeout=2)
except subprocess.TimeoutExpired:
pass
try:
os.killpg(process.pid, signal.SIGKILL)
except ProcessLookupError:
pass
try:
process.wait(timeout=2)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=2)
def output_text(output):
if isinstance(output, bytes):
return output.decode(errors="replace")
return output or ""
def run_process_group(command, *, cwd, env, timeout):
options = {
"cwd": cwd,
"env": env,
"stderr": subprocess.STDOUT,
"stdout": subprocess.PIPE,
"text": True,
}
if os.name == "nt":
options["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
else:
options["start_new_session"] = True
process = subprocess.Popen(command, **options)
try:
stdout, _ = process.communicate(timeout=timeout)
except subprocess.TimeoutExpired as error:
stdout = output_text(error.stdout)
terminate_process_tree(process)
if process.stdout:
process.stdout.close()
raise subprocess.TimeoutExpired(
command,
timeout,
output=stdout,
) from error
return_code = process.returncode
terminate_process_tree(process)
return subprocess.CompletedProcess(command, return_code, stdout)
def execute_test(python, source_dir, label, environment, timeout):
return run_process_group(
[
python,
"manage.py",
"test",
label,
"--noinput",
"--verbosity",
"2",
],
cwd=source_dir,
env=environment,
timeout=timeout,
)
def execute_attempt(
python,
source_dir,
label,
environment,
timeout,
sentinel,
):
try:
result = execute_test(python, source_dir, label, environment, timeout)
except subprocess.TimeoutExpired as error:
output = error.stdout or ""
if isinstance(output, bytes):
output = output.decode(errors="replace")
return {
"exit_code": None,
"status": "inconclusive",
"signature": None,
"timed_out": True,
}, output
status = classify_test(result.returncode, result.stdout, label, sentinel)
return {
"exit_code": result.returncode,
"status": status,
"signature": failure_signature(result.stdout, label)
if status == "fail"
else None,
"timed_out": False,
}, result.stdout
def cleanup_databases(python, database_names, environment, timeout=30):
for name in database_names:
if not re.fullmatch(r"[A-Za-z0-9_]{1,100}", name):
raise RuntimeError(f"unsafe cleanup database name: {name}")
cleanup_environment = environment.copy()
cleanup_environment.pop("PYTHONPATH", None)
cleanup_environment.pop("REGRESSION_POLICE_TEST_ID", None)
cleanup_environment.pop("REGRESSION_POLICE_SENTINEL", None)
cleanup_environment["REGRESSION_POLICE_DATABASES"] = json.dumps(database_names)
code = (
"import json, os, pyodbc\n"
"driver = os.environ['MSSQL_DRIVER']\n"
"host = os.environ['MSSQL_HOST']\n"
"port = os.environ.get('MSSQL_PORT', '1433')\n"
"user = os.environ.get('MSSQL_USER', 'sa')\n"
"password = os.environ['MSSQL_PASSWORD']\n"
"password_key = 'P' + 'WD'\n"
"extra = os.environ.get('MSSQL_EXTRA_PARAMS', '')\n"
"connection = pyodbc.connect(\n"
" f'DRIVER={{{driver}}};SERVER={host},{port};UID={user};'\n"
" f'{password_key}={password};DATABASE=master;{extra}',\n"
" autocommit=True,\n"
" timeout=5,\n"
")\n"
"try:\n"
" cursor = connection.cursor()\n"
" for name in json.loads(os.environ['REGRESSION_POLICE_DATABASES']):\n"
" cursor.execute('SELECT 1 FROM sys.databases WHERE name = ?', name)\n"
" if cursor.fetchone():\n"
" quoted = '[' + name.replace(']', ']]') + ']'\n"
" cursor.execute(\n"
" f'ALTER DATABASE {quoted} SET SINGLE_USER '\n"
" 'WITH ROLLBACK IMMEDIATE'\n"
" )\n"
" cursor.execute(f'DROP DATABASE {quoted}')\n"
"finally:\n"
" connection.close()\n"
)
result = run_process_group(
[python, "-c", code],
cwd=Path.cwd(),
env=cleanup_environment,
timeout=timeout,
)
if result.returncode:
raise RuntimeError(f"database cleanup failed: {result.stdout.strip()}")
def run_test(
python,
source_dir,
label,
database_suffix,
output_path,
timeout,
hook_dir,
sentinel=None,
):
environment = os.environ.copy()
environment["MSSQL_DB_NAME"] = f"rp_{database_suffix}"
environment["MSSQL_DB_NAME_OTHER"] = f"rp_{database_suffix}_other"
sentinel = sentinel or f"REGRESSION_POLICE_EXECUTED:{uuid.uuid4().hex}"
environment["REGRESSION_POLICE_TEST_ID"] = label
environment["REGRESSION_POLICE_SENTINEL"] = sentinel
environment["PYTHONPATH"] = os.pathsep.join(
filter(None, (str(hook_dir), environment.get("PYTHONPATH")))
)
hook_path = hook_dir / "sitecustomize.py"
hook_digest = bounded_file_digest(hook_path)
database_names = (
f"test_{environment['MSSQL_DB_NAME']}",
f"test_{environment['MSSQL_DB_NAME_OTHER']}",
)
cleanup_error = None
try:
first, output = execute_attempt(
python,
source_dir,
label,
environment,
timeout,
sentinel,
)
attempts = [first]
status = attempts[0]["status"]
hook_changed = not hook_matches(hook_path, hook_digest)
if hook_changed:
status = "inconclusive"
elif status == "fail":
rerun, rerun_output = execute_attempt(
python,
source_dir,
label,
environment,
timeout,
sentinel,
)
attempts.append(rerun)
output = f"{output}\n\n--- deterministic rerun ---\n\n{rerun_output}"
matching_failure = rerun["status"] == "fail" and (
rerun["signature"] == first["signature"]
)
hook_changed = not hook_matches(hook_path, hook_digest)
if not matching_failure or hook_changed:
status = "inconclusive"
finally:
try:
cleanup_databases(python, database_names, environment)
except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as error:
cleanup_error = f"{type(error).__name__}: {error}"
if cleanup_error:
status = "inconclusive"
output = (
f"{output}\n\n--- database cleanup failure ---\n\n"
f"{cleanup_error}\n"
)
output_path.write_text(output, encoding="utf-8")
return {
"exit_code": first["exit_code"],
"status": status,
"attempts": attempts,
"database_names": database_names,
"cleanup_error": cleanup_error,
"hook_changed": hook_changed,
}
def verdict(base_result, head_result):
outcomes = (base_result["status"], head_result["status"])
if "inconclusive" in outcomes:
return "inconclusive"
return {
("pass", "fail"): "introduced-regression",
("fail", "pass"): "confirmed-fix",
("fail", "fail"): "pre-existing-or-incomplete",
("pass", "pass"): "disproved-in-tested-configuration",
}[outcomes]
def final_verdict(failure, base_result, head_result):
return "inconclusive" if failure else verdict(base_result, head_result)
def parse_args():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base-ref", required=True)
parser.add_argument("--test-file", required=True)
parser.add_argument("--test-label", required=True)
parser.add_argument("--head-sha", required=True)
parser.add_argument(
"--wheelhouse",
default=os.environ.get("REGRESSION_POLICE_WHEELHOUSE"),
)
parser.add_argument(
"--django",
help="Exact Django spec from .github/workflows/test.yml.",
)
parser.add_argument("--timeout-seconds", type=int, default=300)
parser.add_argument("--output-dir")
return parser.parse_args()
def make_run_id(args, head_sha, test_file):
configuration = json.dumps(
{
"base_ref": args.base_ref,
"django": args.django,
"head_sha": args.head_sha,
"python": sys.executable,
"test_file": str(test_file),
"test_label": args.test_label,
"timeout_seconds": args.timeout_seconds,
},
sort_keys=True,
)
digest = hashlib.sha256(configuration.encode()).hexdigest()[:12]
return f"{head_sha[:8]}-{test_file.stem}-{digest}-{uuid.uuid4().hex[:8]}"
def create_execution_hook(directory):
directory.mkdir(parents=True)
(directory / "sitecustomize.py").write_text(
"import os\n"
"import unittest\n"
"\n"
"_original = unittest.TestCase._callTestMethod\n"
"_test_id = os.environ['REGRESSION_POLICE_TEST_ID']\n"
"_sentinel = os.environ['REGRESSION_POLICE_SENTINEL']\n"
"\n"
"def _call_test_method(self, method):\n"
" if self.id() != _test_id:\n"
" return _original(self, method)\n"
" try:\n"
" result = _original(self, method)\n"
" except BaseException:\n"
" print(f'{_sentinel}:FAIL', flush=True)\n"
" raise\n"
" print(f'{_sentinel}:PASS', flush=True)\n"
" return result\n"
"\n"
"unittest.TestCase._callTestMethod = _call_test_method\n",
encoding="utf-8",
)
def bounded_file_digest(path, max_bytes=16384):
metadata = path.lstat()
if not stat.S_ISREG(metadata.st_mode) or metadata.st_size > max_bytes:
raise RuntimeError(f"unsafe hook file: {path}")
flags = os.O_RDONLY
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
descriptor = os.open(path, flags)
try:
opened = os.fstat(descriptor)
if not stat.S_ISREG(opened.st_mode) or opened.st_size > max_bytes:
raise RuntimeError(f"unsafe hook file: {path}")
with os.fdopen(descriptor, "rb", closefd=False) as stream:
contents = stream.read(max_bytes + 1)
finally:
os.close(descriptor)
if len(contents) > max_bytes:
raise RuntimeError(f"hook file exceeds {max_bytes} bytes: {path}")
return hashlib.sha256(contents).hexdigest()
def hook_matches(path, expected_digest):
try:
return bounded_file_digest(path) == expected_digest
except (OSError, RuntimeError):
return False
def source_fingerprint(source_dir, test_file):
digest = hashlib.sha256()
ignored_names = {
".coverage",
".git",
"coverage.xml",
"db.sqlitetest",
"result.xml",
}
ignored_directories = {
".mypy_cache",
".pytest_cache",
"__pycache__",
"logs",
}
files = []
for path in source_dir.rglob("*"):
relative_path = path.relative_to(source_dir)
ignored_directory = any(
part in ignored_directories for part in relative_path.parts
)
egg_info = any(
part.endswith(".egg-info") for part in relative_path.parts
)
generated_file = (
path.name in ignored_names or path.suffix in {".pyc", ".pyo"}
)
if ignored_directory or egg_info or generated_file:
continue
mode = path.lstat().st_mode
if stat.S_ISREG(mode) or stat.S_ISLNK(mode):
files.append((relative_path, path))
for relative_path, path in sorted(files):
digest.update(str(relative_path).encode())
digest.update(b"\0")
mode = path.lstat().st_mode
if stat.S_ISLNK(mode):
digest.update(b"symlink:")
digest.update(os.readlink(path).encode())
else:
digest.update(path.read_bytes())
digest.update(b"\0")
return digest.hexdigest()
def verify_import_source(python, source_dir):
code = (
"from pathlib import Path; import mssql; "
f"source=Path({str(source_dir)!r}).resolve(); "
"loaded=Path(mssql.__file__).resolve(); "
"loaded.relative_to(source)"
)
result = run_process_group(
[python, "-c", code],
cwd=source_dir,
env=os.environ.copy(),
timeout=30,
)
if result.returncode:
raise RuntimeError(
f"mssql did not import from {source_dir}: {result.stdout.strip()}"
)
def main():
args = parse_args()
repo = Path(git(Path.cwd(), "rev-parse", "--show-toplevel"))
test_file = Path(args.test_file)
head_sha = git(repo, "rev-parse", "HEAD")
default_output = Path(
os.environ.get("RUNNER_TEMP", tempfile.gettempdir())
).joinpath("regression-police")
output_root = Path(args.output_dir or default_output)
output_root.mkdir(parents=True, exist_ok=True)
run_id = make_run_id(args, head_sha, test_file)
report_path = output_root / f"{run_id}.json"
head_log = output_root / f"{run_id}-head.log"
base_log = output_root / f"{run_id}-base.log"
base_sha = None
failure = None
head_result = {"exit_code": None, "status": "inconclusive"}
base_result = {"exit_code": None, "status": "inconclusive"}
try:
django_specs = supported_django(repo)
args.django = args.django or django_specs[-1]
test_file = validate_test_file(repo, args.test_file)
validate_test_label(test_file, args.test_label)
if args.django not in django_specs:
raise RuntimeError(
f"unsupported Django probe spec {args.django}; "
f"choose one from {django_specs}"
)
if not re.fullmatch(r"[0-9a-f]{40}", args.head_sha):
raise RuntimeError("head SHA from pull request metadata must be 40 lowercase hex digits")
if head_sha != args.head_sha:
raise RuntimeError(f"stale checkout: expected {args.head_sha}, found {head_sha}")
if not args.wheelhouse:
raise RuntimeError(
"REGRESSION_POLICE_WHEELHOUSE or --wheelhouse is required"
)
wheelhouse = Path(args.wheelhouse).resolve()
if not wheelhouse.is_dir():
raise RuntimeError(f"wheelhouse does not exist: {wheelhouse}")
if args.timeout_seconds < 1:
raise RuntimeError("timeout must be at least one second")
git(repo, "check-ref-format", "--branch", args.base_ref)
base_refspec = (
f"+refs/heads/{args.base_ref}:refs/remotes/origin/{args.base_ref}"
)
subprocess.run(
["git", "fetch", "--no-tags", "origin", base_refspec],
cwd=repo,
check=True,
timeout=SETUP_TIMEOUT,
)
base_sha = git(repo, "merge-base", head_sha, f"origin/{args.base_ref}")
changed_dependencies = git(
repo,
"diff",
"--name-only",
base_sha,
head_sha,
"--",
*DEPENDENCY_FILES,
).splitlines()
if changed_dependencies:
joined = ", ".join(changed_dependencies)
raise RuntimeError(
f"dependency metadata changed ({joined}); "
"use independently pinned environments"
)
with tempfile.TemporaryDirectory(prefix="regression-police-") as temporary:
temporary_path = Path(temporary)
head_dir = temporary_path / "head"
base_dir = temporary_path / "base"
head_hook_dir = temporary_path / "head-hook"
base_hook_dir = temporary_path / "base-hook"
subprocess.run(
["git", "worktree", "add", "--detach", head_dir, head_sha],
cwd=repo,
check=True,
stdout=subprocess.DEVNULL,
timeout=SETUP_TIMEOUT,
)
try:
subprocess.run(
["git", "worktree", "add", "--detach", base_dir, base_sha],
cwd=repo,
check=True,
stdout=subprocess.DEVNULL,
timeout=SETUP_TIMEOUT,
)
head_test_file = head_dir / test_file
base_test_file = base_dir / test_file
head_test_file.parent.mkdir(parents=True, exist_ok=True)
base_test_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(repo / test_file, head_test_file)
shutil.copy2(repo / test_file, base_test_file)
head_fingerprint = source_fingerprint(head_dir, test_file)
base_fingerprint = source_fingerprint(base_dir, test_file)
head_python = create_environment(
sys.executable,
temporary_path / "head-venv",
wheelhouse,
args.django,
)
base_python = create_environment(
sys.executable,
temporary_path / "base-venv",
wheelhouse,
args.django,
)
if source_fingerprint(head_dir, test_file) != head_fingerprint:
raise RuntimeError("head source changed during environment setup")
if source_fingerprint(base_dir, test_file) != base_fingerprint:
raise RuntimeError("base source changed during environment setup")
verify_import_source(head_python, head_dir)
verify_import_source(base_python, base_dir)
database_token = hashlib.sha256(run_id.encode()).hexdigest()[:12]
create_execution_hook(base_hook_dir)
base_result = run_test(
base_python,
base_dir,
args.test_label,
f"b_{database_token}",
base_log,
args.timeout_seconds,
base_hook_dir,
)
create_execution_hook(head_hook_dir)
head_result = run_test(
head_python,
head_dir,
args.test_label,
f"h_{database_token}",
head_log,
args.timeout_seconds,
head_hook_dir,
)
if source_fingerprint(head_dir, test_file) != head_fingerprint:
raise RuntimeError("head source changed during probe execution")
if source_fingerprint(base_dir, test_file) != base_fingerprint:
raise RuntimeError("base source changed during probe execution")
finally:
subprocess.run(
["git", "worktree", "remove", "--force", base_dir],
cwd=repo,
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
subprocess.run(
["git", "worktree", "remove", "--force", head_dir],
cwd=repo,
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
final_head_sha = git(repo, "rev-parse", "HEAD")
if final_head_sha != head_sha:
raise RuntimeError(
f"checkout changed during review: {head_sha} -> {final_head_sha}"
)
except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as error:
failure = f"{type(error).__name__}: {error}"
report = {
"base_ref": args.base_ref,
"base_sha": base_sha,
"head_sha": head_sha,
"python": sys.executable,
"wheelhouse": args.wheelhouse,
"django": args.django,
"test_file": str(test_file),
"test_label": args.test_label,
"timeout_seconds": args.timeout_seconds,
"base_exit_code": base_result["exit_code"],
"base_status": base_result["status"],
"base_attempts": base_result.get("attempts", []),
"base_database_names": base_result.get("database_names", []),
"base_cleanup_error": base_result.get("cleanup_error"),
"base_hook_changed": base_result.get("hook_changed"),
"head_exit_code": head_result["exit_code"],
"head_status": head_result["status"],
"head_attempts": head_result.get("attempts", []),
"head_database_names": head_result.get("database_names", []),
"head_cleanup_error": head_result.get("cleanup_error"),
"head_hook_changed": head_result.get("hook_changed"),
"verdict": final_verdict(failure, base_result, head_result),
"failure": failure,
"base_log": str(base_log),
"head_log": str(head_log),
}
report_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
print(json.dumps(report, indent=2))
print(f"report: {report_path}")
return 2 if report["verdict"] == "inconclusive" else 0
if __name__ == "__main__":
raise SystemExit(main())
microsoft-mssql-django-171ed6e/.github/skills/code-review/test_run_differential.py 0000664 0000000 0000000 00000064010 15253215657 0030551 0 ustar 00root root 0000000 0000000 import os
import json
import subprocess
import sys
import tempfile
import time
import unittest
from argparse import Namespace
from pathlib import Path
from subprocess import TimeoutExpired
from types import SimpleNamespace
from unittest.mock import patch
from run_differential import (
bounded_file_digest,
classify_test,
cleanup_databases,
create_execution_hook,
failure_signature,
final_verdict,
main,
make_run_id,
run_process_group,
run_test,
source_fingerprint,
supported_django,
validate_test_label,
verdict,
)
class ResultClassificationTests(unittest.TestCase):
label = "test.module.Class.test_behavior"
sentinel = "REGRESSION_POLICE_EXECUTED:test"
failure = (
f"{sentinel}:FAIL\n"
"FAIL: test_behavior (test.module.Class.test_behavior)\n"
"----------------------------------------------------------------------\n"
"Traceback (most recent call last):\n"
' File "/tmp/test_probe.py", line 12, in test_behavior\n'
"AssertionError: expected 1, got 2\n"
"----------------------------------------------------------------------\n"
"Ran 1 test\n\n"
"FAILED (failures=1)\n"
)
def test_pass_requires_an_executed_test(self):
self.assertEqual(
classify_test(
0,
f"{self.sentinel}:PASS\nRan 1 test\n\nOK\n",
self.label,
self.sentinel,
),
"pass",
)
self.assertEqual(
classify_test(
0,
f"{self.sentinel}:PASS\nRan 0 tests\n\nOK\n",
self.label,
self.sentinel,
),
"inconclusive",
)
self.assertEqual(
classify_test(0, "Ran 1 test\n\nOK\n", self.label, self.sentinel),
"inconclusive",
)
def test_non_executed_outcomes_are_inconclusive(self):
for outcome in ("skipped", "expected failures", "unexpected successes"):
with self.subTest(outcome=outcome):
output = (
f"{self.sentinel}:PASS\n"
f"Ran 1 test\n\nOK ({outcome}=1)\n"
)
self.assertEqual(
classify_test(0, output, self.label, self.sentinel),
"inconclusive",
)
def test_test_failure_and_setup_error_are_distinct(self):
fixture_error = (
"ERROR: setUpClass (test.module.Class)\n"
"RuntimeError: database unavailable\n"
"Ran 1 test\n\n"
"FAILED (errors=1)\n"
)
self.assertEqual(
classify_test(1, self.failure, self.label, self.sentinel),
"fail",
)
self.assertEqual(
classify_test(1, fixture_error, self.label, self.sentinel),
"inconclusive",
)
self.assertEqual(
classify_test(
1,
"database connection failed",
self.label,
self.sentinel,
),
"inconclusive",
)
class_cleanup_error = self.failure + (
"\nERROR: tearDownClass (test.module.Class)\n"
' File "/tmp/test_probe.py", line 30, in tearDownClass\n'
"RuntimeError: class cleanup failed\n"
)
self.assertEqual(
classify_test(
1,
class_cleanup_error,
self.label,
self.sentinel,
),
"inconclusive",
)
teardown_error = (
f"{self.sentinel}:PASS\n"
"ERROR: test_behavior (test.module.Class.test_behavior)\n"
' File "/tmp/test_probe.py", line 20, in tearDown\n'
"RuntimeError: cleanup failed\n"
"Ran 1 test\n\n"
"FAILED (errors=1)\n"
)
self.assertEqual(
classify_test(1, teardown_error, self.label, self.sentinel),
"inconclusive",
)
cleanup_error = (
f"{self.sentinel}:PASS\n"
"ERROR: test_behavior (test.module.Class.test_behavior)\n"
' File "/tmp/test_probe.py", line 25, in cleanup\n'
"RuntimeError: cleanup failed\n"
"Ran 1 test\n\n"
"FAILED (errors=1)\n"
)
self.assertEqual(
classify_test(1, cleanup_error, self.label, self.sentinel),
"inconclusive",
)
method_and_cleanup_failure = self.failure + (
"\nERROR: test_behavior (test.module.Class.test_behavior)\n"
' File "/tmp/test_probe.py", line 25, in cleanup\n'
"RuntimeError: cleanup failed\n"
)
self.assertEqual(
classify_test(
1,
method_and_cleanup_failure,
self.label,
self.sentinel,
),
"inconclusive",
)
def test_failure_signature_includes_details(self):
changed_failure = self.failure.replace("got 2", "got 3")
self.assertNotEqual(
failure_signature(self.failure, self.label),
failure_signature(changed_failure, self.label),
)
def test_failure_signature_ignores_duration(self):
first = self.failure.replace("FAIL:", "FAIL [0.10s]:")
second = self.failure.replace("FAIL:", "FAIL [0.25s]:")
self.assertEqual(
failure_signature(first, self.label),
failure_signature(second, self.label),
)
def test_failure_signature_includes_assertion_location(self):
changed_failure = self.failure.replace("line 12", "line 15")
self.assertNotEqual(
failure_signature(self.failure, self.label),
failure_signature(changed_failure, self.label),
)
def test_failure_signature_requires_a_traceback_frame(self):
no_frame = self.failure.replace(
' File "/tmp/test_probe.py", line 12, in test_behavior\n',
"",
)
self.assertIsNone(failure_signature(no_frame, self.label))
def test_failure_signature_accepts_bare_assertion_error(self):
bare = self.failure.replace(
"AssertionError: expected 1, got 2",
"AssertionError",
)
self.assertIsNotNone(failure_signature(bare, self.label))
def test_verdicts(self):
def result(status):
return {"status": status}
self.assertEqual(
verdict(result("pass"), result("fail")),
"introduced-regression",
)
self.assertEqual(
verdict(result("fail"), result("pass")),
"confirmed-fix",
)
self.assertEqual(
verdict(result("fail"), result("fail")),
"pre-existing-or-incomplete",
)
self.assertEqual(
verdict(result("pass"), result("pass")),
"disproved-in-tested-configuration",
)
self.assertEqual(
verdict(result("inconclusive"), result("pass")),
"inconclusive",
)
@patch("run_differential.execute_test")
def test_failure_requires_a_matching_rerun(self, execute_test):
failure = SimpleNamespace(
returncode=1,
stdout=self.failure,
)
success = SimpleNamespace(
returncode=0,
stdout=f"{self.sentinel}:PASS\nRan 1 test\n\nOK\n",
)
execute_test.side_effect = [failure, success]
with tempfile.TemporaryDirectory() as temporary, patch(
"run_differential.cleanup_databases"
):
Path(temporary, "sitecustomize.py").write_text("hook\n")
result = run_test(
"python",
Path("."),
self.label,
"head",
Path(temporary) / "test.log",
300,
Path(temporary),
self.sentinel,
)
self.assertEqual(result["status"], "inconclusive")
self.assertEqual(len(result["attempts"]), 2)
@patch("run_differential.execute_test")
def test_repeated_failure_is_deterministic(self, execute_test):
failure = SimpleNamespace(
returncode=1,
stdout=self.failure,
)
execute_test.side_effect = [failure, failure]
with tempfile.TemporaryDirectory() as temporary, patch(
"run_differential.cleanup_databases"
):
Path(temporary, "sitecustomize.py").write_text("hook\n")
result = run_test(
"python",
Path("."),
self.label,
"head",
Path(temporary) / "test.log",
300,
Path(temporary),
self.sentinel,
)
self.assertEqual(result["status"], "fail")
self.assertEqual(len(result["attempts"]), 2)
@patch("run_differential.execute_test")
def test_different_rerun_failure_is_inconclusive(self, execute_test):
first = SimpleNamespace(returncode=1, stdout=self.failure)
second = SimpleNamespace(
returncode=1,
stdout=self.failure.replace("got 2", "got 3"),
)
execute_test.side_effect = [first, second]
with tempfile.TemporaryDirectory() as temporary, patch(
"run_differential.cleanup_databases"
):
Path(temporary, "sitecustomize.py").write_text("hook\n")
result = run_test(
"python",
Path("."),
self.label,
"head",
Path(temporary) / "test.log",
300,
Path(temporary),
self.sentinel,
)
self.assertEqual(result["status"], "inconclusive")
@patch("run_differential.execute_test")
def test_timeout_is_inconclusive(self, execute_test):
execute_test.side_effect = TimeoutExpired(["python"], 1)
with tempfile.TemporaryDirectory() as temporary, patch(
"run_differential.cleanup_databases"
):
Path(temporary, "sitecustomize.py").write_text("hook\n")
result = run_test(
"python",
Path("."),
self.label,
"head",
Path(temporary) / "test.log",
1,
Path(temporary),
self.sentinel,
)
self.assertEqual(result["status"], "inconclusive")
self.assertTrue(result["attempts"][0]["timed_out"])
@patch("run_differential.execute_test")
def test_hook_mutation_is_inconclusive(self, execute_test):
with tempfile.TemporaryDirectory() as temporary, patch(
"run_differential.cleanup_databases"
):
hook = Path(temporary, "sitecustomize.py")
hook.write_text("original\n")
def mutate_hook(*args):
hook.write_text("mutated\n")
return SimpleNamespace(
returncode=0,
stdout=f"{self.sentinel}:PASS\nRan 1 test\n\nOK\n",
)
execute_test.side_effect = mutate_hook
result = run_test(
"python",
Path("."),
self.label,
"head",
Path(temporary) / "test.log",
300,
Path(temporary),
self.sentinel,
)
self.assertEqual(result["status"], "inconclusive")
self.assertTrue(result["hook_changed"])
def test_hook_digest_rejects_symlink_and_oversized_file(self):
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
target = directory / "target"
target.write_text("hook\n")
link = directory / "link"
try:
link.symlink_to(target)
except OSError as error:
self.skipTest(f"symlinks unavailable: {error}")
with self.assertRaises(RuntimeError):
bounded_file_digest(link)
oversized = directory / "oversized"
oversized.write_bytes(b"x" * 16385)
with self.assertRaises(RuntimeError):
bounded_file_digest(oversized)
@patch("run_differential.uuid.uuid4")
def test_report_id_includes_the_full_probe_configuration(self, uuid4):
uuid4.return_value = SimpleNamespace(hex="a" * 32)
arguments = Namespace(
base_ref="dev",
django="django>=6.1,<6.2",
head_sha="1" * 40,
test_label="test.module.Class.test_one",
timeout_seconds=300,
)
first = make_run_id(arguments, arguments.head_sha, Path("test_probe.py"))
arguments.test_label = "test.module.Class.test_two"
second = make_run_id(arguments, arguments.head_sha, Path("test_probe.py"))
self.assertNotEqual(first, second)
@patch("run_differential.uuid.uuid4")
def test_report_id_is_unique_for_concurrent_probes(self, uuid4):
uuid4.side_effect = [
SimpleNamespace(hex="a" * 32),
SimpleNamespace(hex="b" * 32),
]
arguments = Namespace(
base_ref="dev",
django="django>=6.1,<6.2",
head_sha="1" * 40,
test_label="test.module.Class.test_one",
timeout_seconds=300,
)
first = make_run_id(arguments, arguments.head_sha, Path("test_probe.py"))
second = make_run_id(arguments, arguments.head_sha, Path("test_probe.py"))
self.assertNotEqual(first, second)
def test_supported_django_comes_from_test_workflow(self):
with tempfile.TemporaryDirectory() as temporary:
repo = Path(temporary)
workflow = repo / ".github" / "workflows" / "test.yml"
workflow.parent.mkdir(parents=True)
workflow.write_text(
'- { django-spec: "django>=5.2,<5.3" }\n'
'- { django-spec: "django>=6.1,<6.2" }\n'
)
self.assertEqual(
supported_django(repo),
["django>=5.2,<5.3", "django>=6.1,<6.2"],
)
def test_test_label_must_resolve_inside_copied_module(self):
test_file = Path("testapp/tests/test_probe.py")
validate_test_label(
test_file,
"testapp.tests.test_probe.ProbeTest.test_behavior",
)
with self.assertRaises(ValueError):
validate_test_label(
test_file,
"testapp.tests.test_other.OtherTest.test_behavior",
)
def test_harness_failure_forces_inconclusive_verdict(self):
passing = {"status": "pass"}
self.assertEqual(
final_verdict("checkout changed", passing, passing),
"inconclusive",
)
def test_source_fingerprint_detects_untracked_files_and_symlink_targets(self):
with tempfile.TemporaryDirectory() as temporary:
source = Path(temporary)
probe = source / "probe.py"
probe.write_text("pass\n")
original = source_fingerprint(source, Path("probe.py"))
(source / "new.py").write_text("ADDED = True\n")
with_untracked = source_fingerprint(source, Path("probe.py"))
self.assertNotEqual(original, with_untracked)
first_target = source / "first-target"
second_target = source / "second-target"
first_target.write_text("first\n")
second_target.write_text("second\n")
link = source / "link"
try:
link.symlink_to(first_target)
except OSError as error:
self.skipTest(f"symlinks unavailable: {error}")
null_link = source_fingerprint(source, Path("probe.py"))
link.unlink()
link.symlink_to(second_target)
zero_link = source_fingerprint(source, Path("probe.py"))
self.assertNotEqual(null_link, zero_link)
@unittest.skipIf(os.name == "nt", "POSIX process-group assertion")
def test_timeout_terminates_descendant_processes(self):
with tempfile.TemporaryDirectory() as temporary:
pid_file = Path(temporary) / "child.pid"
script = (
"import pathlib, subprocess, sys, time; "
"child=subprocess.Popen([sys.executable, '-c', "
"'import signal, time; "
"signal.signal(signal.SIGTERM, signal.SIG_IGN); "
"time.sleep(30)']); "
f"pathlib.Path({str(pid_file)!r}).write_text(str(child.pid)); "
"time.sleep(30)"
)
with self.assertRaises(subprocess.TimeoutExpired):
run_process_group(
[sys.executable, "-c", script],
cwd=Path(temporary),
env=os.environ.copy(),
timeout=0.5,
)
child_pid = int(pid_file.read_text())
deadline = time.monotonic() + 2
while time.monotonic() < deadline:
try:
os.kill(child_pid, 0)
except ProcessLookupError:
break
proc_stat = Path(f"/proc/{child_pid}/stat")
if proc_stat.exists() and proc_stat.read_text().split()[2] == "Z":
break
time.sleep(0.05)
else:
self.fail("descendant process remained running after timeout")
def test_timeout_with_output_returns_text(self):
script = "import time; print('started', flush=True); time.sleep(30)"
with self.assertRaises(subprocess.TimeoutExpired) as raised:
run_process_group(
[sys.executable, "-c", script],
cwd=Path.cwd(),
env=os.environ.copy(),
timeout=0.5,
)
self.assertIsInstance(raised.exception.output, str)
self.assertIn("started", raised.exception.output)
def test_execution_hook_marks_targeted_method_outcome(self):
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
hook = directory / "hook"
create_execution_hook(hook)
script = directory / "probe.py"
script.write_text(
"import unittest\n"
"class Probe(unittest.TestCase):\n"
" def test_pass(self):\n"
" pass\n"
" def test_fail(self):\n"
" self.fail('boom')\n"
"unittest.main(verbosity=2)\n"
)
for method, marker, return_code in (
("test_pass", "PASS", 0),
("test_fail", "FAIL", 1),
):
with self.subTest(method=method):
sentinel = f"sentinel-{method}"
environment = os.environ.copy()
environment["REGRESSION_POLICE_TEST_ID"] = (
f"__main__.Probe.{method}"
)
environment["REGRESSION_POLICE_SENTINEL"] = sentinel
environment["PYTHONPATH"] = str(hook)
result = run_process_group(
[sys.executable, script, f"Probe.{method}"],
cwd=directory,
env=environment,
timeout=5,
)
self.assertEqual(result.returncode, return_code)
self.assertIn(f"{sentinel}:{marker}", result.stdout)
@patch("run_differential.run_process_group")
def test_cleanup_rejects_unsafe_database_names(self, run_group):
with self.assertRaises(RuntimeError):
cleanup_databases(
"python",
["safe", "unsafe]; DROP DATABASE master"],
os.environ.copy(),
)
run_group.assert_not_called()
@patch("run_differential.run_process_group")
def test_cleanup_does_not_load_probe_hook(self, run_group):
run_group.return_value = subprocess.CompletedProcess([], 0, "")
environment = os.environ.copy()
environment.update(
{
"PYTHONPATH": "/untrusted/hook",
"REGRESSION_POLICE_TEST_ID": "test.id",
"REGRESSION_POLICE_SENTINEL": "sentinel",
}
)
cleanup_databases("python", ["test_rp_safe"], environment)
cleanup_environment = run_group.call_args.kwargs["env"]
self.assertNotIn("PYTHONPATH", cleanup_environment)
self.assertNotIn("REGRESSION_POLICE_TEST_ID", cleanup_environment)
self.assertNotIn("REGRESSION_POLICE_SENTINEL", cleanup_environment)
def test_setup_does_not_execute_checkout_package_metadata(self):
repo = Path(__file__).resolve().parents[3]
workflow_directory = repo / ".github" / "workflows"
setup_workflow = (workflow_directory / "copilot-setup-steps.yml").read_text()
self.assertNotIn('pip install -e ".[test]"', setup_workflow)
for workflow in workflow_directory.glob("*.yml"):
lines = workflow.read_text().splitlines()
for index, line in enumerate(lines):
if "uses: actions/checkout@" not in line:
continue
checkout_block = "\n".join(lines[index:index + 8])
self.assertIn(
"persist-credentials: false",
checkout_block,
workflow.name,
)
def test_successful_main_orchestration(self):
with tempfile.TemporaryDirectory() as temporary:
repo = Path(temporary) / "repo"
output = Path(temporary) / "output"
wheelhouse = Path(temporary) / "wheelhouse"
test_file = Path("testapp/tests/test_probe.py")
(repo / test_file).parent.mkdir(parents=True)
(repo / test_file).write_text("probe = True\n")
workflow = repo / ".github" / "workflows" / "test.yml"
workflow.parent.mkdir(parents=True)
workflow.write_text(
'- { django-spec: "django>=6.1,<6.2" }\n'
)
wheelhouse.mkdir()
head_sha = "1" * 40
base_sha = "2" * 40
def fake_git(_repo, *arguments):
if arguments == ("rev-parse", "--show-toplevel"):
return str(repo)
if arguments == ("rev-parse", "HEAD"):
return head_sha
if arguments[0] == "merge-base":
return base_sha
if arguments[0] == "diff":
return ""
if arguments[0] == "check-ref-format":
return ""
raise AssertionError(arguments)
def fake_subprocess(command, **kwargs):
if command[:3] == ["git", "worktree", "add"]:
Path(command[4]).mkdir(parents=True)
return subprocess.CompletedProcess(command, 0)
passing = {
"exit_code": 0,
"status": "pass",
"attempts": [],
"database_names": [],
"cleanup_error": None,
"hook_changed": False,
}
arguments = [
"run_differential.py",
"--base-ref",
"dev",
"--head-sha",
head_sha,
"--wheelhouse",
str(wheelhouse),
"--test-file",
str(test_file),
"--test-label",
"testapp.tests.test_probe.Probe.test_behavior",
"--output-dir",
str(output),
]
with (
patch("run_differential.git", side_effect=fake_git),
patch("run_differential.subprocess.run", side_effect=fake_subprocess),
patch("run_differential.source_fingerprint", return_value="stable"),
patch("run_differential.create_environment", return_value="python"),
patch("run_differential.verify_import_source"),
patch(
"run_differential.run_test",
side_effect=[passing.copy(), passing.copy()],
) as run_probe,
patch.object(sys, "argv", arguments),
):
return_code = main()
report = json.loads(next(output.glob("*.json")).read_text())
self.assertEqual(return_code, 0)
self.assertEqual(
report["verdict"],
"disproved-in-tested-configuration",
)
self.assertEqual(run_probe.call_count, 2)
base_call, head_call = run_probe.call_args_list
self.assertEqual(base_call.args[1].name, "base")
self.assertEqual(head_call.args[1].name, "head")
self.assertNotEqual(head_call.args[6], base_call.args[6])
def test_invalid_label_writes_inconclusive_report(self):
repo = Path(__file__).resolve().parents[3]
head_sha = subprocess.check_output(
["git", "rev-parse", "HEAD"],
cwd=repo,
text=True,
).strip()
with tempfile.TemporaryDirectory() as temporary:
result = subprocess.run(
[
sys.executable,
Path(__file__).resolve().with_name("run_differential.py"),
"--base-ref",
"dev",
"--head-sha",
head_sha,
"--test-file",
".github/skills/code-review/test_run_differential.py",
"--test-label",
"wrong.module.Test.test_method",
"--output-dir",
temporary,
],
cwd=repo,
check=False,
text=True,
capture_output=True,
)
report_path = next(Path(temporary).glob("*.json"))
report = json.loads(report_path.read_text())
self.assertEqual(result.returncode, 2)
self.assertEqual(report["verdict"], "inconclusive")
self.assertIn("ValueError", report["failure"])
if __name__ == "__main__":
unittest.main()
microsoft-mssql-django-171ed6e/.github/workflows/ 0000775 0000000 0000000 00000000000 15253215657 0022142 5 ustar 00root root 0000000 0000000 microsoft-mssql-django-171ed6e/.github/workflows/codeql-analysis.yml 0000664 0000000 0000000 00000005273 15253215657 0025764 0 ustar 00root root 0000000 0000000 # For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
# This workflow is triggered on pushes to the dev branch, pull requests targeting
permissions:
actions: read
contents: read
security-events: write
on:
push:
branches: [ dev ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ dev ]
schedule:
- cron: '40 13 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
microsoft-mssql-django-171ed6e/.github/workflows/copilot-setup-steps.yml 0000664 0000000 0000000 00000010741 15253215657 0026633 0 ustar 00root root 0000000 0000000 name: Copilot Setup Steps
on:
workflow_dispatch:
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
- .github/workflows/test.yml
- ".github/skills/code-review/**"
permissions: {}
jobs:
# GitHub requires this job name for Copilot code review setup.
copilot-setup-steps:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
services:
sqlserver:
image: mcr.microsoft.com/mssql/server:2025-latest
env:
ACCEPT_EULA: "Y"
# Throwaway password for the ephemeral test container. Keep this in sync with
# the MSSQL_PASSWORD exported by Configure review environment below.
MSSQL_SA_PASSWORD: "MyPassword42"
ports:
- 1433:1433
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
cache-dependency-path: setup.py
- name: Install ODBC Driver 18
run: |
set -euo pipefail
curl -sSL -O "https://packages.microsoft.com/config/ubuntu/$(grep VERSION_ID /etc/os-release | cut -d '"' -f 2)/packages-microsoft-prod.deb"
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb
# apt on the hosted runner occasionally holds the dpkg lock; retry a few times.
for i in 1 2 3 4 5; do
sudo apt-get update && break
echo "apt-get update failed (attempt $i), retrying in 10s..."
sleep 10
done
sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev
- name: Configure review environment
run: |
{
echo "MSSQL_PASSWORD=MyPassword42"
echo "MSSQL_HOST=127.0.0.1"
echo "MSSQL_DRIVER=ODBC Driver 18 for SQL Server"
echo "MSSQL_EXTRA_PARAMS=TrustServerCertificate=yes"
} >> "$GITHUB_ENV"
- name: Install test dependencies
run: |
python -m pip install --upgrade pip wheel setuptools
wheelhouse="$RUNNER_TEMP/regression-police-wheelhouse"
mkdir -p "$wheelhouse"
python -m pip download --dest "$wheelhouse" \
setuptools wheel pyodbc pytz "unittest-xml-reporting>=3.2.0"
mapfile -t django_specs < <(
python - <<'PY'
import importlib.util
from pathlib import Path
path = Path(".github/skills/code-review/run_differential.py")
spec = importlib.util.spec_from_file_location("run_differential", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
print(*module.supported_django(Path.cwd()), sep="\n")
PY
)
for django_spec in "${django_specs[@]}"
do
python -m pip download --dest "$wheelhouse" "$django_spec"
done
echo "REGRESSION_POLICE_WHEELHOUSE=$wheelhouse" >> "$GITHUB_ENV"
python -m pip install --no-index --find-links "$wheelhouse" \
pyodbc pytz "unittest-xml-reporting>=3.2.0" \
"${django_specs[-1]}"
- name: Validate Regression Police harness
run: |
cd .github/skills/code-review
python test_run_differential.py
- name: Wait for SQL Server
run: |
set -euo pipefail
for i in $(seq 1 30); do
if python - <<'PY'
import os
import pyodbc
try:
connection = pyodbc.connect(
f"DRIVER={{{os.environ['MSSQL_DRIVER']}}};"
f"SERVER={os.environ['MSSQL_HOST']},1433;"
f"UID=sa;PWD={os.environ['MSSQL_PASSWORD']};"
f"DATABASE=master;{os.environ['MSSQL_EXTRA_PARAMS']}",
timeout=5,
)
connection.close()
except pyodbc.Error as error:
print(error)
raise SystemExit(1)
PY
then
echo "SQL Server is ready."
exit 0
fi
echo "SQL Server is not ready (attempt $i/30)."
sleep 5
done
echo "SQL Server did not become ready in time."
exit 1
microsoft-mssql-django-171ed6e/.github/workflows/devskim.yml 0000664 0000000 0000000 00000001752 15253215657 0024334 0 ustar 00root root 0000000 0000000 # This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: DevSkim
on:
push:
branches: [ dev, master ]
pull_request:
branches: [ dev ]
schedule:
- cron: '29 14 * * 3'
jobs:
lint:
name: DevSkim
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run DevSkim scanner
uses: microsoft/DevSkim-Action@4b5047945a44163b94642a1cecc0d93a3f428cc6 # v1.0.16
- name: Upload DevSkim scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with:
sarif_file: devskim-results.sarif
microsoft-mssql-django-171ed6e/.github/workflows/forked-pr-coverage.yml 0000664 0000000 0000000 00000007163 15253215657 0026356 0 ustar 00root root 0000000 0000000 name: Post Coverage Comment (Forked PRs)
# This workflow handles posting coverage comments for FORKED PRs.
#
# Why a separate workflow?
# - Forked PRs have restricted GITHUB_TOKEN permissions for security
# - They cannot write comments directly to the base repository's PRs
# - workflow_run triggers run in the BASE repository context with full permissions
# - This allows us to safely post comments on forked PRs
#
# Same-repo PRs post comments directly in pr-code-coverage.yml (faster)
# Forked PRs use this workflow (required for permissions)
on:
workflow_run:
workflows: ["PR Code Coverage"]
types:
- completed
jobs:
post-comment:
runs-on: ubuntu-latest
if: >
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout repo
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download coverage data
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if ! gh run download ${{ github.event.workflow_run.id }} \
--repo ${{ github.repository }} \
--name coverage-comment-data 2>&1; then
echo "⚠️ No coverage-comment-data artifact found"
echo "This is expected for same-repo PRs (they post comments directly)"
exit 0
fi
if [[ ! -f pr-info.json ]]; then
echo "⚠️ Artifact downloaded but pr-info.json not found"
exit 1
fi
- name: Read coverage data
id: coverage
run: |
if [[ ! -f pr-info.json ]]; then
echo "❌ pr-info.json not found"
exit 1
fi
cat pr-info.json
PR_NUMBER="$(jq -r '.pr_number' pr-info.json)"
COVERAGE_PCT="$(jq -r '.coverage_percentage' pr-info.json)"
COVERED_LINES="$(jq -r '.covered_lines' pr-info.json)"
TOTAL_LINES="$(jq -r '.total_lines' pr-info.json)"
PATCH_PCT="$(jq -r '.patch_coverage_pct' pr-info.json)"
LOW_COV_FILES="$(jq -r '.low_coverage_files' pr-info.json)"
PATCH_SUMMARY="$(jq -r '.patch_coverage_summary' pr-info.json)"
ADO_URL="$(jq -r '.ado_url' pr-info.json)"
echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_ENV
echo "COVERAGE_PERCENTAGE=${COVERAGE_PCT}" >> $GITHUB_ENV
echo "COVERED_LINES=${COVERED_LINES}" >> $GITHUB_ENV
echo "TOTAL_LINES=${TOTAL_LINES}" >> $GITHUB_ENV
echo "PATCH_COVERAGE_PCT=${PATCH_PCT}" >> $GITHUB_ENV
echo "ADO_URL=${ADO_URL}" >> $GITHUB_ENV
DELIM1="DELIM_$(uuidgen | tr -d '-')"
DELIM2="DELIM_$(uuidgen | tr -d '-')"
{
echo "LOW_COVERAGE_FILES<<${DELIM1}"
echo "$LOW_COV_FILES"
echo "${DELIM1}"
} >> $GITHUB_ENV
{
echo "PATCH_COVERAGE_SUMMARY<<${DELIM2}"
echo "$PATCH_SUMMARY"
echo "${DELIM2}"
} >> $GITHUB_ENV
- name: Comment coverage summary on PR
uses: ./.github/actions/post-coverage-comment
with:
pr_number: ${{ env.PR_NUMBER }}
coverage_percentage: ${{ env.COVERAGE_PERCENTAGE }}
covered_lines: ${{ env.COVERED_LINES }}
total_lines: ${{ env.TOTAL_LINES }}
patch_coverage_pct: ${{ env.PATCH_COVERAGE_PCT }}
low_coverage_files: ${{ env.LOW_COVERAGE_FILES }}
patch_coverage_summary: ${{ env.PATCH_COVERAGE_SUMMARY }}
ado_url: ${{ env.ADO_URL }}
microsoft-mssql-django-171ed6e/.github/workflows/pr-code-coverage.yml 0000664 0000000 0000000 00000024375 15253215657 0026022 0 ustar 00root root 0000000 0000000 name: PR Code Coverage
on:
pull_request:
branches:
- dev
jobs:
coverage-report:
runs-on: ubuntu-latest
# The ADO build (definition 2195) that publishes the coverage artifact takes
# ~1 hour. The poll windows below wait up to ~65 min for it; bound the whole
# job at 90 min (well under GitHub's 6-hour hosted-runner job execution cap).
timeout-minutes: 90
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout repo
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Setup git for diff-cover
run: |
git fetch origin dev:dev
echo "Available branches:"
git branch -a
git show-ref --verify refs/heads/dev || echo "Warning: dev branch not found"
- name: Wait for ADO build to start
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
PR_SHA=${{ github.event.pull_request.head.sha }}
API_URL="https://dev.azure.com/sqlclientdrivers/public/_apis/build/builds?definitions=2195&queryOrder=queueTimeDescending&%24top=30&api-version=7.1-preview.7"
echo "Waiting for Azure DevOps build to start for PR #$PR_NUMBER (SHA: $PR_SHA)..."
for i in {1..30}; do
echo "Attempt $i/30: Checking if build has started..."
API_RESPONSE=$(curl --fail --silent --show-error "$API_URL")
if ! echo "$API_RESPONSE" | jq . >/dev/null 2>&1; then
echo "❌ Invalid JSON response from Azure DevOps API"
echo "Response received: $API_RESPONSE"
exit 1
fi
# Match on both PR number and source SHA for accuracy
BUILD_INFO=$(echo "$API_RESPONSE" | jq -c --arg PR "$PR_NUMBER" --arg SHA "$PR_SHA" \
'[.value[]? | select(.triggerInfo["pr.number"]?==$PR and (.triggerInfo["pr.sourceSha"]?==$SHA or .sourceVersion?==$SHA))] | .[0] // empty' 2>/dev/null)
# Fall back to PR number only if SHA match fails (e.g. ADO hasn't updated triggerInfo yet)
if [[ -z "$BUILD_INFO" || "$BUILD_INFO" == "null" || "$BUILD_INFO" == "empty" ]]; then
BUILD_INFO=$(echo "$API_RESPONSE" | jq -c --arg PR "$PR_NUMBER" \
'[.value[]? | select(.triggerInfo["pr.number"]?==$PR)] | sort_by(.id) | reverse | .[0] // empty' 2>/dev/null)
fi
if [[ -n "$BUILD_INFO" && "$BUILD_INFO" != "null" && "$BUILD_INFO" != "empty" ]]; then
STATUS=$(echo "$BUILD_INFO" | jq -r '.status // "unknown"')
RESULT=$(echo "$BUILD_INFO" | jq -r '.result // "unknown"')
BUILD_ID=$(echo "$BUILD_INFO" | jq -r '.id // "unknown"')
WEB_URL=$(echo "$BUILD_INFO" | jq -r '._links.web.href // "unknown"')
echo "✅ Found build: ID=$BUILD_ID, Status=$STATUS, Result=$RESULT"
echo "🔗 Build URL: $WEB_URL"
echo "ADO_URL=$WEB_URL" >> $GITHUB_ENV
echo "BUILD_ID=$BUILD_ID" >> $GITHUB_ENV
if [[ "$STATUS" == "completed" && "$RESULT" == "failed" ]]; then
echo "❌ Azure DevOps build $BUILD_ID failed early"
exit 1
fi
echo "🚀 Build has started, proceeding to poll for coverage artifacts..."
break
else
echo "⏳ No build found for PR #$PR_NUMBER yet... (attempt $i/30)"
fi
if [[ $i -eq 30 ]]; then
echo "❌ Timeout: No build found for PR #$PR_NUMBER after 30 attempts"
exit 1
fi
sleep 10
done
- name: Download and parse coverage report
run: |
BUILD_ID=${{ env.BUILD_ID }}
ARTIFACTS_URL="https://dev.azure.com/sqlclientdrivers/public/_apis/build/builds/$BUILD_ID/artifacts?api-version=7.1-preview.5"
echo "📥 Polling for coverage artifacts for build $BUILD_ID..."
COVERAGE_ARTIFACT=""
# The ADO build takes ~1 hour to publish the coverage artifact, so poll
# up to ~65 min (130 * 30s) instead of giving up after 30 min.
for i in {1..130}; do
echo "Attempt $i/130: Checking for coverage artifacts..."
ARTIFACTS_RESPONSE=$(curl --fail --silent --show-error "$ARTIFACTS_URL")
if ! echo "$ARTIFACTS_RESPONSE" | jq . >/dev/null 2>&1; then
echo "⚠️ Invalid JSON response from artifacts API (attempt $i/130)"
if [[ $i -eq 130 ]]; then
echo "❌ Persistent API issues after 130 attempts"
exit 1
fi
sleep 30
continue
fi
echo "🔍 Available artifacts:"
echo "$ARTIFACTS_RESPONSE" | jq -r '.value[]?.name // "No artifacts found"'
COVERAGE_ARTIFACT=$(echo "$ARTIFACTS_RESPONSE" | jq -r '.value[]? | select(.name | test("Code Coverage Report")) | .resource.downloadUrl // empty' 2>/dev/null)
if [[ -n "$COVERAGE_ARTIFACT" && "$COVERAGE_ARTIFACT" != "null" && "$COVERAGE_ARTIFACT" != "empty" ]]; then
echo "✅ Found coverage artifact on attempt $i!"
break
else
echo "⏳ Coverage report not ready yet (attempt $i/130)..."
if [[ $i -eq 130 ]]; then
echo "❌ Timeout: Coverage report artifact not found after 130 attempts"
exit 1
fi
sleep 30
fi
done
echo "📊 Downloading coverage report..."
if ! curl -L "$COVERAGE_ARTIFACT" -o coverage-report.zip --fail --silent; then
echo "❌ Failed to download coverage report from Azure DevOps"
exit 1
fi
if ! unzip -o -q coverage-report.zip; then
echo "❌ Failed to extract coverage artifacts"
exit 1
fi
# Find coverage.xml (Cobertura format from coverage.py)
COVERAGE_FILE=$(find . -name "coverage.xml" -path "*/Code Coverage Report*" | head -1)
if [[ -z "$COVERAGE_FILE" ]]; then
COVERAGE_FILE=$(find . -name "*coverage*.xml" -type f | head -1)
fi
if [[ -z "$COVERAGE_FILE" ]]; then
echo "❌ No coverage XML file found in artifacts"
find . -type f | head -20
exit 1
fi
echo "✅ Found coverage file: $COVERAGE_FILE"
cp "$COVERAGE_FILE" ./coverage.xml
# Parse Cobertura XML directly (much simpler than HTML parsing)
python3 .github/scripts/parse_coverage.py
echo "✅ Coverage data extracted successfully"
- name: Generate patch coverage report
run: |
pip install diff-cover
if [[ ! -f coverage.xml ]]; then
echo "❌ coverage.xml not found"
exit 1
fi
echo "✅ coverage.xml found, size: $(wc -c < coverage.xml) bytes"
# Generate diff coverage against dev branch
diff-cover coverage.xml \
--compare-branch=dev \
--json-report patch-coverage.json \
--markdown-report patch-coverage.md || {
echo "⚠️ diff-cover failed, continuing with empty patch coverage"
}
if [[ -f patch-coverage.json ]]; then
PATCH_COVERAGE=$(jq -r '.total_percent_covered // "N/A"' patch-coverage.json)
echo "✅ Patch coverage: ${PATCH_COVERAGE}%"
echo "PATCH_COVERAGE_PCT=${PATCH_COVERAGE}%" >> $GITHUB_ENV
else
echo "PATCH_COVERAGE_PCT=N/A" >> $GITHUB_ENV
fi
if [[ -f patch-coverage.md ]]; then
DELIM="DELIM_$(uuidgen | tr -d '-')"
{
echo "PATCH_COVERAGE_SUMMARY<<${DELIM}"
cat patch-coverage.md
echo "${DELIM}"
} >> $GITHUB_ENV
else
echo "PATCH_COVERAGE_SUMMARY=Patch coverage report could not be generated." >> $GITHUB_ENV
fi
- name: Save coverage data for comment
run: |
mkdir -p coverage-comment-data
jq -n \
--arg pr_number "${{ github.event.pull_request.number }}" \
--arg coverage_percentage "${{ env.COVERAGE_PERCENTAGE }}" \
--arg covered_lines "${{ env.COVERED_LINES }}" \
--arg total_lines "${{ env.TOTAL_LINES }}" \
--arg patch_coverage_pct "${{ env.PATCH_COVERAGE_PCT }}" \
--arg low_coverage_files "$LOW_COVERAGE_FILES" \
--arg patch_coverage_summary "$PATCH_COVERAGE_SUMMARY" \
--arg ado_url "${{ env.ADO_URL }}" \
'{
pr_number: $pr_number,
coverage_percentage: $coverage_percentage,
covered_lines: $covered_lines,
total_lines: $total_lines,
patch_coverage_pct: $patch_coverage_pct,
low_coverage_files: $low_coverage_files,
patch_coverage_summary: $patch_coverage_summary,
ado_url: $ado_url
}' > coverage-comment-data/pr-info.json
echo "✅ JSON validation:"
jq . coverage-comment-data/pr-info.json > /dev/null || {
echo "❌ Invalid JSON generated"
cat coverage-comment-data/pr-info.json
exit 1
}
cat coverage-comment-data/pr-info.json
- name: Upload coverage comment data
if: github.event.pull_request.head.repo.full_name != github.repository
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-comment-data
path: coverage-comment-data/
retention-days: 7
- name: Comment coverage summary on PR
if: github.event.pull_request.head.repo.full_name == github.repository
uses: ./.github/actions/post-coverage-comment
with:
pr_number: ${{ github.event.pull_request.number }}
coverage_percentage: ${{ env.COVERAGE_PERCENTAGE }}
covered_lines: ${{ env.COVERED_LINES }}
total_lines: ${{ env.TOTAL_LINES }}
patch_coverage_pct: ${{ env.PATCH_COVERAGE_PCT }}
low_coverage_files: ${{ env.LOW_COVERAGE_FILES }}
patch_coverage_summary: ${{ env.PATCH_COVERAGE_SUMMARY }}
ado_url: ${{ env.ADO_URL }}
microsoft-mssql-django-171ed6e/.github/workflows/test.yml 0000664 0000000 0000000 00000014315 15253215657 0023650 0 ustar 00root root 0000000 0000000 # Runs the fast `testapp` suite against a SQL Server 2025 service container on every PR
# to `dev`. This is the GitHub-Actions half of the dual-run CI (the Azure DevOps pipeline
# keeps the full tox matrix, including the ~6600-test upstream Django suite via test.sh).
# The goal is an executable green/red signal on the backend so PRs are validated against a
# real database, not just reasoned about statically.
#
# Scope is deliberately narrow: only `manage.py test testapp` (~200 tests, a couple of
# minutes), never `test.sh`. The heavy Django-suite coverage stays on ADO during the
# dual-run period.
name: Test
on:
push:
branches:
- dev
pull_request:
branches:
- dev
workflow_dispatch:
concurrency:
group: test-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
regression-police:
name: Regression Police harness
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Validate Regression Police harness
working-directory: .github/skills/code-review
run: python test_run_differential.py
testapp:
name: Py ${{ matrix.stack.python }} / Django ${{ matrix.stack.django }} / ${{ matrix.driver }}
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
# A representative sample of the SUPPORTED support tier, not the full matrix
# (that lives on ADO during the dual-run). Every currently-supported Django minor
# is covered, each on its lowest supported Python and on the newest Python it runs
# on, so the Python floor/ceiling of each Django is exercised. EOL Django versions
# (<= 5.1) are excluded from both this workflow and the ADO matrix.
matrix:
driver: [pyodbc, mssql_python]
stack:
- { python: "3.10", django: "5.2", django-spec: "django>=5.2,<5.3" }
- { python: "3.13", django: "5.2", django-spec: "django>=5.2,<5.3" }
- { python: "3.12", django: "6.0", django-spec: "django>=6.0,<6.1" }
- { python: "3.14", django: "6.0", django-spec: "django>=6.0,<6.1" }
- { python: "3.12", django: "6.1", django-spec: "django>=6.1,<6.2" }
- { python: "3.14", django: "6.1", django-spec: "django>=6.1,<6.2" }
services:
sqlserver:
image: mcr.microsoft.com/mssql/server:2025-latest
env:
ACCEPT_EULA: "Y"
# Throwaway password for an ephemeral, per-run test container. Not a secret, so it
# is hardcoded rather than pulled from repo secrets; that keeps the workflow working
# for pull requests from forks. Kept in sync with the job-level MSSQL_PASSWORD below.
MSSQL_SA_PASSWORD: "MyPassword42"
ports:
- 1433:1433
env:
MSSQL_PYTHON_DRIVER: ${{ matrix.driver }}
MSSQL_EXPECTED_NATIVE_DRIVER: msodbcsql18
PYTHONUNBUFFERED: "1"
MSSQL_PASSWORD: "MyPassword42"
MSSQL_HOST: "127.0.0.1"
# Driver 18 encrypts by default. Trust the ephemeral test container's self-signed
# certificate while keeping the connection encrypted.
MSSQL_DRIVER: "ODBC Driver 18 for SQL Server"
MSSQL_EXTRA_PARAMS: "TrustServerCertificate=yes"
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Python ${{ matrix.stack.python }}
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.stack.python }}
cache: pip
cache-dependency-path: setup.py
- name: Install ODBC Driver 18
run: |
set -euo pipefail
curl -sSL -O "https://packages.microsoft.com/config/ubuntu/$(grep VERSION_ID /etc/os-release | cut -d '"' -f 2)/packages-microsoft-prod.deb"
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb
# apt on the hosted runner occasionally holds the dpkg lock; retry a few times.
for i in 1 2 3 4 5; do
sudo apt-get update && break
echo "apt-get update failed (attempt $i), retrying in 10s..."
sleep 10
done
sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev
- name: Install mssql-django
run: |
python -m pip install --upgrade pip wheel setuptools
pip install -e ".[test]" "${{ matrix.stack.django-spec }}"
- name: Pin the released driver
if: matrix.driver == 'mssql_python'
run: python -m pip install "mssql-python==1.15.0"
- name: Wait for SQL Server
run: |
set -euo pipefail
for i in $(seq 1 30); do
if python - <<'PY'
import os, sys
import pyodbc
try:
driver = os.environ["MSSQL_DRIVER"]
host = os.environ["MSSQL_HOST"]
port = os.environ.get("MSSQL_PORT", "1433")
user = os.environ.get("MSSQL_USER", "sa")
cn = pyodbc.connect(
f"DRIVER={{{driver}}};SERVER={host},{port};UID={user};"
"PWD=" + os.environ["MSSQL_PASSWORD"] + ";DATABASE=master;"
+ os.environ["MSSQL_EXTRA_PARAMS"],
timeout=5,
)
cn.close()
except Exception as exc: # noqa: BLE001
print(exc, file=sys.stderr)
sys.exit(1)
PY
then
echo "SQL Server is accepting authenticated connections (attempt $i)."
exit 0
fi
echo "Attempt $i/30: SQL Server not ready yet, waiting 5s..."
sleep 5
done
echo "SQL Server did not become ready in time."
exit 1
- name: Check selected database driver
run: python -m testapp.check_driver
- name: Run testapp suite
run: python manage.py test testapp --noinput -v 2
microsoft-mssql-django-171ed6e/.gitignore 0000664 0000000 0000000 00000000211 15253215657 0020527 0 ustar 00root root 0000000 0000000 *.py[co]
*.sw[a-z]
*.orig
*~
.DS_Store
Thumbs.db
*.egg-info
*.dll
tests/local_settings.py
# Virtual Env
/venv/
.idea/
logs/
result.xml
microsoft-mssql-django-171ed6e/AGENTS.md 0000664 0000000 0000000 00000012713 15253215657 0020054 0 ustar 00root root 0000000 0000000 # AGENTS.md — mssql-django
Repository context for automated agents and code review. This file holds durable
project facts and review judgment. SQL Server limitations, the `as_microsoft`
extension pattern, and the `EXCLUDED_TESTS` mechanism are documented in
`.github/copilot-instructions.md`; this file does not repeat them.
## What this project is
`mssql-django` is a Django database backend for Microsoft SQL Server. It translates
Django's database abstraction layer into T-SQL. The backend lives entirely under
`mssql/`; `testapp/` is the test project we own.
## Project facts
- **Default branch:** `dev` (not `main`). Open pull requests against `dev`.
- **Version and support policy:** `setup.py` declares the package version and supported
Python/Django ranges (`install_requires`, classifiers, and `python_requires` when
present). `azure-pipelines.yml` defines the executed CI matrix, `tox.ini` defines its
environment dependencies and commands, and `README.md` documents supported database
platforms. Treat those files as the source of truth rather than duplicating ranges here.
- **Retired-version code:** compatibility branches below the currently declared support
floor can remain in the distributed package, but those runtimes are no longer tested or
declared supported. Treat changes that add logic to or depend on those branches as work
on an untested path; steer the fix onto a supported branch instead of asking contributors
to maintain retired versions.
- **Tests:** `python manage.py test testapp --noinput` runs the suite we own. A live
SQL Server is required; connection settings come from environment variables read in
`testapp/settings.py` (`MSSQL_HOST`, `MSSQL_PASSWORD`, ...). The full upstream Django
suite is driven by `test.sh` (clones Django, runs `runtests.py` against SQL Server).
## Module map (`mssql/`)
| File | Responsibility |
|------|----------------|
| `schema.py` | Schema editor; `_alter_field` constraint drop/recreate. Largest, most sensitive file. |
| `compiler.py` | SQL query compilation: pagination (OFFSET/FETCH, TOP), ORDER BY handling. |
| `operations.py` | SQL Server operations: quoting, parameter/type handling, datetime adaptation. |
| `functions.py` | Function overrides via the `as_microsoft` pattern; parameter-limit splitting. |
| `base.py` | `DatabaseWrapper`, connection-string construction, cursor handling. |
| `features.py` | Capability flags (`supports_*`) declaring what SQL Server can and can't do. |
| `introspection.py` | Type-code → Django field mapping; table/relation introspection. |
| `creation.py` | Test-database creation and teardown. |
| `client.py` | Database shell command selection (`sqlcmd` / `isql`). |
Direct DBAPI imports and type-code coupling are concentrated in `base.py` and
`introspection.py`. Other modules may legitimately branch on backend capabilities such as
MARS or database-shell selection. Review SQL and ORM behavior independently of the Python
driver, and flag new direct DBAPI coupling outside the adapter boundary unless it is
explicitly justified.
## Commit and PR conventions
- **Title prefix (uppercase, colon):** `FEAT:`, `FIX:`, `CHORE:`, `DOCS:`, `REFACTOR:`,
`RELEASE:`, `AI:` (AI-initiative changes).
- Reference issues in the body: `Closes #` or `Issue: #`.
- **Every `FIX:` and `FEAT:` ships a regression test in the same PR**, added under
`testapp/tests/`. For a `FIX:`, the test must fail on `dev` without the change and
pass with it, asserting the exact trigger combination from the issue. The only
exception is a change that genuinely cannot be covered from `testapp` (e.g. a path
exercised only under `USE_TZ=True`, which `testapp` does not run) — state that
explicitly in the PR body.
## History rules
- No force-push (not even `--force-with-lease`) and no rebase of already-pushed
branches. Resolve conflicts with `git merge origin/dev`, then a normal push.
## Review judgment (apply across all files)
- **Fix the root cause, not the symptom.** Solve problems at Django's structured
expression/node level (e.g. override `get_order_by()`, use the `as_microsoft`
pattern). Do not post-process compiled SQL strings with regex or `str.format`.
- **Fixing one input shape is not fixing the bug class.** If a change narrows a crash
for one input, check whether the same failure mode survives on adjacent inputs. A
narrow fix can be worth shipping, but call it out as narrow.
- **Classify a change by its blast radius, not its label.** A PR labeled `FIX:` that
reroutes a hot-path helper (e.g. every identifier through a new parser) or changes a
default is an enhancement with backward-compatibility implications. Review it as such.
- **Silent-wrong-results outrank crashes.** A change that can return incorrect rows with
no error deserves more scrutiny than one that raises — the crash gets reported, the
wrong `filter()` ships into someone's business logic.
- **Migrating off a deprecated Django API requires a whole-file sweep.** A deprecated
call often still works (warns on the current Django, breaks on a future major), so CI
and correctness tests stay green while a missed second call site lingers. Grep the
full symbol across the file, not just the path the failing test walked.
- **Test-exclusion comments are documentation.** State whether an exclusion is a
permanent platform limitation or an unimplemented capability — a wrong reason there
becomes received wisdom for years. `@expectedFailure` / `@skip` documents a known
failure; it does not mean the bug is fixed.
microsoft-mssql-django-171ed6e/CHANGELOG.md 0000664 0000000 0000000 00000014530 15253215657 0020361 0 ustar 00root root 0000000 0000000 # Changelog
All notable user-facing changes to mssql-django are documented in this file.
## [Unreleased]
## [2.0.0] - 2026-09-18
### Added
- Added per-connection opt-in support for mssql-python while retaining pyodbc
as the default driver ([#596]).
### Changed
- Made mssql-python 1.15.0 or newer a required dependency while retaining
pyodbc as the default database driver ([#599]).
- Narrowed declared support to CPython 3.10-3.14, Django 5.2-6.1,
supported mssql-python platforms, and SQL Server 2017-2025 ([#598],
[#605]).
- Replaced pytz with standard-library zoneinfo and tzdata, stabilizing named
timezone offsets across the year ([#535]).
- Honored explicit `MARS_Connection` values for Microsoft Fabric Warehouse
connections and buffered ORM iteration when MARS is disabled ([#600]).
- Allowed future SQL Server releases to use the latest capabilities known to
the backend instead of failing version validation ([#609]).
### Fixed
- Defaulted an omitted `HOST` to `localhost` for mssql-python connections,
matching pyodbc's local-server behavior ([#613]).
- Escaped SQL Server `[` wildcards in pattern lookups that use `F()`
expressions ([#575]).
- Escaped single quotes in `inspectdb --schema` metadata queries ([#583]).
## [1.8.0] - 2026-08-07
### Added
- Added Django 6.1 support ([#553], [#554], [#555], [#556], [#563], [#564]).
- Added Django 6.1 foreign-key introspection support, including the database
`ON DELETE` rule ([#556]).
### Changed
- Declared database-level referential actions and bitwise aggregates
unsupported on Django 6.1 so they fail explicitly instead of producing
incorrect behavior ([#553], [#554]).
- Updated query compilation for Django 6.1's identifier quoting API ([#555],
[#563]).
## [1.7.4] - 2026-07-24
### Fixed
- Preserved escaped `%%` literals in raw and annotated `GROUP BY` queries
([#537]).
- Supported `IntegerChoices` parameters in raw `GROUP BY` queries ([#541]).
## [1.7.3] - 2026-06-22
### Fixed
- Fixed `KeyError` when connecting through a `DatabaseWrapper` subclass
([#532]).
- Stopped adding `Trusted_Connection` or SSPI when an explicit
`Authentication=` mode is configured ([#533]).
## [1.7.2] - 2026-05-22
### Fixed
- Fixed `.explain()` compatibility with Django 4.0 and newer ([#524]).
- Preserved timezone offsets from `DATETIMEOFFSET` values and used
`SYSDATETIMEOFFSET()` for `Now()` when `USE_TZ=True` ([#484]).
## [1.7.1] - 2026-04-24
### Added
- Added Microsoft Fabric SQL Database support for `EngineEdition=12`
([#518]).
### Fixed
- Fixed `AlterField` migrations for descending fields in `Meta.indexes`
([#519]).
## [1.7.0] - 2026-03-06
### Added
- Added Django 6.0 and Python 3.14 support ([#488], [#506], [#507], [#508],
[#509], [#511], [#512]).
- Added SQL Server 2025 support ([#489]).
- Added Django 5.2 composite primary key support for ordering, bulk updates,
tuple lookups, and JSON expressions ([#462], [#465], [#501], [#504]).
### Changed
- Made Microsoft ODBC Driver 18 the default, with automatic fallback to
Driver 17 ([#493]).
- Improved quoting for identifiers and aliases containing periods ([#490]).
### Fixed
- Restored `Meta.indexes` after field alterations ([#498]).
- Fixed JSON lookup paths and ordering ([#506], [#509]).
- Fixed batching and `ORDER BY` compatibility ([#507]).
- Fixed bulk inserts and returned rows for fields with `db_default` ([#508],
[#512]).
- Fixed ordered `StringAgg`, including `OuterRef` handling ([#511]).
- Excluded `testapp` from distributed packages ([#503]).
[Unreleased]: https://github.com/microsoft/mssql-django/compare/2.0.0...dev
[2.0.0]: https://github.com/microsoft/mssql-django/compare/1.8.0...2.0.0
[1.8.0]: https://github.com/microsoft/mssql-django/releases/tag/1.8.0
[1.7.4]: https://github.com/microsoft/mssql-django/releases/tag/1.7.4
[1.7.3]: https://github.com/microsoft/mssql-django/releases/tag/1.7.3
[1.7.2]: https://github.com/microsoft/mssql-django/releases/tag/1.7.2
[1.7.1]: https://github.com/microsoft/mssql-django/releases/tag/1.7.1
[1.7.0]: https://github.com/microsoft/mssql-django/releases/tag/1.7
[#462]: https://github.com/microsoft/mssql-django/pull/462
[#465]: https://github.com/microsoft/mssql-django/pull/465
[#484]: https://github.com/microsoft/mssql-django/pull/484
[#488]: https://github.com/microsoft/mssql-django/pull/488
[#489]: https://github.com/microsoft/mssql-django/pull/489
[#490]: https://github.com/microsoft/mssql-django/pull/490
[#493]: https://github.com/microsoft/mssql-django/pull/493
[#498]: https://github.com/microsoft/mssql-django/pull/498
[#501]: https://github.com/microsoft/mssql-django/pull/501
[#503]: https://github.com/microsoft/mssql-django/pull/503
[#504]: https://github.com/microsoft/mssql-django/pull/504
[#506]: https://github.com/microsoft/mssql-django/pull/506
[#507]: https://github.com/microsoft/mssql-django/pull/507
[#508]: https://github.com/microsoft/mssql-django/pull/508
[#509]: https://github.com/microsoft/mssql-django/pull/509
[#511]: https://github.com/microsoft/mssql-django/pull/511
[#512]: https://github.com/microsoft/mssql-django/pull/512
[#518]: https://github.com/microsoft/mssql-django/pull/518
[#519]: https://github.com/microsoft/mssql-django/pull/519
[#524]: https://github.com/microsoft/mssql-django/pull/524
[#532]: https://github.com/microsoft/mssql-django/pull/532
[#533]: https://github.com/microsoft/mssql-django/pull/533
[#535]: https://github.com/microsoft/mssql-django/pull/535
[#537]: https://github.com/microsoft/mssql-django/pull/537
[#541]: https://github.com/microsoft/mssql-django/pull/541
[#553]: https://github.com/microsoft/mssql-django/pull/553
[#554]: https://github.com/microsoft/mssql-django/pull/554
[#555]: https://github.com/microsoft/mssql-django/pull/555
[#556]: https://github.com/microsoft/mssql-django/pull/556
[#563]: https://github.com/microsoft/mssql-django/pull/563
[#564]: https://github.com/microsoft/mssql-django/pull/564
[#575]: https://github.com/microsoft/mssql-django/pull/575
[#583]: https://github.com/microsoft/mssql-django/pull/583
[#596]: https://github.com/microsoft/mssql-django/pull/596
[#598]: https://github.com/microsoft/mssql-django/pull/598
[#599]: https://github.com/microsoft/mssql-django/pull/599
[#600]: https://github.com/microsoft/mssql-django/pull/600
[#605]: https://github.com/microsoft/mssql-django/pull/605
[#609]: https://github.com/microsoft/mssql-django/pull/609
[#613]: https://github.com/microsoft/mssql-django/pull/613
microsoft-mssql-django-171ed6e/CODE_OF_CONDUCT.md 0000664 0000000 0000000 00000000673 15253215657 0021352 0 ustar 00root root 0000000 0000000 # Microsoft Open Source Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
Resources:
- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns microsoft-mssql-django-171ed6e/CONTRIBUTING.md 0000664 0000000 0000000 00000003725 15253215657 0021005 0 ustar 00root root 0000000 0000000 # Contributing
## How to contribute
### Run unit tests
After changes made to the project, it's a good idea to run the unit tests before making a pull request.
1. **Run SQL Server**
Make sure you have SQL Server running in your local machine.
Download and install SQL Server [here](https://www.microsoft.com/en-us/sql-server/sql-server-downloads), or you could use docker. Change `testapp/settings.py` to match your SQL Server login username and password.
```
docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Placeholder' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2019-latest
```
2. **Clone Django**
In `mssql-django` folder.
```
# Install your local mssql-django
pip install -e .
# The unit test suite are in `Django` folder, so we need to clone it
git clone https://github.com/django/django.git --depth 1
```
3. **Install Tox**
```
# we use `tox` to run tests and install dependencies
pip install tox
```
4. **Run Tox**
```
# eg. run django 3.1 tests with Python 3.7
tox -e py37-django31
```
---
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
When you submit a pull request, a CLA bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
microsoft-mssql-django-171ed6e/CodeQL.yml 0000664 0000000 0000000 00000000053 15253215657 0020375 0 ustar 00root root 0000000 0000000 path_classifiers:
library:
- "django" microsoft-mssql-django-171ed6e/LICENSE.txt 0000664 0000000 0000000 00000003211 15253215657 0020365 0 ustar 00root root 0000000 0000000 Project Name: mssql-django
BSD 3-Clause License
Copyright (c) 2021, Microsoft Corporation
2019, ES Solutions AB
2018, Michiya Takahashi
2008, 2009 django-pyodbc developers
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
microsoft-mssql-django-171ed6e/MANIFEST.in 0000664 0000000 0000000 00000000162 15253215657 0020302 0 ustar 00root root 0000000 0000000 include LICENSE.txt
include MANIFEST.in
include README.md
recursive-include mssql *.py
recursive-exclude docker *
microsoft-mssql-django-171ed6e/NOTICE.md 0000664 0000000 0000000 00000003377 15253215657 0020062 0 ustar 00root root 0000000 0000000 # Notices
This repository incorporates material as listed below or described in the code.
## django-mssql-backend
Please see below for the associated license for the incorporated material from django-mssql-backend (https://github.com/ESSolutions/django-mssql-backend).
### BSD 3-Clause License
Copyright (c) 2019, ES Solutions AB
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
microsoft-mssql-django-171ed6e/OneBranchPipelines/ 0000775 0000000 0000000 00000000000 15253215657 0022255 5 ustar 00root root 0000000 0000000 microsoft-mssql-django-171ed6e/OneBranchPipelines/build-release-package-pipeline.yml 0000664 0000000 0000000 00000013760 15253215657 0030720 0 ustar 00root root 0000000 0000000 # =============================================================================
# OneBranch Build & Package pipeline for mssql-django.
# =============================================================================
# Builds the pure-Python package (sdist + universal wheel via `python -m build`),
# bundles the regex_clr.dll CLR assembly, generates an SBOM, and publishes the
# OneBranch 'drop' artifact consumed by the Official-Release and Dummy-Release
# pipelines.
#
# mssql-django is pure Python -> ONE universal wheel + sdist. There is no
# platform matrix, no native compilation, no symbols (unlike mssql-python).
#
# regex_clr.dll: the SQL CLR assembly that backs REGEXP_LIKE. It is NOT checked
# into the repo (Microsoft FCIB policy prohibits binaries in production repos).
# It is stored as an Azure DevOps Secure File and pulled at build time with
# DownloadSecureFile@1, then its SHA-256 is verified against the known-good
# value before it is bundled into the package (setup.py package_data). A
# checksum mismatch fails the build, so only the approved assembly can ship.
#
# Pipeline source = Azure DevOps mssql-django mirror (checkout: self), keeping
# the pipeline source ADO-only for SDL.
# =============================================================================
name: $(Year:YY)$(DayOfYear)$(Rev:.r)
trigger:
branches:
include:
- main
pr:
branches:
include:
- main
schedules:
- cron: "0 21 * * *" # daily nightly build
displayName: Daily nightly build
branches:
include:
- main
always: true
variables:
- template: /OneBranchPipelines/variables/common-variables.yml@self
- template: /OneBranchPipelines/variables/onebranch-variables.yml@self
# Non-secret build config (RegexClrSha256 = known-good regex_clr.dll SHA-256).
# Kept in the Library rather than in-repo so the hash doesn't trip
# high-entropy/secret scanners.
- group: 'mssql-django-build'
resources:
repositories:
- repository: templates
type: git
name: 'OneBranch.Pipelines/GovernedTemplates'
ref: 'refs/heads/main'
extends:
template: 'v2/OneBranch.Official.CrossPlat.yml@templates'
parameters:
globalSdl:
sbom:
enabled: true
credscan:
enabled: true
suppressionsFile: $(Build.SourcesDirectory)/.config/CredScanSuppressions.json
policheck:
break: true
tsa:
enabled: true
configFile: $(Build.SourcesDirectory)/.config/tsaoptions.json
stages:
- stage: build
displayName: 'Build mssql-django package'
jobs:
- job: build
displayName: 'Build sdist + wheel'
pool:
type: linux
isCustom: true
name: Django-1ES-pool
demands:
- imageOverride -equals Ubuntu22.04-AzurePipelines
variables:
# Wheel + sdist are staged under $(ob_outputDirectory)/dist and published
# explicitly as the 'drop_build_build' artifact (see PublishPipelineArtifact
# below) so the release pipelines consume a known artifact name + path.
ob_outputDirectory: '$(Build.SourcesDirectory)/out'
steps:
- task: UsePythonVersion@0
displayName: 'Use Python 3.x'
inputs:
versionSpec: '3.x'
addToPath: true
- task: PipAuthenticate@1
displayName: 'Pip Authenticate'
inputs:
artifactFeeds: '$(PIP_FEED)'
- task: DownloadSecureFile@1
name: regexClr
displayName: 'Download regex_clr.dll (Secure File)'
inputs:
secureFile: 'regex_clr.dll'
- task: Bash@3
displayName: 'Verify regex_clr.dll checksum + stage into mssql/'
inputs:
targetType: 'inline'
script: |
set -euo pipefail
EXPECTED='$(RegexClrSha256)'
SRC="$(regexClr.secureFilePath)"
ACTUAL="$(sha256sum "$SRC" | awk '{print $1}')"
echo "expected SHA-256: $EXPECTED"
echo "actual SHA-256: $ACTUAL"
if [ -z "$EXPECTED" ]; then
echo "##[error]RegexClrSha256 variable is empty - check the 'mssql-django-build' variable group is linked."
exit 1
fi
if [ "$ACTUAL" != "$EXPECTED" ]; then
echo "##[error]regex_clr.dll checksum mismatch - refusing to package."
exit 1
fi
cp -v "$SRC" "$(Build.SourcesDirectory)/mssql/regex_clr.dll"
ls -la "$(Build.SourcesDirectory)/mssql/regex_clr.dll"
- task: Bash@3
displayName: 'Build sdist + wheel'
inputs:
targetType: 'inline'
script: |
set -euo pipefail
cd "$(Build.SourcesDirectory)"
python -m pip install --upgrade pip build
python -m build
mkdir -p "$(Build.SourcesDirectory)/out/dist"
cp -v "$(Build.SourcesDirectory)"/dist/* "$(Build.SourcesDirectory)/out/dist/"
echo "=== packaged artifacts ==="
ls -la "$(Build.SourcesDirectory)/out/dist"
# Publish wheel + sdist as an explicitly-named artifact so the official/dummy
# release pipelines consume a known name + path ($(ob_outputDirectory)/dist).
# SBOM is generated automatically by the OneBranch Official template
# (globalSdl.sbom.enabled) - no explicit ManifestGeneratorTask needed.
- task: PublishPipelineArtifact@1
displayName: 'Publish drop_build_build'
inputs:
targetPath: '$(ob_outputDirectory)'
artifact: 'drop_build_build'
microsoft-mssql-django-171ed6e/OneBranchPipelines/dummy-release-pipeline.yml 0000664 0000000 0000000 00000010043 15253215657 0027352 0 ustar 00root root 0000000 0000000 # =============================================================================
# OneBranch DUMMY / TEST Release pipeline for mssql-django.
# WARNING: TEST ONLY -- never publishes to PyPI.
# =============================================================================
# Exercises the full download + ESRP release path against a Maven ContentType
# (instead of PyPI) so the ESRP wiring can be validated WITHOUT any risk of a
# real production release. Always NonOfficial.
# =============================================================================
name: $(Year:YY)$(DayOfYear)$(Rev:.r)-Dummy-Release
trigger: none
pr: none
parameters:
- name: performDummyRelease
displayName: '[TEST] Perform Dummy ESRP Release (Maven - NOT PyPI)'
type: boolean
default: true # Safe: Maven ContentType, never touches PyPI.
variables:
- template: /OneBranchPipelines/variables/common-variables.yml@self
- template: /OneBranchPipelines/variables/onebranch-variables.yml@self
- group: 'ESRP Federated Creds (AME)'
resources:
repositories:
- repository: templates
type: git
name: 'OneBranch.Pipelines/GovernedTemplates'
ref: 'refs/heads/main'
pipelines:
- pipeline: buildPipeline
source: 'Build-Release-Package-Pipeline'
trigger: none
extends:
template: 'v2/OneBranch.NonOfficial.CrossPlat.yml@templates'
parameters:
globalSdl:
sbom:
enabled: true
credscan:
enabled: true
policheck:
break: true
stages:
- stage: TestReleasePackage
displayName: '[TEST] Dummy Release - Validate ESRP Workflow'
jobs:
- job: PrepAndValidate
displayName: '[TEST] Download and Validate Artifact'
pool:
type: linux
isCustom: true
name: Django-1ES-pool
demands:
- imageOverride -equals Ubuntu22.04-AzurePipelines
variables:
ob_outputDirectory: '$(Build.ArtifactStagingDirectory)'
steps:
- download: buildPipeline
displayName: '[TEST] Download drop from Build pipeline'
- task: Bash@3
displayName: '[TEST] List artifact contents'
inputs:
targetType: 'inline'
script: |
set -euo pipefail
DIST="$(Pipeline.Workspace)/buildPipeline/drop_build_build/dist"
echo "=== [TEST] artifact contents ==="
ls -la "$DIST"
- ${{ if eq(parameters.performDummyRelease, true) }}:
- job: DummyRelease
displayName: '[TEST] Dummy ESRP Release (Maven)'
dependsOn: PrepAndValidate
templateContext:
type: releaseJob
isProduction: false
pool:
type: linux
variables:
ob_outputDirectory: '$(Build.ArtifactStagingDirectory)'
steps:
- download: buildPipeline
displayName: '[TEST] Download drop from Build pipeline'
- task: EsrpRelease@9
displayName: '[TEST] Dummy ESRP Release (Maven - NOT PyPI)'
inputs:
connectedservicename: '$(ESRPConnectedServiceName)'
usemanagedidentity: true
keyvaultname: '$(AuthAKVName)'
signcertname: '$(AuthSignCertName)'
clientid: '$(EsrpClientId)'
Intent: 'PackageDistribution'
ContentType: 'Maven' # placeholder - deliberately NOT PyPI
ContentSource: 'Folder'
FolderLocation: '$(Pipeline.Workspace)/buildPipeline/drop_build_build/dist'
WaitForReleaseCompletion: true
Owners: '$(owner)'
Approvers: '$(approver)'
ServiceEndpointUrl: 'https://api.esrp.microsoft.com'
MainPublisher: 'ESRPRELPACMAN'
DomainTenantId: '$(DomainTenantId)'
microsoft-mssql-django-171ed6e/OneBranchPipelines/github-ado-sync.yml 0000664 0000000 0000000 00000011032 15253215657 0025772 0 ustar 00root root 0000000 0000000 # =============================================================================
# GitHub -> Azure DevOps mirror sync for microsoft/mssql-django.
# =============================================================================
# Keeps the ADO mirror in step with GitHub using the PULL REQUEST mechanism
# (same as mssql-python) while PRESERVING FULL COMMIT HISTORY -- real commits,
# identical SHAs -- instead of python's squashed replace-all commit.
#
# 1. Fetch GitHub dev's real commit graph.
# 2. Push those actual commits (fast-forward) to a fresh intermediate ADO
# sync branch -- never a direct push to main, never a force-push.
# 3. Open a PR from the sync branch into main and auto-complete it as a MERGE
# (--squash false) so every individual commit + SHA is retained on main.
# Every sync PR links the tracking work item (46645, "GH<>ADO Sync PRs").
#
# Runs on windows-latest (Microsoft-hosted) where the Azure CLI is preinstalled,
# so PR creation uses `az repos pr` -- same tooling as the mssql-python sync.
#
# No direct writes to main. No force. No ado-only hacks (this file mirrors to
# GitHub like everything else). SDL-clean: the pipeline SOURCE is the ADO mirror
# (checkout: self); it only does a plain, unauthenticated `git fetch` of the
# PUBLIC GitHub repo -- no GitHub service connection.
#
# Branch mapping: GitHub `dev` -> ADO `main` (ADO/1ES conventions expect main).
#
# Build Service (System.AccessToken) permissions required on this repo:
# - Contribute (push the sync branch)
# - Contribute to PRs (create + auto-complete the pull request)
# =============================================================================
name: GitHub-Sync-$(Date:yyyyMMdd)$(Rev:.r)
schedules:
- cron: "30 11 * * *" # daily 17:00 IST (matches mssql-python)
displayName: Daily GitHub -> ADO mirror
branches:
include:
- main
always: true
trigger: none
pr: none
pool:
vmImage: 'windows-latest'
steps:
- checkout: self # Azure DevOps mssql-django mirror (source = ADO)
persistCredentials: true # git push to ADO via System.AccessToken
fetchDepth: 0
fetchTags: true
- pwsh: |
$ErrorActionPreference = 'Stop'
git config user.email "sync@microsoft.com"
git config user.name "ADO Mirror Bot"
# 1. Fetch GitHub dev's real commits (public repo -> no auth, SDL-clean).
git remote add github https://github.com/microsoft/mssql-django.git
git fetch --tags github dev
if ($LASTEXITCODE -ne 0) { throw "git fetch github dev failed" }
$ghSha = (git rev-parse FETCH_HEAD).Trim()
Write-Host "GitHub dev is at $ghSha"
git fetch origin main
$mainSha = (git rev-parse FETCH_HEAD).Trim()
Write-Host "ADO main is at $mainSha"
# No-op if main already contains GitHub dev's tip.
git merge-base --is-ancestor $ghSha $mainSha
if ($LASTEXITCODE -eq 0) {
Write-Host "ADO main already contains GitHub dev ($ghSha). Nothing to sync."
exit 0
}
# 2. Push GitHub dev's actual commit graph to a fresh sync branch. GitHub
# history is append-only, so this is a fast-forward -- no force, and it
# never touches main. History + SHAs are preserved exactly.
$syncBranch = "sync/github-dev-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
Write-Host "Pushing GitHub dev commits to $syncBranch"
git push origin "$($ghSha):refs/heads/$syncBranch"
if ($LASTEXITCODE -ne 0) { throw "push to $syncBranch failed" }
# 3. Open a PR sync -> main and auto-complete as a MERGE (--squash false) so
# the individual commits are retained on main.
$env:AZURE_DEVOPS_EXT_PAT = "$(System.AccessToken)"
az extension add --name azure-devops --only-show-errors
az repos pr create `
--organization "$(System.TeamFoundationCollectionUri)" `
--project "$(System.TeamProject)" `
--repository mssql-django `
--source-branch "$syncBranch" `
--target-branch main `
--title "Sync GitHub dev -> main ($ghSha)" `
--description "Automated history-preserving sync of GitHub microsoft/mssql-django dev into ADO main. Auto-completed as a MERGE (not squash) to retain individual commits and their SHAs." `
--squash false `
--work-items 46645 `
--delete-source-branch true `
--auto-complete true `
--output table
if ($LASTEXITCODE -ne 0) { throw "az repos pr create failed" }
Write-Host "PR created and set to auto-complete (merge, no force). Sync done."
displayName: Sync GitHub dev -> ADO main via PR (history preserved, no force)
env:
AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
microsoft-mssql-django-171ed6e/OneBranchPipelines/official-release-pipeline.yml 0000664 0000000 0000000 00000012461 15253215657 0030001 0 ustar 00root root 0000000 0000000 # =============================================================================
# OneBranch Official Release pipeline for mssql-django.
# =============================================================================
# Downloads the 'drop' artifact (sdist + wheel) produced by the
# Build-Release-Package pipeline and releases it to PyPI via ESRP.
#
# ALWAYS Official. Gated by `releaseToPyPI` (default FALSE = dry run) so a run
# never publishes by accident -- flip to true + re-run to actually release.
# =============================================================================
name: $(Year:YY)$(DayOfYear)$(Rev:.r)-Release
# Manual trigger only - releases must be deliberate.
trigger: none
pr: none
parameters:
- name: releaseToPyPI
displayName: 'Release to PyPI (Production)'
type: boolean
default: false # Safety: default false to prevent accidental releases.
variables:
- template: /OneBranchPipelines/variables/common-variables.yml@self
- template: /OneBranchPipelines/variables/onebranch-variables.yml@self
- group: 'ESRP Federated Creds (AME)' # ESRP signing credentials
resources:
repositories:
- repository: templates
type: git
name: 'OneBranch.Pipelines/GovernedTemplates'
ref: 'refs/heads/main'
pipelines:
- pipeline: buildPipeline
source: 'Build-Release-Package-Pipeline' # the build pipeline (PR #548)
trigger: none
extends:
template: 'v2/OneBranch.Official.CrossPlat.yml@templates'
parameters:
globalSdl:
sbom:
enabled: true
credscan:
enabled: true
policheck:
break: true
tsa:
enabled: true
configFile: $(Build.SourcesDirectory)/.config/tsaoptions.json
stages:
- stage: ReleasePackage
displayName: 'Release mssql-django to PyPI'
jobs:
# Job 1: download + validate the built artifact.
- job: PrepAndValidate
displayName: 'Download and Validate Artifact'
pool:
type: linux
isCustom: true
name: Django-1ES-pool
demands:
- imageOverride -equals Ubuntu22.04-AzurePipelines
variables:
ob_outputDirectory: '$(Build.ArtifactStagingDirectory)'
steps:
- download: buildPipeline
displayName: 'Download drop from Build pipeline'
- task: Bash@3
displayName: 'Verify dist contents'
inputs:
targetType: 'inline'
script: |
set -euo pipefail
DIST="$(Pipeline.Workspace)/buildPipeline/drop_build_build/dist"
echo "=== artifact contents ==="
ls -la "$DIST"
test -n "$(find "$DIST" -name '*.whl' -o -name '*.tar.gz')" \
|| { echo "No wheel/sdist found in drop artifact"; exit 1; }
- ${{ if eq(parameters.releaseToPyPI, false) }}:
- task: Bash@3
displayName: 'Dry Run - Release Skipped'
inputs:
targetType: 'inline'
script: |
echo "===================================="
echo "DRY RUN MODE - No Release Performed"
echo "releaseToPyPI=false. Verified the drop artifact only."
echo "To release: set releaseToPyPI=true and re-run."
echo "===================================="
# Job 2: ESRP release to PyPI (only when releaseToPyPI=true).
- ${{ if eq(parameters.releaseToPyPI, true) }}:
- job: PyPIRelease
displayName: 'ESRP Release to PyPI'
dependsOn: PrepAndValidate
templateContext:
type: releaseJob
isProduction: true
pool:
type: linux
variables:
ob_outputDirectory: '$(Build.ArtifactStagingDirectory)'
steps:
- download: buildPipeline
displayName: 'Download drop from Build pipeline'
- task: EsrpRelease@9
displayName: 'ESRP Release to PyPI'
inputs:
connectedservicename: '$(ESRPConnectedServiceName)'
usemanagedidentity: true
keyvaultname: '$(AuthAKVName)'
signcertname: '$(AuthSignCertName)'
clientid: '$(EsrpClientId)'
Intent: 'PackageDistribution'
ContentType: 'PyPI'
ContentSource: 'Folder'
FolderLocation: '$(Pipeline.Workspace)/buildPipeline/drop_build_build/dist'
WaitForReleaseCompletion: true
Owners: '$(owner)'
Approvers: '$(approver)'
ServiceEndpointUrl: 'https://api.esrp.microsoft.com'
MainPublisher: 'ESRPRELPACMAN'
DomainTenantId: '$(DomainTenantId)'
- task: Bash@3
displayName: 'Release Summary'
inputs:
targetType: 'inline'
script: |
echo "ESRP release to PyPI submitted."
echo "Verify at https://pypi.org/project/mssql-django/"
microsoft-mssql-django-171ed6e/OneBranchPipelines/variables/ 0000775 0000000 0000000 00000000000 15253215657 0024225 5 ustar 00root root 0000000 0000000 microsoft-mssql-django-171ed6e/OneBranchPipelines/variables/common-variables.yml 0000664 0000000 0000000 00000002056 15253215657 0030211 0 ustar 00root root 0000000 0000000 # Common variables shared across the mssql-django release pipelines
# (build-release-package, official-release, dummy-release).
# mssql-django is a pure-Python package, so there is no native/ODBC/symbol
# machinery here — only the paths, package identity, and the regex_clr.dll
# source coordinates.
variables:
# Repository root
- name: REPO_ROOT
value: $(Build.SourcesDirectory)
readonly: true
# Artifact staging path
- name: ARTIFACT_PATH
value: $(Build.ArtifactStagingDirectory)
readonly: true
# Package identity
- name: PACKAGE_NAME
value: 'mssql-django'
readonly: true
# Azure Artifacts feed used for pip authentication during build
- name: PIP_FEED
value: 'mssql-django/mssql-django_PublicPackages'
readonly: true
# regex_clr.dll (SQL CLR assembly backing REGEXP_LIKE) is NOT stored in the
# repo (FCIB policy) nor a storage account. It is an Azure DevOps Secure File
# named 'regex_clr.dll', pulled at build time via DownloadSecureFile@1 and
# checksum-verified in build-release-package-pipeline.yml.
microsoft-mssql-django-171ed6e/OneBranchPipelines/variables/onebranch-variables.yml 0000664 0000000 0000000 00000001065 15253215657 0030657 0 ustar 00root root 0000000 0000000 # OneBranch-specific variables for the mssql-django release pipelines.
variables:
# OneBranch auto-publishes everything placed in this directory as artifacts.
- name: ob_outputDirectory
value: '$(ARTIFACT_PATH)'
# SDL on for all release builds.
- name: ob_sdl_enabled
value: true
# TSA upload (Official pipelines only; NonOfficial/dummy ignores it).
- name: ob_tsa_enabled
value: true
# Linux build container used by the OneBranch Linux pool.
- name: LinuxContainerImage
value: 'mcr.microsoft.com/onebranch/cbl-mariner/build:2.0'
microsoft-mssql-django-171ed6e/README.md 0000664 0000000 0000000 00000030627 15253215657 0020034 0 ustar 00root root 0000000 0000000 # Django Backend for Microsoft SQL
mssql-django is the official Microsoft‑supported Django database backend for SQL Server, Azure SQL and SQL Database in Microsoft Fabric.
It provides a reliable, enterprise‑grade database connectivity option for the Django web framework, enabling Python developers to build and run production‑ready applications on Microsoft’s data platform.
This project is the continuation and evolution of earlier community efforts, and it builds on the strong foundation established by django-mssql-backend and its predecessors. mssql-django focuses on long‑term stability, performance, security, and compatibility with both Django and SQL Server.
## Supportability
| Component | Supported Versions |
|---|---|
| Django | 5.2, 6.0, 6.1 |
| Python | CPython 3.10 – 3.14 (Django 6.0 and 6.1 require 3.12+) |
| Platforms | Windows x64; Windows ARM64 with Python 3.11+; macOS 15+ on Intel or Apple silicon; Linux x64/ARM64 with glibc 2.28+ or musl 1.2+ |
| SQL Server | 2017, 2019, 2022, 2025 |
| Azure SQL | Database, Managed Instance, SQL Database in Microsoft Fabric |
| Database driver | pyodbc (default), mssql-python |
| Native connectivity | Microsoft ODBC Driver 17/18 with pyodbc; Microsoft ODBC Driver 18 supplied by `mssql-python-odbc` with mssql-python |
mssql-django 2.0 requires mssql-python and therefore supports only platforms
with a compatible mssql-python distribution. SUSE Linux ARM64 is not supported.
Users on other platforms must remain on mssql-django 1.8.0.
## Quick Start
1. Install mssql-django (pulls in Django, mssql-python, pyodbc, and tzdata automatically):
pip install mssql-django
2. Configure your `settings.py`:
```python
DATABASES = {
'default': {
'ENGINE': 'mssql',
'NAME': 'mydb',
'USER': 'user@myserver',
'PASSWORD': 'password',
'HOST': 'myserver.database.windows.net',
'PORT': '',
},
}
# Set this to False to turn off connection pooling for both database drivers.
DATABASE_CONNECTION_POOLING = False
```
## Configuration Reference
### Standard Django Settings
| Setting | Type | Description |
|---|---|---|
| `ENGINE` | String | Must be `"mssql"` |
| `NAME` | String | Database name. Required. |
| `HOST` | String | SQL Server instance in `"server\instance"` format |
| `PORT` | String | Server instance port. Empty string means default port. |
| `USER` | String | Database user name. If not given, MS Integrated Security is used. |
| `PASSWORD` | String | Database user password |
| `TOKEN` | String | Access token for Microsoft Entra ID authentication (e.g. via `azure.identity`) |
| `AUTOCOMMIT` | Boolean | Set to `False` to disable Django's transaction management |
| `Trusted_Connection` | String | Default `"yes"`. Set to `"no"` if required. |
### TEST Settings
| Setting | Type | Description |
|---|---|---|
| `NAME` | String | Test database name. Default: `"test_" + NAME` |
| `COLLATION` | String | Collation for test database. Default: instance default. |
| `DEPENDENCIES` | String | Creation-order dependencies of the database |
| `MIRROR` | String | Alias of database to mirror during testing |
### OPTIONS
| Option | Type | Default | Description |
|---|---|---|---|
| `python_driver` | String | Unset | Set to `"mssql_python"` per alias to opt in; unset or `"pyodbc"` keeps the default. See [Selecting the database driver](#selecting-the-database-driver). |
| `driver` | String | `"ODBC Driver 18 for SQL Server"` | Native driver override for pyodbc. When omitted, Driver 18 is attempted first with automatic fallback to Driver 17. Explicit values do not fall back. |
| `isolation_level` | String | `None` | [Transaction isolation level](https://docs.microsoft.com/en-us/sql/t-sql/statements/set-transaction-isolation-level-transact-sql): `READ UNCOMMITTED`, `READ COMMITTED`, `REPEATABLE READ`, `SNAPSHOT`, or `SERIALIZABLE` |
| `dsn` | String | Unset | Named DSN, can be used instead of `HOST` (pyodbc only) |
| `unicode_results` | Boolean | `False` | Activate pyodbc's unicode\_results feature |
| `extra_params` | String | Unset | Additional connection parameters (`"param=value;param=value"`), passed unchanged and validated by the selected driver. See [authentication](https://github.com/microsoft/mssql-django/wiki/Azure-AD-Authentication) and [driver-specific rules](#selecting-the-database-driver). |
| `collation` | String | `None` | Collation for text field lookups (e.g. `"Chinese_PRC_CI_AS"`) |
| `connection_timeout` | Integer | `0` | Connection timeout in seconds (`0` = disabled) |
| `connection_retries` | Integer | `5` | Number of connection retry attempts |
| `connection_retry_backoff_time` | Integer | `5` | Back-off time in seconds between retries |
| `query_timeout` | Integer | `0` | Query timeout in seconds (`0` = disabled) |
| `setencoding` / `setdecoding` | List | Unset | Forwarded to the selected connection. Each dictionary must match that driver's method signature and is not portable between drivers. mssql-python requires `sqltype` in each `setdecoding` entry. See the pyodbc [encoding](https://github.com/mkleehammer/pyodbc/wiki/Connection#setencoding) / [decoding](https://github.com/mkleehammer/pyodbc/wiki/Connection#setdecoding) reference. |
| `return_rows_bulk_insert` | Boolean | `False` | Allow returning rows from bulk insert. Must be `False` if tables have triggers. |
#### Disabling MARS
On the pyodbc path, the backend enables Multiple Active Result Sets (MARS) by
default with Microsoft ODBC drivers on Windows. To connect to an endpoint that does not support MARS,
such as Microsoft Fabric Warehouse, set `MARS_Connection=no` in that database
alias's `extra_params`:
```python
'OPTIONS': {
'driver': 'ODBC Driver 18 for SQL Server',
'extra_params': 'Authentication=ActiveDirectoryServicePrincipal;MARS_Connection=no',
},
```
Keep your existing `HOST`, `NAME`, `USER` (client ID), and `PASSWORD` (client
secret) settings. For other authentication methods, keep the corresponding
authentication settings and append `MARS_Connection=no` to `extra_params`.
An explicit MARS setting is honored case-insensitively, without adding a
conflicting default. With MARS disabled, ORM iteration buffers results before
yielding them so nested queries can use the same connection; this can use more
memory for large querysets. This connection setting does not imply full
Warehouse support for Django migrations or other SQL Server features.
### Selecting the database driver
mssql-django installs both **pyodbc** and
[mssql-python](https://github.com/microsoft/mssql-python) >=1.15.0. pyodbc
remains the default. Select mssql-python per alias without changing `ENGINE`:
```python
'OPTIONS': {
'python_driver': 'mssql_python',
},
```
Omit `python_driver` or set it to `"pyodbc"` to roll back. Aliases can use
different drivers.
- **Runtime.** pip automatically installs the `mssql-python-odbc` companion
package, which supplies Microsoft ODBC Driver 18. Private indexes and
`--no-deps` installations must provide the companion package separately.
Install mssql-python's [platform prerequisites](https://github.com/microsoft/mssql-python#installation),
including OpenSSL on macOS and the required Linux libraries.
- **Connections.** `HOST` and optional `PORT` become `SERVER=host,port`.
`driver`, `dsn`, `host_is_server`, and `unicode_results` are ignored; Driver
17 fallback is pyodbc-only. An empty `HOST` uses `localhost`, matching the
pyodbc local-server default.
- **Additional parameters.** mssql-python 1.15 validates `extra_params` against
a strict allowlist. Supported options include `Authentication`, `Encrypt`,
`TrustServerCertificate`, `HostnameInCertificate`, `ServerCertificate`,
`ServerSPN`, `MultiSubnetFailover`, `ApplicationIntent`,
`ConnectRetryCount`, `ConnectRetryInterval`, `KeepAlive`,
`KeepAliveInterval`, `IpAddressPreference`, and `PacketSize`. Common
pyodbc-only keywords such as `APP`, `LongAsMax`, `ColumnEncryption`, `WSID`,
`AnsiNPW`, `QuotedId`, `Regional`, `UseFMTONLY`, `Current Language`,
`Network Library`, `Description`, and `Connect Timeout` are rejected, as are
`DRIVER`, `DSN`, `SERVERNAME`, and `MARS_Connection`. Remove unsupported
keywords before switching; use the `connection_timeout` option instead of
`Connect Timeout`. Explicit supported values override generated ones.
- **Security and authentication.** Use trusted certificates in production.
`TrustServerCertificate=yes` is for trusted local development only. Use a
[supported authentication mode](https://github.com/microsoft/mssql-python/wiki/Microsoft-Entra-ID-support)
in `extra_params`, or supply `TOKEN` without `USER`, `PASSWORD`, or
`Authentication`; the application owns token acquisition and renewal.
- **Iteration.** mssql-python does not enable MARS. To allow nested queries on
one connection, `QuerySet.iterator()` buffers the full result before
yielding, regardless of `chunk_size`; account for that memory on large
querysets. `MARS_Connection=no` remains a pyodbc-only setting for endpoints
covered by [Disabling MARS](#disabling-mars).
### Backend-Specific Settings
| Setting | Type | Default | Description |
|---|---|---|---|
| `DATABASE_CONNECTION_POOLING` | Boolean | `True` | Set to `False` before opening connections to disable driver pooling. Applies to pyodbc and mssql-python. |
## Known Limitations
The following limitations apply when using SQL Server with Django:
- Altering a model field from or to AutoField at migration
- Floating point arithmetic in some annotate functions
- Annotate/exists function in `order_by`
- Righthand power and arithmetic with datetimes
- Timezones and timedeltas not fully supported
- Rename field/model with foreign key constraint
- Database level constraints and filtered indexes
- Date extract function
- Bulk insert with triggers and returning rows
### Version-Specific Notes
| Version | Notes |
|---|---|
| Django 5.2 | Tuple lookups require Django 5.2.4+ for full support. Some JSONField bulk/CASE WHEN update edge cases. See [test exclusions](https://github.com/microsoft/mssql-django/blob/dev/testapp/settings.py) for details. |
| Django 6.0 | Requires Python 3.12+. Backend handles the 6.0 API changes transparently. |
| Django 6.1 | Requires Python 3.12+. Database-level referential actions (`DB_CASCADE`, `DB_SET_NULL`, `DB_SET_DEFAULT`) are unavailable because SQL Server disallows multiple cascade paths to the same table; using one raises `fields.E324`. Bitwise aggregates (`BitAnd`, `BitOr`, `BitXor`) are not implemented and raise `NotSupportedError`. See [test exclusions](https://github.com/microsoft/mssql-django/blob/dev/testapp/settings.py) for details. |
JSONField lookups have additional limitations — see the [JSONField wiki page](https://github.com/microsoft/mssql-django/wiki/JSONField).
## Helpful Links
| Resource | Link |
|---|---|
| Wiki & Guides | [mssql-django Wiki](https://github.com/microsoft/mssql-django/wiki) |
| Contributing | [Contributing Guide](https://github.com/microsoft/mssql-django/blob/dev/CONTRIBUTING.md) |
| Code of Conduct | [Microsoft Open Source Code of Conduct](https://github.com/microsoft/mssql-django/blob/dev/CODE_OF_CONDUCT.md) |
## Still have questions?
Check the [FAQ](https://github.com/microsoft/mssql-django/wiki/Frequently-Asked-Questions) or [open an issue](https://github.com/microsoft/mssql-django/issues/new) on GitHub.
## Contributing
We welcome contributions and suggestions! See [CONTRIBUTING.md](https://github.com/microsoft/mssql-django/blob/dev/CONTRIBUTING.md) for details.
All contributors are listed on GitHub: [Contributor Insights](https://github.com/microsoft/mssql-django/graphs/contributors)
Most contributions require a Contributor License Agreement (CLA). For details, visit https://cla.opensource.microsoft.com. A CLA bot will guide you when you submit a pull request.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
## Security
For security reporting instructions please refer to [`SECURITY.md`](https://github.com/microsoft/mssql-django/blob/dev/SECURITY.md).
## Trademarks
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
trademarks or logos is subject to and must follow
[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
Any use of third-party trademarks or logos are subject to those third-party's policies.
microsoft-mssql-django-171ed6e/SECURITY.md 0000664 0000000 0000000 00000005250 15253215657 0020340 0 ustar 00root root 0000000 0000000 # Security
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below.
## Reporting Security Issues
**Please do not report security vulnerabilities through public GitHub issues.**
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs.
## Preferred Languages
We prefer all communications to be in English.
## Policy
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).