Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Doc/library/sqlite3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1751,6 +1751,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.
Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ os
process via a pidfd. Available on Linux 5.6+.
(Contributed by Maurycy Pawłowski-Wieroński in :gh:`149464`.)

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`.)

xml
---

Expand Down
46 changes: 46 additions & 0 deletions Lib/test/test_sqlite3/test_dbapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -1390,6 +1390,16 @@ 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""

def test_blob_set_slice(self):
self.blob[0:5] = b"12345"
expected = b"12345" + self.data[5:]
Expand All @@ -1400,12 +1410,48 @@ def test_blob_set_empty_slice(self):
self.blob[0:0] = b""
self.assertEqual(self.blob[:], self.data)

def test_blob_set_empty_slice_wrong_type(self):
# Assigning a non-buffer object to an empty slice must raise TypeError
# even when the slice length is zero.
with self.assertRaises(TypeError):
self.blob[5:5] = None

def test_blob_set_empty_slice_wrong_size(self):
# Assigning a non-empty bytes object to an empty slice must raise
# IndexError because the sizes do not match.
with self.assertRaisesRegex(IndexError, "wrong size"):
self.blob[5:5] = b"123"

def test_blob_set_slice_with_skip(self):
self.blob[0:10:2] = b"12345"
actual = self.cx.execute("select b from test").fetchone()[0]
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)
# Assigning a non-empty sequence to an empty slice must raise.
with self.assertRaisesRegex(IndexError, "wrong size"):
self.blob[3:8:-1] = b"abc"

def test_blob_mapping_invalid_index_type(self):
msg = "indices must be integers"
with self.assertRaisesRegex(TypeError, msg):
Expand Down
Original file line number Diff line number Diff line change
@@ -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`.
54 changes: 40 additions & 14 deletions Modules/_sqlite/blob.c
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,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;
}
Expand All @@ -456,10 +463,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);
Expand Down Expand Up @@ -531,31 +540,48 @@ ass_subscript_slice(pysqlite_Blob *self, PyObject *item, PyObject *value)
return -1;
}

if (len == 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there a different bug? Like blob[5:5] = None and blob[5:5] = b'123'? We need tests for such cases.

Copy link
Copy Markdown
Contributor Author

@ever0de ever0de May 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, these are bugs

the original code silently ignores both type errors and size mismatches when the slice length is zero, while the same violations raise errors for non-empty slices:

Operation Result (Python 3.13.4)
blob[0:3] = b'12345' IndexError: Blob slice assignment is wrong size
blob[0:3] = None TypeError: a bytes-like object is required
blob[5:5] = b'123' no error (blob unchanged)
blob[5:5] = None no error

The same invariant (correct type, matching size) should hold regardless of whether the slice is empty.
I've added tests to cover both cases.

return 0;
}

Py_buffer vbuf;
if (PyObject_GetBuffer(value, &vbuf, PyBUF_SIMPLE) < 0) {
return -1;
}

int rc = -1;
// For extended slices the right-hand side must have the exact same
// element count as the slice, even when that count is zero.
if (vbuf.len != len) {
PyErr_SetString(PyExc_IndexError,
"Blob slice assignment is wrong size");
PyBuffer_Release(&vbuf);
return -1;
}
else if (step == 1) {

if (len == 0) {
PyBuffer_Release(&vbuf);
return 0;
}

int rc;
if (step == 1) {
rc = inner_write(self, vbuf.buf, len, start);
}
else {
PyObject *blob_bytes = read_multiple(self, stop - start, start);
rc = -1;
// Compute the contiguous blob region covering all slice elements, then
// update 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 write_offset = Py_MIN(start, last);
Py_ssize_t write_length = Py_ABS(start - last) + 1;
PyObject *blob_bytes = read_multiple(self, write_length, write_offset);
if (blob_bytes != NULL) {
char *blob_buf = PyBytes_AS_STRING(blob_bytes);
for (Py_ssize_t i = 0, j = 0; i < len; i++, j += step) {
blob_buf[j] = ((char *)vbuf.buf)[i];
size_t cur;
Py_ssize_t i;
for (cur = (size_t)start, i = 0; i < len;
cur += (size_t)step, i++) {
blob_buf[(Py_ssize_t)cur - write_offset] =
((char *)vbuf.buf)[i];
}
rc = inner_write(self, blob_buf, stop - start, start);
rc = inner_write(self, blob_buf, write_length, write_offset);
Py_DECREF(blob_bytes);
}
}
Expand Down
Loading