Issue: test_insert_behavior fails on Python 3.11
Author: carlwgeorge
State: closed
Created: 2022-09-20 21:55:03

While running the tests with Python 3.11.0rc2, I'm seeing this failure.

```
FAIL: test_insert_behavior (tests.sqlite.TestSqliteReturningConfig.test_insert_behavior)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/carl/development/peewee/tests/sqlite.py", line 2506, in test_insert_behavior
    self.assertEqual(iq.execute(), 2)
AssertionError: 0 != 2
```

Steps to reproduce on Fedora 36 or 37:

```
dnf -y install gcc git-core sqlite-devel python3.11-devel
git clone https://github.com/coleifer/peewee.git
cd peewee
python3.11 -m venv py311
. py311/bin/activate
pip install cython
python setup.py install
python setup.py build_ext -i
python runtests.py
```

EDIT:  Forgot to include the exact versions involved.  Let me know if others are needed.

F36:
python3.11-3.11.0~rc2-1.fc36
sqlite-3.36.0-5.fc36

F37:
python3-3.11.0~rc2-1.fc37
sqlite-3.39.2-2.fc37

I wonder if this is a regression in the standard lib sqlite3. Is it possible for you to also try pip installing `pysqlite3` and see if the test passes? Peewee should try to use pysqlite3, you can check by running:

```
from peewee import sqlite3
print(sqlite3)
```

This should hopefully be pysqlite3.

Trying to install pysqlite3 with pip seems to fail, even though it says success.

```
(py311) [root@f37-container:~/peewee]# pip install pysqlite3
Collecting pysqlite3
  Using cached pysqlite3-0.4.7.tar.gz (40 kB)
  Preparing metadata (setup.py) ... done
Building wheels for collected packages: pysqlite3
  Building wheel for pysqlite3 (setup.py) ... done
  WARNING: Legacy build of wheel for 'pysqlite3' created no files.
  Command arguments: /root/peewee/py311/bin/python3.11 -u -c '
  exec(compile('"'"''"'"''"'"'
  # This is <pip-setuptools-caller> -- a caller that pip uses to run setup.py
  #
  # - It imports setuptools before invoking setup.py, to enable projects that directly
  #   import from `distutils.core` to work with newer packaging standards.
  # - It provides a clear error message when setuptools is not installed.
  # - It sets `sys.argv[0]` to the underlying `setup.py`, when invoking `setup.py` so
  #   setuptools doesn'"'"'t think the script is `-c`. This avoids the following warning:
  #     manifest_maker: standard file '"'"'-c'"'"' not found".
  # - It generates a shim setup.py, for handling setup.cfg-only projects.
  import os, sys, tokenize
  
  try:
      import setuptools
  except ImportError as error:
      print(
          "ERROR: Can not execute `setup.py` since setuptools is not available in "
          "the build environment.",
          file=sys.stderr,
      )
      sys.exit(1)
  
  __file__ = %r
  sys.argv[0] = __file__
  
  if os.path.exists(__file__):
      filename = __file__
      with tokenize.open(__file__) as f:
          setup_py_code = f.read()
  else:
      filename = "<auto-generated setuptools caller>"
      setup_py_code = "from setuptools import setup; setup()"
  
  exec(compile(setup_py_code, filename, "exec"))
  '"'"''"'"''"'"' % ('"'"'/tmp/pip-install-kkm14zbf/pysqlite3_fce9f2061a1141be9cb19473624dd0c8/setup.py'"'"',), "<pip-setuptools-caller>", "exec"))' bdist_wheel -d /tmp/pip-wheel-am9z47vw
  Command output: [use --verbose to show]
  Running setup.py clean for pysqlite3
Failed to build pysqlite3
Installing collected packages: pysqlite3
  Running setup.py install for pysqlite3 ... done
Successfully installed pysqlite3
(py311) [root@f37-container:~/peewee]# pip list
Package    Version
---------- -------
Cython     0.29.32
pip        22.2.2
setuptools 65.3.0
wheel      0.37.1
```

Hmm, I'm not sure what to try besides:

```
git clone https://github.com/coleifer/pysqlite3
cd pysqlite3
python setup.py build
python setup.py install
```

That at least may give an indication of the issue?

It doesn't seem pysqlite3 likes Python 3.11.

