Support Matrix¶
Tested combinations of CUBRID server, Python version, and driver/framework.
What "tested" / "CI-verified" means here: evidence comes from a specific CI tier and event, not from every pull request (see CI tiers).
smoke-test.ymlruns the full, fail-closedmake verifyon CUBRID 11.4 / Python 3.12 on every push tomainand every day, and on CUBRID 11.2 and 11.4 / Python 3.12 in its weekly scheduled run (Mondays), on manual runs and in release verification (#480); it covers every golden (expected/*.expected; counts in Recipe Coverage), then the FastAPI quickstart, migration and performance, Flask and FastAPI, Django/Celery, AI agent and Streamlit dashboard live suites, each on job-owned CUBRID containers of the same version (the examples database plus two dedicated, empty databases), then the AI agent lane a second time. Pull requests run only the live lanes their changed paths select (scripts/ci_scope.py), on CUBRID 11.4 / Python 3.12: the touched golden examples (make verify-goldens), the touched recipe family's suite, the focusedmake verify-quickstart/verify-performance/verify-ai-agentlanes for those paths, and CUBRID 11.2 or Python endpoints only for version-sensitive or shared-infrastructure changes. A docs-only pull request starts no live CUBRID. Locally, the same fullmake verifyruns everything below (see Live verification).
CUBRID Server Versions¶
| CUBRID | Status | Notes |
|---|---|---|
| 11.2 | ✅ CI-verified | Every golden-backed example and the recipe pytest suites in the weekly scheduled smoke, manual runs and release verification (not on every main push or daily run); pull requests only for version-sensitive or shared-infrastructure changes |
| 11.4 | ✅ CI-verified | Same CAS protocol as 11.2; the full smoke on every main push, daily, weekly, on manual runs and in release verification, plus the representative version for path-selected pull-request lanes |
| 11.0 | ⚠️ Untested | Should work (same CAS protocol) |
| 10.2 | ⚠️ Untested | Should work (same CAS protocol) |
| > Scope note: CI exercises CUBRID 11.2 and 11.4 with Python 3.12 | ||
> (11.4 on every main push and daily; both versions weekly, on manual runs and in |
||
| > release verification). The weekly run also compares the moving | ||
> cubrid/cubrid:latest image with 11.4 by digest and engine version and, only |
||
> if latest is a newer engine build, runs an advisory canary on it; that canary |
||
| > never adds a version to this table. Older versions (10.2, 11.0) share the same CAS protocol and should | ||
> work but are not exercised in CI. The drivers (pycubrid, |
||
> sqlalchemy-cubrid) themselves run the full 10.2–11.4 matrix in their own |
||
| > repositories. |
Python Versions¶
| Python | Status | Resolved on 2026-10-11 (ci.yml run 38125283280) |
|---|---|---|
| 3.14 | CI-gated representative pycubrid and SQLAlchemy recipes on CUBRID 11.4 | pycubrid 1.10.0, sqlalchemy-cubrid 1.10.0, SQLAlchemy 2.1.4 |
| 3.13 | CI-gated representative pycubrid and SQLAlchemy recipes on CUBRID 11.4 | pycubrid 1.10.0, sqlalchemy-cubrid 1.10.0, SQLAlchemy 2.1.4 |
| 3.12 | Full smoke matrix default and the representative pull-request Python; also in the compatibility matrix | pycubrid 1.10.0, sqlalchemy-cubrid 1.10.0, SQLAlchemy 2.1.4 |
| 3.11 | Minimum; CI-gated representative pycubrid and SQLAlchemy recipes on CUBRID 11.4; also the full release-verification smoke on CUBRID 11.4 | pycubrid 1.10.0, sqlalchemy-cubrid 1.10.0, SQLAlchemy 2.1.4 |
| 3.10 | ❌ Not supported by the current cookbook (upstream end of life 2026-10-01); use an older cookbook/driver combination or upgrade Python | n/a |
| 3.9 | ❌ Not supported | n/a |
The resolved versions column is a dated record, not the current state: what
the four Python compatibility jobs installed in ci.yml run
38125283280
(cookbook main at 27f98d6, 2026-10-11; the Successfully installed line of each
job's setup step). The drivers came from PyPI as unpinned >= resolutions, so a
later run can resolve newer releases. The record covers only the two connect
recipes those jobs run on CUBRID 11.4, not every recipe or server version. It is
refreshed only from a later run's actual output, never with versions no run
installed (see Refreshing version statements).
The ci.yml compatibility matrix covers Python 3.11–3.14 with
fundamentals/pycubrid/01_connect.py and
fundamentals/sqlalchemy/01_connect_and_session.py on separate CUBRID 11.4
jobs on every push to main, the weekly schedule and manual runs. Pull
requests run the 3.11 and 3.14 endpoints only when they change those two
fundamentals, shared live infrastructure or the CI policy; other pull requests
rely on the Python 3.12 their selected live lanes use. Each runs real read-only queries, compares normalized output with its
existing golden, and checks imports and compilation of the example sources.
This is not a claim that every framework/template or every server version is
tested on every Python version; the full smoke matrix remains Python 3.12, except that a
release verification (smoke-test.yml) also runs one full CUBRID 11.4 smoke job on Python 3.11.
Failure, cancellation or a skip the classifier did not select fails the CI gate.
Driver & Framework Versions¶
| Component | Version | Status |
|---|---|---|
| pycubrid | ≥ 1.6.1 | ✅ Required |
| sqlalchemy-cubrid | ≥ 1.0 | ✅ Required for SQLAlchemy recipes (≥ 1.4.2 for the advanced recipes, ≥ 1.5 for the async cubrid+aiopycubrid:// recipe [^async]) |
| SQLAlchemy | 2.0, 2.1 | ✅ Supported range of the dialect; 2.2 is not supported (see below). Async recipes install the sqlalchemy[asyncio] extra for the required greenlet runtime |
| Flask | ≥ 3.0 | ✅ |
| Flask-SQLAlchemy | ≥ 3.1 | ✅ |
| FastAPI | ≥ 0.100 | ✅ |
| Pandas | ≥ 2.0 | ✅ |
| Streamlit | ≥ 1.30 | ✅ |
| Django | ≥ 5.2 | ✅ (minimal recipe; floor checked with manage.py check and the /health, /items views on 4.2, 5.0 and 5.2, CI runs the live bridge suite on the latest release) |
[^async]: The sync SQLAlchemy dialect (cubrid+pycubrid://) works from sqlalchemy-cubrid 1.0. The async dialect (cubrid+aiopycubrid://, used by fundamentals/async/02_async_sqlalchemy.py) first became installable from PyPI in 1.2.3 (its entry points were missing from the 1.2.0–1.2.1 releases and 1.2.2 was yanked; 1.2.1 only shipped the get_pool_class()/create_async_engine() fix), but 1.4.x fails with SQLAlchemy 2.1 (which its metadata allows; the async adapter calls the removed await_ attribute), so fundamentals/async pins ≥ 1.5, the first release the floor lane passes on. The other advanced SQLAlchemy recipes (pandas, Django, dashboard) keep ≥ 1.4.2; fundamentals/sqlalchemy needs ≥ 1.9 (see below).
The fundamentals/orm-basics requirements use
pycubrid>=1.7,<2 and sqlalchemy-cubrid>=1.7,<2.
fundamentals/pycubrid requires pycubrid>=1.9,<2: its
16_batch_error_handling and 20_timezone_datetime goldens assume the
errno-carrying batch errors (#390) and the CAS session kept across
commit() (#468/#472) that first shipped in pycubrid 1.8.0, and its
10_collection_columns binds collections with the typed
Set/Multiset/Sequence parameters that first shipped in pycubrid 1.9.0 (#567).
fundamentals/sqlalchemy requires pycubrid>=1.9,<2 and sqlalchemy-cubrid>=1.9:
07_collection_types binds Python lists/sets to SET/MULTISET/SEQUENCE columns
(sqlalchemy-cubrid 1.9 wrapping them in pycubrid 1.9's typed parameters) and reads
them back decoded through the ?decode_collections=true URL option (pycubrid 1.2.0+).
fundamentals/alembic requires sqlalchemy-cubrid>=1.8: its golden prints the
dialect's runtime transactional_ddl (True since 1.8.0, sqlalchemy-cubrid#503), and its
env.py relies on the automatic CubridImpl registration (sqlalchemy-cubrid#504) instead of
importing sqlalchemy_cubrid.alembic_impl. These
example-specific floors do not change the minimum versions for other recipes
in the table above.
fundamentals/error-handling has goldens but no requirements.txt; its recipes import
only pycubrid and follow the global floor. The connect, crud, transactions,
error-handling/05_error_handling.py and lob-handling topic entries are redirects to
the canonical fundamentals/pycubrid recipes (which require pycubrid>=1.9,<2, so the
LOB recipe also avoids 1.6.x's escaped-newline CLOB values) and carry no goldens or
requirements of their own. As a result the 1.6.1 floor lane now verifies only error-handling, json, reserved-words and the migration recipes, and the 1.7 lane only orm-basics; plain CRUD, transactions, LOB and connect are no longer exercised at those floors, because the canonical recipes require pycubrid>=1.9.
These floors are installed and run, not only checked as text (#241). The
Driver floors (CUBRID 11.4) job in ci.yml runs scripts/driver_floors.py,
which installs each documented floor exactly (pycubrid==X,
sqlalchemy-cubrid==Y, read from the recipes' own requirements.txt) in its own
virtual environment and compares the goldens of representative directories
(the global-floor pycubrid and SQLAlchemy examples, fundamentals/error-handling without a requirements.txt, fundamentals/pandas,
async, sqlalchemy, pycubrid, alembic and orm-basics) with live CUBRID 11.4
output on Python 3.12. It runs on pull requests that change SUPPORT_MATRIX.md,
the floor checker or lane, or one of those directories' requirements.txt, on
the weekly ci.yml schedule and on manual ci.yml runs; other dependencies
(SQLAlchemy, pandas, …) resolve to their latest compatible releases.
tests/test_dependency_floors.py fails if a documented floor is not exercised.
The full example apps under templates/ (flask, api-service-fastapi,
ai-agent, async-worker, batch-etl, and each of their standalone
per-recipe requirements.txt) pin pycubrid>=1.7,<2 — the flask family
(including its recipes) also pins sqlalchemy-cubrid>=1.7,<2 — instead of the
bare global floor above. This is a deliberate, separate policy for installable
apps (they used to install both drivers from git+…@main; see "Templates now
install pycubrid and sqlalchemy-cubrid from PyPI" in the changelog) and is
not itself a correctness floor for any single recipe; scripts/check_dependency_floors.py
treats it as a documented per-family exception rather than drift.
The smoke workflow selects published drivers, constrains all later dependency installs to those exact versions, and verifies their versions and package-index origin after installation. Its Tested upstream versions summary describes the drivers actually exercised, rather than versions recorded before example installs. Conflicting requirements or VCS/local replacements fail the job. Release dispatches also install the exact requested package version, with bounded PyPI publication retry, and verify it after all dependency installs. The final summary includes requested/installed versions, origin, verification commit, actual server version when available, and result even on failed runs. See Release smoke dependencies.
FastAPI recipe templates/api-service-fastapi/recipes/10-cqrs-event-sourcing
also has dedicated Python 3.12 jobs on live CUBRID 11.2 and 11.4 in
ci.yml on the weekly schedule, manual runs and pull requests that change its
requirements.txt or shared live infrastructure; pushes to main and other pull
requests that touch the recipe run the 11.4 job only (#480). They install its own requirements.txt, run pip check, record
installed versions with pip freeze, and execute its seven existing pytest
tests with CUBRID_TEST_URL set. This validates that recipe's pinned framework
versions; the driver ranges still resolve normally. The broad non-PR smoke
suites use a shared dependency list and may install different framework versions.
This page answers three separate questions. The table and the per-recipe floors above are the current support policy: what the cookbook supports and requires. The published package requirements below record what a released artifact declares, which is what pip will install. The dated records say which exact combination a specific run tested, on which date; they are history and are not refreshed automatically (see Refreshing version statements).
Current support policy¶
SQLAlchemy 2.2 is not in the supported range. The cookbook follows the reviewed
sqlalchemy-cubrid support contract, which documents SQLAlchemy 2.0 and 2.1 as supported and
2.2 and later as not supported
(docs/SUPPORT_MATRIX.md at da9c397).
What a package's metadata lets pip install is a separate question from what is
supported, and no cookbook recipe is verified on SQLAlchemy 2.2. Any
forward-compatibility run against a newer SQLAlchemy is advisory only.
Published package requirements¶
Each row is read from the published wheel itself, not from the PyPI JSON
metadata alone: the wheel's METADATA Requires-Dist, with the wheel's SHA-256
checked against the digest PyPI lists for that file. A driver's main branch or
an open release PR is not a published release, and a lookup that fails is "not
verified", so neither appears here.
| Package | Version | Declares | Evidence | Checked |
|---|---|---|---|---|
| sqlalchemy-cubrid | 1.11.0 | sqlalchemy>=2.0,<2.2 |
sqlalchemy_cubrid-1.11.0-py3-none-any.whl SHA-256 594ec50a6294dea5f40dcd557e806af9fb7d8555f6728acf90a774c1d7f355cc |
2026-10-16 |
| sqlalchemy-cubrid | 1.10.0 | sqlalchemy>=2.0,<2.3 |
sqlalchemy_cubrid-1.10.0-py3-none-any.whl SHA-256 76dfe2912e736fd7a532a86bae383002c563b02d5c9569d8a386a87676e30164 |
2026-10-16 |
1.11.0 is the first release whose metadata caps SQLAlchemy below 2.2, matching the support policy. 1.10.0 still lets pip install SQLAlchemy 2.2 once that is released: an installation range, not a support claim.
Dated record: 2026-10-11¶
Kept as written then. On 2026-10-11 the unreleased main of sqlalchemy-cubrid
(checked at a6339df) required sqlalchemy>=2.0,<2.2, and the newest published
release, 1.10.0, still declared sqlalchemy>=2.0,<2.3; the latest SQLAlchemy
on PyPI was 2.1.4. The <2.2 cap shipped in 1.11.0 (see the table above).
Recipe Coverage¶
The table below is the source of truth for recipe counts;
scripts/check_support_matrix_counts.py (run by make check-docs) fails
when a row no longer matches the repository. Verification is split:
- Golden-backed recipes carry stdout goldens (
expected/*.expected) and are checked by the fullmake verifyon CUBRID 11.4 / Python 3.12 onmainand daily, on 11.2 and 11.4 weekly, on manual runs and in release verification, and bymake verify-goldenson 11.4 for pull requests that touch them (fundamentals, migration, the SQLAlchemy quickstart, the first CUBRID tutorial, the batch-etl template, and the reserved-word DDL check behindpitfalls/). - The Flask and FastAPI recipes are covered by pytest suites that the smoke
job runs against its live CUBRID container on 11.4 for every push to
mainand daily, and on 11.2 and 11.4 weekly and on manual runs. Pull requests that touch a Flask or FastAPI recipe run that recipe's own suite on 11.4, in a fresh environment with its ownrequirements.txt. Shared Flask/FastAPI files also run the representative01-basic-crudsuites and the root FastAPI app in addition to the changed recipes; only beyond the cap of 25 changed recipes (above today's suite count) do they stand in for the recipes left out, which the job lists as not validated. Recipe 10 has its own CQRS job. Each suite'sconftest.pyreadsCUBRID_TEST_URL; without it the suites fall back to SQLite for local runs. - The Streamlit recipes are covered by
templates/dashboard/tests/test_dashboard.py(AppTest-based: seeded table viewer rows, non-empty KPI metrics, and a filter that changes the rendered row count). On pushes tomain, daily, weekly and on manual runs, the smoke job's fullmake verifyruns it on its own disposable CUBRID container (CUBRID_DASHBOARD_TEST_URL) for each matrix version (11.4; 11.2 and 11.4 weekly and on manual runs). A pull request that touches the dashboard runs a separateci.ymljob against its own CUBRID 11.4 container. It falls back to a shared SQLite file whenCUBRID_TEST_URLis unset. Itsconftest.pydrops its owncookbook_dashboard_salesandcookbook_dashboard_productsin FK-safe order; isolation—not test ordering or cross-recipe deletion—keeps that fixture away from other suites' tables (#142). The dashboard tables no longer share a name with Flask recipe 01 (#327). - The FastAPI quickstart has an offline pytest suite in PR CI for host and
Compose configuration, startup, HTTP responses,
/health, and database failure cleanup. It uses mocked DB-API connections rather than a live server. Its live suite (tests/test_live.py) checks/healthand CRUD responses against the stored rows through a separate connection. The smoke job's fullmake verifyruns it on 11.4 for every push tomainand daily, and on 11.2 and 11.4 weekly, on manual runs and in release verification, and a pull request that changes the quickstart (other than Markdown) runsmake verify-quickstartin the CUBRID 11.4 smoke lane. - The five AI agent scripts run via
templates/ai-agent/tests/test_live.pyagainst live 11.4 on every smoke-test trigger and on 11.2 too weekly, on manual runs and in release verification (the fullmake verify, thenmake verify-ai-agentagain on the same database), and twice withmake verify-ai-agenton 11.4 for pull requests that touch them. Each script runs with a 60-second limit; the suite drops its own example tables before and after each case and runs twice to verify repeatability. Independent database reads compare the persisted sessions, messages, tool calls, RAG documents, chunks, retrieval log and chat rows with explicit expectations after one and two runs, and the MCP case checks the protocol handshake, the structured table list and that the rejectedDROP TABLEleaves the table and its rows intact.make verify-ai-agent(and the fullmake verify) runs the live suite fail-closed (no URL, an unreachable database or a skip fails).templates/ai-agent/tests/test_offline.pychecks ID handling, MCP errors and cleanup. - The Celery async-worker database tasks run through
templates/async-worker/testswithout a Redis broker or worker. A dedicated CUBRID 11.4 job gates pull requests that touch it; the non-PR smoke matrix runs the same tests in its fullmake verify(11.4 onmainand daily, 11.2 and 11.4 weekly, on manual runs and in release verification). The suite asserts persisted job state/result and report/email rows, refuses preexisting worker tables, and removes its own tables afterward. Broker delivery and Celery's result backend are not tested. - The Django template uses Django for HTTP requests and SQLAlchemy for
CUBRID persistence; it has no native Django CUBRID backend or Django ORM
model.
templates/django/testsgates the existing/healthand/itemsbridge on a dedicated live 11.4 job for pull requests that touch it and in the fullmake verifyof the non-PR smoke matrix (11.4 onmainand daily, 11.2/11.4 weekly, on manual runs and in release verification). The suite refuses a preexistingcookbook_django_itemstable and removes only its own table. - The migration and performance suite (
performance/tests/test_benchmarks.py: the five Java-to-Python migration goldens, the three benchmarks and their cleanup on failure; timings are parsed, never compared) runs in the smoke job's fullmake verify(11.4 onmainand daily, 11.2 and 11.4 weekly, on manual runs and in release verification), and asmake verify-performancein the CUBRID 11.4 smoke lane of pull requests that changeperformance/or the test module. - CUBRID 11.4 runs in every CI smoke run; 11.2 runs in the weekly, manual and
release smoke runs (its
make verifygoldens are checked on both versions there).
| Category | Recipes | Verified by |
|---|---|---|
| pycubrid fundamentals | 22 | make verify (CI, 11.2 + 11.4) |
| SQLAlchemy fundamentals | 7 | make verify (CI, 11.2 + 11.4) |
| SQLAlchemy ORM basics | 6 | make verify (CI, 11.2 + 11.4) |
| Pandas fundamentals | 6 | make verify (CI, 11.2 + 11.4) |
| Error-handling topic | 3 | make verify (CI, 11.2 + 11.4) |
| Async + Alembic + JSON + Isolation | 5 | make verify (CI, 11.2 + 11.4) |
| Java-to-Python migration | 5 | make verify (CI, 11.2 + 11.4) |
| CUBRIDdb-to-pycubrid migration | 4 | make verify (CI, 11.2 + 11.4) |
| SQLAlchemy quickstart | 1 | make verify (CI, 11.2 + 11.4) |
| First CUBRID tutorial | 4 | make verify (CI, 11.2 + 11.4) |
| Pandas batch-etl template | 5 | make verify (CI, 11.2 + 11.4) |
| Pitfalls: reserved-word DDL | 1 | make verify (CI, 11.2 + 11.4) |
| Flask templates | 11 | pytest (CI on main + daily 11.4; weekly 11.2 + 11.4) |
| FastAPI templates | 12 | pytest (CI on main + daily 11.4; weekly 11.2 + 11.4) |
| FastAPI quickstart | 1 | offline pytest (make test, PR CI); live pytest (make verify: CI on main + daily 11.4, weekly 11.2 + 11.4; make verify-quickstart: PR CI 11.4 when touched) |
| AI agent template | 5 | live pytest, two runs (make verify: CI on main + daily 11.4, weekly 11.2 + 11.4; make verify-ai-agent: PR CI 11.4 when touched) |
| Streamlit templates | 5 | pytest (CI on main + daily 11.4, weekly 11.2 + 11.4; PR CI 11.4 when touched) |
| Django template | 1 | Django HTTP + SQLAlchemy bridge: live pytest (PR CI 11.4 when touched; main + daily 11.4, weekly 11.2 + 11.4); no native Django ORM |
| Celery async-worker template | 1 | database tasks: live pytest (PR CI 11.4 when touched; main + daily 11.4, weekly 11.2 + 11.4); broker/worker manual |
| Total | 105 | 69 golden-backed via make verify on 11.2 + 11.4; Flask, FastAPI, Streamlit and Django bridge pytest suites in CI; async-worker database tasks and AI agent suite live-tested; Celery broker/worker and native Django ORM are not covered |
Known Limitations by Version¶
| Issue | CUBRID 11.2 | CUBRID 11.4 | Workaround |
|---|---|---|---|
| CARDINALITY() broken | ❌ | ❌ | Use COUNT(*) + TABLE() unnest |
| Reserved word errors | ⚠️ Cryptic error | ⚠️ Cryptic error | Use double-quotes or rename |
| No RETURNING clause | ❌ | ❌ | Use LAST_INSERT_ID() |
| DDL is transactional only while autocommit is off | By design | By design | Keep autocommit off (pycubrid default) for migrations; commit promptly, since uncommitted DDL holds schema locks |
| Duplicate index on indexed columns | ❌ | ❌ | Drop index=True on primary key / unique columns |
Verified live on CUBRID 11.2 and 11.4 (this cookbook) and on 10.2 and 11.4 (sqlalchemy-cubrid's transactional-DDL tests).
See KNOWN_ISSUES.md for details and workarounds.
Docker Images¶
# docker-compose.yml — 11.2 by default; set CUBRID_VERSION to select another image
image: cubrid/cubrid:${CUBRID_VERSION:-11.2}
How to Test Against a Specific Version¶
Image selection does not migrate database data. The commands below retain the existing volume; use fresh isolated test data when version compatibility has not been established, rather than implicitly deleting an existing database.
# Stop the current service without deleting its data, then select a version:
make docker-down
CUBRID_VERSION=11.4 make docker-up
# Omit CUBRID_VERSION for the default 11.2; make docker-up uses bounded readiness probes.
# Run all tests
# Flask recipe tests use live CUBRID when CUBRID_TEST_URL is set and fall back
# to temporary SQLite databases when it is not
pip install flask flask-sqlalchemy pycubrid sqlalchemy-cubrid httpx pytest
export CUBRID_TEST_URL="cubrid+pycubrid://dba@localhost:33000/testdb"
( cd templates/flask && for d in */tests; do python3 -m pytest "$d" -q; done )
# FastAPI recipe tests use live CUBRID when CUBRID_TEST_URL is set and fall back
# to in-memory SQLite when it is not
pip install fastapi sqlalchemy pycubrid sqlalchemy-cubrid "email-validator>=2" httpx pytest pytest-asyncio
export CUBRID_TEST_URL="cubrid+pycubrid://dba@localhost:33000/testdb"
( cd templates/api-service-fastapi/recipes && for d in */tests; do python3 -m pytest "$d" -q; done )
# Streamlit dashboard recipe tests (AppTest) use live CUBRID when
# CUBRID_TEST_URL is set and fall back to a shared SQLite file when it is not
pip install -r templates/dashboard/requirements.txt pytest
export CUBRID_TEST_URL="cubrid+pycubrid://dba@localhost:33000/testdb"
python3 -m pytest templates/dashboard/tests -q
# Run fundamentals
for f in fundamentals/pycubrid/*.py; do python3 "$f"; done
for f in fundamentals/sqlalchemy/*.py; do python3 "$f"; done
for f in fundamentals/pandas/*.py; do python3 "$f"; done
The pytest suites create and drop their tables in whichever database
CUBRID_TEST_URL points at. Every example directory declares its own distinct,
cookbook_-prefixed table names, each with one set of columns. The guard,
tests/test_table_prefix.py, reads ORM models, Table(...) and to_sql(...)
calls and raw CREATE TABLE text (including f-strings over module constants) in
every example file outside tests/ directories (#327). The Flask/FastAPI suites
can therefore run concurrently against one instance. CI still runs the dashboard
suite against a separate database/container, because its fixture drops and
recreates cookbook_dashboard_products and cookbook_dashboard_sales (a foreign
key links them); the dashboard fixture only deletes tables in its own database.