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

serhiy-storchaka <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
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]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.