```
(py311) [root@f37-container:~/pysqlite3]# python setup.py build
running build
running build_py
creating build
creating build/lib.linux-x86_64-cpython-311
creating build/lib.linux-x86_64-cpython-311/pysqlite3
copying pysqlite3/__init__.py -> build/lib.linux-x86_64-cpython-311/pysqlite3
copying pysqlite3/dbapi2.py -> build/lib.linux-x86_64-cpython-311/pysqlite3
running build_ext
Builds a C extension linking against libsqlite3 library
building 'pysqlite3._sqlite3' extension
creating build/temp.linux-x86_64-cpython-311
creating build/temp.linux-x86_64-cpython-311/src
gcc -Wsign-compare -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DNDEBUG -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -fPIC -DMODULE_NAME=\"pysqlite3.dbapi2\" -I/usr/include -I/root/peewee/py311/include -I/usr/include/python3.11 -c src/blob.c -o build/temp.linux-x86_64-cpython-311/src/blob.o
gcc -Wsign-compare -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DNDEBUG -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -fPIC -DMODULE_NAME=\"pysqlite3.dbapi2\" -I/usr/include -I/root/peewee/py311/include -I/usr/include/python3.11 -c src/cache.c -o build/temp.linux-x86_64-cpython-311/src/cache.o
gcc -Wsign-compare -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DNDEBUG -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -fPIC -DMODULE_NAME=\"pysqlite3.dbapi2\" -I/usr/include -I/root/peewee/py311/include -I/usr/include/python3.11 -c src/connection.c -o build/temp.linux-x86_64-cpython-311/src/connection.o
src/connection.c: In function ‘_pysqlite_step_callback’:
src/connection.c:725:31: warning: implicit declaration of function ‘_PyObject_CallNoArg’; did you mean ‘PyObject_CallNoArgs’? [-Wimplicit-function-declaration]
  725 |         *aggregate_instance = _PyObject_CallNoArg(aggregate_class);
      |                               ^~~~~~~~~~~~~~~~~~~
      |                               PyObject_CallNoArgs
src/connection.c:725:29: warning: assignment to ‘PyObject *’ {aka ‘struct _object *’} from ‘int’ makes pointer from integer without a cast [-Wint-conversion]
  725 |         *aggregate_instance = _PyObject_CallNoArg(aggregate_class);
      |                             ^
src/connection.c: In function ‘_progress_handler’:
src/connection.c:1175:9: warning: assignment to ‘PyObject *’ {aka ‘struct _object *’} from ‘int’ makes pointer from integer without a cast [-Wint-conversion]
 1175 |     ret = _PyObject_CallNoArg((PyObject*)user_arg);
      |         ^
gcc -Wsign-compare -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DNDEBUG -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -fPIC -DMODULE_NAME=\"pysqlite3.dbapi2\" -I/usr/include -I/root/peewee/py311/include -I/usr/include/python3.11 -c src/cursor.c -o build/temp.linux-x86_64-cpython-311/src/cursor.o
gcc -Wsign-compare -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DNDEBUG -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -fPIC -DMODULE_NAME=\"pysqlite3.dbapi2\" -I/usr/include -I/root/peewee/py311/include -I/usr/include/python3.11 -c src/microprotocols.c -o build/temp.linux-x86_64-cpython-311/src/microprotocols.o
gcc -Wsign-compare -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DNDEBUG -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -fPIC -DMODULE_NAME=\"pysqlite3.dbapi2\" -I/usr/include -I/root/peewee/py311/include -I/usr/include/python3.11 -c src/module.c -o build/temp.linux-x86_64-cpython-311/src/module.o
gcc -Wsign-compare -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DNDEBUG -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -fPIC -DMODULE_NAME=\"pysqlite3.dbapi2\" -I/usr/include -I/root/peewee/py311/include -I/usr/include/python3.11 -c src/prepare_protocol.c -o build/temp.linux-x86_64-cpython-311/src/prepare_protocol.o
src/prepare_protocol.c: In function ‘pysqlite_prepare_protocol_setup_types’:
src/prepare_protocol.c:81:43: error: lvalue required as left operand of assignment
   81 |     Py_TYPE(&pysqlite_PrepareProtocolType)= &PyType_Type;
      |                                           ^
error: command '/usr/bin/gcc' failed with exit code 1
```

This is with gcc 12.2.1 if that matters.

