https://github.com/python/cpython/commit/c72ea530c3d7cffebc6497ea3c9d54f1055a3059
commit: c72ea530c3d7cffebc6497ea3c9d54f1055a3059
branch: main
author: Jiseok CHOI <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-14T09:25:38+03:00
summary:

gh-150449: Support negative steps in sqlite3.Blob slices (GH-150450)

Reading or writing a slice with a negative step computed a negative length
for sqlite3_blob_read() and sqlite3_blob_write(), so it failed instead of
returning or storing the selected bytes.

Compute the contiguous region which covers all selected bytes, and index it
with a size_t cursor, so that a step of any sign and magnitude works.

Co-authored-by: Serhiy Storchaka <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-05-26-15-53-50.gh-issue-150449.GfDWxl.rst
M Doc/library/sqlite3.rst
M Doc/whatsnew/3.16.rst
M Lib/test/test_sqlite3/test_dbapi.py
M Modules/_sqlite/blob.c

diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst
index 3a75d44f3f7d21..5aa3d2b0adcbf6 100644
--- a/Doc/library/sqlite3.rst
+++ b/Doc/library/sqlite3.rst
@@ -1760,6 +1760,10 @@ Blob objects
 
    .. versionadded:: 3.11
 
+   .. versionchanged:: next
+      :class:`Blob` now supports negative-step slices
+      (e.g. ``blob[9:0:-2]``) for both reading and writing.
+
    A :class:`Blob` instance is a :term:`file-like object`
    that can read and write data in an SQLite :abbr:`BLOB (Binary Large 
OBject)`.
    Call :func:`len(blob) <len>` to get the size (number of bytes) of the blob.
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index fc09afc96b1676..9d3bdd21509a9e 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -466,6 +466,13 @@ shlex
   a string, even if it is already safe for a shell without being quoted.
   (Contributed by Jay Berry in :gh:`148846`.)
 
+sqlite3
+-------
+
+* :class:`sqlite3.Blob` now supports negative-step slices for reading and
+  writing (e.g. ``blob[9:0:-2]``).  Previously, such slices would raise
+  :exc:`SystemError` or :exc:`ValueError`.
+  (Contributed by Jiseok CHOI in :gh:`150449`.)
 
 symtable
 --------
diff --git a/Lib/test/test_sqlite3/test_dbapi.py 
b/Lib/test/test_sqlite3/test_dbapi.py
index 2cf3556f66d963..c21448a92361d7 100644
--- a/Lib/test/test_sqlite3/test_dbapi.py
+++ b/Lib/test/test_sqlite3/test_dbapi.py
@@ -1390,6 +1390,19 @@ def test_blob_get_slice_negative_index(self):
     def test_blob_get_slice_with_skip(self):
         self.assertEqual(self.blob[0:10:2], b"ti lb")
 
+    def test_blob_get_slice_with_negative_step(self):
+        # gh-150449: negative-step slices must not crash
+        self.assertEqual(self.blob[9:0:-2], self.data[9:0:-2])
+        self.assertEqual(self.blob[9::-2], self.data[9::-2])
+        self.assertEqual(self.blob[::-1], self.data[::-1])
+        # When start <= stop with a negative step the slice is empty; this
+        # must return b"" rather than crashing or raising an exception.
+        self.assertEqual(self.blob[3:8:-1], self.data[3:8:-1])   # b""
+        self.assertEqual(self.blob[5:5:-1], self.data[5:5:-1])   # b""
+        # Extreme step values: cur += (size_t)step must not overflow.
+        self.assertEqual(self.blob[5::sys.maxsize], self.data[5::sys.maxsize])
+        self.assertEqual(self.blob[::-sys.maxsize - 1], 
self.data[::-sys.maxsize - 1])
+
     def test_blob_set_slice(self):
         self.blob[0:5] = b"12345"
         expected = b"12345" + self.data[5:]
@@ -1430,6 +1443,43 @@ def test_blob_set_slice_with_skip(self):
         expected = b"1h2s3b4o5 " + self.data[10:]
         self.assertEqual(actual, expected)
 
+    def test_blob_set_slice_with_negative_step(self):
+        # gh-150449: negative-step slice assignment must not crash
+        expected = bytearray(self.data)
+        expected[9:0:-2] = b"12345"
+        self.blob[9:0:-2] = b"12345"
+        actual = self.cx.execute("select b from test").fetchone()[0]
+        self.assertEqual(actual, bytes(expected))
+
+        # Also verify a slice that includes index 0
+        expected2 = bytearray(self.data)
+        expected2[9::-2] = b"12345"
+        self.blob[9::-2] = b"12345"
+        actual2 = self.cx.execute("select b from test").fetchone()[0]
+        self.assertEqual(actual2, bytes(expected2))
+
+        # When start <= stop with a negative step the slice is empty;
+        # assigning b"" to it must be a no-op (blob contents unchanged).
+        state_before = bytes(self.blob[:])
+        self.blob[3:8:-1] = b""
+        self.assertEqual(bytes(self.blob[:]), state_before)
+
+    def test_blob_set_slice_with_extreme_positive_step(self):
+        expected = bytearray(self.data)
+        expected[5::sys.maxsize] = b"\xab"
+        self.blob[5::sys.maxsize] = b"\xab"
+        actual = self.cx.execute("select b from test").fetchone()[0]
+        self.assertEqual(actual, bytes(expected))
+        self.assertEqual(actual[5], 0xab)
+
+    def test_blob_set_slice_with_extreme_negative_step(self):
+        expected = bytearray(self.data)
+        expected[::-sys.maxsize - 1] = b"\xcd"
+        self.blob[::-sys.maxsize - 1] = b"\xcd"
+        actual = self.cx.execute("select b from test").fetchone()[0]
+        self.assertEqual(actual, bytes(expected))
+        self.assertEqual(actual[-1], 0xcd)
+
     def test_blob_mapping_invalid_index_type(self):
         msg = "indices must be integers"
         with self.assertRaisesRegex(TypeError, msg):
