Repository navigation
fix(cbinding): return caller-owned string from gosqlx_version; add Python CI - #847
Conversation
…thon CI gosqlx_version returned a sync.Once-cached C string while its doc comment, gosqlx_free's doc, and pygosqlx.version() all treat it as caller-owned. The second version() call in a process therefore freed the same pointer twice and macOS aborted with "pointer being freed was not allocated". - gosqlx_version now returns a fresh C.CString on every call, matching the documented contract of every other export; the cache is removed. - Go tests: getVersion frees its result; new TestCGosqlxVersion_CallerOwned calls gosqlx_version twice and frees both; TestCGosqlxFree_Nil. - pkg/cbinding/tests/test_ctypes.py declared results as c_char_p, so gosqlx_free received pointers to Python-owned bytes copies; use c_void_p and free the version string. - python: regression test calling version() repeatedly, __version__ / setup.py vs pyproject consistency tests; align __version__ with the declared 0.2.0; declare readme in pyproject so editable installs work with current setuptools. - .github/workflows/python.yml: build libgosqlx and run pytest plus the raw ctypes ABI tests on ubuntu/macos x Python 3.10/3.13. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
Claude finished @ajitpratap0's task in 15s —— View job Review of PR #847Verdict: looks good. The fix is correct and minimal, and the tests target the real failure. I only read the diff and did not run the tests or CI. Nothing below blocks merging. Fix (
Tests
CI (
Packaging
Remaining item: the checklist still shows the Python Bindings workflow as not yet green on all 4 matrix legs. Please confirm that before merging. |
|
Claude finished @ajitpratap0's task in 15s —— View job Review of PR #847Verdict: looks good. I read the diff and checked a few files in the checkout. I did not run the tests or CI.
Fix (
Tests
CI (
Packaging
Before merging: the test plan still shows the Python Bindings workflow as not green on all 4 matrix legs. Please confirm that. |
Summary
Bug: since #331,
gosqlx_version()has returned async.Once-cached C string. Its doc comment,gosqlx_free's doc, andpygosqlx.version()all treat that string as caller-owned and free it. As a result, the secondpygosqlx.version()call in a process double-frees. macOS aborts withpointer being freed was not allocated; on glibc it is heap corruption. I reproduced it onmain:pytest python/testsaborts inTestVersion::test_version_format.Fix (C side follows the documented contract):
gosqlx_versionnow returns a freshC.CString(VERSION)on every call, and I removed the cache. The comments ongosqlx_versionandgosqlx_freenow state the ownership rule and thatgosqlx_free(NULL)is a no-op.toJSONon both success and error paths, including the marshal-failure fallback. None returns NULL or a static pointer. NULL input goes throughC.GoString(NULL)and becomes"".gosqlx_versionwas the only mismatch in the library.pkg/cbinding/tests/test_ctypes.pydeclared results asc_char_p. That makes ctypes return a Pythonbytescopy, sogosqlx_freereceived a pointer it never allocated. It now usesc_void_pplusstring_at, and it frees the version string too.Tests
getVersion()now frees its result; previously the stress test's "cached singleton, just read it" comment covered a call that never freed. The newTestCGosqlxVersion_CallerOwnedcallsgosqlx_versiontwice, asserts distinct pointers and equal strings equal toVERSION, and frees both. I confirmed it fails on the old code. Also addedTestCGosqlxFree_Nil. I renamedTestStressVersionSingletontoTestStressVersionConcurrent.TestVersion::test_version_called_twiceis the regression test.test_package_version.pyassertspygosqlx.__version__and thesetup.pyversion both equal the pyproject version.CI: the new
.github/workflows/python.ymlruns on PRs and on pushes to main, filtered topkg/**,python/**,go.mod,go.sumand the workflow file. It covers ubuntu-latest and macos-latest with Python 3.10 and 3.13, on Go 1.26. Steps: build withpkg/cbinding/build.sh, fail if the library is missing (so integration tests can't skip without anyone noticing),pip install -e "python[dev]", a two-callversion()smoke test,pytest python/tests -v -rs, and the raw ctypes ABI tests. Actions are pinned to the same SHAs the repo already uses (checkout v4.4.0, setup-go v5.6.0, setup-python v5.6.0). Permissions arecontents: read.Design decisions I made (scoped worker, conservative choices):
pygosqlx.__version__was"0.1.0"while pyproject and setup.py declare0.2.0. The required__version__ == pyprojecttest can only pass if they agree, so I changed__version__to"0.2.0". This is byte-identical to the same hunk in release PR docs(release): prepare v1.16.0 #845, so the two merge cleanly in either order. The package version itself is unchanged.pip install -e "python[dev]"fails on current setuptools becausesetup.pypasseslong_descriptionwhile pyproject's[project]table doesn't declarereadme, which raisesAttributeErrorin_long_description. I addedreadme = "README.md"to[project]. This changes metadata only, not the API or the version.Test Plan
mainwithgo build -buildmode=c-shared+GOSQLX_LIB_PATH=… pytest python/testsgofmt -l pkg/cbinding(clean),go vet ./pkg/cbinding/,golangci-lint run ./pkg/cbinding/...(0 issues)CGO_ENABLED=1 go test -race -count=1 ./pkg/cbinding/passes; the new regression test fails against the oldcbinding.gobuild.sh,pip install -e "python[dev]", smoke test,pytest python/tests(79 passed),test_ctypes.py(25 OK);pip wheel ./pythonbuildspygosqlx-0.2.0Breaking Changes
None for correct callers. A C caller that relied on the old (undocumented) behaviour and never freed the
gosqlx_version()result now leaks a few bytes per call. The documented contract has always requiredgosqlx_free.Release note
🤖 Generated with Claude Code
https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9