Thank you, I'll investigate tomorrow. I only wanted to check because we've found a different regression in the 3.11 sqlite3.

Thanks, I appreciate it.  Let me know if I can try any other scenarios to pin it down.  Fedora packages multiple Python runtimes and it's easy to spin up various Fedora versions in containers to try different combinations of things.

I tried one more combination that might be interesting to you.  Using Fedora 36 with Python 3.10.6 and sqlite 3.36.0, I was able to reproduce the same error but with a stack trace this time.

```
======================================================================
ERROR: test_insert_behavior (tests.sqlite.TestSqliteReturningConfig)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/root/peewee/peewee.py", line 3221, in execute_sql
    cursor.execute(sql, params or ())
sqlite3.OperationalError: database table is locked

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/root/peewee/tests/base.py", line 246, in tearDown
    self.database.drop_tables(self.requires, safe=True)
  File "/root/peewee/peewee.py", line 3417, in drop_tables
    model.drop_table(**kwargs)
  File "/root/peewee/peewee.py", line 6845, in drop_table
    cls._schema.drop_all(safe, drop_sequences, **options)
  File "/root/peewee/peewee.py", line 5949, in drop_all
    self.drop_table(safe, **options)
  File "/root/peewee/peewee.py", line 5818, in drop_table
    self.database.execute(self._drop_table(safe=safe, **options))
  File "/root/peewee/peewee.py", line 3234, in execute
    return self.execute_sql(sql, params, commit=commit)
  File "/root/peewee/peewee.py", line 3218, in execute_sql
    with __exception_wrapper__:
  File "/root/peewee/peewee.py", line 2994, in __exit__
    reraise(new_type, new_type(exc_value, *exc_args), traceback)
  File "/root/peewee/peewee.py", line 191, in reraise
    raise value.with_traceback(tb)
  File "/root/peewee/peewee.py", line 3221, in execute_sql
    cursor.execute(sql, params or ())
peewee.OperationalError: database table is locked

======================================================================
FAIL: test_insert_behavior (tests.sqlite.TestSqliteReturningConfig)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/root/peewee/tests/sqlite.py", line 2506, in test_insert_behavior
    self.assertEqual(iq.execute(), 2)
AssertionError: 0 != 2

----------------------------------------------------------------------
```

In this environment I was able to pip install pysqlite3, and it caused the tests to pass.

I think you're right about this being a regression.  I spun up a Fedora 36 container with Python 3.10.5, and the tests pass there (without pysqlite3).  I think something changed between 3.10.5 and 3.10.6 that is also in 3.11.0rc2.

cc @erlend-aasland if you see this

> cc @erlend-aasland if you see this

Thanks for the heads-up; highly appreciated. I'll take a look as soon as I am back at my laptop.

