Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
Tags v0.1.0 and v0.2.0 predate this file and have no sections.
[Unreleased]¶
Upgrade notes¶
- The
templates/api-service-fastapiandtemplates/dashboardCompose files publish only on localhost (#469) — the CUBRID broker (33000, whose labdbaaccount has no password) is now127.0.0.1:33000, and the API (8000) and the Streamlit dashboard (8501) default to127.0.0.1too, so a stack started from an earlier file stops answering from other machines. Your browser on the same host is unaffected. To expose the application on purpose, setAPI_HOST_IP=0.0.0.0(API) orDASHBOARD_HOST_IP=0.0.0.0(dashboard) when you rundocker compose up; the broker has no such switch. The API template's CUBRID service also gains the named volumescubrid_dataandcubrid_databases: the firstdocker compose upafter updating starts from an empty database (the old data stays in the unnamed volumes of the earlier containers until you remove them), after whichdocker compose downkeeps the data and onlydocker compose down -vdeletes it. - The recipe Compose files no longer fix a host or container name, so a volume created by an earlier file needs a one-time recovery (#463) —
quickstart/5min-fastapi,quickstart/5min-sqlalchemy,templates/api-service-fastapiandtemplates/dashboarddrophostname:(cubrid-fastapi,cubrid-sqlalchemy,cubrid-api-service,cubrid-dashboard) andcontainer_name:(cubrid-db,fastapi-service,cubrid-dashboard) and setCUBRID_DB_HOST: localhoston the CUBRID service, as the root lab does since #436. A database created by an earlier file still names its old host indatabases.txtand fails to open with-353in the new container; the data is kept. In that recipe's folder, apply the recovery fromGETTING_STARTED.md("A lab created before the October 2026 lab changes"):docker compose up -d cubrid, rewrite the host tolocalhostindatabases.txtwith thesedcommand shown there,docker compose restart cubrid, then start the stack. Each recipe README repeats these steps. Do not rundocker compose down -vto recover; it deletes the data. Containers are now named by Compose, so address them as services (docker compose exec cubrid ...,docker compose logs cubrid); the dashboard upgrade commands that useddocker inspect cubrid-dashboardnow usedocker compose ps -aq cubrid. - The root lab no longer has a fixed container name or host name, so an existing lab needs a one-time recovery (#436) —
docker-compose.ymldropscontainer_name: cubrid-cookbookandhostname: cubrid-cookbookand setsCUBRID_DB_HOST: localhost, which registers a new database forlocalhost. The container is now named by Compose (for examplecubrid-cookbook-python-cubrid-1): address it as the service,docker compose exec cubrid ..., instead of by the old container name. A database created by an earlier file still names its old host (cubrid-cookbookor a container ID) indatabases.txtand fails to open with-353in the new container; the data is kept, andGETTING_STARTED.md("A lab created before the October 2026 lab changes") gives the recovery: rewrite the host tolocalhostand restart, after first copying an 11.4 database from its unnamed volume if the lab predates #385. Do not runmake docker-cleanordocker compose down -vto recover; they delete the data. make clean,make checkandmake verify VERIFY_PATHS=...changed meaning (#437) —make cleannow removes only generated files, caches and test results (.live-evidence/,__pycache__,.pytest_cache,.ruff_cache, the staged docs pages andsite/) and never touches Docker; to delete the lab's containers and volumes, database data included, run the newmake docker-clean(make downstill stops the lab and keeps the data).make checkis now the non-test quality gate (lint,format-check,check-docs,check-coverage) and no longer runs the offline tests: runmake check testfor the full offline gate, as CI now does.make lintruns onlyruff check;ruff format --checkmoved to the newmake format-check, and the newmake formatis the only target that rewrites files.make verify VERIFY_PATHS=<dirs>on the command line, kept as the golden-only check in #416 / #434, now fails with a one-line message: runmake verify-goldens VERIFY_PATHS=<dirs>instead. A baremake verify(an exportedVERIFY_PATHSincluded) is unchanged, the full live verification.make verifyis now the full live verification (#416) — the golden-only check thatmake verifyran before is nowmake verify-goldens(make verify VERIFY_PATHS=<dirs>on the command line still runs only those goldens). A baremake verifyalso runs every live pytest suite and needs two dedicated, empty CUBRID databases inCUBRID_TEST_URLandCUBRID_DASHBOARD_TEST_URLbesides themake updatabase;CONTRIBUTING.md("Live verification") shows how to start them. Runmake depsagain first: it now installs every example's requirements andpytest-asyncio.- A local lab started from the earlier
docker-compose.ymlneeds a one-time recovery (#385) — the new lab file recreates thecubrid-cookbookcontainer with a fixed host name and named volumes, and an existing database then fails to open with-353, because CUBRID recorded the old host name. The data is kept:GETTING_STARTED.md("A lab created before the October 2026 lab fix") gives the tested recovery for a default 11.2 lab (rewrite the host indatabases.txt, then restart) and for an 11.4 lab (copy the database from its unnamed volume into the new named volume first). Do not usedocker compose down -vto recover; it deletes the data.
Added¶
- Live runs write
test-results/verify/with one versioned summary (#462) —make verifyand everymake verify-*lane (scripts/live_gate.py) now also writetest-results/verify/(ignored by git, removed bymake clean) with thescripts/report_results.pyhelpers thatmake testuses:summary.json(schema_version, an overallstatusofPASS,FAILorINCOMPLETE,scope/partial, total counts, and per lane its status, whether itexecuted, counts, JUnit file, logs and reasons; a lane that never ran isINCOMPLETE,"executed": falseand innot_run),environment.json(command, Python and platform, package versions, CUBRID server versions with password-hidden URLs, commit, CI run identifiers), one merged JUnit file per pytest lane underjunit/<lane>.xml, the lane logs underlogs/<lane>/andsummary.md, a Markdown rendering for a CI step summary. The last console line is built fromsummary.json.make verifystarts from an empty directory; a focused lane replaces only its own files and merges the other lanes recorded for the same commit, so consecutive lanes end with one partial summary. Passwords fromCUBRID_TEST_URLandCUBRID_DASHBOARD_TEST_URL(raw, percent-encoded or XML-escaped) are now replaced with***in every file oftest-results/verify/and.live-evidence/, and a test pins that none reaches a result file..live-evidence/keeps its layout, files and exit codes for at least one release becausesmoke-test.ymland the pull-request smoke jobs read and upload it. make testwrites JUnit files and a versionedsummary.json(#461) — every run of the offline suites now writestest-results/test/(ignored by git, removed bymake clean): one JUnit file per suite underjunit/andsummary.jsonwithschema_version, astatusofPASS,FAILorINCOMPLETE, totals and per-suite counts (selected,passed,failed,errors,skipped,xfailed,xpassed, each test counted once with its worst outcome), the suites not run after a failure, and"coverage": {"measured": false}. The run ends with a one-line summary built from the same data. pytest suites use--junitxml; the unittest suites run through the newscripts/unittest_junit.py, which keeps the console output and exit status ofpython -m unittestand writes the same JUnit shape; the shell-script suite counts as one check. The offline suites measure no coverage, so nocoverage.xmlis written and no coverage gate is added. The suite list, console output, stop-at-first-failure behavior and exit status are unchanged;scripts/report_results.py(stdlib XML parsing, likescripts/live_gate.py) is reusable by the live side, andtests/test_report_results.pyjoinsmake test. CI does not publish the files yet.- CUBRIDdb-to-pycubrid migration guide with four runnable recipes (#445) — the new
migration/cubriddb-to-pycubrid/lesson moves a Python application from the officialCUBRIDdbC-extension driver to pycubrid, separately from the Java-to-Python guide. Its README covers installation (a pinned cubrid-python source build versuspip install pycubrid, and why the 2014CUBRID-Python9.3.0 PyPI archive is no substitute), connection mapping (CCI DSN to keyword arguments, default userpublicversusdba, autocommit ON versus OFF,threadsafety2 versus 1, charset), parameter handling (CCI typed binding versus driver-side literal conversion, the pycubrid 1.12 opt-inexecutemany(..., prepared=True)batch), rows, LOBs,errno/code/sqlstateversus the officialargstuple, transaction failures, disconnects with an unknown outcome, the native features ordinary pycubrid does not offer, and decision points that link the dated cubrid-benchmark comparisons without quoting numbers. Official-driver statements are read from cubrid-pythonv11.3.0.51and labelled as not run; there is no drop-in or full CCI parity claim.01_connection_mapping.py,02_parameterized_crud.py,03_fetch_and_errors.pyand04_transaction_visibility.pyeach create a markedcookbook_cdb_*table, refuse to touch an unmarked table of the same name, and drop their table infinally; their goldens run inmake verify(pycubrid>=1.6.1, same output on 1.6.1 and 1.13.1). The guide is linked fromREADME.md,llms.txt,GETTING_STARTED.mdand the site navigation (staged asmigration-cubriddb-to-pycubrid), with a Korean translation indocs/ko/migration-cubriddb-to-pycubrid.md.SUPPORT_MATRIX.mdgains a "CUBRIDdb-to-pycubrid migration" row (4 recipes; 105 in total, 69 golden-backed), andTHIRD_PARTY_LICENSES.mdlists the newrequirements.txt. - Partial golden and lane runs are labeled
PARTIAL(#460) —make verify-goldens(scoped byVERIFY_PATHSor not) now ends withPARTIAL: checked only the goldens under: <roots>after the unchangedResults:line, and a focused lane (make verify-quickstart,verify-performance,verify-web,verify-django-celery,verify-ai-agent,verify-dashboard,verify-templates) printsPARTIAL: checked only the <lanes> lane(s), repeats it in itssummary.txtand records"partial": truein.live-evidence/<lane>/summary.json. The fullmake verifyis the only run that records"partial": false(in its lane summaries andverify-summary.json) and its goldens part prints noPARTIALline (scripts/verify_goldens.shreadsVERIFY_SCOPE=full, set only byscripts/live_gate.py all; an ambient value is ignored). No golden or suite is added, removed or skipped.tests/test_live_gate.pyandtests/test_make_commands.pypin the label, the evidence field and that a scoped run cannot carry the full marker. - FastAPI quickstart
GET /healthand live CRUD tests (#420) —quickstart/5min-fastapi/app.pygainsGET /health, which runsSELECT 1 FROM db_rooton a new connection and returns{"status":"ok","database":"ok"}, or HTTP503with{"status":"error","database":"unavailable"}and no error details when the database cannot be reached. The mocked suite covers both answers. The newquickstart/5min-fastapi/tests/test_live.pydrives the app withTestClientagainst a running CUBRID named byCUBRID_TEST_URLand checks each answer against the table through a separate pycubrid connection: startup and/health,POST /itemswith the committed row,GET /itemsandGET /items/{id},404for a missingid,422for three invalid bodies with no row written, a rejected over-long insert that leaves no row, and503from a real connection attempt to a closed port.get_conn()now passesconnect_timeout=10, so/healthanswers503instead of hanging on an unreachable host. Tests write only rows marked with a randomlive-qs-prefix and delete exactly those, so repeated runs pass.make testleavestest_live.pyout, so it never needs or writes to a database; run on their own, the live tests skip withoutCUBRID_TEST_URL, while a configured but unreachable database fails.make verify-quickstart(and the fullmake verify) runs them withCUBRID_LIVE_GATE=1(a missing URL also fails) and writes JUnit and a summary to.live-evidence/quickstart/(ignored by git). The quickstart README, its Korean page andSUPPORT_MATRIX.mddocument the endpoint and the command. CI does not run the live suite yet. - Release verification also runs on Python 3.11 (#238) —
smoke-test.ymltakes the Python version from a newpythonmatrix axis (["3.12"]). A release verification (repository_dispatch, aworkflow_dispatchpinned to a package, or aworkflow_callfrom a driver's release workflow) adds exactly one cell, CUBRID 11.4 on Python 3.11, the supported minimum, running the same full job (make verify, AI-agent, Flask/FastAPI, async-worker, Django and dashboard suites, offline guards). Push tomain, the daily schedule and latest-release manual runs keep the two Python 3.12 cells, so no schedule and no other run costs more; the release adds one job (about the length of an 11.4 cell). The caller contract is unchanged: same inputs and outputs, the existing job names (Smoke Tests (CUBRID 11.2)/(CUBRID 11.4)) and artifact names stay, the new cell isSmoke Tests (CUBRID 11.4, Python 3.11)with the artifactrelease-verification-part-cubrid-11.4-py3.11. Each reportmatrixentry gains apythonfield (scripts/release_smoke.py summary --python), and a failure reason names the cell (CUBRID 11.4, Python 3.11: …); a release report now also fails unless exactly the three expected cells (11.2/3.12,11.4/3.12,11.4/3.11) each reported once, naming any missing, duplicate or unexpected cell, and every cell must have passed with the requested version (this also closes the earlier gap where a missing 11.2 or 11.4 part went unnoticed); non-release runs are unchanged, and the Python is left out of a cell name for the default 3.12. Job hygiene is unchanged (60-minute timeout, pinned actions,persist-credentials: false,contents: read).tests/test_release_verification.pyandtests/test_release_dispatch.pyfail if the extra cell is wrong or unconditional, the expected cell set drifts from the workflow matrix, a cell is missing or duplicated, the report matrix is not sorted,summary --pythonis not recorded, the Python is hard-coded again, the artifact names collide, orinputs.reaches a step.CONTRIBUTING.mdandSUPPORT_MATRIX.mddescribe the cell. - Stricter guard tests for the driver lanes (#239, #241 review follow-up) —
tests/test_ci_scope.pynow asserts exact lines instead of substrings: the caller'sif: github.event_name == 'schedule', the activity-guard comparison (-ge "$ACTIVE_DAYS"),ACTIVE_DAYS: "8", the commit checkok &= got == wantand the report job's default-branch clause. A new test ties thedriver-main.ymlRECIPESlist to the directories ofscripts/driver_floors.pyFLOOR_SETS(parsed, not hard-coded), so both lanes cover the same recipes. The workflow comment andCONTRIBUTING.mddocument that an open tracking issue is closed only by the next run that executes, and that a manual dispatch closes it when both drivers are idle. No workflow logic changed. - Whole-second timestamps normalize like fractional ones (#265) —
scripts/normalize_output.shnow maps{{DATE}} HH:MM:SSand{{DATE}}THH:MM:SSwith no fractional part to{{DATETIME}}, so goldens such asfundamentals/pycubrid/09_serial_order_numbers.pyno longer fail about 1 run in 1000 when aDATETIMElands on.000and Python'sstr(datetime)drops the fraction. Values followed by a UTC offset,Zor an uppercase zone name (the fixed20_timezone_datetimeoutputs) stay unscrubbed, and every existing golden normalizes unchanged.scripts/test_normalize_output.shcovers whole-second, millisecond and microsecond variants for both separators. - Documented driver floors are installed and run, not only checked as text (#241) — the new
Driver floors (CUBRID 11.4)job inci.ymlrunsscripts/driver_floors.py, which installs every documentedpycubrid/sqlalchemy-cubridfloor exactly (==, read from the recipes' ownrequirements.txt) in one virtual environment per floor set and diffs 60 representative goldens against live CUBRID 11.4 on Python 3.12: the global floor (pycubrid==1.6.1,sqlalchemy-cubrid==1.0), the advanced SQLAlchemy floor (1.4.2,fundamentals/pandas),fundamentals/async, the1.7line (fundamentals/connect,orm-basics,lob-handling), the1.9line (fundamentals/pycubrid,fundamentals/sqlalchemy) andfundamentals/alembic. It is one job (about 3 minutes) selected by pull requests that changeSUPPORT_MATRIX.md, the floor checker or lane, or a lane directory'srequirements.txt, by the existing weeklyci.ymlschedule and by manualci.ymlruns; it does not run on pushes tomain, adds no schedule and is enforced byCI Gateonly when selected. The golden directories without arequirements.txt(fundamentals/crud,error-handling,lob-handling,transactions) are now covered, andscripts/check_dependency_floors.pyaccepts extras (sqlalchemy-cubrid[pycubrid]>=1.0) and spaced specifiers.tests/test_dependency_floors.pyfails when a documented floor is not installed by any floor set, a directory-specific floor runs on another directory, the lane pins drift from the requirements, or a floor change would not select the lane. Floors raised where the lane failed:fundamentals/asyncnow needssqlalchemy-cubrid>=1.5(1.4.x allows SQLAlchemy 2.1, where its async adapter calls the removedawait_attribute; 1.5.0 is the first passing release), andfundamentals/lob-handlingis documented as needingpycubrid>=1.7(1.6.x returns CLOB values with escaped newlines). - Startup smoke test for the root FastAPI template app (#243) —
templates/api-service-fastapi/tests/test_root_app.pyimports the realapp.main, runs its lifespan and checks/healththrough FastAPI'sTestClient, using a throwaway SQLiteDATABASE_URLso it needs no CUBRID. Previously CI only byte-compiledapp/, so an import error or a missing dependency such aspydantic-settings(declared in the template'srequirements.txtbut absent from the shared smoke install list) could ship. Both the requiredpytest-suiteslane inci.yml(selected by anytemplates/api-service-fastapi/*change) and the non-PRsmoke-test.ymlsuites now install the template's ownrequirements.txtand run this suite in its own process.tests/test_ci_scope.pycovers the lane selection and workflow wiring. SQLite does not exercise CUBRID-specific SQL; the recipe suites continue to do that. make depsand a time-limited, classifiedmake verify(#235, #236) —make depsinstallspytest,sqlalchemy-cubrid[pycubrid]and therequirements.txtof every golden-backed example (a directory owningexpected/) in one pip resolver call that honoursPIP_CONSTRAINT; the discovery lives in the newscripts/example_requirements.py, whichscripts/release_smoke.py install-examplesnow shares. It validates every path before printing any, andmake depsfails (installing nothing) when a golden-backedrequirements.txtpath contains whitespace.make verifyfirst checks that the selected examples' requirements are installed (failing once with "runmake depsfirst"), then runs each script through the new portable, POSIX-onlyscripts/run_example.py(stdin from/dev/null, own process group; the whole group, including grandchildren, is killed on timeout, when the script exits and when the runner gets SIGINT, SIGTERM or SIGHUP; exit 124 plus arun_example: timed out afterline on timeout; no GNUtimeout; on a non-POSIX system it exits with "run_example.py requires POSIX process groups") underVERIFY_TIMEOUTseconds (default 60; the slowest golden took 7.2 s on CUBRID 11.4). Results are classified as✓ PASS,✗ MISMATCH(diff),✗ EXEC-ERROR(exit status and last 20 lines; 128 + N for a script killed by signal N),⏱ TIMEOUT(last 20 lines; only when exit 124 comes with the runner's timeout line, so a script that itself exits 124 is anEXEC-ERROR),✗ NORMALIZE-ERROR,✗ READ-ERRORor? SKIP; the summary keepsN passed, N failed, N skippedand adds a count per failure class, and under GitHub Actions each failure emits an::error file=...::annotation.GETTING_STARTED.md,docs/quickstart.mdanddocs/ko/getting-started.mdnow installsqlalchemy-cubrid[pycubrid]in section 2, list theai-agenttemplate, and verify a checkout with a Python 3.11+ venv →make up→make deps→make verify; CONTRIBUTING documents both targets.tests/test_make_commands.pycovers themake depsfile list (pip replaced by a recorder) and its whitespace failure, the timeout and each failure class, a script exiting 124, a script killed by SIGTERM (exit 143),/dev/nullstdin, and that no script or grandchild outlives a timeout, a normal exit, or SIGTERM/SIGHUP to the runner.- Selectable local CUBRID version (#169) —
CUBRID_VERSION=11.4 make upselects the root Compose image while retaining the 11.2 default and the existing bounded readiness helper. Version-switch instructions no longer require editing YAML, a fixed sleep, or automatic volume deletion; image selection is not a database-format upgrade. - Advisory lane against pycubrid and sqlalchemy-cubrid
main(#239) — the new.github/workflows/driver-main.ymlinstalls both drivers from theirmainbranches (pinned to the commits resolved at the start of the run;direct_url.jsonis checked after install) together with the requirements of 15 representative golden directories, then runsmake verifyon them against live CUBRID 11.4 on Python 3.12, so a driver regression shows up before it is released instead of after. It is advisory only: the recipe job hascontinue-on-error, no pull request runs it, andCI Gatedoes not depend on it. A failure opens or updates one tracking issue (ci: cookbook recipes failing against driver main, labelci), which is closed automatically when the recipes pass again — the reporting pattern of sqlalchemy-cubrid's upstream canary. No new schedule: the existing weeklyci.ymlschedule calls it (jobdriver-main-advisory), skipped when neither driver'smainchanged in the last 8 days, and it can be dispatched manually. It runs only in the upstream repository.tests/test_ci_scope.pyfails if the lane joins the gate, gains its own schedule or a pull-request trigger, losescontinue-on-error, or stops installing the drivers from theirmaincommits. - Representative Python compatibility matrix (#130) — required PR/main CI checks Python 3.11–3.14 on CUBRID 11.4 using the existing read-only pycubrid connection and SQLAlchemy engine/session goldens, plus imports and source compilation. This small matrix does not duplicate the full Python 3.12 smoke/framework suite, and failure, cancellation or unexpected skip blocks the CI gate.
- Offline contributor command —
make checkcombines lint, documentation, normalizer, golden-coverage, and isolated offline regression suites;make test-offlineruns the suites without a database. CI uses the same command, and both live smoke jobs run the Make/readiness guards before driver selection. - AI agent template (#111) —
templates/ai-agent/with 5 examples: agent state management (sessions, messages, tool calls with JSON columns), MCP tool chain (programmatic server invocation), RAG metadata hybrid (document store with SET tagging + SEQUENCE chunk tracking), agent loop (query → think → act → observe), and AI chatbot backend (SQLAlchemy ORM with JSON LLM I/O). Requirescubrid-mcp-server>=0.3. - One-command dashboard demo —
templates/dashboard/now ships its owndocker-compose.yml(CUBRID 11.4 + Streamlit at http://localhost:8501), and the five recipes readDATABASE_URLfrom the environment (default unchanged:cubrid+pycubrid://dba@localhost:33000/testdb) so the compose service can point them at the co-located container. - Streamlit dashboard pytest suite (#103) —
templates/dashboard/tests/test_dashboard.pyusesstreamlit.testing.v1.AppTestto exercise all 5 dashboard recipes: a parametrized smoke test that every recipe runs without raising, plus assertions that the seeded table viewer renders rows, the KPI page's four metric cards are non-empty, and narrowing the category filter shrinks the rendered dataframe. The suite runs against a live CUBRID instance whenCUBRID_TEST_URLis set and otherwise falls back to a shared SQLite file for local/offline runs;conftest.pymakes the recipes' CUBRID/MySQL-styleAUTO_INCREMENTDDL SQLite-compatible via a dialect-gated SQLAlchemy event hook, without changing the recipes themselves, and drops the demo tables in FK-safe order before and after the session.SUPPORT_MATRIX.md's "Streamlit templates" row moves from "manual run" to this pytest suite. This replaces the unwired, CWD-relativetests/test_dashboard.pyadded by #120. - Gate the Streamlit dashboard pytest suite in CI (#142) — Every pull request runs
templates/dashboard/testsin its own required live-CUBRID 11.4 job, separate from the Flask/FastAPI database. On pushes tomain, nightly, and manual smoke runs, each existing 11.2/11.4 matrix job first finishes the Flask/FastAPI suites, then stops their container and runs the dashboard suite in a separate disposable CUBRID container of the same version. This removes the shared-schema FK failure (cookbook_salesreferencescookbook_products) without deleting another recipe's tables or relying on suite order. The dashboard's own fixture still drops its tables in FK-safe order, and its SQLite-onlyAUTO_INCREMENThook is scoped to its test URL and removed even when teardown fails.SUPPORT_MATRIX.mdanddocs/index.mddescribe the trigger-specific coverage and isolation. - Live async-worker database task checks (#129) —
templates/async-worker/testsinvokes the existing sales aggregation, report, and batch email tasks directly without Redis, then confirms CUBRID persisted their job states, JSON results, reports, and email logs. The suite requires an empty dedicated database, fails on a configured connection error or preexisting worker tables, and removes only its own tables. A separate required CUBRID 11.4 PR job and the non-PR 11.2/11.4 smoke matrix gate this database path; broker delivery and Celery's result backend remain manual coverage. - Live Django SQLAlchemy-bridge checks (#128) —
templates/django/testsexercises the existing Django HTTP/healthand/itemsviews against CUBRID, then updates and deletes the persisted row through the template's SQLAlchemy bridge. It does not add a native Django CUBRID backend, ORM model, or migration. The suite rejects non-CUBRID or unreachable configured URLs and preexistingcookbook_items, cleans up only its own table, and runs in a separate required CUBRID 11.4 PR job plus the non-PR 11.2/11.4 smoke matrix after colliding examples. - CUBRID-distinctive SQL feature recipes (6 new pycubrid scripts):
fundamentals/pycubrid/17_window_functions.py— ROW_NUMBER/RANK/DENSE_RANK, LAG, running SUM over partitions (with deterministic tie-breakers)fundamentals/pycubrid/18_recursive_cte.py—WITH RECURSIVEnumber series and hierarchy path building (contrast with 08 CONNECT BY)fundamentals/pycubrid/19_pagination.py—LIMIT/OFFSETvs CUBRID-idiomaticFOR ORDERBY_NUM() BETWEEN, plus theROWNUMcaveatfundamentals/pycubrid/20_timezone_datetime.py—DATETIMETZ/DATETIMELTZnative reads andSET TIME ZONE;TIMESTAMPTZrendered via server-sideTO_CHARto sidestep pycubrid#289fundamentals/pycubrid/21_enum_type.py— ENUM declaration-order sorting,col + 0ordinal, out-of-set rejectionfundamentals/pycubrid/22_date_formatting.py—TO_CHAR/TO_DATEdate and number formatting with visible fixed-width padding- v1.6.x feature recipes (8 new scripts):
fundamentals/async/— pycubrid.aio + SQLAlchemy async enginefundamentals/alembic/— programmatic Alembic migration with CubridImplfundamentals/json/— native JSON columns, JSON_EXTRACT/UNQUOTE patternsfundamentals/isolation-levels/— 6 CUBRID levels + dirty-read demofundamentals/sqlalchemy/07_collection_types.py— SET/MULTISET/SEQUENCE ORMfundamentals/pycubrid/15_cursor_memory_bound.py— fetch_size + tracemallocfundamentals/pycubrid/16_batch_error_handling.py— executemany_batch error paths- Python examples: FastAPI, Django, Flask, SQLAlchemy, pycubrid, Pandas, Celery, Streamlit (already in the v0.1.0 tag, which predates this changelog)
- llms.txt for AI agent discoverability
- PRD with Example-first Design Philosophy
Changed¶
- The template Compose files no longer publish the CUBRID broker on all interfaces, and the API template keeps its database (#469) — see the Upgrade note.
tests/test_quickstart_config.pypins that no quickstart or template Compose file publishes the broker without a127.0.0.1host IP and that the API template's CUBRID service mounts both data paths. The READMEs (and the dashboard's Korean page) say what is published and how to reset the data. - The recipe Compose files drop their fixed host and container names (#463) — see the Upgrade note. Published ports and their exposure are unchanged (the two quickstarts bind
127.0.0.1; the API-service and dashboard templates keep their existing all-interface33000/8000/8501bindings).tests/test_quickstart_config.pynow fails if a quickstart or template Compose file setshostname:orcontainer_name:, or a CUBRID service losesCUBRID_DB_HOST: localhost. - Canonical Docker lifecycle targets and a portable lab service (#436) —
make docker-upstarts the root Compose servicecubrid(databasetestdb) and waits for it,make docker-downstops and removes its containers and network and keeps the volumes, andmake docker-clean(unchanged) also deletes the volumes;make upandmake downremain aliases, andmake demousesdocker-up. The broker is published on127.0.0.1:${CUBRID_TEST_PORT:-33000}:CUBRID_TEST_PORTmoves the lab, for example to run a second one under its ownCOMPOSE_PROJECT_NAME, while the examples, their goldens and the examples-database lanes still connect tolocalhost:33000.CUBRID_DB_HOST: localhostreplaces the fixed host name, so a container recreated bydocker-downanddocker-upreopens the kept database; this was verified live on CUBRID 11.2 and 11.4 (a row written beforedocker-downread back afterdocker-upin a new container), as was the recovery of labs from the #385 file (11.2 and 11.4) and of a pre-#385 11.4 lab.scripts/agent_eval.py run --liveprobes the image andcubrid_relthrough the Compose service instead of thecubrid-cookbookcontainer name and drops an inheritedCUBRID_TEST_PORT. README,GETTING_STARTED.md,CONTRIBUTING.md,SUPPORT_MATRIX.md,AGENTS.md, the first-cubrid and quickstart pages, the fundamentals READMEs and their Korean pages show the lifecycle commands and the service name.tests/test_make_commands.pypins the Compose contract (onecubridservice,testdb, theCUBRID_VERSIONimage, the localhost port binding,CUBRID_DB_HOST, nocontainer_name/hostname, both named volumes), runs the lifecycle targets and their aliases against a fakedocker, and fails if a tracked file still addresses the old container name. - Makefile targets have predictable semantics (#437) —
make test(aliasmake test-offline) is onescripts/run_offline_tests.pyinvocation; the script holds the suite list, runs each suite in its own process as before and now also runsmake test-normalize. Themake verify-goldensrecipe is onescripts/verify_goldens.shinvocation: the golden-run and result-classification shell moved there unchanged (per-golden lines,VERIFY_RESULTSrows,::errorannotations, timeouts, theResults:summary and exit status).make helpgroups the targets into database, setup and cleanup, offline tests, live verification, quality checks and documentation.ci.ymlrunsmake check test,driver-main.ymlrunsmake verify-goldens VERIFY_PATHS=..., andscripts/agent_eval.py run --livecleans up withmake docker-clean(ormake cleanon a task base SHA that predates it). The focusedmake verify-*lanes are unchanged. make testandmake verifyare the two test commands (#416) —make testruns the unit and mocked suites with no database or Docker (make test-offlinestays as an alias, andmake checkruns it).make verifywithoutVERIFY_PATHSis now the full live verification:scripts/live_gate.py allfirst checks every database URL, server and example dependency, then runs every golden (make verify-goldens, the former golden-onlymake verify) and every live pytest suite in a fixed order, one at a time: the FastAPI quickstart, migration and performance on themake updatabase (localhost:33000/testdb, which the examples hard-code), the Flask/FastAPI, Django/Celery and AI agent suites onCUBRID_TEST_URL, and the dashboard onCUBRID_DASHBOARD_TEST_URL. Both URLs must name dedicated, empty databases, distinct from each other and from themake updatabase. It fails closed on an unreachable database, a SQLite or other non-cubrid+pycubridURL, a missing dependency, a skipped or empty suite, a suite that leaves or drops tables, and a failed golden, and writes.live-evidence/verify-summary.jsonwith golden and pytest results reported separately, the CUBRID server, Python and package versions and the commit.make verify VERIFY_PATHS=<dirs>keeps the golden-only check of those roots and now says it is partial.make depsinstallspytest-asyncio(which the FastAPI recipe suites import) and every example'srequirements.txt(golden-backed examples and the live suites) in one pip call. The live lanes added earlier in this release are now feature-namedmake verify-*targets (verify-quickstart,verify-performance,verify-web,verify-django-celery,verify-ai-agent,verify-dashboard,verify-templates), the gate variable isCUBRID_LIVE_GATEand evidence goes to.live-evidence/<lane>/.make demorunsverify-goldens.- The performance benchmarks print the workload they ran (#423) —
performance/fetch-optimization/benchmark.pyprints the rows each fetch strategy returned,performance/bulk-insert/benchmark.pyprints aSELECT COUNT(*)of the rows each strategy stored (taken after its timer stops) instead of the configured count, andperformance/connection-pooling/benchmark.pyprints how many connections each strategy opened (SQLAlchemyconnectevent). Timings, strategies and table cleanup are unchanged; the three READMEs describe the new output. - The FastAPI and SQLAlchemy quickstart labs behave like the root lab (#386) —
quickstart/5min-fastapi/docker-compose.ymlandquickstart/5min-sqlalchemy/docker-compose.ymlnow publish their ports on127.0.0.1only (the lab'sdbaaccount has no password), give the database container a fixed host name (cubrid-fastapi,cubrid-sqlalchemy) and keep the database in two named volumes (cubrid_data,cubrid_databases), sodocker compose downkeeps the data and onlydocker compose down -vdeletes it. The FastAPIapiservice gains a health check on/openapi.json: without it,docker compose up --waitreturned while Uvicorn was still starting and the first request gotConnection reset by peer. The image (cubrid/cubrid:11.2), the databasetestdb, the port numbers and the apps are unchanged. A quickstart container created from the earlier files kept its data in unnamed volumes; to start clean, rundocker compose down -vin that quickstart's folder before you update.tests/test_quickstart_config.pyfails if a quickstart publishes a port beyond127.0.0.1, loses its fixed host name or a named volume, or the FastAPI API loses its health check. - The outbox recipe's ack and fail require the lease attempt (#339) — in
templates/flask/08-transactional-outbox,POST /api/outbox/<id>/ackand/failidentified the caller's lease only byprocessor_idand comparedattemptswith the value the request read, so a stale request sent after the sameprocessor_idleased the message again settled the newer lease. Request change: both endpoints now requirelease_attempt, theattemptsvalue/outbox/leasereturned (a missing value returns 409, a boolean or other non-integer value 400). The conditional UPDATE comparesattemptswith that value, a stale attempt returns 409 ("this lease is stale") and leaves the newer lease untouched, and/faildecides dead-letter versus requeue from it. New tests lease asworker-1(attempt 1), expire the lease, lease again asworker-1(attempt 2) and send the attempt-1 ack and fail (409, attempt 2 intact, no attempt row; 200 on the previous code), then settle attempt 2 normally; the in-window race test also runs with a same-processor re-lease, and a test covers the 400 cases. The recipe README documents the request body. - Docs-sync allowlist entries must record a reason (#174) —
scripts/check_docs_sync.pyload_allowlist()used to drop everything after#and keep the path, so an entry with no reason (performance/foo) was silently accepted althoughscripts/docs-sync-allowlist.txtasks for one. It now followsscripts/check_expected_coverage.py: blank lines and full-line#comments are still ignored,path # reasonis accepted (the reason may contain further#), and an entry without a reason or with an empty one raises an error naming the file and line, which makes the gate exit 1 instead of reporting success.tests/test_docs_sync.pycovers each case, the non-zero exit and the repository's own allowlist; the allowlist header andCONTRIBUTING.mdstate the rule. - Compose files drop the obsolete
versionkey (#173) — the rootdocker-compose.yml,quickstart/5min-fastapi/docker-compose.yml,quickstart/5min-sqlalchemy/docker-compose.ymlandtemplates/api-service-fastapi/docker-compose.ymlno longer start withversion: "3.8"/"3.9". Compose v2 ignores the key and warns that it is obsolete;docker compose confignow prints no warning. Services, images, ports and volumes are unchanged. - Flask recipes 02 and 07 get their own table names (#277) — Flask 02 (
02-categories) now createscookbook_flask_categories(its self-referencing and productForeignKeystrings follow) instead ofcookbook_categories, which the root FastAPI template app keeps, and Flask 07 (07-vendor-feed) now createscookbook_feed_productsinstead ofcookbook_products, which Flask 01 keeps. No recipe shares a table with another any more, so the FastAPI root app with Flask 02, and Flask 01 with Flask 07, can run their live-CUBRID suites concurrently against one instance. Routes and API responses are unchanged. A database created by an earlier version keeps the old tables; the two recipes create new ones, so drop the old Flask 02 and Flask 07 tables by hand if they are no longer needed.KNOWN_SHAREDintests/test_table_prefix.pyis now empty, so the guard is a strict no-sharing check across recipes;SUPPORT_MATRIX.mddrops the sequential-run advice for these suites (the dashboard suite still needs its own database because it reusescookbook_products). - FastAPI recipes 09-12 and Flask recipes 09-11 prefix their tables with
cookbook_(#139) — the 23 tables these seven recipes created without a prefix (orders,users,roles,documents,inventory_items, …) are nowcookbook_<name>, with the 12 matchingForeignKeystrings, asAGENTS.mdandCONTRIBUTING.mdalready required. Generic names could collide with a reader's own tables.inventory_itemswas shared by FastAPI recipe 09 and Flask recipe 11, so FastAPI 09 now uses the distinctcookbook_saga_inventory_items(Flask 11 keepscookbook_inventory_items), and likewisecookbook_saga_orders(FastAPI 09) andcookbook_rbac_documents(Flask 09) avoid the names FastAPI 02 and FastAPI 07 use and their live-CUBRID suites can run concurrently against one instance. Routes, URL paths and API responses are unchanged. A database created by an earlier version keeps its old tables; the renamed recipes simply create new ones, so drop the old tables by hand if they are no longer needed. The newtests/test_table_prefix.py(inmake test-offline) fails when any__tablename__orTable("…")intemplates/**/models.pylacks the prefix or when a new table name is declared by two recipes.templates/ai-agent/(agent_*,rag_*,chat_*tables, with a test and README that name them) is left for a separate change. - Five more topic recipes are thin redirects to the canonical pycubrid recipes (#149) —
fundamentals/connect/01_connect.py,fundamentals/crud/02_crud.py,fundamentals/transactions/03_transactions.py,fundamentals/error-handling/05_error_handling.pyandfundamentals/lob-handling/06_lob.pywere byte-for-byte copies offundamentals/pycubrid/01_connect.py,02_crud.py,03_transactions.py,05_error_handling.pyand06_lob.py, goldens included, somake verifycounted each recipe twice. Following the #79 approach, the topic entries now print a pointer to the canonical recipe, theirexpected/goldens are removed (fundamentals/error-handlingkeeps its three other goldens), andconnect,crud,transactionsandlob-handlingare allowlisted inscripts/docs-sync-allowlist.txt; topic paths and discovery are unchanged. Because the redirects import nothing,fundamentals/connect/requirements.txtis removed, and the driver-floor lane (scripts/driver_floors.py), thedriver-main.ymlrecipe list,scripts/check_dependency_floors.py,scripts/ci_scope.pyandSUPPORT_MATRIX.mdno longer list the removed goldens (coverage of connect, CRUD, transactions and LOB at the 1.6.1 and 1.7 floors is dropped: the canonical recipes requirepycubrid>=1.9and run only in the 1.9 line). The Recipe Coverage table drops the combined "Connect, CRUD, errors, transactions, LOB" row (8 goldens) for an "Error-handling topic" row (3), taking the totals from 102 recipes / 66 golden-backed to 97 / 61. - Performance figures re-measured on pycubrid 1.10.0 (#245) — the figures under
performance/quoted pycubrid 0.5.0+16a8634 (undated) on CUBRID 11.2. The fetch, bulk-insert and connection-pooling numbers are now the median of 5 runs of each recipe's ownbenchmark.pyon pycubrid 1.10.0 / sqlalchemy-cubrid 1.10.0 / CUBRID 11.4.6 (Intel i5-9400F, Python 3.12, 2026-10-09), each with its range. The "19% faster than afetchone()loop" claim is removed: on 1.10.0fetchall()and afetchone()loop both took 52 ms for 10K rows. The 0.5.0 before/after and row-scaling tables are kept but labelled historical, since the scripts cannot reproduce them. - Collection recipes show current read and bind behaviour (#242) —
fundamentals/sqlalchemy/07_collection_types.pyno longer says collection columns read back as opaque bytes and cannot be bound: it adds?decode_collections=trueto the engine URL (pycubrid 1.2.0+), binds Python lists/sets to theSET/MULTISET/SEQUENCEcolumns with plaininsert()/update()(sqlalchemy-cubrid 1.9 wraps them in pycubrid 1.9's typedSet/Multiset/Sequenceparameters) and readsfrozenset/listvalues withselect(), replacing the hand-built{...}literals and theTABLE()unnest read helper.fundamentals/pycubrid/10_collection_columns.pyconnects withdecode_collections=True, bindsSet/Multiset/Sequencethrough?placeholders (including empty collections and a typedSUBSETEQfilter), and prints the decoded column values instead of reading only predicate results through a bytes/latin1 workaround. Goldens for both were regenerated and checked on CUBRID 11.4. Floors raised:fundamentals/pycubridnow needspycubrid>=1.9,<2(was>=1.8,<2) andfundamentals/sqlalchemyneedspycubrid>=1.9,<2andsqlalchemy-cubrid>=1.9(were>=1.6.1and>=1.4.2), because typed collection binding first shipped in pycubrid 1.9.0;SUPPORT_MATRIX.md,CONTRIBUTING.md,scripts/check_dependency_floors.pyand its test record the new floors. Reading withdecode_collectionsalone would have needed only the older floors.templates/ai-agentkeeps itspycubrid>=1.7floor and default raw-bytes connection; its comment and README no longer point at a removed limitation. Both recipes avoid the known CUBRID collection/NUMERICIFcrash (pycubrid#675). - Flask 01-11 and FastAPI 09-12 templates no longer use deprecated
datetime.utcnow(#246) — each recipe now produces naive UTC throughdatetime.now(timezone.utc).replace(tzinfo=None)in a local helper (naive_utc_now(), orutcnow_naive()in FastAPI 12), and the*_atvalues the Flask 04, 05, 07 and 10 apps and the FastAPI 09 routes wrote explicitly as aware datetimes now use it too, so stored timestamps are consistently naive UTC like CUBRIDDATETIMEcolumns. This also fixes FastAPI09-sagaPOST /orders/{order_key}/recover, which subtracted a naive storedupdated_atfrom an aware clock and raisedTypeError; it now has tests for the 409 and success paths. FastAPI11-rate-limitercompared an awareutcnow()against naive stored window starts; its route-levelutcnow()now delegates tomodels.naive_utc_now. Column types are unchanged.tests/test_template_timestamps.py(run bymake test-offline) fails on any.utcnowattribute undertemplates/and runs every helper under a non-UTCTZ, requiring a naive result within seconds of the real UTC time. - License inventory records the pinned docs tools (#253) —
scripts/build_license_inventory.pyno longer listsmkdocs-materialandpymdown-extensionsunpinned inTOOLING; it reads.github/requirements-docs.txtas its ownDOCSrequirement set, soTHIRD_PARTY_LICENSES.mdrecords the versionsdocs.ymland theci.ymldocs-site job install (mkdocs1.6.1,mkdocs-material9.7.7,pymdown-extensions12.1). Regenerated with the recorded--exclude-newer 2026-10-09T00:00:00Z: no package or version changed, only the set column of the docs tools and their transitive dependencies (TOOLINGbecomesDOCS).tests/test_third_party_licenses.pynow fails if a docs tool reappears inTOOLING, if a pin is not exact, or if an inventoried docs-tool version differs from its pin. Transitive docs dependencies stay resolved, not pinned. - Pinned documentation build tools (#247) —
.github/requirements-docs.txtpinsmkdocs,mkdocs-materialandpymdown-extensionsto the versions the strict site build uses today.docs.ymland theci.ymldocs-site job both install from it, so an upstream release can no longer breakmkdocs build --strictwithout a repository change, and Dependabot now updates the pins. A stdlib test guards the file and both install steps. No theme or content change. - Django template floor raised to the supported LTS (5.2) (#244) —
templates/django/requirements.txtandSUPPORT_MATRIX.mdnow requiredjango>=5.2: the supported LTS (5.2); 4.2/5.0 are past upstream end of support. The template uses onlyJsonResponse,path(),csrf_exemptand the test client, somanage.py checkand the/healthand/itemsviews were checked offline (SQLAlchemy engine swapped for SQLite) on Django 4.2, 5.0 and 5.2 with no difference. The live CUBRID bridge job still installs the latest Django. - Least-privilege workflow tokens and no persisted checkout credentials (#237) —
ci.ymlnow declares top-levelpermissions: contents: read; the reusabledoc-lintcall explicitly grants thecontents: readandpull-requests: readthat workflow declares. Everyactions/checkoutstep in every workflow setspersist-credentials: false. The Dependabot auto-merge job now also requiresgithub.event.pull_request.user.login == 'dependabot[bot]'(not only the actor) and holds itscontents: write/pull-requests: writegrants at job level only.tests/test_workflow_permissions.py(inmake test-offline) fails if a workflow loses its top-level permissions, a checkout persists credentials outside the push allowlist, or the auto-merge guard weakens. The repository's default workflow permissions setting is unchanged and remains a maintainer decision. - Tiered CI validation; no duplicate PR live matrices (#222) — the new
scripts/ci_scope.pyclassifies everyci.ymlrun from its event and changed paths, and the live CUBRID lanes now start only when selected: docs-only pull requests start no live CUBRID; a recipe pull request runs only its family's live lane (the representative Flask/FastAPI01-basic-crudsuites, dashboard, async-worker, Django, CQRS) on CUBRID 11.4 / Python 3.12; golden examples and AI-agent changes runmake verifyscoped to the touched examples. Shared live infrastructure (Makefile, Compose, normalizer, readiness/coverage scripts) and CI-policy changes fan out to every lane, CUBRID 11.2 + 11.4 and the Python 3.11/3.14 endpoints; changes tofundamentals/pycubridorfundamentals/sqlalchemyadd the endpoints; recipe 10'srequirements.txtruns CQRS on both versions. Pushes tomain, a new weekly schedule and manualci.ymlruns keep Python 3.11–3.14 and CQRS on 11.2 + 11.4.smoke-test.ymlno longer runs on pull requests: the PR golden/AI-agent/MCP smoke moved into twoci.ymljobs (shared steps in.github/actions/pr-smoke) that keep the required check namesSmoke Tests (CUBRID 11.2)andSmoke Tests (CUBRID 11.4), so branch protection is unchanged and a skipped lane reports skipped.smoke-test.ymlkeeps the broad push/nightly 11.2/11.4 run and its exact release verification inputs, outputs and fail-closed reporting; the daily schedule stays because 10 of 37 recent scheduled runs caughtmake verifyfailures, mostly on days without amainpush. The cubrid-mcp-server tool-contract smoke moved verbatim intoscripts/mcp_smoke.py, shared by both workflows.CI Gate (all checks must pass)is a fail-closed allow-list: a lane may be skipped only when the classifier output for it is exactlyfalse; a failure, cancellation, unexpected skip or failed classification fails it.tests/test_ci_scope.py(run bymake check) covers the classifier, the gate and the workflow wiring. Measured on real runs and the last 40 merged pull requests: a docs-only PR goes from 21 jobs (12 live) / ~24 billed minutes to 10 jobs (0 live) / ~10, a Dependabot recipe bump to 11 jobs (2 live) / ~11, and the 40-PR total from ~960 to ~734 billed minutes;CONTRIBUTING.md"CI tiers" has the full table andSUPPORT_MATRIX.mdnow states which tier/event backs each "CI-verified" claim. Dependabot grouping is unchanged. - CODEOWNERS routing for high-blast-radius surfaces (#223) — new
.github/CODEOWNERSroutes review of workflows (including the reusablesmoke-test.ymlrelease verification contract),dependabot.yml,SECURITY.mdand shared validation/golden infrastructure (Makefile,scripts/,tests/) to the maintainers; recipes are intentionally unowned and the file is routing only, not a security boundary.SECURITY.mdgains a durable supported-versions statement and notes that private vulnerability reporting is not enabled (email remains the private route). Repository settings are unchanged. - Python 3.11 or later required (#220) — Python 3.10 reached upstream end of life on 2026-10-01, so the cookbook's current support range is now 3.11–3.14: the README badge and prerequisites,
SUPPORT_MATRIX.md,CONTRIBUTING.md,llms.txtand recipe READMEs say 3.11+, Ruff targetspy311, and Python 3.10 is removed from the required representative compatibility matrix (no jobs were added; real-query/golden checks continue on 3.11–3.14). Migration: Python 3.10 users need an older compatible cookbook/driver combination (earlier cookbook commits and thepycubrid/sqlalchemy-cubridreleases they pin) or must upgrade Python. Standalone reciperequirements.txtfiles were audited and contain no explicit Python 3.10 constraints; framework and driver floors are unchanged, and teaching examples were not rewritten for 3.11 syntax. Historical entries and measurements are preserved. - Recipe dependency floors aligned with the documented support contract (#152) — several standalone
requirements.txtfiles pinnedpycubridwell below the documentedSUPPORT_MATRIX.mdfloor (migration/java-to-python,quickstart/5min-fastapiat>=0.5; two FastAPI recipes at>=0.2.6;fundamentals/pandas,fundamentals/sqlalchemy,templates/dashboard,templates/djangoat>=1.3.2;fundamentals/isolation-levels,alembic,json,asyncandpitfalls/reserved-wordsat the stale>=1.6.0), andquickstart/5min-sqlalchemyleft both drivers unbounded. Every standalone FastAPI and Flask reciperequirements.txt(templates/api-service-fastapi/recipes/*,templates/flask/*) also listedpycubrid/sqlalchemy-cubridwith no version at all, even though each recipe installs its ownrequirements.txtindependently per its README. All of these now pin the documented floor for their category: the global minimum (pycubrid>=1.6.1,sqlalchemy-cubrid>=1.0), the>=1.4.2advanced-SQLAlchemy floor, or the>=1.7,<2/>=1.8,<2per-recipe/per-template exceptions already inSUPPORT_MATRIX.md— which now also documents thetemplates/family's>=1.7,<2convention explicitly. The newscripts/check_dependency_floors.py(run bymake check) fails on a bare driver requirement, a floor below its documented minimum, or an undocumented custom pin, so this can't silently drift again;tests/test_dependency_floors.pycovers it. CONTRIBUTING documents the policy. Release-smoke VCS/direct-URL contamination is unaffected: it is already handled separately by the existing driver-selection guard (#147/#161, documented in CONTRIBUTING's "Release smoke dependencies"). - Dependabot version updates configured; workflow actions aligned (#153) —
dependabot-auto-merge.ymlexisted without any.github/dependabot.yml, so no scheduled version updates were ever opened. The new config checks GitHub Actions weekly (one grouped PR) and every standalone reciperequirements.txtmonthly, grouped by dependency name so a package bump is one PR across all recipes rather than one per recipe directory; CUBRID driver major updates are ignored because their floors are theSUPPORT_MATRIX.mdcontract, andversioning-strategy: increase-if-necessarykeeps Dependabot from raising a recipe's own floor on a minor/patch bump when the existing range already allows the new version. PR titles and squash commits use thechore(deps):prefix, and the existing auto-merge workflow (patch/minor only, gated on the required checks) is unchanged.ci.yml,smoke-test.ymlanddocs-sync.ymlnow use the sameactions/checkoutv7.0.1 andactions/setup-pythonv7.0.0 SHA pins asdocs.yml(previously v4.4.0/v5.6.0 there).tests/test_dependabot_config.py(run bymake check) fails when a reciperequirements.txtfalls outside the configured directories, the commit prefix changes, the pip versioning strategy changes, or an action is not SHA-pinned or pinned to different versions across workflows. CONTRIBUTING documents the dependency maintenance policy. - Unified issue, pull request and commit titles —
CONTRIBUTING.mdnow defines the cubrid-lab title rule (type(scope)!: descriptionwithfeat,fix,docs,test,perf,refactor,ci,build,chore,style,revert; no bracket prefixes or issue numbers), andAGENTS.md, the pull request template and both issue forms point to it. The issue forms now prefillfix:(bug report) andfeat:(example request) instead of[Bug]:and[Example]:. The new unprivilegedPR titleworkflow (.github/workflows/pr-title.yml) checks every pull request title, andtests/test_pr_title.py(run bymake test-offline) exercises its embedded validator. - Manual release smoke runs pin the requested release (#177) —
smoke-test.yml'sworkflow_dispatchnow takespackage(latest,pycubrid,sqlalchemy-cubrid,cubrid-mcp-server) andversion(MAJOR.MINOR.PATCHorvMAJOR.MINOR.PATCH) inputs.scripts/release_smoke.pyreads them from the event JSON and routes a pinned manual run through the same exact PyPI install, bounded retry, constraint pinning, package-index origin check and job summary as an upstreamrepository_dispatch, so the upstreamRELEASING.mdfallback (gh workflow run smoke-test.yml -R cubrid-lab/cubrid-cookbook-python -f package=<package> -f version=<X.Y.Z>) verifies the exact release instead of floating latest. Invalid or unavailable requests fail; a manual run with default inputs keeps the ordinary latest-release smoke behavior and is reported as not a release verification. - Release verification runs are identified by request id and report the installed version (#187) —
smoke-test.ymlaccepts an optionalrequest_id(^[A-Za-z0-9._-]{8,80}$) in therepository_dispatchpayload and as aworkflow_dispatchinput, read and validated from the event JSON byscripts/release_smoke.py. A release verification run is namedRelease verification <package>@<version> [request_id=<id>], so the upstream release workflows (cubrid-lab/pycubrid#539) can find exactly their run through the Actions API, and the request id joins the non-cancelling concurrency group. A newRelease verification reportjob combines both CUBRID jobs into a job summary, step outputs and arelease-verification-<request_id>artifact (release-verification.json, schema version 1: requested and installed version, status, reasons, run and per-job details) and fails when a job failed or installed a different version. The contract is documented in CONTRIBUTING; the bounded publication retry and the no-fallback rule are unchanged. - Package release workflows can call the smoke test without a token (#190) —
smoke-test.ymlnow also runs onworkflow_call, so the release workflows inpycubrid,sqlalchemy-cubridandcubrid-mcp-servercan run it as a job with their ownGITHUB_TOKEN(uses: cubrid-lab/cubrid-cookbook-python/.github/workflows/smoke-test.yml@<sha>; inputspackage,version,request_id; outputsstatus,requested_version,installed_version,artifact) instead of a cross-repository dispatch token. Each job detects the call (job.workflow_refdiffers from the caller'sgithub.workflow_ref), checks outcubrid-lab/cubrid-cookbook-pythonat the called workflow's own commit, andscripts/release_smoke.pyreads the call inputs fromRELEASE_INPUT_*environment variables instead of the caller's event JSON, with the same exact-install, bounded-retry, no-fallback and requested == installed rules. Release verification concurrency groups now also include the run, so a called run never shares or cancels the caller's group.repository_dispatchandworkflow_dispatchare unchanged; the call contract is documented in CONTRIBUTING. - Release smoke verifications are no longer cancelled by other runs (#177) —
smoke-test.yml's concurrency group now includes the event name, requested package and version/ref, andcancel-in-progressis off for release verifications (repository_dispatch, orworkflow_dispatchwith a non-latestpackage), so a push tomainor a second release no longer cancels an in-flight release check. Push, PR, schedule and unpinned manual runs still cancel superseded runs of the same kind. - Removed stale nested Flask recipe copies (#136) — deleted the seven duplicated
01-basic-cruddirectories from the category templates while retaining the canonicaltemplates/flask/01-basic-crud/recipe. Thanks @jackgedge. batch-etlquiets Alembic's plugin-discovery logging —templates/batch-etl/04_etl_pipeline.pyconfigures the root logger at INFO.sqlalchemy-cubrid1.8.0 (#504) imports its AlembicDefaultImplwhen the CUBRID dialect loads and Alembic is installed, and Alembic >=1.18 logssetup plugin ...at INFO fromalembic.runtime.pluginson import — noise from Alembic's own plugin discovery, not the recipe's ETL output, that only surfaces when Alembic happens to be installed alongside this example (e.g. the fullmake verify/ smoke-test run, which also installsfundamentals/alembic's requirements into the same environment).configure_logging()now sets thealembiclogger toWARNINGafterbasicConfig()so the recipe's own INFO lines stay unaffected. The golden output is unchanged;batch-etlis the only template that configures INFO-level root logging vialogging.basicConfig, so no other recipe is affected.- pycubrid 1.8.0 goldens —
fundamentals/pycubrid/16_batch_error_handlingnow expects no exception-detail suffix: pycubrid 1.8.0 keepserrnoon batch errors (#390), sostr(exc)ends with(errno=-670, description=..., sqlstate='23000'), which the existing normalizer rule strips (1.7.x printed only(sqlstate='23000')).fundamentals/pycubrid/20_timezone_datetimenow expects DATETIMELTZ in the pinnedAsia/Seoulsession zone (+09:00, same instants): pycubrid 1.8.0 keeps the CAS session acrosscommit()(#468/#472), whereas 1.7.x reconnected on commit and the read ran in a fresh UTC session. The recipe's comments that described UTC-aware LTZ reads as driver behavior are corrected. These goldens assume pycubrid >= 1.8.0, sofundamentals/pycubrid/requirements.txtis raised frompycubrid>=1.7,<2topycubrid>=1.8,<2(documented consistently in its README andSUPPORT_MATRIX.md);fundamentals/connectandfundamentals/orm-basicskeeppycubrid>=1.7,<2, since their goldens do not depend on either behavior. - Make commands fail closed —
make verifyrejects missing/empty roots, zero golden targets, orphaned goldens, and discovery/read errors.make upbounds in-container readiness probes and failure diagnostics, preserving containers and data for inspection. Offline command tests use fake recipes and Compose processes, not live database substitutes. - FastAPI recipe tests can run against live CUBRID (#122): every
templates/api-service-fastapi/recipes/*/tests/directory now has aconftest.pythat provides the databaseenginefixture. It connects toCUBRID_TEST_URLwhen set and falls back to in-memory SQLite otherwise, points the recipe's ownDATABASE_URLat the same test database, drops and recreates the recipe's tables around each test (so an interrupted run cannot break the next one), and reports which database is in use in the pytest header. The 12test_main.pyfiles lose their copy-pasted SQLite engine setup, and recipes 11 and 12 no longer forceDATABASE_URLto SQLite. All 82 tests pass on CUBRID 11.2, CUBRID 11.4, and the SQLite fallback. The recipe READMEs, the template README, andSUPPORT_MATRIX.mddocumentCUBRID_TEST_URL. - Templates now install
pycubridandsqlalchemy-cubridfrom PyPI instead ofgit+…@main— theasync-worker,batch-etl, andflasktemplates'requirements.txtpinned both drivers to themainbranch heads, so a template install could break whenevermainmoved and required a git toolchain at deploy time. They now pinpycubrid>=1.7,<2andsqlalchemy-cubrid>=1.7,<2(the current PyPI releases). Theapi-service-fastapitemplate'spycubrid>=0.0.4floor — a pre-release number predating the first public PyPI release — is raised to the same>=1.7,<2pin. - Flask recipe tests can run against live CUBRID (groundwork for #131): each of the 11
templates/flask/*/tests/directories now has aconftest.pythat provides adatabase_configfixture forcreate_app(). It usesCUBRID_TEST_URLwhen set and falls back to a temporary SQLite file per test otherwise, drops the recipe's tables before and after each test (so an interrupted run cannot break the next one), disables connection pooling so the per-test apps do not leave idle connections on the CUBRID broker, puts the recipe directory onsys.path, and reports which database is in use in the pytest header. The test files pass the fixture tocreate_app()instead of building SQLite URLs. All 89 tests pass on CUBRID 11.2, CUBRID 11.4, and the SQLite fallback. Every recipe README now has test instructions (previously only 01 and 09 did), andSUPPORT_MATRIX.mddocumentsCUBRID_TEST_URL. - Refactored to Python-only repository (removed planned Go and Node.js examples)
Fixed¶
- The pandas clean-and-transform golden passes on pandas 3.1 (#443) —
fundamentals/pandas/03_clean_and_transform.pyprinted its seven-column result withprint(df), which truncates a wide frame to the display width; pandas 3.1.0 changed that truncated layout, so the golden failed on every CI run that installed it. The recipe now printsDataFrame.to_string(), which shows every column and gives the same output on pandas 3.0 and 3.1; the golden is regenerated. - Agent-eval tasks can record
make verify-goldensas their live check (#440) —scripts/agent_eval.pyaccepted onlymake verify ...and pytest commands invalidation.live, so a task based on a commit after #437, where the golden check ismake verify-goldens VERIFY_PATHS=..., failed validation.run --livealso keyed the interpreter override and the goldenResults:grading onmake verifyalone, so such a command would have run withoutPYTHON=and been graded as a pytest suite. Both now treatmake verify-goldenslikemake verify; existing tasks, which recordmake verify VERIFY_PATHS=...for their base SHAs, are unchanged. - The dashboard and FastAPI API service templates keep their database when Compose recreates the CUBRID container (#413) — the image registers the database in
databases.txtfor the container's host name, its random ID, and a recreated container (docker compose up -d --force-recreate, or a changed service configuration) inherits the old anonymous volumes but gets a new ID, so every connection failed with -353. Both templates'cubridservices now set a fixedhostname:, asdocker-compose.ymland the quickstarts do (#385). The dashboard template, on CUBRID 11.4, also mounted its named volume only at/var/lib/cubrid, which 11.4 does not use, so its database lived in an anonymous volume anddownlost it; a second named volume now keeps/home/cubrid/CUBRID/databases, anddocker compose down -vremoves both. Verified live: both templates keepcsql SELECT 1after a forced recreate, and the dashboard keeps a table acrossdownandup.tests/test_quickstart_config.pypins the host names and the dashboard's volumes. - The FastAPI API service template starts with its own Compose file (#412) —
templates/api-service-fastapi/docker-compose.ymlnever reached a healthy database, sodocker compose up --buildstopped withdependency failed to start: container cubrid-db is unhealthyand the API never started. Its health check greppedcubrid service statusforServer is running, which CUBRID 11.2 does not print (Server cubdb (rel 11.2, pid ...)), and the service set noCUBRID_DB, so the image created its defaultcubdbwhile.env.exampleconnects totestdb(-677). The service now createstestdband the health check runscsql -u dba testdb -c "SELECT 1", as the quickstarts do; verified live withcp .env.example .envanddocker compose up --build, after whichGET /healthreturned{"status": "ok", "database": "connected"}.tests/test_quickstart_config.pynow checks that the template's Compose database, its health check and.env.exampleagree. - The SQL echo example no longer depends on the driver's connection setup queries (#410) —
fundamentals/orm-basics/01_engine.pypassedecho=Truetocreate_engine(), so its golden recorded the dialect's first-connect queries (SELECT VERSION(),SELECT SCHEMA()). sqlalchemy-cubridmainadds a backslash-mode probe (SELECT CHAR_LENGTH('\\'), sqlalchemy-cubrid #874), which made the advisory driver-mainlane fail. The example now opens one connection before settingengine.echo = True, so the log and the golden show only the example's own statements; a comment notes thatecho=Trueat creation also logs the setup queries. Verified with released drivers (pycubrid 1.13.0, sqlalchemy-cubrid 1.11.0) and with both drivers'mainagainst CUBRID 11.4. scripts/run_example.pyrejects a non-finite--timeout(#382) — the wall-clock runner behindmake verifychecked onlytimeout <= 0, so--timeout nan,infand an overflowing1e309passed as a "deadline" and reached the child process. It now requires a finite value greater than 0 and exits with the usage errorneeds a finite positive --timeout and a command(status 2) before starting anything. Finite values, including fractions such as0.25, the timeout exit 124, exit-status propagation and process-group cleanup are unchanged. New tests intests/test_make_commands.pyassert that neitherrun()norPopenis reached for these inputs.- The async-worker cleanup task rejects a negative retention period (#383) —
cleanup_old_records(days)computednow - timedelta(days=days)unchecked, sodays=-1moved the cutoff into the future and deleted recent completed Jobs and Reports, then recorded success. It now requires anintof0or more (rejectingbool, floats, strings andNonewithTypeError, negative values withValueError, and values too large to compute a date withValueError), before it records a Job or opens a session.0keeps its existing meaning, every completed record created before now, and the default of 30 days and the selection and deletion SQL are unchanged. New tests intemplates/async-worker/tests/test_retention.pyprove the rejection happens before any database work and, against live CUBRID with a fixed clock, that only completed records strictly older than the cutoff are deleted for 30 and 0 days, and that a rejected call leaves recent records in place. The template README documents the rules. - The async-worker example finishes failed and retried job attempts (#384) — each task committed a
STARTEDJob before its work, but only success, and the last ofsend_notification's retries, updated it. A caught failure, and every retriedsend_notificationattempt, left a row that looked as if it were still running and thatcleanup_old_recordsnever removed. Each Job is now documented as one attempt: a failure after the row exists recordsFAILED, asend_notificationattempt that Celery retries recordsRETRY, both withcompleted_at, and the task then re-raises its own error or Celery's retry signal unchanged.aggregate_salesvalidates its dates before it writes a row. If recording the status fails too, that is logged and the original error still propagates;send_notificationrecords its success outside the retried block, so a failed status update can no longer resend the email. Successful results are unchanged. New live-CUBRID tests intemplates/async-worker/tests/test_job_finalization.pycover an invalid date, workload and batch failures, a failed status update, a retry followed by success and exhausted retries, and the template README documents the Job states and the limits (an unavailable status store or a killed worker still leavesSTARTED). - The local lab keeps a usable database across
docker compose down, and listens on localhost only (#385) — two problems madedownlose the lab. The 11.4 image keeps its databases in/home/cubrid/CUBRID/databases, whichdocker-compose.ymldid not mount, sodownorphaned the database in an anonymous volume. And CUBRID records the host name indatabases.txt, so even a kept database (11.2's/var/lib/cubrid) failed to open in the next container, with a new random host name (-353). A second named volume now keeps the 11.4 path,hostname: cubrid-cookbookkeeps the host name stable, anddocker compose down -vremoves both volumes. This was verified on 11.4 and 11.2 withstop/start,down/upanddown -v. The published port is now127.0.0.1:33000, because the lab'sdbaaccount has no password. CI starts its own CUBRID containers, so CI is unaffected. - A relative link with both a query string and a fragment was staged as a broken link (#361) —
scripts/stage_docs.py_split()tested#before?instead of cutting at whichever delimiter appears first, so a target such asSUPPORT_MATRIX.md?view=raw#versionskept?view=rawinside its path; the repository existence check then looked for a file literally namedSUPPORT_MATRIX.md?view=raw, staging reported a broken link and exited non-zero, andmake docs(the strict docs-site build in CI) failed on it._split()now splits at the first#or?and keeps the remainder in its original order, so anchor-only links (pitfalls/#top), query-only links and links whose fragment itself contains a?are rewritten exactly as before, and a missing local source still produces the broken-link error naming the path.tests/test_stage_docs.pycovers a staged file, a staged directory and the missing-target case; the module doctest pins the mixed query-plus-fragment form. - The collection-types example cleans up after a failed setup (#342) —
fundamentals/sqlalchemy/07_collection_types.pystarted its cleanuptryafterBase.metadata.drop_all(),create_all()and the first prints, so a setup failure left the engine undisposed and could leave the table behind. Thetrynow starts right aftercreate_engine(), as in the other SQLAlchemy examples; output and golden are unchanged. Withcreate_all()forced to fail, the cleanup drop now runs and the engine is disposed of (neither happened before). - Drifted comments, benchmark labels and nondeterministic goldens are corrected (#329) — The READMEs of FastAPI recipes 09–12 used a developer-machine path (
cd /data/GitHub/cubrid-cookbook/python/...); they nowcd templates/api-service-fastapi/recipes/<recipe>from the repository root.fundamentals/parameterized-queries/README.mdsaid pycubrid has no typed parameter payload; it now says that is the default path and thatexecutemany(..., prepared=True)(pycubrid 1.12.0) sends PREPARE plus EXECUTE_ARRAY with typed binds. Docstrings promised demos the code did not contain:03_transactions.pypoints tofundamentals/isolation-levels/for isolation,19_pagination.pystates its ROWNUM caveat as a note, andfundamentals/async/01_async_pycubrid.pynow usesasync withfor the connection and the cursor (its golden adds one summary line; it also passes on thepycubrid==1.6.1floor). Benchmarks:performance/fetch-optimization/benchmark.pyran the same 5-column query asSELECT *under a second name; that slot is now afetchmany(500)loop (49 ms, 47–77, median of 5 on pycubrid 1.12.0;performance/README.mdand the recipe README say which row changed), andbulk-insert/benchmark.pysays its per-row figure is one INSERT plus its COMMIT. Wrong claims:migration/java-to-python/05_batch_operations.pysaid a failed single-commit batch rolls back by itself; it now rolls back in anexceptblock.15_cursor_memory_bound.pyconcluded that a smallerfetch_sizecaps memory while measuringfetchall(); it now streams withfetchmany(10)at each fetch_size, prints afetchall()peak next to them, and its takeaway says peak memory follows the rows kept (the measured fetch_size peaks differ by only a few KB). Unstable goldens:fundamentals/orm-basics/05_dml_extensions.pyrenames its reserved-word columns (key→config_key,value→val,count→cnt) and orders the initial counters by name;13_atomic_counters.pypasses the recorded day to the ranking instead of reading the clock again, so a run that crosses UTC midnight no longer prints an empty ranking. Cleanup:fundamentals/alembic/alembic_programmatic.pyremoves its temp working directory, and it,fundamentals/isolation-levels/01_isolation_levels.pyandfundamentals/sqlalchemy/07_collection_types.pyrun their cleanup in afinallyblock.scripts/mcp_smoke.pyno longer calls v0.2.1 the pinned MCP server version. Goldens were regenerated throughscripts/normalize_output.sh; all 61 pass twice in a row on live CUBRID 11.4. - Leases, chat turns and transfers are checked in SQL (#328) —
templates/flask/08-transactional-outbox:/ackand/failchecked the lease in Python and then wrote the message unconditionally, so a lease that expired and was taken again between the read and the write was overwritten (a second processor's lease markedsent, or requeued, and an attempt row added). Both now settle the message with one conditional UPDATE that checksleased_by,leased_until,statusand theattemptsvalue they read, return 409 unless exactly one row changed, and only then add the attempt row; the README describes it.templates/ai-agent/05_chatbot_backend.pyrecordedturn: 0on every message becauselen(conv.messages)was read from a relationship loaded once while empty; the turn now comes from the loop, the transcript prints it, and the recipe checks that the turns are 0 to 3.migration/java-to-python/04_transaction.pycommitted the debit even when the credit updated no rows (an unknown receiver), so money disappeared; the Python code and the Java comparison now check the credit's row count and roll back, and the demo transfers $50 to an unknown receiver (failed (no receiver), balances unchanged).fundamentals/pycubrid/12_pool_retry_worker.pysaid its claim stopped two workers from taking the same job while its docstring said single-worker, and it read back anyprocessingrow; it now picks a queued id, claims it withUPDATE ... WHERE id = ? AND status = 'queued', retries when the row count is 0, and reads the claimed job back by id; a newClaim racesection has two connections read the same next job, shows that only the first conditional claim wins (True, thenFalse (rowcount 0)) and that the loser picks the next id. New tests run a re-lease on another connection between the endpoint's read and its write (ack and fail: 409 and the second lease intact; both return 200 on the previous code), and the live AI-agent test checks the chat turns. The04_transactionand12_pool_retry_workergoldens were regenerated throughscripts/normalize_output.sh. - Every example table is prefixed and has one schema, and the guard scans all example files (#327) —
templates/ai-agentcreated unprefixed tables (agent_sessions,agent_messages,agent_tool_calls,rag_documents,rag_chunks,rag_retrieval_log,chat_users,chat_conversations,chat_messages); they are nowcookbook_agent_*,cookbook_rag_*andcookbook_chat_*. Thirteencookbook_names were created by several directories with different columns, so running those examples against one database collided (cookbook_saleshad three schemas, and the dashboard's FK tocookbook_productscould block another example'sDROP). One owner keeps each name and the others are renamed:cookbook_salesstays withbatch-etl(dashboard →cookbook_dashboard_sales,fundamentals/pycubrid/11→cookbook_pipeline_sales,17→cookbook_window_sales);cookbook_productsstays with Flask recipe 01 (dashboard →cookbook_dashboard_products,fundamentals/pandas/01→cookbook_pandas_products,fundamentals/pycubrid/04→cookbook_prepared_products,07→cookbook_merge_products,fundamentals/sqlalchemy/04→cookbook_bulk_products,migration/java-to-python/03→cookbook_jdbc_products);cookbook_itemsstays with the FastAPI template app (FastAPI quickstart →cookbook_quickstart_items, Django →cookbook_django_items,migration/java-to-python/02→cookbook_jdbc_items);cookbook_orderswith FastAPI recipe 02 (fundamentals/pandas/02→cookbook_pandas_orders,fundamentals/pycubrid/05→cookbook_error_orders,09→cookbook_serial_orders);cookbook_userswith Flask recipe 09 (fundamentals/pycubrid/02→cookbook_crud_users,fundamentals/sqlalchemy/02→cookbook_orm_users);cookbook_documentswith FastAPI recipe 07 (fundamentals/pycubrid/06→cookbook_lob_documentsandcookbook_lob_files);cookbook_jobswithasync-worker(fundamentals/pycubrid/12→cookbook_retry_jobs);cookbook_taskswith FastAPI recipe 01 (fundamentals/pycubrid/14→cookbook_cascade_accounts,_projects,_tasks,_comments);cookbook_accountswithfundamentals/pycubrid/03(migration/java-to-python/04→cookbook_jdbc_accounts);cookbook_counterswithfundamentals/pycubrid/13(fundamentals/orm-basics/05→cookbook_dml_counters);cookbook_departments/cookbook_employeeswithfundamentals/sqlalchemy/03(fundamentals/orm-basics/02→cookbook_core_*,fundamentals/pycubrid/18→cookbook_cte_employees); andcookbook_ref_authorswithfundamentals/sqlalchemy/06(fundamentals/orm-basics/06→cookbook_reflect_authors/cookbook_reflect_articles, listing tables by thecookbook_reflect_prefix).tests/test_table_prefix.pyused to read onlytemplates/**/models.py; it now parses every*.pyunder the example roots outsidetests/directories (ORM__tablename__with its mapped columns,Table(...), pandasto_sql(...), and rawCREATE TABLEtext including f-strings over module-level constants) and fails when a table lacks the prefix, when two directories declare the same name, or when one name is declared with two column sets (pitfalls/reserved-words/01_reserved_words.pyis exempt with a reason: it creates one table with rejected, corrected and quoted columns). It fails on the previous code and passes now. The renamed goldens were regenerated throughscripts/normalize_output.sh; all 61 goldens, the AI-agent suite (twice), the Django and dashboard suites pass on live CUBRID 11.4.SUPPORT_MATRIX.md, the affected READMEs, the demo recording commands and the CI workflow comments use the new names; no workflow logic changed. - Fundamentals examples, READMEs and goldens match the pycubrid 1.12 APIs (#325) —
fundamentals/json/01_json_crud.pypassed'"light"'toJSON_REPLACE, which stores the quotes as part of the JSON string ("\"light\"", confirmed in csql), and its golden recorded'"light"'; it now binds'light'as a?parameter, prints'light', and the cheat sheet notes that a SQL string becomes a JSON string.fundamentals/pycubrid/README.mddocumentedconn.create_lob("CLOB", data), which raisesTypeError(the signature iscreate_lob(lob_type: int), andexecute()and the defaultexecutemany()cannot bind the returnedLob,ProgrammingError: unsupported parameter type; onlyexecutemany(..., prepared=True)with autocommit off binds a same-connectionLob, since pycubrid 1.12.0); the LOB section now shows what06_lob.pydoes (bindstr/bytes, read back throughpycubrid.lob.Lob, checked live), the API table gives the real signature, and the01_connect.pysample output matches its golden (val, current type codes, placeholders for the server version, host name and date, the multiple-queries block);llms.txtno longer points atcreate_lob().20_timezone_datetime.pyreadsTIMESTAMPTZnatively next toDATETIMETZ/DATETIMELTZ(pycubrid#289 was fixed in 1.7.0, below the directory'spycubrid>=1.9floor) and keepsTO_CHAR(... TZR)for the zone region name.16_batch_error_handling.pydescribed theexecutemany_batchreturn value as(result_code, affected_count); it is(statement_type, result_count), with the statement type aCUBRIDStatementTypevalue. Nine files annotated withpycubrid.Connection/pycubrid.Cursor, which pycubrid does not export (it worked only becausefrom __future__ import annotationsnever evaluates them); they importConnectionfrompycubrid.connectionandCursorfrompycubrid.cursor. The three affected goldens were regenerated throughscripts/normalize_output.shand pass live on CUBRID 11.4. - The CQRS rebuild replays every event into the read model (#326) — in
templates/api-service-fastapi/recipes/10-cqrs-event-sourcing, the projection skipped every state withsequence_no <= last_sequence_no, which also applied to/rebuild, so even{"from_scratch": 1}left a drifted read model unchanged (balance_centsstayed at 1 instead of 4000)._project_read_model()takes a keyword-onlyreplaceflag that/rebuildsets, so both rebuild modes overwrite the row with the replayed state; command projections keep skipping states they already reached. The projection UPDATE keeps its version guard and addslast_sequence_no <= :sequence_no, so a rebuild whose replay was overtaken by a newly committed command returns 409 instead of rewinding the read model. A drift-then-rebuild test for both modes fails on the previous code and passes on SQLite and live CUBRID 11.4, and a race test commits a deposit between the replay and the write; the recipe README explains the difference. - The rate-limiter recipe stores a rotated window's count and checks its reset version in SQL (#323) — in
templates/api-service-fastapi/recipes/11-rate-limiter, a window rotation setcurrent_count = 0only on the Python object (the session usesautoflush=False), and the followingUPDATE current_count = current_count + costadded to the old database value: consume 4, wait past the window, consume 1, and the stored count was 5 instead of 1. The first consume of a new window now writescurrent_count = cost./resetcomparedexpected_versionin Python and then let the ORM flush an unconditional UPDATE, so a consume that committed between the read and the write was lost; it is now a conditional UPDATE withversion = expected_versionin itsWHEREclause and returns 409 unless exactly one row changed. Two new tests fail on the previous code and pass on SQLite and live CUBRID 11.4; the recipe README describes both checks. - Saga recovery no longer repeats completed steps (#322) — in
templates/api-service-fastapi/recipes/09-saga,/recoverresets a stuck order topending, but/executethen ran every step again: after a crash followingreserve_inventory, recover plus execute reserved the stock twice (reserved_qty6 instead of 3) and charged the payment twice./executenow reads which steps are alreadydone(each step's effect commits in the same transaction as itsdonestatus) and skips them, and a later failure still compensates the skipped steps. New tests leave the database as a run that crashed after the reservation, or after the charge, then recover and execute: stock and payment change exactly once, and a failing payment releases the reservation made before the crash. They fail on the previous code and pass on SQLite and live CUBRID 11.4. The recipe README documents/recoverand the resume behavior. - The SQLAlchemy quickstart connects to the database its Compose file creates (#324) —
quickstart/5min-sqlalchemy/docker-compose.ymlcreateddemodbwhileapp.pyconnected totestdb, so following the README failed at connect. The Compose file and its health check now create and probetestdb, the README starts it withdocker compose up -d --waitand ends withdocker compose down, andapp.pydrops each of its twocookbook_tables on its own and disposes of the engine in afinallyblock, so a run leaves nothing behind even after a failure. The newtests/test_quickstart_config.py(inmake test-offline) fails when any quickstart'sCUBRID_DB, health-check database and app connection drift apart. The app's output and its golden are unchanged. - Renaming
README.mdno longer skips the Korean translation requirement (#319) —scripts/check_translation_sync.pyand the changed-file list indocs-sync.ymlrangit diff --name-only, which prints only the new path of a rename, so renaming a source file hid the old path (README.md, or an example directory) from the check. Both now pass--no-renames, so a rename is reported as the old path deleted and the new path added and both are listed; a copy never listed its unchanged source, and still lists only the new path.tests/test_translation_sync.pyruns the realgit diffin a scratch repository for a modification, a rename, a rename with edits and a copy;tests/test_docs_reason.pypins the flag in the workflow. - AI agent recipes run again on the same database (#302) —
templates/ai-agent/01_agent_state.pyand04_agent_loop.pyraisedIntegrityError(errno -670) on a second run because their fixedsession_key(demo-001,agent-loop-demo) isUNIQUEand the tables are kept;05_chatbot_backend.pyfailed the same way on thealiceusername, and03_rag_metadata.pyre-ingested its three documents on every run, so retrieval returned duplicates. The demonstration identifiers are now recipe-owned (cookbook-agent-state-demo,cookbook-agent-loop-demo,cookbook_demo_alice, and a"recipe": "cookbook-ai-agent-rag-demo"metadata marker on the sample documents), and each recipe first deletes only its own rows, children before parents: 01 addsdelete_session()(tool calls, messages, then the session for one key), which 04 reuses; 03 addsremove_demo_documents()(chunks, then the marked documents); 05 deletes its demo user's messages, conversations and user. A reader's rows with a generic key, the usernamealiceor a sample source URL are not touched, andrag_retrieval_logstill accumulates. The printed output is unchanged apart from auto-increment IDs and the demo username. The new live testtest_ai_agent_recipes_rerun_on_same_databaseruns each recipe twice without resetting tables (four of the five failed before this change) and checks that such look-alike rows survive the rerun; offline tests check the deletion order and that 01 and 04 delete before they create. - Alembic recipe reports
transactional_ddlfrom the dialect; DDL rollback docs corrected (#234) —fundamentals/alembic/alembic_programmatic.pyprinted a fixedtransactional_ddl = False -> each DDL statement auto-commits.and its golden locked that in, although sqlalchemy-cubrid has setCubridImpl.transactional_ddl = Truesince 1.8.0 (sqlalchemy-cubrid#503). The recipe now prints the impl class name andtransactional_ddlread from a configured AlembicMigrationContextat runtime, so the golden fails if the dialect value ever changes. Its generatedenv.pyno longer importssqlalchemy_cubrid.alembic_impl(with an outdated comment about the removedalembic.ddlentry point): sqlalchemy-cubrid registersCubridImplautomatically since 1.8.0 (sqlalchemy-cubrid#504), so the recipe now shows a defaultenv.pyworking. The closing pointer to a nonexistentREADME.mdnow links sqlalchemy-cubrid'sdocs/ALEMBIC.md, and the docstring andllms.txtwere updated.fundamentals/alembic/requirements.txtraises its floor tosqlalchemy-cubrid>=1.8(1.7.1 fails withKeyError: 'cubrid'without the manual import);scripts/check_dependency_floors.pyguards it as a documentedEXACT_SQLALCHEMY_FLOORexception, including the stale-exception check, andSUPPORT_MATRIX.mddocuments it.KNOWN_ISSUES.md§3 ("DDL Auto-Commits": migrations not transactional, DDL cannot be rolled back) is rewritten: CUBRID DDL is transactional while autocommit is off (pycubrid's default),ROLLBACKundoes it, schema locks are held until commit, and client autocommit (csql's default,conn.autocommit = True) commits every statement. TheSUPPORT_MATRIX.mdlimitations row is replaced to match. Verified live on CUBRID 11.2.9 and 11.4.6. - Empty emphasized link captions no longer exempt documentation updates (#213) — synchronize
scripts/check_docs_reason.pywith the reviewed pycubrid#573 helper so emphasis around an empty comment or image is not counted as a visible explanation. Regression tests retain all four pycubrid#429 case families and a populated emphasized-reason control; existing cookbook event-JSON tests and maintainer-label exceptions are unchanged. - Release dispatch smoke tests verify the requested artifact (#148) — validate upstream package names and canonical release refs from event JSON, install the exact PyPI version with bounded publication retry, and pin/check it through all later dependency installs. Requested MCP releases use the PyPI artifact; its existing git-tag fallback remains only for non-requested MCP. Always report requested and installed versions, origin, verification commit, actual CUBRID server version when available, and result, including failed runs.
- AI-agent smoke validation follows release verification (#147) — after the AI examples joined PR smoke coverage, their pytest dependency is installed under the frozen release constraints before the final version/provenance check. Both live repetitions run after that check; no dependency install can replace a driver after the tested-version report. Added guards for missing constraints, optional requirements, and same-version local replacements.
- Release smoke tests preserve and report the drivers actually exercised (#147) — removed four
git+maindriver references from thepycubrid,connect, andorm-basicsfundamentals requirements in favor of>=1.7,<2. Exact constraints now protect the selected published drivers during every later install. The workflow checks versions and package-index origin after all example/framework dependencies are installed, then records the tested versions; conflicting requirements and same-version VCS/local replacements fail. Added offline regression guards, while preserving the intentional MCP tag fallback. The separate upstream tag/PyPI publication race remains unresolved. - The host-run FastAPI quickstart could not connect (#146) — default
CUBRID_HOSTtolocalhost, and set it tocubridonly for the Compose API service. The host instructions start only the database and wait for its health check, leaving port 8000 for Uvicorn. Replace deprecated startup events with lifespan startup and ensure startup and request database operations commit on success, roll back on failure, and close cursors and connections. Add offline HTTP/configuration/failure regression tests to PR CI. - AI agent examples now run against released drivers (#125) — capture
cursor.lastrowidbefore commit, bind SET/SEQUENCE members as scalar placeholders, and read collections viaTABLE()instead of raw driver payloads. The agent-loop example creates its own sample products table. Complete and validate MCP initialization/tool responses so connection and protocol errors fail the demo. Close recipe connections and dispose the ORM engine on errors. The five live scripts now have bounded process isolation and per-example table cleanup, and run twice on both CUBRID smoke-test servers, including pull requests; offline tests cover ID handling and error cleanup. - The FastAPI template and nine of its recipes could not create their tables on CUBRID (#122):
templates/api-service-fastapi/app/models.pyand recipes 01-08 declared 20 primary key columns withindex=True, and recipe 11 indexedclient_rate_windows.client_idon top of itsUniqueConstraint. CUBRID rejects a second index over already-indexed columns (Index "pk_cookbook_tasks_id" already defined), soBase.metadata.create_all()failed at app startup on both 11.2 and 11.4. SQLite accepts the redundant index, which is why the SQLite-only test suites never caught it. Removed the redundant indexes and documented the behavior asKNOWN_ISSUES.md#5 and in theSUPPORT_MATRIX.mdlimitations table. - FastAPI recipes 02, 04, and 05 were missing
email-validator(#122): their schemas use pydantic'sEmailStr, which requiresemail-validator, but it was not in theirrequirements.txt, so the apps and their tests failed on import after a clean install. Added it asemail-validator>=2. - The documented FastAPI test command could not import recipes 01-08 (#122): the
SUPPORT_MATRIX.mdloop (for d in */tests; do python3 -m pytest "$d" -q; done) failed withModuleNotFoundError: No module named 'database'for recipes 01-08 because nothing put the recipe directory onsys.path. The newconftest.pydoes, so the loop runs all 12 suites. The 01-08 README test instructions now also installpytest pytest-asyncio httpx, which theirrequirements.txtfiles do not include. - Regenerated the two LOB goldens that recorded pycubrid's pre-1.7.0 escaping bug —
fundamentals/pycubrid/expected/06_lob.expectedandfundamentals/lob-handling/expected/06_lob.expectedwere captured while pycubrid silently negotiated the wrongno_backslash_escapesmode (the bug fixed loud in pycubrid 1.7.0, pycubrid#263/#293), so every newline in the round-tripped CLOB text carried a stray backslash (64 chars→66 chars). pycubrid 1.7.0 fixed the corruption, which made the nightly smoke red against these stale goldens since Sep 4. Goldens regenerated from the now-correct output against CUBRID 11.2;make verifypasses 23/23. - Flask recipes 06, 08, and 11 crashed comparing naive and aware datetimes: the
datetime.utcnow()replacement for #30 (#34) switched these apps todatetime.now(timezone.utc), but theirDateTimecolumns store naive UTC values, so comparing a value read back from the database with the new aware "now" raisedTypeError: can't compare offset-naive and offset-aware datetimes. Claiming an already-claimed case (06), leasing outbox messages (08), and confirming or sweeping reservations (11) returned HTTP 500, and 11 of their tests failed; it went unnoticed because the Flask suites do not run in CI. The apps now use anaive_utc_now()helper for these comparisons. Writes of aware UTC values elsewhere (recipes 04, 05, 07) were checked on CUBRID 11.2 and 11.4 and store correctly, so they are unchanged. - Flask recipe 09 could not start from its own
requirements.txt: it did not listpycubridandsqlalchemy-cubrid, socreate_app()failed withCan't load plugin: sqlalchemy.dialects:cubrid.pycubrid. Added both with the template's>=1.7,<2pins. - The documented Flask test command could not import recipe 11: its test module imported
appwithout putting the recipe directory onsys.path, so theSUPPORT_MATRIX.mdloop failed withModuleNotFoundError: No module named 'app'. The newconftest.pyadds it. - De-duplicated the parameterized-queries recipe golden (#79) —
fundamentals/parameterized-queries/04_parameterized.pywas a byte-for-byte copy of the canonicalfundamentals/pycubrid/04_prepared.py, and both shippedexpected/goldens, somake verifycounted the same recipe twice. The topic entry is now a thin redirect to the canonical recipe (itsexpected/golden removed and the folder allowlisted inscripts/docs-sync-allowlist.txt), dropping the CI golden count from 46 to 45 while keeping topic-based discovery intact. - Fixed 3 broken examples the missing-golden gap was hiding (#89) — backfilled
expected/goldens for 11 previously-unverified scripts and, in doing so, uncovered and fixed three broken examples:fundamentals/error-handling/03_query_timeout.py(a server-sidelock_timeoutsystem parameter did not bound the blocked client on CUBRID 11.2, so the demo hung forever — rewritten to use a client-sidepycubrid.connect(..., read_timeout=…));fundamentals/pycubrid/12_pool_retry_worker.py(hung at cleanup because a pooled connection still held a lock whenDROP TABLEran — thefinallynow closes the pool before dropping); andfundamentals/pycubrid/16_batch_error_handling.py(crashed on a duplicate-key retry that assumed post-failure statements were skipped — rewritten to teach the real CUBRID all-or-nothingrollback()semantics, and a module-levelprintthat scrambled output order was moved intomain()). - Added datetime-microsecond and bulk-insert timing rules to
scripts/normalize_output.sh—{{DATE}} HH:MM:SS.ffffffnow normalizes to{{DATETIME}}and theexecute(insert, rows):/add_all:perf-summary lines normalize their seconds to{{TIME}}s, so the merge/serial/bulk-insert examples produce reproducible goldens. Covered by newscripts/test_normalize_output.shcases. 07_merge_upsertoutput was flaky when a timestamp landed on a whole second (#189) —fundamentals/pycubrid/07_merge_upsert.pyprinted thelast_seen_utcDATETIMEcolumn with Python's defaultdatetime.__str__(), which omits the fractional part entirely whenever the microsecond component is exactly 0 (about 1 run in 1000, since CUBRID stores millisecond precision), soscripts/normalize_output.sh's{{DATE}} HH:MM:SS.ffffffrule (which requires a fractional part) didn't match and the golden comparison failed. The recipe now has aformat_timestamp()helper that always renders the fractional part (strftime("%Y-%m-%d %H:%M:%S.%f"), truncated to milliseconds), so the existing normalizer rule always matches; widening the normalizer rule itself was considered and rejected; it would have also swallowed the fixed, meaningful whole-second timestampsfundamentals/pycubrid/20_timezone_datetime.py's golden depends on. Newscripts/test_normalize_output.shcases cover a zero-padded-milliseconds whole-second value (now normalizes) and guard that a bare whole-second value with no fractional part still does not (it can be meaningful, as in20_timezone_datetime). The golden is unchanged (it already compares against the{{DATETIME}}token).- Python lint errors and code formatting across all examples
- All examples verified against live CUBRID instance
fundamentals/sqlalchemy/07_collection_types.py— SET/MULTISET/SEQUENCE collection columns now render correct single-quoted SQL literals (with quote escaping) instead of malformed inline SQL, and the example is verified against a goldenexpected/07_collection_types.expectedoutput (Closes #56)
Documentation¶
-
README and docs site show the transparent brand icon (#533) — the icon URL pinned in the README, its translations and
mkdocs.yml(logo and favicon) now points at brand revisione55990a, where the Cookbook icon's open book is two transparent pages instead of white pages with text lines. Documentation only; behavior is unchanged. -
README and docs site show the brand icon with white marks (#507) — the icon URL pinned in the README, its translations and
mkdocs.yml(logo and favicon) now points at brand revision3a385be, where the Python Yellow icons draw white role marks on a bare cube like every other language. Documentation only; behavior is unchanged. - README and docs site show the emblem-aligned brand icon (#505) — the icon URL pinned in the README, its translations and
mkdocs.yml(logo and favicon) now points at the brand revision538bad3, whose Python Yellow keyline keeps the cube silhouette of the organization emblem (open seams, no flat bottom). Documentation only; behavior is unchanged. -
README and docs site show the 2026-10-25 brand icon (#499) — the icon URL pinned in the README, its translations and
mkdocs.yml(logo and favicon) now points at the approved CUBRID Lab brand revision8aa81b7. Documentation only; behavior is unchanged. -
The README and the documentation site pin the approved 2026-10-25 Cookbook icon (#497) —
README.md,docs/README.ko.mdand the site logo and favicon inmkdocs.ymlloaded the icon from an older cubrid-lab/brand commit that predates the brand icon fixes (glyphs kept off the cube seams, redrawn Cookbook mark). They now pin brand commit2de5d23, the revision recorded as approved in cubrid-lab/brand#6. make testthenmake verifycome first; the focused lanes are advanced (#459) —README.md,GETTING_STARTED.md,CONTRIBUTING.mdandmake help(withdocs/README.ko.mdanddocs/ko/getting-started.md) now presentmake test(offline, results intest-results/test/) andmake verify(complete live validation) as the standard sequence, and moveverify-goldens,verify-quickstart,verify-performance,verify-web,verify-django-celery,verify-ai-agent,verify-dashboard,verify-templatesanddemounder "Advanced debugging lanes", a separatemake helpgroup that says each printsPARTIAL.make check teststays the documented pre-PR command. No target runs anything different;tests/test_make_commands.pypins themake helpgrouping.-
One beginner route with five lessons, and reader-based navigation (#392) — steps 4 and 5 of the beginner tutorial are now lessons of their own, in English and Korean:
quickstart/first-cubrid/save-a-row.md(lesson 3),rollback.md(lesson 4) andfinish.md(lesson 5), staged asfirst-cubrid-save-a-row,first-cubrid-rollbackandfirst-cubrid-finish. Each states what the earlier lessons produced and has the command, the checked output, what to do when it is run again or interrupted, and the next step. The scripts and their goldens are unchanged, and the old step headings stay as short landings. The site navigation now has Start here (the five lessons), Optional goals (the FastAPI and SQLAlchemy quickstarts), How-to guides (Getting Started as "Choose a path and verify a checkout", the recorded walkthroughs and the recipe pages), Reference, Help (known issues, the issue forms andSECURITY.md), a peer 한국어 group with the same order, and Project.docs/quickstart.mdbecomes a compatibility page whose old headings point to the tutorial or to Getting Started, and keeps the walkthroughs; the site home links Getting Started for the three paths;GETTING_STARTED.mdand its Korean page point newcomers to the tutorial.CONTRIBUTING.mdcompletes the page map and adds a short convention for writing a tutorial lesson. -
The first query is its own lesson (#392) —
quickstart/first-cubrid/first-query.md("Lesson 2: Run your first query", Koreandocs/ko/first-cubrid-first-query.md) is the first lesson split from the beginner tutorial: what lesson 1 must have produced, the unchanged01_first_query.py, the command, the checked output, what to do when the lab is not ready, what the connection and cursor did, and the next step. Step 3 ofquickstart/first-cubrid/README.mdis now a short landing that links to the lesson, the tutorial lists its lessons in order, and every heading stays, so old links still land.CONTRIBUTING.mdgains a Documentation structure map (source, site page, reader, question, decision and neighbours for every affected page). The lesson follows the tutorial in the site navigation, and pages now show previous and next links. SUPPORT_MATRIX.mdseparates the support policy, published package requirements and dated test records (#393) — beside the current guidance it still said the sqlalchemy-cubrid<2.2SQLAlchemy cap was on unreleasedmainand unpublished, although 1.11.0 had shipped it. The page now has a current support policy, a "Published package requirements" table read from each published wheel'sMETADATAwith its SHA-256 checked against PyPI (sqlalchemy-cubrid 1.11.0 declaressqlalchemy>=2.0,<2.2, 1.10.0sqlalchemy>=2.0,<2.3, checked 2026-10-16), and the 2026-10-11 observation kept as a dated record; the Python table's resolved-versions column is labelled a dated record with its run, source commit, origin and scope.scripts/check_support_matrix_counts.py(inmake check-docs) now rejects a published-requirement row without wheel and SHA-256 evidence, or one for amain, candidate or unverified version, andCONTRIBUTING.md("Refreshing version statements") names the source of each statement and the documentation maintainer's refresh steps.- The demo-recording recipe removes its database volume (#394) —
demos/README.mdremoved the throwawaydemo-cubridcontainer withdocker rm -f, which leaves the anonymous volume thecubrid/cubrid:11.4image declares for/home/cubrid/CUBRID/databases; every recording run left one behind. It now usesdocker rm -f -v demo-cubrid, with a comment on what-vdeletes. - The FastAPI and SQLAlchemy quickstarts are self-contained (#386) — each README now works when opened directly: the assumed OS and shell, the tools and whether host Python is used;
git clone, the exact folder, and a venv where one is needed; the observed healthydocker compose psstate; the real output of a create-then-read through the FastAPI API (POST /itemsthenGET /items/1, with theidmarked as variable on later runs because the table persists) and of the SQLAlchemy script (matching its golden, ids1on every run because it recreates its tables); stopping, rerunning, dropping only the quickstart's own table, andstopversusdownversusdown -v; the shared port 33000 and Compose project names when switching between the quickstarts and the root lab; and short troubleshooting with the real messages for a missing Docker, an unhealthy database, an occupied port, an inactive venv and a missing or different database. FastAPI keeps two labelled ways, everything in Docker (recommended) and the API on the host with Uvicorn, never both on port 8000, and explainslocalhostversus the Compose service namecubridwhere each applies. "5 minutes" is qualified as an estimate once Docker (and Python) are installed. Both pages are now published on the site (Getting Started), their next steps are links that work on GitHub and the site, and the Korean learning pages aredocs/ko/quickstart-fastapi.mdanddocs/ko/quickstart-sqlalchemy.md. The rootREADME.mdanddocs/README.ko.mdquickstart blocks start fromgit clone, andllms.txtdescribes both quickstarts.tests/test_quickstart_config.pyfails if the SQLAlchemy README or its Korean page stops showing the golden output. - A guided first tutorial for readers new to CUBRID (#385) —
quickstart/first-cubrid/is the new "New to CUBRID? Start here" entry point, ahead of the FastAPI and SQLAlchemy quickstarts inREADME.md, the site navigation, the site home page andllms.txt. In order, it explains the database server versus the pycubrid connector and the host, port, database and user; prepares the local lab (clone, working directory, venv,docker compose up -dwith the readiness script); and runs four small scripts with their real output: a first query; saving rows with?placeholders, committing them and reading them from a second connection; rollback; and a cleanup that drops only the tutorial's own table. It then covers interrupted runs, re-running, and the difference betweenstop,downanddown -v. The scripts carrymake verifygoldens, so CI keeps the published output current; a table that is not the tutorial's stops step 4 and cleanup without changes. The Korean learning path isdocs/ko/first-cubrid.md, linked from the Korean README. - Agent PR review route: Copilot or a maintainer (#351) — maintainer decision of 2026-10-15: a standalone-agent PR needs a substantive submitted Copilot review or a review by a maintainer (a code owner other than the PR's author), recorded on the PR. Reviews by other people or by other AI agents do not satisfy the route. No CI check enforces it; whether to add one is pending (#351). The two-round AI review budget stays. New: under every route, changes to workflows that hold write permissions or that sibling repositories call to verify a release (
smoke-test.yml), and toscripts/release_smoke.pyandscripts/mcp_smoke.py, need a review that explicitly assesses them, confirmed by a maintainer.AGENTS.md,CONTRIBUTING.mdand the PR template are updated; the Korean docs carry no review rules. - The README and the documentation site show the CUBRID Lab repository icon (#349) —
README.mdand the Koreandocs/README.ko.mdopen with the cubrid-cookbook-python Cookbook icon, centered and without a caption, and the documentation site uses it as its header logo and favicon.docs/catalog.mdis staged from the README and follows. All of them load the SVG from one immutable cubrid-lab/brand commit. - Performance READMEs say how to run their benchmarks —
performance/README.mdand thefetch-optimization,bulk-insertandconnection-poolingREADMEs now give the exact commands (CUBRID_VERSION=11.4 docker compose up -d --wait cubrid,pip installof the driver,python performance/<section>/benchmark.py,docker compose down) and the rough run time of each script. The commands were run againstcubrid/cubrid:11.4. No script or measured figure changed. - The demo recording steps no longer say the agent recipes need an empty database (#310) —
demos/README.mdstill said that01_agent_state.pyand04_agent_loop.pyneed an empty database for each run and that the recipes reset their tables, which stopped being true when the AI agent recipes became rerunnable (#302). It now says that the recipes delete only their own previous demonstration rows, and the comment on theDROP TABLEbetween the two VHS tapes says the drop is optional and only keeps the printed IDs starting at 1, as in the committed recordings. The recordings andrecording-provenance.jsonare unchanged. - README demo GIFs no longer start moving on their own (#308) —
README.mdand the Koreandocs/README.ko.mdembedded three autoplaying, looping GIFs (docs/demo-agent-state.gif,demo-fastapi.gif,demo-dashboard.gif, 9–12 s each) with no way to pause, stop or hide them (WCAG 2.2 success criterion 2.2.2). Each embed is now a still poster (docs/demo-*.png, the GIF's last frame, 49–99 KB) that links to its GIF, with a "Play the recording (animated GIF, N seconds)" link under it, so the animation plays only when the reader selects it; the GIFs themselves are unchanged.docs/catalog.mdis staged from the README and follows.demos/build_media.shwrites the posters from the GIFs,demos/README.mdlists them and explains why,demos/recording-provenance.jsonrecords their hashes and sizes (and the script's new hash), andTHIRD_PARTY_LICENSES.mdnames them with the recordings. - The dashboard table viewer is described as refreshing on a button, not automatically (#304) —
templates/dashboard/README.md,README.ko.mdand the01_table_viewer.pymodule docstring said the table viewer auto-refreshes, but the app re-reads the table only when the reader clicks its Refresh button, and its own caption says "no auto-refresh". The three texts now describe the Refresh button. Auto-refresh was not added: the caption shows the manual refresh is deliberate, the dashboard demo recording clicks Refresh, and Streamlit's timed reruns (run_everyon a fragment) need Streamlit 1.33 or later (st.fragmentis stable since 1.37) while the template supportsstreamlit>=1.30.docs/template-dashboard.mdis staged from the README and follows. - The agent loop recipe no longer claims to run through the MCP whitelist (#305) — the module docstring of
templates/ai-agent/04_agent_loop.pysaid step 3 "Executes against CUBRID via the MCP read-only whitelist", and the template README's architecture diagram labelled the act step "Act (MCP) (read-only)", butSimpleDataAgent.act()runs the planned tool directly withpycubridand never callscubrid-mcp-server, so a non-SELECTstatement passed toexecute_querywould not be rejected. The docstring, the README's recipe table and diagram now say that 04 uses MCP-named tools executed directly, and a note after the diagram points to02_mcp_toolchain.pyas the example that goes through the MCP server and its read-only whitelist. The docstring and the README also warn that, once an LLM writes the SQL,execute_queryunder the defaultdbaconnection can modify or drop data, and recommend the MCP read-only path or read-only validation plus a least-privilege user before connecting one. The recipe's code is unchanged. - Real demo recordings for the README and the docs site (#113) —
docs/demo-agent-state.gifwas drawn bydemos/render_gif.pyfrom scripted text indemos/agent-state.json, which no longer matched the recipe (it showed 4 messages;01_agent_state.pyprints 3). It is now a VHS recording of the recipe running against CUBRID 11.4, and the renderer and its JSON script are removed. New recordings, all of real runs:docs/demo-fastapi.gif(the 5-minute FastAPI quickstart's Swagger UI creating and listing an item on CUBRID 11.2) anddocs/demo-dashboard.gif(the Streamlit dashboard template) in the README and the Korean README, and three docs-site videos embedded indocs/quickstart.mdunderdocs/assets/videos/:dashboard.mp4(docker compose up -d, then the browser),agent-state.mp4(01_agent_state.pyand04_agent_loop.py) andmcp-toolchain.mp4(02_mcp_toolchain.pydrivingcubrid-mcp-server). Sources live indemos/: four VHS tapes (VHS 0.12.1 or later; 0.12.0 silently writes no file),record_browser.py(Playwright),build_media.sh(ffmpeg excerpts and joins), a README with the steps and what is cut (only the waits for the CUBRID health check and the dashboard's pip install), andrecording-provenance.jsonwith package versions, image digests, tool versions and media hashes. TheDEMOlicense-inventory set now coversdemos/record_browser.py(playwright, withgreenlet,pyeeandtyping_extensions) instead of the renderer (imageio,pillow);THIRD_PARTY_LICENSES.mdwas regenerated with the recorded command and--exclude-newertimestamp, which changed only that set, and describes the recordings. GIFs are 8–12 s and at most 1.8 MB; videos are 31–42 s H.264. - Korean README for the Flask template (#137) — adds
templates/flask/README.ko.md, a Korean translation oftemplates/flask/README.md(requirements, recipe layout, running a recipe, tests and further detail), following the header and terminology oftemplates/dashboard/README.ko.md. The English README stays canonical. - Project Structure section in the Korean README (#248) —
docs/README.ko.mdgains## 프로젝트 구조, a translation of the English README's Project Structure tree, placed before 관련 프로젝트 as inREADME.md. Directory names and counts match the English tree; only the comments are translated. - ROADMAP describes the Python-only cookbook (#155) —
ROADMAP.mdno longer plans Rust examples or claims TypeScript, Go and Rust coverage, and drops the Drizzle/GORM framework list. Its blanket "all examples tested against CUBRID 11.2" line now points toSUPPORT_MATRIX.md, which records which CUBRID and Python versions and CI tiers verify each recipe, and the framework list points to the README instead of being restated. The bug report form drops its Node.js and Go example options and asks for the Python version only. The duplicate[Unreleased]headings #155 also reported were already consolidated onmainand are enforced byscripts/lint_changelog.py. - Resolved driver versions recorded for the Python endpoint cells (#218) — the Python Versions table in
SUPPORT_MATRIX.mdgains a dated column with thepycubrid,sqlalchemy-cubridand SQLAlchemy versions the Python 3.11–3.14 compatibility jobs resolved (ci.ymlrun 38125283280). Documentation only. mainbranch protection baseline and security contact route (#223) —CONTRIBUTING.mddocuments themainruleset (pull request required,CI Gate (all checks must pass)andPR titleas the only required checks, no deletion or force push, no required approval and no bypass actor) and states that outside contributors need no internal tools, label permissions or live CUBRID access.SECURITY.mdlists email, GitHub private vulnerability reporting when offered, and a new detail-free "Security contact request" issue form as the fallback, which the issue chooser also links.- SQLAlchemy support claim matches the verified dialect range (#278) —
SUPPORT_MATRIX.md,llms.txt,docs/index.md,README.md,docs/README.ko.mdand the issue-template link now state SQLAlchemy 2.0 and 2.1 as supported instead of2.0–2.2, matching the sqlalchemy-cubrid support contract.SUPPORT_MATRIX.mdseparates the supported range from installation ranges: sqlalchemy-cubridmainrequiressqlalchemy>=2.0,<2.2but that cap is unreleased, while the published 1.10.0 still declares<2.3, so its metadata would let pip install SQLAlchemy 2.2 once released, but 2.2 is not supported or verified by any recipe.scripts/check_support_matrix_counts.py(part ofmake check-docs) now fails when the SQLAlchemy row or thellms.txt/docs/index.mddescription promotes 2.2, andtests/test_support_matrix_counts.pycovers a fixture that does. Historical entries are unchanged. - Korean README for the dashboard template (#123) — adds
templates/dashboard/README.ko.md, a Korean translation of the dashboard README. - GitHub Release policy and standard CHANGELOG sections —
AGENTS.mdgains the shared "GitHub Release Policy" (Release titles equal their tag, stale-draft handling, release-note sections), andCONTRIBUTING.mddocuments the section list. This repository publishes no GitHub Releases and adds no release automation. The[Unreleased]headings were renamed (DocstoDocumentation), merged by moving whole lines and put in the standard order; no entry was reworded. - Design: verify a driver release candidate before PyPI publish (#240) —
docs/internal/rc-verification-design.mdproposes an artifact (candidate) mode for thesmoke-test.ymlworkflow-call contract: the driver's release workflow passes its builtrelease-distartifact and SHA-256 map, the cookbook verifies that exact wheel in the three release cells (CUBRID 11.2 and 11.4 on Python 3.12, CUBRID 11.4 on Python 3.11) with the unchanged smoke scope, andpublishwaits for the result; post-publish verification stays unchanged. It compares commit mode and TestPyPI staging, records the 2026-10-09 post-publish failures (PyPI index propagation, not recipe regressions), and lists the follow-up issues for this repository, pycubrid, sqlalchemy-cubrid and cubrid-mcp-server. No workflow behavior changes. - Dependency license inventory covers every requirements file (#217) —
THIRD_PARTY_LICENSES.mdwas a singlepip-licensesunion of the template and some fundamentals requirements. It now covers all 43requirements*.txtfiles, the CI/docs tooling the workflows and the PR-smoke action install directly, and the demo GIF renderer's imports: files with identical contents form 25 requirement sets, each installed into its own fresh environment (no flattened install), and the consolidated package table names the sets that pull each package in with every observed version. The new standard-libraryscripts/build_license_inventory.pyregenerates it at a recorded commit, Python and OS, with resolution pinned by--exclude-newer, using the generator shared with pycubrid, sqlalchemy-cubrid and cubrid-mcp-server. MPL-2.0 (certifi,pathspec) is classified as weak file-level copyleft instead of under a blanket "no copyleft" statement; numpy (bundledlibgfortranunder GPL-3.0-or-later with the GCC Runtime Library Exception andlibquadmathunder LGPL-2.1-or-later), Pillow (bundled libraries, FreeType under the FreeType License) and protobuf are reviewed from their own license files. The document separates installed dependencies from material stored in the repository:docs/demo-agent-state.gifis drawn by this project'sdemos/render_gif.pyfrom its own recorded output, with glyphs rasterised from a system font (DejaVu Sans Mono, else Liberation Mono, else Pillow's default).tests/test_third_party_licenses.py, run bymake test-offline, fails when a requirements file or its declared packages, a workflow's or action's direct installs (folded, continued and quoted commands included), or the GIF renderer's imports are not reflected in the inventory, or when a category disagrees with the generator; version bumps do not fail it. - Flask template README matches the recipe layout (#135) —
templates/flask/README.mdno longer describes a nonexistentapp/package and/productsHTML pages; it lists the eleven numbered recipes, shows how to run01-basic-crud, and documents its test suite (includingCUBRID_TEST_URLfor live-CUBRID runs). Thanks @soyeladice-svg. - Docs site links repaired and internal pages unpublished (#154) —
scripts/stage_docs.shcopied repository READMEs intodocs/unchanged, so their repository-relative links (fundamentals/connect/,SUPPORT_MATRIX.md,LICENSE,../../GETTING_STARTED.md, the README demo GIF) no longer resolved after the move;mkdocs build --strictreported 51 of them at INFO level and passed becausemkdocs.ymlsetnot_found: ignore. Staging now lives inscripts/stage_docs.py(the shell script delegates to it): a link to another staged source points at its site page, a link to an authoreddocs/file stays on the site, any other repository path becomes a GitHub URL onmain, and a link to a missing path fails staging.mkdocs.ymlnow warns on unresolved, unrecognized and broken-anchor links, so--strictfails on them, and excludesdocs/internal/(the PRD) from the rendered site and search index. The newDocs site build (strict)CI job runsmake docson every PR and is part of the CI Gate;tests/test_stage_docs.pyand the script's doctests (run bymake check-docs) pin the rewriting rules, the missing-link failure, that sources are never modified, and themkdocs.ymlsettings. CONTRIBUTING documents the site policy, including keeping the hand-maintaineddocs/quickstart.mdsite guide in step withGETTING_STARTED.md. check_docs_sync.pynow coverspitfalls/*example directories (#182) —pitfallswas excluded fromCATEGORY_ROOTSbecause it used to be a single README, not a set of example subdirectories; #181 added a real recipe subdirectory,pitfalls/reserved-words/, that the gate never scanned.pitfallsis now a category root whose subdirectories are only treated as examples when they contain a.pyfile, so the README-onlypitfalls/README.mdlanding page (and any future non-code subdirectory) stays exempt whilepitfalls/reserved-wordsis scanned like every other example.README.mdandllms.txtnow referencepitfalls/reserved-wordsdirectly, andtests/test_docs_sync.py(run bymake check-docs) pins both behaviors: an undocumented pitfalls example fails the gate, and a README-only pitfalls page passes.- AI guidance and MVCC isolation descriptions corrected (#150) —
llms.txtandfundamentals/README.mddescribed "all 6 numeric isolation levels plus a dirty-read demonstration"; they now describe the three levels CUBRID accepts under MVCC — READ COMMITTED (4, default), REPEATABLE READ (5), SERIALIZABLE (6) — and the recipe's no-dirty-read demonstration. The isolation recipe itself called SERIALIZABLE "full serializable" and said numeric codes are no longer accepted: the CUBRID 10.2/11.x manual documents SERIALIZABLE as identical to REPEATABLE READ snapshot isolation (write skew still possible), and codes 4–6 are accepted while 1–3 and READ UNCOMMITTED are rejected (all confirmed live on CUBRID 10.2, 11.2 and 11.4), so its docstring, printed description and golden are corrected.llms.txtalso lists the previously missingtemplates/ai-agent, drops hardcoded recipe counts (the pycubrid "14" was stale) in favour of the SUPPORT_MATRIX Recipe Coverage table, scopes its verification claim to what CI actually checks instead of "every example", and states the reserved-word and async facts below.scripts/check_docs_sync.py(run bymake check-docs) now fails when an example directory is missing fromllms.txt, as it already did forREADME.md. - Executable reserved-word advice and async driver guidance (#151) — the
pitfalls/"correct" DDL still used the reserved wordkeyunquoted and failed; it now usessetting_key.nameis removed from the reserved-word table (it is not in the official 10.2/11.2/11.4 keyword lists and works unquoted),key/date/dayare added, the three quoting forms CUBRID accepts (double quotes, square brackets, backticks) are documented, and the official 11.2 and 11.4 keyword pages are linked. The new golden-backedpitfalls/reserved-words/01_reserved_words.pyruns every statement from that section undermake verify(CUBRID 11.2 and 11.4 in CI; its SUPPORT_MATRIX row brings the table to 102 recipes, 66 golden-backed). The async pitfall no longer says CUBRID's Python drivers are all synchronous: it presents the nativepycubrid.aiodriver and thecubrid+aiopycubrid://SQLAlchemy engine as the async options, keeps sync handlers andasyncio.to_threadfor synchronous code, and retains the warning against calling the synchronous API insideasync def. - SUPPORT_MATRIX recipe counts corrected and guarded (#126) — the Recipe Coverage table now covers every recipe (101 in total, 65 golden-backed; it previously claimed 68 and 45, and the prose said 62 and 46) and is the only place the counts are written; the prose and the docs home page point at it. The new
scripts/check_support_matrix_counts.py, run bymake check-docs(part ofmake checkin PR CI), maps each table row to one counting rule (goldens per directory, the smoke workflow's*/testssuite discovery for Flask and FastAPI, numbered scripts for the dashboard and AI agent templates) and fails when a row, the total, or an uncategorized golden disagrees with the repository. Thanks @alperensumeroglu. - Added a README "First contribution" guide (with Korean translation) pointing newcomers to the right sibling repo for their first PR, and documented the
good first issue→status: in progresslabel lifecycle in AGENTS.md. - Clarified contributor evidence and maintainer responsibilities for project tools, labels, integration and translation follow-up; corrected the FastAPI recipe run path. Docs exemptions now require a real standalone reason rather than template/example text, with CI-executed helper regressions. Pinned the existing shared doc-lint/live-smoke callers to a reviewed upstream commit without changing their inputs or gates.
- AI agent demo GIF embedded in README — agent state management showing sessions, JSON columns, SET tags.
- 한국어 getting-started 사이트 페이지 (#109) — GETTING_STARTED의 3-경로 사이트 번역판을
docs/ko/getting-started.md로 추가하고 Project → Translations에 노출. - 한국어 README 추가 (
docs/README.ko.md) — README 전문 번역으로 4개 Python 리포 모두 한국어 지원(접근성). 사이트 Project → Translations → 한국어로 노출. 번역 동기화 관리 장치 도입: 모든 번역 파일에 동기화 마커 + docs-sync에translation-sync잡 추가 —README.md가 바뀌는데 번역이 같은 PR에서 안 바뀌면 실패 (translations-deferred라벨로 의도적 보류). - Docs site information architecture unified across the ecosystem — nav aligned to the shared six-tab skeleton (Home / Getting Started / Usage / Reference / Operations / Project): catalog/topic pages and the six templates under Usage, Support Matrix under Reference, Known Issues under Operations, Changelog under Project.
- Added
SUPPORT.md(help channels, templates, security pointer) — root-file parity with the sibling repos instead of relying on the org fallback. - Documentation site launched with full content indexing — mkdocs-material site at https://cubrid-lab.github.io/cubrid-cookbook-python/.
scripts/stage_docs.shstages the repo's real documentation (root README catalog, GETTING_STARTED, SUPPORT_MATRIX, KNOWN_ISSUES, CHANGELOG, fundamentals/performance/pitfalls topic READMEs, all six template READMEs — 19 pages) into the site on every build (CIdocs.ymlandmake docs), so site search indexes the actual docs and the repo remains the single source of truth. README's private org-board link removed (404 for signed-out readers). -
GETTING_STARTED.mdrestructured as the judge/demo entry point — three-path flow (pycubrid first query → ORM/templates →uvx cubrid-mcp-serverwith the four natural-language steps incl. the read-only refusal), expected-output pointers to the CI-checked goldens, and fixed stalefundamentals/connect/-style links that no longer matched the directory layout. -
CUBRID server license line added; copyright notice unified (#99) —
THIRD_PARTY_LICENSES.mdstates the verified upstream licensing (server engine Apache-2.0, APIs/connectors BSD per CUBRID'sCOPYING; GPL v2+ is outdated) and that the examples are independent clients withcubrid/cubridimages used for CI/demo only. LICENSE/NOTICE copyright unified toYeongseon Choe, Gyeongjun Paik(2025-2026), replacing theCUBRID Labsline. -
Added
THIRD_PARTY_LICENSES.mdandNOTICE— pip-licenses-generated inventory of the union of template/example requirements (all permissive; no GPL), and a NOTICE declaring the examples original with no embedded third-party source. Documentation only. -
Corrected the async recipe's
sqlalchemy-cubridfloor from>=1.0to>=1.4.2(#94) —fundamentals/async/02_async_sqlalchemy.pybuilds anAsyncEnginewith thecubrid+aiopycubrid://URL, but that async dialect (cubrid.aiopycubridentry point) only became installable from PyPI insqlalchemy-cubrid1.2.3 — the entry points were missing from the 1.2.0–1.2.1 releases and 1.2.2 was yanked; 1.2.1 only shipped theget_pool_class()/create_async_engine()fix (#116). So the recipe'ssqlalchemy-cubrid>=1.0pin (in itsrequirements.txt, its printed "Requirements:" line, and the matching golden) advertised a version that cannot run it. Bumped all three to>=1.4.2, matching the floor already used by the other advanced SQLAlchemy recipes (pandas, ORM, Django, dashboard) rather than the bare 1.2.3 minimum.SUPPORT_MATRIX.mdnow carries an[^async]footnote documenting that the sync dialect works from 1.0 while the async recipe needs>=1.4.2. The global README badge stays at>=1.0: the synccubrid+pycubrid://recipes (including all FastAPI templates, which usecreate_engine, notcreate_async_engine) genuinely run on 1.0. - SUPPORT_MATRIX bug fixes (#57) — fixed the "Run all tests" commands, which chained two relative
cds so the second (templates/api-service-fastapi/recipes) resolved againsttemplates/flask/and failed; each is now wrapped in a subshell. Added the previously-omittedasync-worker(Celery) andbatch-etl(Pandas) templates to the recipe-coverage table as manual-run entries and updated the total. (The version-badge/matrix and Python-table inconsistencies from the same issue were already resolved in #80 and #81.) - Reconciled the minimum
sqlalchemy-cubridversion to>=1.0(#80) — the README badge advertised≥1.6.0whileSUPPORT_MATRIX.mdlisted≥1.0, and scatteredrequirements.txtfiles pinned stale/invalid versions (>=2.0.0, which does not exist;>=0.4.1;>=0.3.0). Aligned the badge and the invalid/pre-1.0 stragglers to>=1.0(the documented source of truth); feature-specific>=1.4.2pins were left unchanged. - README fundamentals link renamed to "Parameterized queries" (#82) — the link targeting
fundamentals/parameterized-queries/was labelled "Prepared statements", implying server-side prepare that pycubrid does not do; relabelled to "Parameterized queries" with a client-side note to match the recipe's own README warning. - SUPPORT_MATRIX Python table fixed (#81) — added a 3.14 row and re-sorted the Python versions table into consistent descending order (3.14 → 3.9); previously 3.14 was missing and 3.13 was listed after 3.10.
- SUPPORT_MATRIX corrected to match CI (#72) — the matrix claimed CUBRID 11.4 was "fully supported" and "all 62 recipes pass", but CI only runs
make verify(46 stdout goldens) on CUBRID 11.2 / Python 3.12; the Flask/FastAPI/Streamlit/Django pytest suites and CUBRID 11.4 are never exercised in CI. Reworded the server/Python/recipe tables to state exactly what CI enforces vs. what is run manually or merely expected to work, removing the unenforced green checkmarks.
CI¶
- Routine smoke runs CUBRID 11.4 only; weekly, manual and release runs keep 11.2 (#480) — a push to
mainand the daily schedule now run the full, fail-closedmake verifyinsmoke-test.ymlon CUBRID 11.4 / Python 3.12 only, and a push tomainruns the pinned CQRS job (ci.yml,scripts/ci_scope.py) on 11.4 only. The daily 11.4 check stays (see "CI tiers" inCONTRIBUTING.md). The schedule is now two exclusive crons, daily0 0 * * 0,2-6and weekly0 0 * * 1, the Monday of the weeklyci.ymlschedule; the weekly run, manual runs and any other event or cron string keep CUBRID 11.2 + 11.4, and the weeklyci.ymlrun and manual runs keep CQRS on both. Release verification (repository_dispatch, a pinned manual run,workflow_call, candidate mode) is unchanged: the same three cells asscripts/release_smoke.pyRELEASE_CELLS. Pull requests are unchanged. A new advisoryNewest CUBRID image (advisory)job in the weekly (and manual full) smoke comparescubrid/cubrid:latestwith11.4by digest andcubrid_relengine version (scripts/latest_server.py). When both tags are the same image it names that digest as the newest-stable image and starts no database; every smoke cell now records the exact image digest andcubrid_relit ran in its own summary, so the 11.4 cells count as that test only when they show the same digest. Only whenlatesthas a newer engine build does it run a focused canary on that image's immutable digest. A failed lookup is reported as unverified, never as passed. It iscontinue-on-error, outside the release report, and never changes the support matrix or release cells.tests/test_ci_scope.pynow evaluates the smoke matrix expressions for every event, including workflow calls made from a caller's push or schedule, andtests/test_latest_server.pycovers the equal-digest, newer-build and unavailable-image cases; both fail on the previous workflow. Measured per-event job and minute changes are inCONTRIBUTING.md: a push tomaindrops from 4 to 2 live jobs (about 13 to 7 billed minutes), the schedules from 14 to 8 jobs a week (56 to 32 billed minutes, plus the new advisory job).SUPPORT_MATRIX.md,docs/index.mdand the quickstart, performance and AI agent READMEs now state the per-event CUBRID versions. - Every new issue starts in
status: needs triage(#481) — the bug and feature forms add the label, the newissue-triage.ymlworkflow adds it to every opened issue, and the driver-main lane adds it to the issues it files. Priority and size are finalized in maintainer triage, after which the maintainer removes the label. - The pull-request smoke jobs publish the live verification summary and keep
test-results/(#462) —Smoke Tests (CUBRID 11.4)andSmoke Tests (CUBRID 11.2)inci.ymlappendtest-results/verify/summary.md(the lanes the smoke action ran, fromsummary.json) to the job's step summary on success and failure, collectdocker logsand the$CUBRID/logdirectory of thecubridandcubrid-livecontainers intotest-results/cubrid-logs/after a failure, and uploadtest-results/together with.live-evidence/in the existinglive-evidence-pr-cubrid-<version>artifact (same pinnedactions/upload-artifact,if: always(),continue-on-error, 14-day retention). The artifact now holdstest-results/and.live-evidence/folders instead of the bare.live-evidence/contents. No permission, secret or trigger changes;smoke-test.yml, which sibling repositories call for release verification, is unchanged. - CI runs
make verifyand the focused live lanes (#416, #425, #426, #428) —smoke-test.yml(push tomain, nightly, manual runs and release verification; CUBRID 11.2 and 11.4) now runs the full, fail-closedmake verifyinstead of the golden-only check plus hand-written pytest steps: every golden, then the FastAPI quickstart, migration and performance, Flask/FastAPI, Django/Celery, AI agent and dashboard suites one lane at a time, on three job-owned CUBRID containers of the matrix version (the examples database onlocalhost:33000,CUBRID_TEST_URLon:33100andCUBRID_DASHBOARD_TEST_URLon:33200). The dashboard's own container replaces the earlier stop-and-restart of the examples database.make depsinstalls every example's requirements under the selected driver constraints, and the workflow no longer exportsVERIFY_PATHS, which would makemake verifypartial.make verify-ai-agentthen runs a second time on the same database, and both runs keep their evidence. Theci.ymlpull-request smoke lanes runmake deps,make verify-goldens VERIFY_PATHS=<roots>and the focused lanesscripts/ci_scope.pyselects in its newsmoke_lanesoutput:make verify-quickstartforquickstart/5min-fastapi/(other than Markdown),make verify-performanceforperformance/andtests/test_performance_benchmarks.py, andmake verify-ai-agenttwice on a dedicated second container fortemplates/ai-agent/andtests/test_ai_agent.py. Each of these paths also runs every golden, even when a golden root changes beside it, and wider reasons to run the smoke lane select all three lanes;fundamentals/parameterized-queriesis the only example directory left without a live consumer. Every smoke job uploads.live-evidence/once (actions/upload-artifactat the pinned v7.0.2 commit,if: always(),continue-on-error: true); a release workflow call uploads none, so no artifact is written into the caller's run and the release smoke result never depends on the upload. Permissions are unchanged (contents: read). The other pull-request recipe jobs keep their per-recipe environments. -
Optional live grading and manual agent runs for the recipe evaluations (#352) —
scripts/agent_eval.py run --liveruns a task's live checks through the worktree's ownmake up/make verify/make clean, with a time limit per step, and records the requested CUBRID version, the image that really ran and the server'scubrid_relline (a mismatch, such as an old base pinning 11.2 when 11.4 was requested, becomes a record warning, a summary count and a failed claim for the requested version), the currentscripts/ci_scope.pyselection (verify_paths, suites) and the exact commands. Amake verifycounts as passed only when it compared a golden with none failed or skipped.promptgives an agent the task, issue, recipe and report format without the grading key.recordstores one run: tool and model versions, task revision and prompt digest, start SHA, commands, skipped live checks, changed files, wall clock, time limit, review rework rounds, review findings, an optional exclusion for an unfair run, and the rubric result.summarizereports repeated runs per task revision and model as counts and a 95% Wilson interval, never a bare pass percentage.agent-evals/README.mddescribes the manual-only protocol: an isolated clone without later history or a push remote, no credentials, a fixed time limit, and publishing only when allowed. The isolation task gains a fixture from a real live run. Nothing runs an agent or the live path in CI. -
Offline agent evaluation tasks and grader (#352) —
agent-evals/adds a versioned task format for benchmarking coding agents on frozen historical recipe fixes, andscripts/agent_eval.pygrades a candidate's patch, test report and dry-run result. It checks the task's evaluator-owned hidden tests at its base SHA (a pytest run that skips every test is not a pass), the changed-file scope, required goldens and documentation, documented dependencies, values interpolated into SQL, hardcoded secrets,cookbook_table names, and honest live evidence: an offline result reported asmake verifyor a live suite, a live claim without a recorded run (checked clause by clause), or a missing not verified statement fails. Three tasks come from merged fixes: the ai-agent reserved column and TEXT type (#125), the MVCC isolation levels and their golden (#54) and the async recipe'ssqlalchemy-cubridfloor (#94). Their fixtures cover a missing golden, an unsafe query, an undocumented dependency, an unrelated-file change and false claims.make agent-eval-checkre-grades them deterministically and runs inmake test-offline; it calls no model and needs no database.scripts/ci_scope.pyand every CI lane are unchanged, and no score gates a pull request. Seeagent-evals/README.md. - The documentation check's shell scanner matches pycubrid again (#405) —
scripts/_markdown.pytakes pycubrid's final_substitution_end()byte for byte (cubrid-lab/pycubrid#978):;∧;&end acaseclause, a word continued by a quote or backslash (esac"x",esac\*) is a pattern rather thanesac, and a#comment at a word boundary inside a substitution changes nothing, with word boundaries tracked across escapes and quotes. The file now differs from pycubrid's and sqlalchemy-cubrid's only in its header line and one issue reference; pycubrid's 13 further regression cases are added. No other checker rule changes. - The documentation check reads shell substitutions as pycubrid and sqlalchemy-cubrid do (#403) —
scripts/_markdown.pynow takes the rest of the shared parser's shell handling (cubrid-lab/pycubrid#947, cubrid-lab/sqlalchemy-cubrid#935): acasepattern's)does not close a$(...), substitutions inside double quotes run (single-quoted ones do not), text the tokenizer rejects names nomakegoal, arithmetic$((...))runs only the substitutions nested in it,caseandesacare keywords only at the start of a command, and an unquoted newline in a substitution body separates commands. Review of the port found two more gaps in the shared_substitution_end(), fixed here:caseinside arithmetic ($((case + 1))) is a variable name, not a keyword,esaccloses acasewith no pattern ($(case x in esac)) but is a pattern after(or|((esac),x|esac)), and a quoted subject (case "x" in) is one word; these used to drop themakegoals of the whole command or report words as goals. The function now tracks arithmetic parentheses and eachcase's subject,in, clause start, pattern and body, and the unused_keyword_at()is removed; the same change goes to the pycubrid and sqlalchemy-cubrid copies so they stay identical. 22 newmake_goalscases cover these rules (7 failed on the old parser, 8 more on the pycubrid version). The checker's other rules are unchanged. - The documentation check computes site anchors with this site's slugifier (#388) —
scripts/_markdown.py, the parser shared with pycubrid and sqlalchemy-cubrid, modelled every site anchor onpymdownx.slugs.slugify, which those repositories configure. This site'smkdocs.ymlsets noslugify, so MkDocs uses Python-Markdown's default:## Résuméis#resume, not#résumé, and a Korean heading has no letters left and is numbered#_1,#_2. A valid site link to such an anchor faileddocs-check. The parser now reads the repository'smkdocs.ymland uses the slugifier it configures (default_site_slug()here;pymdownx.slugselsewhere), and allocates site ids with thetocextension's own rule: an empty id becomes_1, and a taken_Nis incremented (_1then_2). GitHub anchors are unchanged. Tests cover an accented and a Korean heading under both slugifiers, themkdocs.ymldetection, and agreement with Python-Markdown's ownslugifywhen it is installed. - Deterministic documentation defects block the merge (#353) — a new always-on
docs-checkjob inci.ymlrunsscripts/check_docs_links.py, which is shared with pycubrid and sqlalchemy-cubrid. It reports broken relative links and anchors (adocs/link to a staged page resolves to its source), undefined references, unclosed fences, issue-number lines that the site would render as headings, missingmakegoals andscripts/paths, and recipe commands whosepip install -rfile,pythonscript oruvicornmodule does not exist. It runs offline and also on rename- and delete-only pull requests. Reviewed exceptions live inscripts/docs-check-allowlist.txtwith a reason each; inline markers are not honoured. The shareddoc-lintstays advisory for style.CONTRIBUTING.mddescribes which check owns what, and a stale script reference in its CHANGELOG section is corrected. agent-reviewno longer counts empty reply-only Copilot records as review rounds (#380) — a Copilot record with no body and no thread-opening comment, such as a reply to an existing thread, is now ignored like an error or quota notice instead of counting as a summary-only round, which could put a PR past the two-round budget and reject valid evidence. A record that opens a thread still counts. Ported unchanged from cubrid-lab/cubrid-mcp-server#349, so the gate copies stay identical outside their constants.- Advisory
agent-reviewcheck for review evidence (#351) —.github/workflows/agent-review.ymlrunsscripts/agent_review_gate.pyfrom the base branch with a read-only token on every pull request and review event, and reports whether GitHub's own review records meet the review route of 2026-10-15: a substantive Copilot review within the two-round AI review budget, or a maintainer'sAPPROVEDreview, or aCOMMENTEDreview that opens an inline thread. Changes to any workflow or composite action,.github/dependabot.yml,scripts/release_smoke.py,scripts/mcp_smoke.py, the lane selection inscripts/ci_scope.pyandscripts/driver_floors.py, and the gate itself need a maintainer'sAPPROVEDreview. Pending requests, PR-body checkboxes, summary-only bot text, other people's reviews and stale reviews never count; unresolved or author-resolved critical or security threads and missing API data fail the check. Dependabot PRs are exempt. A review thread without author identity, a resolved one without resolver identity, or a commit without author or committer identity fails closed. The gate is a copy of the cubrid-mcp-server gate (Refs cubrid-lab/cubrid-mcp-server#341) with only its constants block changed (the thread-identity fix is being ported back in cubrid-lab/cubrid-mcp-server#346), andmake test-offlineruns its tests. The check is advisory: a pull request can publish a check with the same name, so it must not be made a required check until it is posted from an identity a PR cannot impersonate (#378).CONTRIBUTING.mddocuments the rules. - Pages permissions only on the deploy job, and labelled automated issues (#330) —
.github/workflows/docs.ymlgrantedpages: writeandid-token: writeto the whole workflow, so thebuildjob that runspip installand MkDocs had them too; the workflow is nowcontents: readand only thedeployjob gets the two Pages permissions.tests/test_workflow_permissions.pyallowlists them fordeployinstead of the top level, so a top-level orbuildgrant now fails. Thedriver-main.ymlreportjob opened its tracking issue with only--label ci; it now also addspriority: mediumandsize: S, as AGENTS.md requires at creation (CONTRIBUTING.mdsays so), and a new test fails when any workflow'sgh issue createlacks exactly one canonicalpriority:and onesize:label. Both tests fail on the previous workflows. scripts/check_translation_sync.pyhas offline tests —tests/test_translation_sync.pycovers theBASE_REFhandling ofchanged_files()(defaultmain,origin/prefix added once, tab-separated lines contributing both paths, a failinggitraised) andmain()(no translations present, unchangedREADME.md, a missing Korean translation failing, a missing community translation only warning, an unknown language labelled by its code). The tests run inmake check-docsand in thedocs-syncworkflow. The script is unchanged.- The closing guard skips a multi-line link reference definition — CommonMark
lets a definition's destination and title run on to later lines, so
Closes, a[a]:line, an indented/targetline and thenowner/repo#Nstill renders nothing between the keyword and the reference. The gap walk now reaches every later line end of the paragraph after a definition line, up to a blank line, which can only add blocking; each line end is scanned once per check. Copilot found this in cubrid-lab/sqlalchemy-cubrid#822;pr-title.ymlstays byte-identical across pycubrid, sqlalchemy-cubrid, cubrid-mcp-server and cubrid-cookbook-python. - The closing guard catches a closing keyword used as link text —
[Closes](url) owner/repo#Nrenders as "Closes owner/repo#N", but the gap after the keyword stopped at]. ThePR titlecheck now crosses a link's],](destination)or][label], and also a quoted continuation line, an image's alt text and a link reference definition line, each of which renders nothing between the keyword and the reference. Scanning stays linear in the body length, so adversarial link syntax at GitHub's 65,536-character limit stays well under the tests' 5-second timeout. The fix came from Copilot's review of cubrid-lab/pycubrid#849, andpr-title.ymlis again byte-identical across pycubrid, sqlalchemy-cubrid, cubrid-mcp-server and cubrid-cookbook-python. - The closing guard also checks the PR title and commit messages, and owners with
an underscore — the PR title and the PR's commit messages make up the squash
commit, so the
PR titlecheck now reads them too, as plain text: Markdown code does not hide a keyword there, and each commit message is checked on its own. The job lists the commits withgh api, using a token that can only read pull requests (the job haspull-requests: read; the workflow-level permissions stay empty), and fails when a PR has 250 or more commits, because GitHub lists only - A test pins that step. Owners with an underscore, as Enterprise Managed Users
have (
alice_acme/repo#N), are now matched in references and links. The job setup and these checks come from pycubrid's guard (cubrid-lab/pycubrid#845); code-block and comment handling of the PR body is unchanged; an underscore inside a word is no longer read as emphasis, so underscore owners are matched there too. cubrid-mcp-server (cubrid-lab/cubrid-mcp-server#276) and sqlalchemy-cubrid (cubrid-lab/sqlalchemy-cubrid#818) have the same change. - The PR-body closing guard catches emphasized, linked, escaped and split
closures, and fails closed where Markdown structure is uncertain — the
PR titlecheck now rejects: - emphasis or HTML around the keyword or the reference (
**Closes**,*Fixes*,__Resolves__,Closes **owner/repo#N**,<b>Closes</b>), a keyword split by markup (Close**s**), and inline comments, processing instructions or CDATA between the keyword and the reference; www.and scheme-lessgithub.com/issue URLs, autolinks, backslash escapes (\#), HTML entities (#, ), a backslash line break, leading zeros, and a foreign issue listed after another one (Closes #1, owner/repo#2);- a closing keyword followed by a link (
[text](…),[text][label],<a href>) when the body links any foreign issue, absolute or relative, since the link text hides its target. This case has its own error message.
Only fenced and indented code blocks and HTML comment blocks are ignored. Inline
code and inline comments are scanned, because hiding them needs inline parsing
that link destinations, HTML attributes and autolinks can defeat. Once a fence
appears where its extent is unknown (in a list item, a block quote, after a list
marker, inside raw HTML, on an indented line, or as a line that only looks like a
fence), no later fence or comment is hidden, so a closing fence can never be
misread as an opener. Raw HTML blocks end where CommonMark ends them, and any
other line starting with a tag is treated as raw HTML until a blank line. Comments
after anything that may start an HTML tag are scanned, a comment also ends at
--!>, and after a raw-text element such as <textarea> or <select> the rest
of the body is scanned. Lines split only at CR and LF, a leading byte order mark
is dropped, and only spaces and tabs count as blank or indentation. A heading,
block quote, fence, thematic break or comment block directly under a list item now
ends the list, so indented code after it is ignored instead of blocking the PR.
Footnote text is treated like list text. A very long issue number is reported
instead of crashing the check, and long adversarial bodies stay fast at GitHub's
65,536-character limit. This is the same guard, with the same regression cases, as
sqlalchemy-cubrid (cubrid-lab/sqlalchemy-cubrid#816) and cubrid-mcp-server
(cubrid-lab/cubrid-mcp-server#275). A test pins the guard block's SHA-256 so every
copy changes together.
- scripts/lint_changelog.py no longer warns that there are no released
versions — the warning was accurate but misleading: tags v0.1.0 and v0.2.0
were cut before CHANGELOG.md existed, and the repository publishes no GitHub
Releases. Writing sections for them now would invent release notes that never
existed (AGENTS.md "Release notes": historical notes are never regenerated, and a
selective fix needs maintainer approval), so the lint lists them in
PRE_CHANGELOG_TAGS and reports them instead. The changelog preamble names them
too. The
[Unreleased] entry for the eight Python examples now says they were already in
the v0.1.0 tag. The script now differs from the pycubrid, sqlalchemy-cubrid and
cubrid-mcp-server copies in this constant and message as well as in
SECTION_POLICY_CUTOFF; only this repository has tags older than its changelog.
- Reject cross-repository issue auto-closure in pull requests — the
required PR title check prevents direct foreign closing keywords in PR bodies,
while leaving local Closes #N and cross-repository Refs owner/repo#N
unchanged. No PR code is checked out and no token permissions are granted.
(Superseded in part by the entries above: the job now has a pull-requests: read
token to list the commit messages, and the PR title and commit messages are
checked too.) One tested scan skips fenced code and HTML comments (a marker inside
one cannot hide live text in the other); indented lines stay checked. (Superseded
in part by the guard entry above: an indented code block outside a list is now
ignored.) Issue/PR templates clarify independent repository ownership.
- ci.yml comments describe the main ruleset (#223 follow-up) — the comment above the two PR smoke lanes no longer says their check names are kept "so branch protection is unchanged": the main ruleset requires only CI Gate (all checks must pass) and PR title, and ci-gate enforces the smoke lanes. The ci-gate and dependabot-auto-merge.yml comments now name the ruleset. Comments only; no job, check name or trigger changed.
- ci.yml installs dependencies with a pinned uv (#229) — the lint-python, quickstart-tests, docs-site and pytest-suites jobs run astral-sh/setup-uv v10.2.0 (pinned to its commit SHA, uv 0.12.17, after actions/setup-python, with the job's python-version and a per-job cache cache-suffix: ${{ github.job }}) and install with uv pip install --system; uv pip freeze --system logs the resolved versions. The pytest-suites job keeps one fresh venv per suite (#285), now created with uv venv and filled with uv pip install --python. pip and uv resolve the same package set for the quickstart, Flask, FastAPI, Django and docs requirements (checked locally, names normalized). Left on pip on purpose: smoke-test.yml and .github/actions/pr-smoke (release verification and candidate provenance read pip's direct_url.json; make deps honours PIP_CONSTRAINT), driver-floors, driver-main.yml, docs.yml, and the setup-command inputs of the reusable live-smoke.yml jobs (they run in the organization's workflow, which does not provide uv). tests/test_workflow_installs.py pins the setup. No runtime change.
- Pull requests run the changed Flask/FastAPI recipes' own suites (#227) — scripts/ci_scope.py now emits web_suites, a JSON list of the changed Flask/FastAPI suite roots (a directory under templates/flask/ or templates/api-service-fastapi/recipes/, or the root FastAPI app, that owns a requirements.txt and tests/). ci.yml's Recipe pytest suites (live CUBRID) job is now a plain job with its own CUBRID 11.4 service: for each selected root it creates a fresh venv, installs only that root's requirements.txt (plus pytest, pytest-asyncio, httpx), and runs its tests/ in a separate pytest process; every suite runs even after a failure, the job fails if any suite fails or passes no test, and CI Gate (all checks must pass) fails with it. Shared Flask/FastAPI files (for example templates/flask/requirements.txt), shared live infrastructure, CI-policy changes and unknown paths add the representative suites (templates/flask/01-basic-crud, templates/api-service-fastapi/recipes/01-basic-crud and the root FastAPI app) on top of the changed recipes; they never count toward the cap. At most MAX_WEB_SUITES (25) changed roots run per pull request, which covers every current suite (23; a test fails when a new recipe outgrows the cap), so a grouped Dependabot bump runs every suite it touches. Changed roots beyond the cap would be emitted as web_suites_skipped, and the job prints a ::warning:: for each plus a "Not validated before merge" list in its summary, pointing to a manual smoke-test.yml run on the branch; smoke-test.yml still runs every suite after merge. Each suite's pytest run is limited to 10 minutes, and the zero-passed guard reads only pytest's final summary line. Recipe 10 stays on its own pinned CQRS job. The roots reach the job only through an env var holding JSON, never through shell interpolation. Previously every Flask/FastAPI pull request ran only the representative suites with one shared install. tests/test_ci_scope.py covers single and multiple recipes, the shared-file fallback, recipe 10, non-recipe changes, a shared file plus three recipes, the grouped Flask bump, the cap and its skipped list, deleted and renamed recipes, spaces in file names, git-quoted paths, unsafe roots and the job wiring; CONTRIBUTING.md "CI tiers" and SUPPORT_MATRIX.md describe the lane.
- Candidate artifact mode for the release workflow call (#240) — smoke-test.yml gains four optional workflow_call inputs: mode (published, the default, or candidate), candidate_artifact, candidate_hashes and candidate_source_sha (the commit the candidate was built from). Candidate mode also requires a request_id that stays the same across run attempts, so gh run rerun --failed replaces only the failed cells' parts. In candidate mode each of the three unchanged release cells downloads the caller's artifact by name from the same run, requires exactly the files in the build job's {"<file>": "<sha256>"} map with every SHA-256 matching, checks the single wheel's filename, archive integrity and METADATA name/version against package/version, and installs only that wheel (no PyPI wait, no index copy). After every later install the requested package must still be that wheel (direct_url.json and digest), every other driver must come from the package index, and the pip cache is restored but never saved. Missing, substituted, corrupt or mismatched input and incompatible mode inputs fail closed; published mode still installs only the exact PyPI release, and neither mode falls back to the other. New outputs evidence (published package / candidate artifact) and candidate_sha256; candidate artifacts are release-candidate-part-<request_id>--cubrid-<v>[-py<p>] and release-candidate-verification-<request_id>. The report keeps schema 1 with additive keys (evidence; per-part packages listing every installed distribution; and for candidates a candidate block with source_sha, expected_sha256 and the verified candidate_sha256, plus the caller repository and run commit); pushes and the schedule are labelled none (not release evidence); driver-main.yml labels its summary representative main (advisory). Existing callers are unaffected: same required inputs, outputs, job names, published artifact names, matrix and contents: read. tests/test_release_candidate.py covers wrong package, version, SHA-256 or digest map, missing artifact or report, dependency substitution, failed, cancelled or skipped cells and incompatible mode inputs. The contract and runbook are in docs/internal/rc-verification-design.md section 13, and the CONTRIBUTING "Release verification contract" points to it (once per mode per caller run). No caller uses candidate mode yet.
- Release verification fails when the smoke result download fails (#232) — actions/download-artifact v8 can fail its digest check after extracting files, so valid-looking part files could still yield a passing report. The report job's download step (still continue-on-error, so the report always runs) now has id: download, and smoke-test.yml passes steps.download.outcome to scripts/release_smoke.py report --download-result. A release report fails unless that outcome is success, recording smoke result download did not succeed: <outcome>; the failure artifact is still written and uploaded. Non-release runs, RELEASE_CELLS, version matching and verify_result handling are unchanged. tests/test_release_verification.py covers failed, cancelled and skipped outcomes, the CLI argument and the workflow wiring. Callers pinning this workflow by SHA need a coordinated bump to pick it up.
- Release verification waits until PyPI serves the requested version (design C4) — before installing package==version for a release request (repository_dispatch, a pinned workflow_dispatch or a workflow_call), scripts/release_smoke.py select now polls the PyPI JSON API (https://pypi.org/pypi/<package>/<version>/json, anonymous) until it lists exactly that version with a wheel or sdist, with 5/10/20/30 s backoff inside a 600 s wall-clock budget. HTTP and network errors, malformed JSON, another version or a release without files count as not yet served; on timeout the job fails with PyPI does not serve <package>==<version> after N s (<last reason>), where the reason names the HTTP/JSON error or what PyPI served instead (served <name> <version> with N wheel/sdist files, to diagnose stale CDN nodes), and never falls back to another version. On 2026-10-09 stale PyPI CDN nodes hid both driver 1.10.0 uploads from one smoke cell for about 95–106 s, exhausting the six install attempts and failing the release runs until the jobs were re-run by hand. The existing six-attempt install retry stays (the JSON API and the simple index can briefly disagree) and now uses pip install --no-cache-dir; the Select released drivers step timeout rises from 10 to 20 minutes to cover both budgets. Inputs, outputs, job names, RELEASE_CELLS, the report and the exact installed-version check are unchanged, and runs without a requested release never poll. tests/test_release_pypi_wait.py covers immediate and delayed availability, the timeout and its message, retried HTTP/JSON errors, rejection of other versions, packages or file-less releases, and that non-release and invalid requests never poll.
- CHANGELOG lint applies the duplicate-heading check only after the cutoff — scripts/lint_changelog.py (rule 5) now rejects a duplicate ### heading within one version section only in [Unreleased] and in releases newer than SECTION_POLICY_CUTOFF (0.2.0), so released history is never rewritten; a duplicate heading before the first release header is still rejected. The script is now identical to pycubrid's, sqlalchemy-cubrid's and cubrid-mcp-server's except for SECTION_POLICY_CUTOFF and, since the entry above, PRE_CHANGELOG_TAGS and its message. tests/test_lint_changelog.py covers adjacent and non-adjacent duplicates after the cutoff, duplicates in released history, and the pre-release preamble; CONTRIBUTING.md documents the cutoff.
- CHANGELOG lint rejects a release header inside an open code fence — scripts/lint_changelog.py now fails with Release header inside an open code fence in CHANGELOG.md when a line starting ## [ appears while a fence is open, because scripts/extract_release_notes.py is not fence-aware and would truncate the GitHub Release body at it. A fenced ### line stays entry content. The script now matches pycubrid's and cubrid-mcp-server's apart from SECTION_POLICY_CUTOFF (and, since the PRE_CHANGELOG_TAGS entry above, that constant and its message); tests/test_lint_changelog.py and CONTRIBUTING.md cover and describe the rule.
- make check-docs lints the CHANGELOG sections — scripts/lint_changelog.py rejects non-standard, empty, duplicate or out-of-order ### headings and an unclosed code fence in [Unreleased] and in releases after 0.2.0. Also checks that [Unreleased] comes first and only once and that release sections are unique and in descending order. Covered by tests/test_lint_changelog.py.
- Exact CQRS recipe dependency validation — the required CI gate now runs FastAPI recipe 10-cqrs-event-sourcing in isolated Python 3.12 jobs on live CUBRID 11.2 and 11.4, installing that recipe's own requirements.txt instead of the broad smoke dependency list. Each job checks package consistency, records installed versions, and runs its seven existing tests; SQLite fallback is not used. Broad latest-package smoke coverage remains separate.
- Aggregate CI Gate (all checks must pass) job in ci.yml: a final if: always() job that needs lint-python, quickstart-tests, docs-sync, doc-lint and pytest-suites and fails unless every one of them succeeded (failure, cancelled or an unexpected skip all fail it; none of these jobs is path-gated). It mirrors pycubrid's gate so branch protection can require a single CI context alongside the two smoke-test jobs. The other jobs are unchanged.
- Async SQLAlchemy recipe installs its required runtime (related to #152) — changed the existing SQLAlchemy requirement to sqlalchemy[asyncio]>=2.0, preserving its version floor while requesting the supported asyncio extra. Plain SQLAlchemy 2.1 omits greenlet and caused 02_async_sqlalchemy.py to fail on import in both live smoke cells. Updated the displayed requirements and matching golden, and documented the install target; all behavioral golden assertions remain intact. This fixes the caller install contract; it does not resolve the broader recipe-floor audit in #152.
- Flask and FastAPI pytest suites run in smoke-test CI (#131): smoke-test.yml now installs the recipe test dependencies and runs every templates/flask/*/tests and templates/api-service-fastapi/recipes/*/tests suite (23 suites, 171 tests) against the job's live CUBRID container on both 11.2 and 11.4, using the CUBRID_TEST_URL support from #132 and #134. The suites run on pushes to main, the nightly schedule, upstream-release dispatches and manual runs; pull requests skip them so the PR gate stays fast. Each suite runs in its own pytest process; all suites run even after a failure and the step then fails, a suite that collects no tests also fails it, results are written to the job summary, and the step has a 15-minute timeout. The dependency list is explicit rather than each recipe's requirements.txt, whose pins conflict, and it leaves the released pycubrid and sqlalchemy-cubrid under test untouched. SUPPORT_MATRIX.md and the docs home page no longer describe these suites as manual-only.
- cubrid-mcp-server now installs from PyPI in smoke tests (was git fallback). Falls back to the v0.4.0 git tag only if PyPI is unreachable.
- Pinned ruff to 0.16.4 in CI (#78) — the lint job installed ruff unpinned, so formatter/linter rule changes in new ruff releases could break CI unpredictably; pinned to 0.16.4 to match the version used by pycubrid and sqlalchemy-cubrid.
- Golden-coverage guard closes the silent-skip gap (#89) — added scripts/check_expected_coverage.py, wired into make verify (via a new check-coverage target) and the smoke-test.yml workflow. make verify is golden-driven, so any *.py inside a directory that owns an expected/ folder but lacks a matching expected/<name>.expected golden was silently never executed in CI. The guard now fails loudly on any such uncovered script (exceptions go in scripts/verify_exclusions.txt with a reason).
- Smoke tests now run a CUBRID 11.2 + 11.4 job matrix — smoke-test.yml previously started a single cubrid/cubrid:11.2 container; it now runs make verify goldens against both 11.2 and 11.4, giving the "expected to work" claim for 11.4 in SUPPORT_MATRIX.md direct CI evidence. SUPPORT_MATRIX.md updated accordingly.
Tests¶
- Nested Flask and FastAPI recipes receive individual smoke-coverage warnings (#475) — the docs-sync smoke check now finds runnable sub-recipes under
templates/flask/andtemplates/api-service-fastapi/recipes/and warns when a recipe has no localtests/orexpected/, even if its siblings are covered. README/llms.txt indexing and the existing warning-only policy are unchanged. - Recipe tests are colocated with examples (#472) — AI-agent offline and live checks now live under
templates/ai-agent/tests/, benchmark and migration regression checks underperformance/tests/, and the output normalizer's shell test undertests/. Themake test,make test-normalize,make verify-ai-agent,make verify-performanceandmake verifycommands, their CI test selection and the existing golden files keep their behavior; this change only relocates tests and updates path references. - AI-agent live suite asserts persisted state, MCP semantics and repeatability (#422) —
tests/test_ai_agent.pyno longer passes on exit code and a✓ ... workingline alone. After each oftemplates/ai-agent/01,03,04and05runs, an independent pycubrid connection reads the rows the recipe persisted (IDs and timestamps left out, foreign keys resolved through joins) and compares them with explicit expectations: the agent session, tags, messages with JSON metadata and tool calls; the RAG documents' source, metadata, tags and chunk count, each chunk's token count andembedding_ref, and the latest retrieval-log row; the agent loop's nine messages, three tool calls and product rows; the chatbot user, conversation and four turns; and every table's row count. The rerun case requires the same state after a second run on the same database, new root-row IDs (fixed keys deleted, not reused) and one more append-only retrieval-log row. A new MCP case runsinitialize,tools/list(thescripts/mcp_smoke.pytool contract),all_table_names(its sorted list must equal thedb_classuser tables asowner.table, so same-named tables of different owners are not merged) and aSELECT COUNT(*)in process, and checks that the rejectedDROP TABLEleaves the target table and a sentinel row unchanged; the MCP case now owns the agent-state tables for cleanup.CUBRID_LIVE_GATE=1makes a missingCUBRID_TEST_URLfail instead of skip; without it the live suite still skips.make verify-ai-agent(and the fullmake verify) runs the live suite with the gate, writes JUnit evidence to.live-evidence/ai-agent/(not tracked) and fails if any case skipped; the offline suite runs inmake test.tests/test_ai_agent_offline.pychecks that a deliberately wrong expectation fails each state comparison and that the gate fails without a URL or with an unreachable database. No recipe changed and no external LLM is called. - Live evidence for the migration goldens and the performance benchmarks (#423) —
make verify-performance(and the fullmake verify) runs the newtests/test_performance_benchmarks.pyagainst the examples'localhost:33000/testdband writes a JUnit report and a summary to.live-evidence/performance/(git-ignored).TestMigrationGoldensruns the fivemigration/java-to-pythongoldens as separately named tests with themake verifycomparison;TestPerformanceBenchmarkshas a named test for each ofperformance/fetch-optimization,bulk-insertandconnection-poolingthat runsbenchmark.pyunderscripts/run_example.pyand fails on a nonzero exit, empty output, a printed error or traceback, a missing comparison line, a time that is not a finite nonnegative number, or a workload that differs from the configured one. It never compares timings. Two more tests inject a failure intofetch-optimizationandbulk-insertafter their table exists and check thatcookbook_fetch_test/cookbook_bulk_testare still dropped, and an unrelated sentinel table must be unchanged after each script-running test (a failure names that test). The report records the CUBRID server, Python, pytest and driver versions. The lane setsCUBRID_LIVE_GATE=1, so an unreachable database fails every database test (the run fails), and the lane fails if any test was skipped; without the variable the module skips when no database answers. The scripts use fixed table names, so the lane must not run at the same time as anothermake verify-performance,make verifyor a manual benchmark run on the same database. No CI lane runs it yet.make helpnow also lists targets whose names contain digits.CONTRIBUTING.mdandperformance/README.mddocument it. - Rerunnable, fail-closed live lanes for the Flask/FastAPI, Django/Celery and dashboard templates (#421) —
make verify-web,make verify-django-celery,make verify-dashboardandmake verify-templatesrunscripts/live_gate.py, which reuses the existing suites (no new test implementations) from a declarative manifest, one pytest process per suite:verify-webruns everytemplates/flask/*/testsandtemplates/api-service-fastapi/recipes/*/testssuite on live CUBRID plus the root FastAPI template's startup test, which stays SQLite by design and is labelled startup-only (not live and not quickstart/healthcoverage);verify-django-celeryrunstemplates/django/testsandtemplates/async-worker/tests, recorded as direct Celery task execution (.run()/ eager.apply()) without Redis or a worker;verify-dashboardrunstemplates/dashboard/testsonCUBRID_DASHBOARD_TEST_URL, a separate database, because the suite drops and recreates its FK-linked tables. The Make targets setCUBRID_LIVE_GATE=1by default (1/true/yes/onor empty/0/false/no/off; any other value is a usage error). Under the gate a missing, unreachable or non-cubrid+pycubrid://URL (including SQLite), a dashboard URL naming the same database asCUBRID_TEST_URL, a database that already holdscookbook_*tables, or a suite that fails, collects no test, skips any test, leaves new tables behind or drops a pre-existing one (each suite against its own pre-run snapshot) fails the lane with a nonzero exit. The suites that print a database header (Flask, FastAPI recipes, dashboard) must printdatabase: live CUBRID (...); the Django and async-worker suites print none and rely on skipping without a URL (a gate failure) and failing on a non-cubrid+pycubridURL. The gate dropsPYTEST_ADDOPTSandPYTEST_PLUGINSfrom the suites' environment. Only a gated run can PASS: withCUBRID_LIVE_GATE=0the suites keep their existing skips and SQLite fallbacks and the summary says INCOMPLETE at best. Each run replaces only.live-evidence/<lane>/(ignored by version control; other lanes' evidence is kept) with per-suite JUnit XML and logs plussummary.json/summary.txt: commands, commit SHA (dirty flag ignores untracked files), the Python and driver/framework versions of the interpreter that runs pytest, the CUBRID server version (SELECT VERSION()), and selected/passed/failed/errors/skipped counts and test-case outcomes per suite. The Django and async-worker fixtures now assert that their teardown removed their tables, so a cleanup gap fails the run that caused it instead of the next run's preexisting-table refusal.tests/test_live_gate.py(inmake test) covers the fail-closed rules with a stub pytest and no database.CONTRIBUTING.mddocuments the lanes. No workflow changed. - The workflow issue-label check rejects non-canonical variants (#344) —
label_problems()intests/test_workflow_permissions.pyrequired one canonicalpriority:and onesize:label on every workflowgh issue createbut ignored other labels, sopriority: mediumnext to a forbidden variant passed. It now also reports any label that reads as a priority or size label without being a canonical name (priority:high,priority-high,Priority: High,priority: urgent,P1,p0,size:S,size-xs,Size: S,size: xs); unrelated labels such asci,area: templatesorpricingare not affected. A new test covers each variant and fails on the previous helper. No workflow changed: the current tracking-issue labels are canonical. - Offline test for the SQLAlchemy quickstart's cleanup after a failed drop (#332) —
tests/test_quickstart_cleanup.py(inmake test-offline) runsquickstart/5min-sqlalchemy/app.py'smain()against a throwaway SQLite file and makes the Core, then the ORM, cleanup drop raiseSQLAlchemyError. Each time the other drop still runs,engine.dispose()is called once,Cleanup failed:is printed and only the failed table remains. It needs onlysqlalchemy, and it fails if the two drops share onetry. The app is unchanged.