azure-functions-db vs Official Azure SQL Bindings¶
azure-functions-db-python and Microsoft's official Azure SQL bindings solve overlapping problems in different ways. This page is a side-by-side comparison on every axis that matters when picking between them.
For the underlying design rationale of the Python wrapper model, see ADR-006 — Python Wrapper over Native Azure Functions Extension.
Quick pick¶
- Azure SQL / SQL Server only, and you want the Functions host to manage everything? → prefer the official Azure SQL bindings.
- PostgreSQL, MySQL, SQLite, or any other SQLAlchemy dialect? → this package.
- Non-SQL data source (Mongo, Kafka, HTTP API) that still fits a polling model? → this package via
SourceAdapter. - Need runtime-native binding metadata visible to Portal and scale controllers? → the official extension. This package deliberately does not register with the host.
- Need
pip installand a fully Python toolchain? → this package.
At a glance¶
| Axis | Official Azure SQL bindings | azure-functions-db-python |
|---|---|---|
| Databases | Azure SQL Database, SQL Server | PostgreSQL, MySQL, SQL Server + any SQLAlchemy dialect (BYOD) |
| Trigger mechanism | SQL Change Tracking (CT) | Cursor-column polling on the timer trigger |
| Trigger scaling | Functions extension scale controller | Timer schedule × PollTrigger batch size |
| Delivery guarantee | Exactly-once (per official docs, on managed sinks) | At-least-once — handlers must be idempotent |
| Checkpoint storage | Leases table in the source DB (az_func schema) |
Azure Blob Storage via BlobCheckpointStore |
| Extension language | C# / .NET (WebJobs SDK extension) | Pure Python decorators |
| Distribution | Extension bundle / func extensions install |
pip install azure-functions-db[<extra>] |
| Native binding metadata | Yes (registered with the Functions host) | No — the host only sees the underlying timer trigger |
| Portal binding visualization | Yes | No — only the timer trigger appears |
| Local testing without host | Requires Functions host + func start |
Plain pytest — the wrapper is a Python function |
| SQLAlchemy support | Not applicable | First-class — every read / write / poll goes through SQLAlchemy |
| Custom source support | No | Yes — implement SourceAdapter for non-SQL sources |
The rest of this page expands each axis with the trade-offs and the concrete implications for Python teams on Azure Functions.
1. Supported databases¶
Official Azure SQL bindings
- Azure SQL Database and SQL Server (managed and self-hosted).
- The trigger specifically requires SQL Change Tracking to be enabled on the target table.
- No PostgreSQL, MySQL, SQLite, or non-SQL sources.
azure-functions-db-python
- Built-in extras with tested drivers:
azure-functions-db[postgres]—psycopgazure-functions-db[mysql]—PyMySQLazure-functions-db[mssql]—pyodbc- BYOD (bring-your-own-dialect) for anything else with a SQLAlchemy dialect: Oracle, DuckDB, CockroachDB, ClickHouse, Snowflake, and so on. The pattern is: install the driver, use the SQLAlchemy URL, done.
- Non-SQL sources via
SourceAdapterfor triggers only (input / output bindings remain SQLAlchemy-based).
2. Trigger mechanism — SQL Change Tracking vs cursor-column polling¶
Official Azure SQL bindings
- Uses SQL Change Tracking on the source table. The extension queries
CHANGETABLE(CHANGES ...)to discover inserts, updates, and deletes since the last delivered change version. - Change Tracking must be enabled on the database and on each triggered table (
ALTER DATABASE ... SET CHANGE_TRACKING = ON). - Deletes are visible.
- The extension owns the change version watermark; it lives in leases stored in the source database.
azure-functions-db-python
- Uses cursor-column polling: on each timer tick,
SqlAlchemySourceselects rows where the configured cursor column (e.g.updated_at,id,version) is greater than the last committed checkpoint, ordered by cursor + primary key, capped at the batch size. - Requires you to model a monotonically non-decreasing cursor column on the source table. This is a schema constraint but works on every RDBMS.
- Deletes are not detected by cursor-column polling. If you need delete detection, use a soft-delete pattern (a
deleted_atcolumn) or a companion tombstone table that is itself cursor-column polled. Alternatively, implement a customSourceAdapterthat surfaces deletes from your DB's own change log (e.g. Postgres logical replication, MySQL binlog). - Cross-dialect: the same trigger model works for PostgreSQL, MySQL, SQL Server, SQLite, Oracle, and any other SQLAlchemy dialect. The trade-off is that no dialect gets native change-log semantics — everyone gets cursor polling.
See Semantics § 1.2 Source Preconditions for the exact contract the cursor column must satisfy.
3. Scaling — extension scale controller vs timer schedule¶
Official Azure SQL bindings
- Integrates with the Functions scale controller via the WebJobs SDK. When Change Tracking reports a growing backlog on the source table, the host can scale worker instances up.
- The scale decision is host-managed; the extension surfaces backlog metrics natively.
azure-functions-db-python
- No dedicated DB scale controller.
@db.triggerruns on top of a real@app.scheduletimer trigger, so worker scaling follows the timer trigger's own scale-controller path. On the Consumption / Elastic Premium plans, the Functions host still scales workers, just via the timer's dispatch rate rather than a bespoke DB-backlog probe. - Throughput is bounded by timer cadence ×
PollTriggerbatch size: - Example:
schedule="0 */1 * * * *"(every minute) withPollTrigger(batch_size=500)= up to 500 rows per minute per instance. - Tighten cadence and raise batch size for higher throughput; loosen both for lower cost.
- If you need push-based sub-second scaling for Azure SQL specifically, prefer the official extension. Cross-dialect users generally accept the timer cadence because polling is the trade for the wider database reach.
See ADR-006 § Functions scale-controller integration is not worth the extension cost for polling workloads for the full rationale.
4. Delivery guarantee¶
Official Azure SQL bindings
- Per the official documentation, the SQL trigger provides exactly-once delivery for each change on managed sinks. Duplicates can still happen on the write side unless the sink itself is idempotent — the guarantee is about change-consumption, not end-to-end.
- Lease bookkeeping lives in
az_func.Leases_<table_id>in the source database.
azure-functions-db-python
- At-least-once delivery by design (ADR-004). Duplicates can be observed during:
- Process crashes between handler success and checkpoint commit.
- Lease transitions when a partition moves between instances.
- Checkpoint store commit failures.
- Handlers must be idempotent. The recommended patterns:
- Use
event.pkplusevent.cursoras a natural deduplication key at the sink. - Prefer upsert (
@db.output(..., action="upsert", conflict_columns=[...])) over plain insert. - Wrap downstream multi-statement writes in
DbWriter.transaction()so a partial write is rolled back on crash and safely re-run.
See Semantics § 1.3 Duplicate and Reprocessing Windows for the formal duplicate-window model and Polling Runtime & Failure Scenarios for the operational reference.
5. Checkpoint storage and lifecycle¶
Official Azure SQL bindings
- Lease and change-version bookkeeping live in the source SQL database itself, under the
az_funcschema (az_func.Leases_<table_id>). - Lifecycle is managed by the extension: leases are created on first trigger run, updated per batch, and cleaned up when the trigger is removed.
- Requires write access to the source database from the Functions identity.
azure-functions-db-python
- Checkpoints live in Azure Blob Storage via
BlobCheckpointStore. One blob per source (identified bysource_descriptor.fingerprint). - No schema changes required in the source database. The source database is treated as read-only from the trigger's perspective (unless the handler explicitly writes back through
@db.outputorDbWriter). - Lifecycle:
- Blob is created on first successful tick, updated after each batch's handler-success + checkpoint-commit pair.
- Fingerprint changes (e.g. cursor column or table changes) create a new blob — the old checkpoint is not migrated automatically, which is a safety feature: schema changes should be intentional.
- Deletion of the blob resets the trigger to the beginning of the cursor sequence.
- See Checkpoint & Lease Spec for the full contract.
Trade-off summary:
| Aspect | Official (SQL leases table) | This package (Blob store) |
|---|---|---|
| Source DB writes required | Yes | No |
| Extra Azure resource | No | Yes (storage account) |
| Cross-region checkpoint replication | Via SQL replication | Via storage geo-replication |
| Visibility to DB admins | Yes — queryable in SQL | No — opaque blob |
| Portable across DB engines | SQL-only | Any SQLAlchemy dialect |
6. Local testing experience¶
Official Azure SQL bindings
- Requires the Functions host:
func startwith the extension installed viafunc extensions installor via the extension bundle. - Local SQL Server / Azure SQL Edge container is typically needed to exercise Change Tracking end-to-end.
- Debugging spans two runtimes: the .NET Functions host (which hosts the extension) and the Python worker (which runs the handler).
azure-functions-db-python
- The wrapper is a plain Python function. You can exercise
@db.input,@db.output, and@db.triggerend-to-end withpytestand an in-process SQLite database — no Functions host required. - The polling loop can be driven directly by calling
PollTrigger.tick()in a test. See the existingtests/test_trigger_runner.pyfor the pattern. - For end-to-end examples against real databases, see
examples/postgresql-poll-trigger/which uses docker-compose for PostgreSQL + Azurite. - Debugging is a single process — set a breakpoint in the handler and step through the whole call chain including the polling loop.
7. SQLAlchemy compatibility and BYOD source adapters¶
Official Azure SQL bindings
- Not applicable. The extension speaks TDS directly through its own driver stack (SqlClient); it does not use SQLAlchemy.
azure-functions-db-python
- Every read, write, and poll goes through SQLAlchemy 2.0+ with a single sync engine path per dialect. See
ADR-002. - Any dialect that ships a SQLAlchemy driver works — the built-in extras just bundle common drivers for convenience. Concretely:
- Install the driver:
pip install oracledb - Use the SQLAlchemy URL:
url="oracle+oracledb://user:pass@host:1521/db" - Pass dialect-specific engine options via
engine_kwargs=...— everything the underlying dialect supports (pool sizing, timeouts, isolation, custom event listeners) flows through unchanged. - BYOD source adapters for non-SQL sources: implement the
SourceAdapterprotocol and pass it to@db.trigger(source=...). The trigger no longer needs SQL at all — this is how MongoDB, Kafka, or REST-API sources are supported. Input / output bindings remain SQLAlchemy-based. - Async handlers are supported (see Async handlers), but the internal engine remains sync — blocking DB calls are offloaded via
asyncio.to_thread. Fully native asyncio drivers (asyncpg,aiomysql) are not used internally; if you need them, drive them yourself outside the binding.
8. Production readiness considerations¶
Official Azure SQL bindings
- Mature, first-party Microsoft product with an official SLA path via Azure support.
- Ships in the extension bundle — no manual extension install for Python v2 apps that use the bundle.
- Native scale-controller integration reduces manual capacity planning.
- Portal binding visualization gives Ops teams a familiar surface.
azure-functions-db-python
- Community project maintained by the DX Toolkit authors; no Microsoft SLA. If you need first-party support, prefer the official extension.
- Before going to production, work through the Production Checklist — Polling Trigger which covers:
- Cursor column choice, indexing, and timezone handling (
updated_atnaivety, monotonicity guarantees). - Lease tuning (lease TTL, renewal interval, partition sizing under
BlobCheckpointStore). - Batch size vs timer cadence for target throughput and end-to-end latency.
- Handler idempotency patterns (upsert-on-conflict, dedupe helpers, external idempotency keys).
- Observability wiring — connect the
MetricsCollectorprotocol to your metrics backend, wire structured logs to Application Insights via azure-functions-logging. - Recommended pool settings for Azure Functions (
pool_pre_ping=True,pool_recycle, per-planpool_size) are documented in EngineProvider & Pooling with per-dialect snippets. - Delivery is at-least-once (ADR-004) — production sinks must tolerate duplicates. This is the single largest operational difference from the official extension.
- No native binding metadata means Portal binding visualization and any tooling that scrapes binding metadata will not see
@db.trigger— only the underlying timer trigger. Plan operational dashboards around timer metrics + your own trigger-level metrics viaMetricsCollector.
Cross-references¶
- ADR-006 — Python Wrapper over Native Azure Functions Extension — long-form rationale for the packaging decision this comparison rests on.
- ADR-001 — Pseudo Trigger over Native Trigger — the data-flow half of the decision.
- ADR-002 — SQLAlchemy-centric Adapter — why every dialect goes through SQLAlchemy.
- ADR-004 — At-least-once as Default Guarantee — the delivery contract.
- Polling Runtime & Failure Scenarios — operational reference for tick lifecycle, duplicate windows, and recovery.
- Production Checklist — Polling Trigger — pre-deployment verification.
- Microsoft Learn — Azure Functions SQL bindings overview
- Microsoft Learn — Azure Functions SQL trigger
- Microsoft Learn — SQL Change Tracking overview