I'll try to get a minimal reproduction -- it looks at first glance like an issue with the `cursor.rowcount`. Compiling python 3.11 now and will update here once I've figured out how to reproduce this w/stdlib sqlite3 (if indeed that's the culprit).

Well it's very strange and I'm not at all sure now that it's a regression. It looks to do in part with cached statements and the use of `returning`. In all likelihood it may be that the behavior of Python 3.11 is correct, but I'll leave that to you to determine @erlend-aasland.

Here is what the test is doing:

```python
import sqlite3

conn = sqlite3.connect(':memory:')
conn.execute('create table "t1" ("id" integer not null primary key, '
             '"col" text not null)')
          
curs = conn.execute('insert into "t1" ("col") values (?), (?) returning "id"',
                    ('u1', 'u2'))
curs.fetchall()  # Here we consume the cursor during the test.

# Same query as above.
curs = conn.execute('insert into "t1" ("col") values (?), (?) returning "id"',
                    ('u3', 'u4'))
assert curs.rowcount == 2
```

The thing that threw me off trying to replicate, however, is that during that first insert query, the returned rowcount is actually 0 on Python < 3.11:

```python
# For example, on python 3.7, the following raises no assertion errors:
# ... create table, same as above ...
curs = conn.execute('insert into "t1" ("col") values (?), (?) returning "id"',
                    ('u1', 'u2'))
assert curs.rowcount == 0  # Here the rowcount is 0 (!!)
curs.fetchall()  # Here we consume the cursor during the test.
assert curs.rowcount == 0  # Here the rowcount is STILL 0

# Same query as above.
curs = conn.execute('insert into "t1" ("col") values (?), (?) returning "id"',
                    ('u3', 'u4'))
assert curs.rowcount == 2  # Here the rowcount is 2
```

On, e.g. Python 3.7 -- if we do *not* consume the cursor in between the executions, then the second insert's rowcount is also 0:

```python
# For example, on python 3.7, the following raises no assertion errors:
# ... create table, same as above ...
curs = conn.execute('insert into "t1" ("col") values (?), (?) returning "id"',
                    ('u1', 'u2'))
assert curs.rowcount == 0  # Here the rowcount is 0 (!!)

# Same query as above.
curs = conn.execute('insert into "t1" ("col") values (?), (?) returning "id"',
                    ('u3', 'u4'))
assert curs.rowcount == 0  # Here the rowcount is also 0
```

On Python 3.11, though, the rowcount is always `0`:

```python
# On python 3.11, the following raises no assertion errors:
# ... create table, same as above ...
curs = conn.execute('insert into "t1" ("col") values (?), (?) returning "id"',
                    ('u1', 'u2'))
assert curs.rowcount == 0  # Here the rowcount is 0
curs.fetchall()  # Here we consume the cursor during the test.

# Same query as above.
curs = conn.execute('insert into "t1" ("col") values (?), (?) returning "id"',
                    ('u3', 'u4'))
assert curs.rowcount == 0  # Here the rowcount is also 0
```

----------------------------------------------------------------

It seems like my test unintentionally revealed some inconsistency in the way the rowcount is being handled, and possibly there is some interplay in the way the statement cache works, because of the way it manifests differently during the second execution.

@carlwgeorge - I've put in a very minimal skip directive to get your build passing for now: 49a64bd23591cd96f8ba832646ccac3256481325

I also have pysqlite3 fixed-up and working on `master`, thanks for sharing those tracebacks with me.

**Update:** I've removed the faulty assertions in light of sqlite3's behavior checked directly in my comment below - commit 73335396e0336332832e52d4d7272b7d1ed9e794

Looks like this has to do with how Sqlite itself handles `sqlite3_changes()` interface when the insert query has a RETURNING clause:

```c
#include <stdio.h>
#include <sqlite3.h>

int main(int argc, char *argv[]) {
    sqlite3 *db;
    sqlite3_stmt *stmt;

    sqlite3_open(":memory:", &db);
    sqlite3_exec(db, "create table t1 (id integer not null primary key, col text not null);", NULL, NULL, NULL);
    sqlite3_prepare_v2(db, "insert into t1 (col) values (?), (?) returning id", -1, &stmt, NULL);
    sqlite3_bind_text(stmt, 1, "u1", -1, NULL);
    sqlite3_bind_text(stmt, 2, "u2", -1, NULL);
    int rc = sqlite3_step(stmt);
    printf("changes: %d\n", sqlite3_changes(db));
    if (rc != SQLITE_DONE) {
        do {
            printf("%d\n", sqlite3_column_int(stmt, 0));
            rc = sqlite3_step(stmt);
        } while (rc != SQLITE_DONE);
    }
    printf("changes: %d\n", sqlite3_changes(db));
}
```

Output is:

```
changes: 0
1
2
changes: 2
```

I'm going to close this as it seems that, if anything, Python 3.11 has it correct and the test was only passing due to a side-effect (as far as I can tell) of the statement cache. The faulty assertions are removed from our tests.

Thank you so much for the quick investigation and resolution for this.  I've added 73335396e0336332832e52d4d7272b7d1ed9e794 as a patch to the Fedora package update I'm working on.

Sorry for not getting back to you, Charles! (I've spent most of the last two weeks on the road, with very fragmented online precence.)

Yeah, the INSERT/UPDATE ... RETURNING thing! We had an issue about that in CPython some months ago. Fixed in `main` (and possibly also backported to 3.11 and 3.10).

FWIW, here's what I used to debug the related issues:

- https://gist.github.com/erlend-aasland/98b41dab6cc51c1082c70eef3659d8d1
- https://gist.github.com/erlend-aasland/5e48b920d882e04f911fe7871fd5ccd9

I don't like the sqlite3_changes() API... :)

Hope you had a nice adventure, thanks for the links on the debug scripts. I came to the same conclusion as you, which was that we can't determine the modified rowcount until everything returned by `RETURNING` has been stepped-through.