diff --git 
a/Misc/NEWS.d/next/Library/2026-05-26-15-53-50.gh-issue-150449.GfDWxl.rst 
b/Misc/NEWS.d/next/Library/2026-05-26-15-53-50.gh-issue-150449.GfDWxl.rst
new file mode 100644
index 00000000000000..f849fe791356a1
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-05-26-15-53-50.gh-issue-150449.GfDWxl.rst
@@ -0,0 +1,3 @@
+:class:`sqlite3.Blob` now supports negative-step slices for reading and
+writing (e.g. ``blob[9:0:-2]``).  Previously, such slices would raise
+:exc:`SystemError` or :exc:`ValueError`.
diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c
index 53d28a06181a9c..a9a98b40bf60b8 100644
--- a/Modules/_sqlite/blob.c
+++ b/Modules/_sqlite/blob.c
@@ -454,7 +454,14 @@ subscript_slice(pysqlite_Blob *self, PyObject *item)
         return read_multiple(self, len, start);
     }
 
-    PyObject *blob = read_multiple(self, stop - start, start);
+    // Compute the contiguous blob region covering all slice elements, then
+    // copy each element using the standard size_t-cursor pattern that handles
+    // both positive and negative steps via unsigned arithmetic.
+    Py_ssize_t last = start + (len - 1) * step;
+    Py_ssize_t read_offset = Py_MIN(start, last);
+    Py_ssize_t read_length = Py_ABS(start - last) + 1;
+
+    PyObject *blob = read_multiple(self, read_length, read_offset);
     if (blob == NULL) {
         return NULL;
     }
@@ -465,10 +472,12 @@ subscript_slice(pysqlite_Blob *self, PyObject *item)
         return NULL;
     }
     char *res_buf = PyBytesWriter_GetData(writer);
-
     char *blob_buf = PyBytes_AS_STRING(blob);
-    for (Py_ssize_t i = 0, j = 0; i < len; i++, j += step) {
-        res_buf[i] = blob_buf[j];
+
+    size_t cur;
+    Py_ssize_t i;
+    for (cur = (size_t)start, i = 0; i < len; cur += (size_t)step, i++) {
+        res_buf[i] = blob_buf[(Py_ssize_t)cur - read_offset];
     }
     Py_DECREF(blob);
     return PyBytesWriter_Finish(writer);
@@ -562,28 +571,31 @@ ass_subscript_slice(pysqlite_Blob *self, PyObject *item, 
PyObject *value)
         rc = inner_write(self, vbuf.buf, len, start);
     }
     else {
-        /* Read the affected region, patch it and write it back.  The
-           object returned by read_multiple() cannot be used as the buffer,
-           because for a single byte it is an immortal singleton. */
-        Py_ssize_t length = stop - start;
-        if (length <= 0) {
-            /* start > stop for a negative step; see gh-150449. */
-            PyErr_SetString(PyExc_ValueError, "size must be >= 0");
+        /* Compute the contiguous blob region covering all slice elements,
+           read it, patch each element and write it back.  The object
+           returned by read_multiple() cannot be used as the buffer, because
+           for a single byte it is an immortal singleton. */
+        Py_ssize_t last = start + (len - 1) * step;
+        Py_ssize_t write_offset = Py_MIN(start, last);
+        Py_ssize_t write_length = Py_ABS(start - last) + 1;
+        char *buf = PyMem_Malloc(write_length);
+        if (buf == NULL) {
+            PyErr_NoMemory();
         }
         else {
-            char *buf = PyMem_Malloc(length);
-            if (buf == NULL) {
-                PyErr_NoMemory();
-            }
-            else {
-                if (inner_read(self, buf, length, start) == 0) {
-                    for (Py_ssize_t i = 0, j = 0; i < len; i++, j += step) {
-                        buf[j] = ((char *)vbuf.buf)[i];
-                    }
-                    rc = inner_write(self, buf, length, start);
+            if (inner_read(self, buf, write_length, write_offset) == 0) {
+                /* The size_t cursor handles both positive and negative steps
+                   via unsigned arithmetic. */
+                size_t cur;
+                Py_ssize_t i;
+                for (cur = (size_t)start, i = 0; i < len;
+                     cur += (size_t)step, i++) {
+                    buf[(Py_ssize_t)cur - write_offset] =
+                        ((char *)vbuf.buf)[i];
                 }
-                PyMem_Free(buf);
+                rc = inner_write(self, buf, write_length, write_offset);
             }
+            PyMem_Free(buf);
         }
     }
     PyBuffer_Release(&vbuf);

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to