diff --git a/Doc/includes/sqlite3/blob.py b/Doc/includes/sqlite3/blob.py
new file mode 100644
index 00000000000000..61994fb82dd72a
--- /dev/null
+++ b/Doc/includes/sqlite3/blob.py
@@ -0,0 +1,12 @@
+import sqlite3
+
+con = sqlite3.connect(":memory:")
+con.execute("create table test(blob_col blob)")
+con.execute("insert into test(blob_col) values (zeroblob(10))")
+
+blob = con.blobopen("test", "blob_col", 1)
+blob.write(b"Hello")
+blob.write(b"World")
+blob.seek(0)
+print(blob.read()) # will print b"HelloWorld"
+blob.close()
diff --git a/Doc/includes/sqlite3/blob_with.py b/Doc/includes/sqlite3/blob_with.py
new file mode 100644
index 00000000000000..d489bd632d867d
--- /dev/null
+++ b/Doc/includes/sqlite3/blob_with.py
@@ -0,0 +1,12 @@
+import sqlite3
+
+con = sqlite3.connect(":memory:")
+
+con.execute("create table test(blob_col blob)")
+con.execute("insert into test(blob_col) values (zeroblob(10))")
+
+with con.blobopen("test", "blob_col", 1) as blob:
+ blob.write(b"Hello")
+ blob.write(b"World")
+ blob.seek(0)
+ print(blob.read()) # will print b"HelloWorld"
diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst
index d213933ba5827f..8c860e15a618b4 100644
--- a/Doc/library/sqlite3.rst
+++ b/Doc/library/sqlite3.rst
@@ -394,6 +394,20 @@ Connection Objects
supplied, this must be a callable returning an instance of :class:`Cursor`
or its subclasses.
+ .. method:: blobopen(table, column, row, /, *, readonly=False, name="main")
+
+ On success, a :class:`Blob` handle to the :abbr:`BLOB (Binary Large
+ OBject)` located in row *row*, column *column*, table *table* in database
+ *name* will be returned. When *readonly* is :const:`True` the blob is
+ opened without write permissions.
+
+ .. note::
+
+ The BLOB size cannot be changed using the :class:`Blob` class. Use the
+ SQL function ``zeroblob`` to create a blob with a fixed size.
+
+ .. versionadded:: 3.11
+
.. method:: commit()
This method commits the current transaction. If you don't call this method,
@@ -1021,6 +1035,65 @@ Exceptions
transactions turned off. It is a subclass of :exc:`DatabaseError`.
+.. _sqlite3-blob-objects:
+
+Blob Objects
+------------
+
+.. versionadded:: 3.11
+
+.. class:: Blob
+
+ A :class:`Blob` instance can read and write the data in a :abbr:`BLOB
+ (Binary Large OBject)`. The :class:`Blob` class implements the file and
+ mapping protocols.
+
+ .. method:: Blob.close()
+
+ Close the BLOB.
+
+ The BLOB will be unusable from this point forward. An
+ :class:`~sqlite3.Error` (or subclass) exception will be raised if any
+ further operation is attempted with the BLOB.
+
+ .. method:: Blob.__len__()
+
+ Return the BLOB size as length in bytes.
+
+ .. method:: Blob.read(length=-1, /)
+
+ Read *length* bytes of data from the BLOB at the current offset position.
+ If the end of the BLOB is reached we will return the data up to end of
+ file. When *size* is not specified or is negative, :meth:`~Blob.read`
+ will read till the end of the BLOB.
+
+ .. method:: Blob.write(data, /)
+
+ Write *data* to the BLOB at the current offset. This function cannot
+ change the BLOB length. Writing beyond the end of the blob will result in
+ an exception being raised.
+
+ .. method:: Blob.tell()
+
+ Return the current access position of the BLOB.
+
+ .. method:: Blob.seek(offset, origin=sqlite3.BLOB_SEEK_START, /)
+
+ Set the current access position of the BLOB to *offset*. The *origin*
+ argument defaults to :data:`os.SEEK_SET` (absolute BLOB positioning).
+ Other values for *origin* are :data:`os.SEEK_CUR` (seek relative to the
+ current position) and :data:`os.SEEK_END` (seek relative to the BLOB’s
+ end).
+
+ :class:`Blob` example:
+
+ .. literalinclude:: ../includes/sqlite3/blob.py
+
+ A :class:`Blob` can also be used as a :term:`context manager`:
+
+ .. literalinclude:: ../includes/sqlite3/blob_with.py
+
+
.. _sqlite3-types:
SQLite and Python types
diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst
index 5563e3d84de6d1..01f03bfbdf4011 100644
--- a/Doc/whatsnew/3.11.rst
+++ b/Doc/whatsnew/3.11.rst
@@ -301,6 +301,10 @@ sqlite3
Instead we leave it to the SQLite library to handle these cases.
(Contributed by Erlend E. Aasland in :issue:`44092`.)
+* Add :meth:`~sqlite3.Connection.blobopen` to :class:`sqlite3.Connection`.
+ :class:`sqlite3.Blob` allows incremental I/O operations on blobs.
+ (Contributed by Aviv Palivoda and Erlend E. Aasland in :issue:`24905`)
+
sys
---
diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py
index 4eb4e180bf117e..b25e6b125f9095 100644
--- a/Lib/test/test_sqlite3/test_dbapi.py
+++ b/Lib/test/test_sqlite3/test_dbapi.py
@@ -20,7 +20,9 @@
# misrepresented as being the original software.
# 3. This notice may not be removed or altered from any source distribution.
+import _testcapi
import contextlib
+import os
import sqlite3 as sqlite
import subprocess
import sys
@@ -989,11 +991,284 @@ def test_same_query_in_multiple_cursors(self):
self.assertEqual(cu.fetchall(), [(1,)])
+class BlobTests(unittest.TestCase):
+ def setUp(self):
+ self.cx = sqlite.connect(":memory:")
+ self.cx.execute("create table test(b blob)")
+ self.data = b"this blob data string is exactly fifty bytes long!"
+ self.cx.execute("insert into test(b) values (?)", (self.data,))
+ self.blob = self.cx.blobopen("test", "b", 1)
+
+ def tearDown(self):
+ self.blob.close()
+ self.cx.close()
+
+ def test_blob_length(self):
+ self.assertEqual(len(self.blob), 50)
+
+ def test_blob_seek_and_tell(self):
+ self.blob.seek(10)
+ self.assertEqual(self.blob.tell(), 10)
+
+ self.blob.seek(10, os.SEEK_SET)
+ self.assertEqual(self.blob.tell(), 10)
+
+ self.blob.seek(10, os.SEEK_CUR)
+ self.assertEqual(self.blob.tell(), 20)
+
+ self.blob.seek(-10, os.SEEK_END)
+ self.assertEqual(self.blob.tell(), 40)
+
+ def test_blob_seek_error(self):
+ msg_oor = "offset out of blob range"
+ msg_orig = "'origin' should be 0, 1, or 2"
+ msg_of = "seek offset result in overflow"
+
+ dataset = (
+ (ValueError, msg_oor, lambda: self.blob.seek(1000)),
+ (ValueError, msg_oor, lambda: self.blob.seek(-10)),
+ (ValueError, msg_orig, lambda: self.blob.seek(10, -1)),
+ (ValueError, msg_orig, lambda: self.blob.seek(10, 3)),
+ )
+ for exc, msg, fn in dataset:
+ with self.subTest(exc=exc, msg=msg, fn=fn):
+ self.assertRaisesRegex(exc, msg, fn)
+
+ n = len(self.data) // 2
+ self.blob.seek(n, os.SEEK_SET)
+ with self.assertRaisesRegex(OverflowError, msg_of):
+ self.blob.seek(_testcapi.INT_MAX, os.SEEK_CUR)
+ with self.assertRaisesRegex(OverflowError, msg_of):
+ self.blob.seek( _testcapi.INT_MAX, os.SEEK_END)
+
+ def test_blob_read(self):
+ buf = self.blob.read()
+ self.assertEqual(buf, self.data)
+ self.assertEqual(len(buf), len(self.data))
+
+ def test_blob_read_too_much(self):
+ buf = self.blob.read(len(self.data) * 2)
+ self.assertEqual(buf, self.data)
+ self.assertEqual(len(buf), len(self.data))
+
+ def test_blob_read_advance_offset(self):
+ n = 10
+ buf = self.blob.read(n)
+ self.assertEqual(buf, self.data[:n])
+ self.assertEqual(self.blob.tell(), n)
+
+ def test_blob_read_start_at_offset(self):
+ new_data = b"b" * 50
+ self.blob.seek(10)
+ self.blob.write(new_data[:10])
+ self.blob.seek(10)
+ self.assertEqual(self.blob.read(10), new_data[:10])
+
+ def test_blob_read_after_row_change(self):
+ self.cx.execute("update test set b='aaaa' where rowid=1")
+ with self.assertRaises(sqlite.OperationalError):
+ self.blob.read()
+
+ def test_blob_write(self):
+ new_data = b"new data".ljust(50)
+ self.blob.write(new_data)
+ row = self.cx.execute("select b from test").fetchone()
+ self.assertEqual(row[0], new_data)
+
+ def test_blob_write_at_offset(self):
+ new_data = b"c" * 50
+ self.blob.seek(25)
+ self.blob.write(new_data[:25])
+ row = self.cx.execute("select b from test").fetchone()
+ self.assertEqual(row[0], self.data[:25] + new_data[:25])
+
+ def test_blob_write_advance_offset(self):
+ new_data = b"d" * 50
+ self.blob.write(new_data[:25])
+ self.assertEqual(self.blob.tell(), 25)
+
+ def test_blob_write_error_length(self):
+ with self.assertRaisesRegex(ValueError, "data longer than blob"):
+ self.blob.write(b"a" * 1000)
+
+ def test_blob_write_error_row_changed(self):
+ self.cx.execute("update test set b='aaaa' where rowid=1")
+ with self.assertRaises(sqlite.OperationalError):
+ self.blob.write(b"aaa")
+
+ def test_blob_write_error_readonly(self):
+ ro_blob = self.cx.blobopen("test", "b", 1, readonly=True)
+ with self.assertRaisesRegex(sqlite.OperationalError, "readonly"):
+ ro_blob.write(b"aaa")
+ ro_blob.close()
+
+ def test_blob_open_error(self):
+ dataset = (
+ (("test", "b", 1), {"name": "notexisting"}),
+ (("notexisting", "b", 1), {}),
+ (("test", "notexisting", 1), {}),
+ (("test", "b", 2), {}),
+ )
+ regex = "no such"
+ for args, kwds in dataset:
+ with self.subTest(args=args, kwds=kwds):
+ with self.assertRaisesRegex(sqlite.OperationalError, regex):
+ self.cx.blobopen(*args, **kwds)
+
+ def test_blob_get_item(self):
+ self.assertEqual(self.blob[5], b"b")
+ self.assertEqual(self.blob[6], b"l")
+ self.assertEqual(self.blob[7], b"o")
+ self.assertEqual(self.blob[8], b"b")
+ self.assertEqual(self.blob[-1], b"!")
+
+ def test_blob_get_item_error(self):
+ dataset = (
+ (b"", TypeError, "Blob indices must be integers"),
+ (105, IndexError, "Blob index out of range"),
+ (-105, IndexError, "Blob index out of range"),
+ (_testcapi.ULLONG_MAX, IndexError, "cannot fit 'int'"),
+ (len(self.blob), IndexError, "Blob index out of range"),
+ )
+ for idx, exc, regex in dataset:
+ with self.subTest(idx=idx, exc=exc, regex=regex):
+ with self.assertRaisesRegex(exc, regex):
+ self.blob[idx]
+
+ def test_blob_get_slice(self):
+ self.assertEqual(self.blob[5:14], b"blob data")
+
+ def test_blob_get_slice_negative_index(self):
+ self.assertEqual(self.blob[5:-5], self.data[5:-5])
+
+ def test_blob_get_slice_invalid_index(self):
+ with self.assertRaisesRegex(TypeError, "indices must be integers"):
+ self.blob[5:b"a"]
+
+ def test_blob_get_slice_with_skip(self):
+ self.assertEqual(self.blob[0:10:2], b"ti lb")
+
+ def test_blob_set_item(self):
+ self.blob[0] = b"b"
+ expected = b"b" + self.data[1:]
+ actual = self.cx.execute("select b from test").fetchone()[0]
+ self.assertEqual(actual, expected)
+
+ def test_blob_set_item(self):
+ self.blob[-1] = b"z"
+ self.assertEqual(self.blob[-1], b"z")
+
+ def test_blob_set_item_error(self):
+ with self.assertRaisesRegex(TypeError, "indices must be integers"):
+ self.blob["a"] = b"b"
+ with self.assertRaisesRegex(ValueError, "must be a single byte"):
+ self.blob[0] = b"abc"
+ with self.assertRaisesRegex(TypeError, "doesn't support.*deletion"):
+ del self.blob[0]
+ with self.assertRaisesRegex(IndexError, "Blob index out of range"):
+ self.blob[1000] = b"a"
+
+ def test_blob_set_slice(self):
+ self.blob[0:5] = b"bbbbb"
+ expected = b"bbbbb" + self.data[5:]
+ actual = self.cx.execute("select b from test").fetchone()[0]
+ self.assertEqual(actual, expected)
+
+ def test_blob_set_empty_slice(self):
+ self.blob[0:0] = b""
+ self.assertEqual(self.blob[:], self.data)
+
+ def test_blob_set_slice_with_skip(self):
+ self.blob[0:10:2] = b"bbbbb"
+ actual = self.cx.execute("select b from test").fetchone()[0]
+ expected = b"bhbsbbbob " + self.data[10:]
+ self.assertEqual(actual, expected)
+
+ def test_blob_get_empty_slice(self):
+ self.assertEqual(self.blob[5:5], b"")
+
+ def test_blob_set_slice_error(self):
+ with self.assertRaises(IndexError):
+ self.blob[5:10] = b"a"
+ with self.assertRaises(IndexError):
+ self.blob[5:10] = b"a" * 1000
+ with self.assertRaises(TypeError):
+ del self.blob[5:10]
+ with self.assertRaises(BufferError):
+ self.blob[5:10] = memoryview(b"abcde")[::2]
+ with self.assertRaises(ValueError):
+ self.blob[5:10:0] = b"12345"
+
+ def test_blob_sequence_not_supported(self):
+ ops = (
+ lambda: self.blob + self.blob,
+ lambda: self.blob * 5,
+ lambda: b"a" in self.blob,
+ )
+ for op in ops:
+ with self.subTest(op=op):
+ self.assertRaises(TypeError, op)
+
+ def test_blob_context_manager(self):
+ data = b"a" * 50
+ with self.cx.blobopen("test", "b", 1) as blob:
+ blob.write(data)
+ actual = self.cx.execute("select b from test").fetchone()[0]
+ self.assertEqual(actual, data)
+
+ def test_blob_closed(self):
+ with memory_database() as cx:
+ cx.execute("create table test(b blob)")
+ cx.execute("insert into test values (zeroblob(100))")
+ blob = cx.blobopen("test", "b", 1)
+ blob.close()
+
+ def assign(): blob[0] = b""
+ ops = [
+ lambda: blob.read(),
+ lambda: blob.write(b""),
+ lambda: blob.seek(0),
+ lambda: blob.tell(),
+ lambda: blob.__enter__(),
+ lambda: blob.__exit__(None, None, None),
+ lambda: len(blob),
+ lambda: blob[0],
+ lambda: blob[0:1],
+ assign,
+ ]
+ msg = "Cannot operate on a closed blob"
+ for op in ops:
+ with self.subTest(op=op):
+ with self.assertRaisesRegex(sqlite.ProgrammingError, msg):
+ op()
+
+ def test_blob_close_bad_connection(self):
+ with memory_database() as cx:
+ cx.execute("create table test(b blob)")
+ cx.execute("insert into test values(zeroblob(1))")
+ blob = cx.blobopen("test", "b", 1)
+ cx.close()
+ self.assertRaisesRegex(sqlite.ProgrammingError,
+ "Cannot operate on a closed database",
+ blob.close)
+
+ def test_closed_blob_read(self):
+ with memory_database() as cx:
+ cx.execute("create table test(b blob)")
+ cx.execute("insert into test(b) values (zeroblob(100))")
+ blob = cx.blobopen("test", "b", 1)
+ cx.close()
+ self.assertRaisesRegex(sqlite.ProgrammingError,
+ "Cannot operate on a closed database",
+ blob.read)
+
+
class ThreadTests(unittest.TestCase):
def setUp(self):
self.con = sqlite.connect(":memory:")
self.cur = self.con.cursor()
- self.cur.execute("create table test(name text)")
+ self.cur.execute("create table test(name text, b blob)")
+ self.cur.execute("insert into test values('blob', zeroblob(1))")
def tearDown(self):
self.cur.close()
@@ -1028,6 +1303,7 @@ def test_check_connection_thread(self):
lambda: self.con.create_collation("foo", None),
lambda: self.con.setlimit(sqlite.SQLITE_LIMIT_LENGTH, -1),
lambda: self.con.getlimit(sqlite.SQLITE_LIMIT_LENGTH),
+ lambda: self.con.blobopen("test", "b", 1),
]
for fn in fns:
with self.subTest(fn=fn):
diff --git a/Misc/NEWS.d/next/Library/2018-04-18-16-15-55.bpo-24905.jYqjYx.rst b/Misc/NEWS.d/next/Library/2018-04-18-16-15-55.bpo-24905.jYqjYx.rst
new file mode 100644
index 00000000000000..0a57f90c12378f
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2018-04-18-16-15-55.bpo-24905.jYqjYx.rst
@@ -0,0 +1,3 @@
+Add :meth:`~sqlite3.Connection.blobopen` to :class:`sqlite3.Connection`.
+:class:`sqlite3.Blob` allows incremental I/O operations on blobs.
+Patch by Aviv Palivoda and Erlend E. Aasland.
diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c
new file mode 100644
index 00000000000000..5fa4cd3f98b63d
--- /dev/null
+++ b/Modules/_sqlite/blob.c
@@ -0,0 +1,570 @@
+#include "blob.h"
+#include "util.h"
+
+#define clinic_state() (pysqlite_get_state_by_type(Py_TYPE(self)))
+#include "clinic/blob.c.h"
+#undef clinic_state
+
+/*[clinic input]
+module _sqlite3
+class _sqlite3.Blob "pysqlite_Blob *" "clinic_state()->BlobType"
+[clinic start generated code]*/
+/*[clinic end generated code: output=da39a3ee5e6b4b0d input=908d3e16a45f8da7]*/
+
+static void
+close_blob(pysqlite_Blob *self)
+{
+ if (self->blob) {
+ sqlite3_blob *blob = self->blob;
+ self->blob = NULL;
+
+ Py_BEGIN_ALLOW_THREADS
+ sqlite3_blob_close(blob);
+ Py_END_ALLOW_THREADS
+ }
+}
+
+static int
+blob_traverse(pysqlite_Blob *self, visitproc visit, void *arg)
+{
+ Py_VISIT(Py_TYPE(self));
+ Py_VISIT(self->connection);
+ return 0;
+}
+
+static int
+blob_clear(pysqlite_Blob *self)
+{
+ Py_CLEAR(self->connection);
+ return 0;
+}
+
+static void
+blob_dealloc(pysqlite_Blob *self)
+{
+ PyTypeObject *tp = Py_TYPE(self);
+ PyObject_GC_UnTrack(self);
+
+ close_blob(self);
+
+ if (self->in_weakreflist != NULL) {
+ PyObject_ClearWeakRefs((PyObject*)self);
+ }
+ tp->tp_clear((PyObject *)self);
+ tp->tp_free(self);
+ Py_DECREF(tp);
+}
+
+// Return 1 if the blob object is usable, 0 if not.
+static int
+check_blob(pysqlite_Blob *self)
+{
+ if (!pysqlite_check_connection(self->connection) ||
+ !pysqlite_check_thread(self->connection)) {
+ return 0;
+ }
+ if (self->blob == NULL) {
+ pysqlite_state *state = self->connection->state;
+ PyErr_SetString(state->ProgrammingError,
+ "Cannot operate on a closed blob.");
+ return 0;
+ }
+ return 1;
+}
+
+
+/*[clinic input]
+_sqlite3.Blob.close as blob_close
+
+Close blob.
+[clinic start generated code]*/
+
+static PyObject *
+blob_close_impl(pysqlite_Blob *self)
+/*[clinic end generated code: output=848accc20a138d1b input=56c86df5cab22490]*/
+{
+ if (!pysqlite_check_connection(self->connection) ||
+ !pysqlite_check_thread(self->connection))
+ {
+ return NULL;
+ }
+ close_blob(self);
+ Py_RETURN_NONE;
+};
+
+void
+pysqlite_close_all_blobs(pysqlite_Connection *self)
+{
+ for (int i = 0; i < PyList_GET_SIZE(self->blobs); i++) {
+ PyObject *weakref = PyList_GET_ITEM(self->blobs, i);
+ PyObject *blob = PyWeakref_GetObject(weakref);
+ if (!Py_IsNone(blob)) {
+ close_blob((pysqlite_Blob *)blob);
+ }
+ }
+}
+
+static Py_ssize_t
+blob_length(pysqlite_Blob *self)
+{
+ if (!check_blob(self)) {
+ return -1;
+ }
+ return sqlite3_blob_bytes(self->blob);
+};
+
+static void
+blob_seterror(pysqlite_Blob *self, int rc)
+{
+ assert(self->connection != NULL);
+#if SQLITE_VERSION_NUMBER < 3008008
+ // SQLite pre 3.8.8 does not set this blob error on the connection
+ if (rc == SQLITE_ABORT) {
+ PyErr_SetString(self->connection->OperationalError,
+ "Cannot operate on an expired blob handle");
+ return;
+ }
+#endif
+ _pysqlite_seterror(self->connection->state, self->connection->db);
+}
+
+static PyObject *
+inner_read(pysqlite_Blob *self, int length, int offset)
+{
+ PyObject *buffer = PyBytes_FromStringAndSize(NULL, length);
+ if (buffer == NULL) {
+ return NULL;
+ }
+
+ char *raw_buffer = PyBytes_AS_STRING(buffer);
+ int rc;
+ Py_BEGIN_ALLOW_THREADS
+ rc = sqlite3_blob_read(self->blob, raw_buffer, length, offset);
+ Py_END_ALLOW_THREADS
+
+ if (rc != SQLITE_OK) {
+ Py_DECREF(buffer);
+ blob_seterror(self, rc);
+ return NULL;
+ }
+ return buffer;
+}
+
+
+/*[clinic input]
+_sqlite3.Blob.read as blob_read
+
+ length: int = -1
+ /
+
+Read data from blob.
+[clinic start generated code]*/
+
+static PyObject *
+blob_read_impl(pysqlite_Blob *self, int length)
+/*[clinic end generated code: output=1fc99b2541360dde input=b4b443e99af5548f]*/
+{
+ if (!check_blob(self)) {
+ return NULL;
+ }
+
+ /* Make sure we never read past "EOB". Also read the rest of the blob if a
+ * negative length is specified. */
+ int blob_len = sqlite3_blob_bytes(self->blob);
+ int max_read_len = blob_len - self->offset;
+ if (length < 0 || length > max_read_len) {
+ length = max_read_len;
+ }
+
+ PyObject *buffer = inner_read(self, length, self->offset);
+ if (buffer == NULL) {
+ return NULL;
+ }
+ self->offset += length;
+ return buffer;
+};
+
+static int
+inner_write(pysqlite_Blob *self, const void *buf, Py_ssize_t len, int offset)
+{
+ int remaining_len = sqlite3_blob_bytes(self->blob) - self->offset;
+ if (len > remaining_len) {
+ PyErr_SetString(PyExc_ValueError, "data longer than blob length");
+ return -1;
+ }
+
+ int rc;
+ Py_BEGIN_ALLOW_THREADS
+ rc = sqlite3_blob_write(self->blob, buf, len, offset);
+ Py_END_ALLOW_THREADS
+
+ if (rc != SQLITE_OK) {
+ blob_seterror(self, rc);
+ return -1;
+ }
+ return 0;
+}
+
+
+/*[clinic input]
+_sqlite3.Blob.write as blob_write
+
+ data: Py_buffer
+ /
+
+Write data to blob.
+[clinic start generated code]*/
+
+static PyObject *
+blob_write_impl(pysqlite_Blob *self, Py_buffer *data)
+/*[clinic end generated code: output=b34cf22601b570b2 input=0dcf4018286f55d2]*/
+{
+ if (!check_blob(self)) {
+ return NULL;
+ }
+
+ int rc = inner_write(self, data->buf, data->len, self->offset);
+ if (rc < 0) {
+ return NULL;
+ }
+ self->offset += (int)data->len;
+ Py_RETURN_NONE;
+}
+
+
+/*[clinic input]
+_sqlite3.Blob.seek as blob_seek
+
+ offset: int
+ origin: int = 0
+ /
+
+Change the access position for a blob.
+[clinic start generated code]*/
+
+static PyObject *
+blob_seek_impl(pysqlite_Blob *self, int offset, int origin)
+/*[clinic end generated code: output=854c5a0e208547a5 input=cc33da6f28af0561]*/
+{
+ if (!check_blob(self)) {
+ return NULL;
+ }
+
+ int blob_len = sqlite3_blob_bytes(self->blob);
+ switch (origin) {
+ case 0:
+ break;
+ case 1:
+ if (offset > INT_MAX - self->offset) {
+ goto overflow;
+ }
+ offset += self->offset;
+ break;
+ case 2:
+ if (offset > INT_MAX - blob_len) {
+ goto overflow;
+ }
+ offset += blob_len;
+ break;
+ default:
+ PyErr_SetString(PyExc_ValueError,
+ "'origin' should be 0, 1, or 2");
+ return NULL;
+ }
+
+ if (offset < 0 || offset > blob_len) {
+ PyErr_SetString(PyExc_ValueError, "offset out of blob range");
+ return NULL;
+ }
+
+ self->offset = offset;
+ Py_RETURN_NONE;
+
+overflow:
+ PyErr_SetString(PyExc_OverflowError, "seek offset result in overflow");
+ return NULL;
+}
+
+
+/*[clinic input]
+_sqlite3.Blob.tell as blob_tell
+
+Return current access position for a blob.
+[clinic start generated code]*/
+
+static PyObject *
+blob_tell_impl(pysqlite_Blob *self)
+/*[clinic end generated code: output=3d3ba484a90b3a99 input=aa1660f9aee18be4]*/
+{
+ if (!check_blob(self)) {
+ return NULL;
+ }
+ return PyLong_FromLong(self->offset);
+}
+
+
+/*[clinic input]
+_sqlite3.Blob.__enter__ as blob_enter
+
+Blob context manager enter.
+[clinic start generated code]*/
+
+static PyObject *
+blob_enter_impl(pysqlite_Blob *self)
+/*[clinic end generated code: output=4fd32484b071a6cd input=fe4842c3c582d5a7]*/
+{
+ if (!check_blob(self)) {
+ return NULL;
+ }
+ return Py_NewRef(self);
+}
+
+
+/*[clinic input]
+_sqlite3.Blob.__exit__ as blob_exit
+
+ type: object
+ val: object
+ tb: object
+ /
+
+Blob context manager exit.
+[clinic start generated code]*/
+
+static PyObject *
+blob_exit_impl(pysqlite_Blob *self, PyObject *type, PyObject *val,
+ PyObject *tb)
+/*[clinic end generated code: output=fc86ceeb2b68c7b2 input=575d9ecea205f35f]*/
+{
+ if (!check_blob(self)) {
+ return NULL;
+ }
+ close_blob(self);
+ Py_RETURN_FALSE;
+}
+
+static int
+get_subscript_index(pysqlite_Blob *self, PyObject *item)
+{
+ Py_ssize_t i = PyNumber_AsSsize_t(item, PyExc_IndexError);
+ if (i == -1 && PyErr_Occurred()) {
+ return -1;
+ }
+ int blob_len = sqlite3_blob_bytes(self->blob);
+ if (i < 0) {
+ i += blob_len;
+ }
+ if (i < 0 || i >= blob_len) {
+ PyErr_SetString(PyExc_IndexError, "Blob index out of range");
+ return -1;
+ }
+ return i;
+}
+
+static PyObject *
+subscript_index(pysqlite_Blob *self, PyObject *item)
+{
+ int i = get_subscript_index(self, item);
+ if (i < 0) {
+ return NULL;
+ }
+ return inner_read(self, 1, i);
+}
+
+static PyObject *
+subscript_slice(pysqlite_Blob *self, PyObject *item)
+{
+ Py_ssize_t start, stop, step, slicelen;
+ if (PySlice_Unpack(item, &start, &stop, &step) < 0) {
+ return NULL;
+ }
+ int blob_len = sqlite3_blob_bytes(self->blob);
+ slicelen = PySlice_AdjustIndices(blob_len, &start, &stop, step);
+
+ if (slicelen <= 0) {
+ return PyBytes_FromStringAndSize("", 0);
+ }
+ else if (step == 1) {
+ return inner_read(self, slicelen, start);
+ }
+
+ PyObject *blob = inner_read(self, stop - start, start);
+ if (blob == NULL) {
+ return NULL;
+ }
+
+ PyObject *result = PyBytes_FromStringAndSize(NULL, slicelen);
+ if (result == NULL) {
+ goto exit;
+ }
+
+ char *blob_buf = PyBytes_AS_STRING(blob);
+ char *res_buf = PyBytes_AS_STRING(result);
+ for (Py_ssize_t i = 0, j = 0; i < slicelen; i++, j += step) {
+ res_buf[i] = blob_buf[j];
+ }
+
+exit:
+ Py_DECREF(blob);
+ return result;
+}
+
+static PyObject *
+blob_subscript(pysqlite_Blob *self, PyObject *item)
+{
+ if (!check_blob(self)) {
+ return NULL;
+ }
+
+ if (PyIndex_Check(item)) {
+ return subscript_index(self, item);
+ }
+ if (PySlice_Check(item)) {
+ return subscript_slice(self, item);
+ }
+
+ PyErr_SetString(PyExc_TypeError, "Blob indices must be integers");
+ return NULL;
+}
+
+static int
+ass_subscript_index(pysqlite_Blob *self, PyObject *item, PyObject *value)
+{
+ if (value == NULL) {
+ PyErr_SetString(PyExc_TypeError,
+ "Blob doesn't support item deletion");
+ return -1;
+ }
+ if (!PyBytes_Check(value) || PyBytes_Size(value) != 1) {
+ PyErr_SetString(PyExc_ValueError,
+ "Blob assignment must be a single byte");
+ return -1;
+ }
+ int i = get_subscript_index(self, item);
+ if (i < 0) {
+ return -1;
+ }
+ const char *buf = PyBytes_AS_STRING(value);
+ return inner_write(self, buf, 1, i);
+}
+
+static int
+ass_subscript_slice(pysqlite_Blob *self, PyObject *item, PyObject *value)
+{
+ if (value == NULL) {
+ PyErr_SetString(PyExc_TypeError,
+ "Blob doesn't support slice deletion");
+ return -1;
+ }
+
+ Py_ssize_t start, stop, step, slicelen;
+ if (PySlice_Unpack(item, &start, &stop, &step) < 0) {
+ return -1;
+ }
+ int blob_len = sqlite3_blob_bytes(self->blob);
+ slicelen = PySlice_AdjustIndices(blob_len, &start, &stop, step);
+
+ Py_buffer vbuf;
+ if (PyObject_GetBuffer(value, &vbuf, PyBUF_SIMPLE) < 0) {
+ return -1;
+ }
+ if (vbuf.len != slicelen) {
+ PyErr_SetString(PyExc_IndexError,
+ "Blob slice assignment is wrong size");
+ PyBuffer_Release(&vbuf);
+ return -1;
+ }
+
+ int rc;
+ if (slicelen == 0) {
+ rc = 0;
+ }
+ else if (step == 1) {
+ rc = inner_write(self, vbuf.buf, slicelen, start);
+ }
+ else {
+ PyObject *read_blob = inner_read(self, stop - start, start);
+ if (read_blob == NULL) {
+ rc = -1;
+ }
+ else {
+ char *blob_buf = PyBytes_AS_STRING(read_blob);
+ for (Py_ssize_t i = 0, j = 0; i < slicelen; i++, j += step) {
+ blob_buf[j] = ((char *)vbuf.buf)[i];
+ }
+ rc = inner_write(self, blob_buf, stop - start, start);
+ Py_DECREF(read_blob);
+ }
+ }
+ PyBuffer_Release(&vbuf);
+ return rc;
+}
+
+static int
+blob_ass_subscript(pysqlite_Blob *self, PyObject *item, PyObject *value)
+{
+ if (!check_blob(self)) {
+ return -1;
+ }
+
+ if (PyIndex_Check(item)) {
+ return ass_subscript_index(self, item, value);
+ }
+ if (PySlice_Check(item)) {
+ return ass_subscript_slice(self, item, value);
+ }
+
+ PyErr_SetString(PyExc_TypeError, "Blob indices must be integers");
+ return -1;
+}
+
+
+static PyMethodDef blob_methods[] = {
+ BLOB_CLOSE_METHODDEF
+ BLOB_ENTER_METHODDEF
+ BLOB_EXIT_METHODDEF
+ BLOB_READ_METHODDEF
+ BLOB_SEEK_METHODDEF
+ BLOB_TELL_METHODDEF
+ BLOB_WRITE_METHODDEF
+ {NULL, NULL}
+};
+
+static struct PyMemberDef blob_members[] = {
+ {"__weaklistoffset__", T_PYSSIZET, offsetof(pysqlite_Blob, in_weakreflist), READONLY},
+ {NULL},
+};
+
+static PyType_Slot blob_slots[] = {
+ {Py_tp_dealloc, blob_dealloc},
+ {Py_tp_traverse, blob_traverse},
+ {Py_tp_clear, blob_clear},
+ {Py_tp_methods, blob_methods},
+ {Py_tp_members, blob_members},
+
+ // Mapping protocol
+ {Py_mp_length, blob_length},
+ {Py_mp_subscript, blob_subscript},
+ {Py_mp_ass_subscript, blob_ass_subscript},
+ {0, NULL},
+};
+
+static PyType_Spec blob_spec = {
+ .name = MODULE_NAME ".Blob",
+ .basicsize = sizeof(pysqlite_Blob),
+ .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
+ Py_TPFLAGS_IMMUTABLETYPE),
+ .slots = blob_slots,
+};
+
+int
+pysqlite_blob_setup_types(PyObject *module)
+{
+ PyObject *type = PyType_FromModuleAndSpec(module, &blob_spec, NULL);
+ if (type == NULL) {
+ return -1;
+ }
+ pysqlite_state *state = pysqlite_get_state(module);
+ state->BlobType = (PyTypeObject *)type;
+ return 0;
+}
diff --git a/Modules/_sqlite/blob.h b/Modules/_sqlite/blob.h
new file mode 100644
index 00000000000000..b4ac4ae0e6c7dc
--- /dev/null
+++ b/Modules/_sqlite/blob.h
@@ -0,0 +1,24 @@
+#ifndef PYSQLITE_BLOB_H
+#define PYSQLITE_BLOB_H
+
+#include "Python.h"
+#include "sqlite3.h"
+#include "connection.h"
+
+#define BLOB_SEEK_START 0
+#define BLOB_SEEK_CUR 1
+#define BLOB_SEEK_END 2
+
+typedef struct {
+ PyObject_HEAD
+ pysqlite_Connection *connection;
+ sqlite3_blob *blob;
+ int offset;
+
+ PyObject *in_weakreflist;
+} pysqlite_Blob;
+
+int pysqlite_blob_setup_types(PyObject *module);
+void pysqlite_close_all_blobs(pysqlite_Connection *self);
+
+#endif
diff --git a/Modules/_sqlite/clinic/blob.c.h b/Modules/_sqlite/clinic/blob.c.h
new file mode 100644
index 00000000000000..8276f8e140c2e8
--- /dev/null
+++ b/Modules/_sqlite/clinic/blob.c.h
@@ -0,0 +1,202 @@
+/*[clinic input]
+preserve
+[clinic start generated code]*/
+
+PyDoc_STRVAR(blob_close__doc__,
+"close($self, /)\n"
+"--\n"
+"\n"
+"Close blob.");
+
+#define BLOB_CLOSE_METHODDEF \
+ {"close", (PyCFunction)blob_close, METH_NOARGS, blob_close__doc__},
+
+static PyObject *
+blob_close_impl(pysqlite_Blob *self);
+
+static PyObject *
+blob_close(pysqlite_Blob *self, PyObject *Py_UNUSED(ignored))
+{
+ return blob_close_impl(self);
+}
+
+PyDoc_STRVAR(blob_read__doc__,
+"read($self, length=-1, /)\n"
+"--\n"
+"\n"
+"Read data from blob.");
+
+#define BLOB_READ_METHODDEF \
+ {"read", (PyCFunction)(void(*)(void))blob_read, METH_FASTCALL, blob_read__doc__},
+
+static PyObject *
+blob_read_impl(pysqlite_Blob *self, int length);
+
+static PyObject *
+blob_read(pysqlite_Blob *self, PyObject *const *args, Py_ssize_t nargs)
+{
+ PyObject *return_value = NULL;
+ int length = -1;
+
+ if (!_PyArg_CheckPositional("read", nargs, 0, 1)) {
+ goto exit;
+ }
+ if (nargs < 1) {
+ goto skip_optional;
+ }
+ length = _PyLong_AsInt(args[0]);
+ if (length == -1 && PyErr_Occurred()) {
+ goto exit;
+ }
+skip_optional:
+ return_value = blob_read_impl(self, length);
+
+exit:
+ return return_value;
+}
+
+PyDoc_STRVAR(blob_write__doc__,
+"write($self, data, /)\n"
+"--\n"
+"\n"
+"Write data to blob.");
+
+#define BLOB_WRITE_METHODDEF \
+ {"write", (PyCFunction)blob_write, METH_O, blob_write__doc__},
+
+static PyObject *
+blob_write_impl(pysqlite_Blob *self, Py_buffer *data);
+
+static PyObject *
+blob_write(pysqlite_Blob *self, PyObject *arg)
+{
+ PyObject *return_value = NULL;
+ Py_buffer data = {NULL, NULL};
+
+ if (PyObject_GetBuffer(arg, &data, PyBUF_SIMPLE) != 0) {
+ goto exit;
+ }
+ if (!PyBuffer_IsContiguous(&data, 'C')) {
+ _PyArg_BadArgument("write", "argument", "contiguous buffer", arg);
+ goto exit;
+ }
+ return_value = blob_write_impl(self, &data);
+
+exit:
+ /* Cleanup for data */
+ if (data.obj) {
+ PyBuffer_Release(&data);
+ }
+
+ return return_value;
+}
+
+PyDoc_STRVAR(blob_seek__doc__,
+"seek($self, offset, origin=0, /)\n"
+"--\n"
+"\n"
+"Change the access position for a blob.");
+
+#define BLOB_SEEK_METHODDEF \
+ {"seek", (PyCFunction)(void(*)(void))blob_seek, METH_FASTCALL, blob_seek__doc__},
+
+static PyObject *
+blob_seek_impl(pysqlite_Blob *self, int offset, int origin);
+
+static PyObject *
+blob_seek(pysqlite_Blob *self, PyObject *const *args, Py_ssize_t nargs)
+{
+ PyObject *return_value = NULL;
+ int offset;
+ int origin = 0;
+
+ if (!_PyArg_CheckPositional("seek", nargs, 1, 2)) {
+ goto exit;
+ }
+ offset = _PyLong_AsInt(args[0]);
+ if (offset == -1 && PyErr_Occurred()) {
+ goto exit;
+ }
+ if (nargs < 2) {
+ goto skip_optional;
+ }
+ origin = _PyLong_AsInt(args[1]);
+ if (origin == -1 && PyErr_Occurred()) {
+ goto exit;
+ }
+skip_optional:
+ return_value = blob_seek_impl(self, offset, origin);
+
+exit:
+ return return_value;
+}
+
+PyDoc_STRVAR(blob_tell__doc__,
+"tell($self, /)\n"
+"--\n"
+"\n"
+"Return current access position for a blob.");
+
+#define BLOB_TELL_METHODDEF \
+ {"tell", (PyCFunction)blob_tell, METH_NOARGS, blob_tell__doc__},
+
+static PyObject *
+blob_tell_impl(pysqlite_Blob *self);
+
+static PyObject *
+blob_tell(pysqlite_Blob *self, PyObject *Py_UNUSED(ignored))
+{
+ return blob_tell_impl(self);
+}
+
+PyDoc_STRVAR(blob_enter__doc__,
+"__enter__($self, /)\n"
+"--\n"
+"\n"
+"Blob context manager enter.");
+
+#define BLOB_ENTER_METHODDEF \
+ {"__enter__", (PyCFunction)blob_enter, METH_NOARGS, blob_enter__doc__},
+
+static PyObject *
+blob_enter_impl(pysqlite_Blob *self);
+
+static PyObject *
+blob_enter(pysqlite_Blob *self, PyObject *Py_UNUSED(ignored))
+{
+ return blob_enter_impl(self);
+}
+
+PyDoc_STRVAR(blob_exit__doc__,
+"__exit__($self, type, val, tb, /)\n"
+"--\n"
+"\n"
+"Blob context manager exit.");
+
+#define BLOB_EXIT_METHODDEF \
+ {"__exit__", (PyCFunction)(void(*)(void))blob_exit, METH_FASTCALL, blob_exit__doc__},
+
+static PyObject *
+blob_exit_impl(pysqlite_Blob *self, PyObject *type, PyObject *val,
+ PyObject *tb);
+
+static PyObject *
+blob_exit(pysqlite_Blob *self, PyObject *const *args, Py_ssize_t nargs)
+{
+ PyObject *return_value = NULL;
+ PyObject *type;
+ PyObject *val;
+ PyObject *tb;
+
+ if (!_PyArg_CheckPositional("__exit__", nargs, 3, 3)) {
+ goto exit;
+ }
+ type = args[0];
+ val = args[1];
+ tb = args[2];
+ return_value = blob_exit_impl(self, type, val, tb);
+
+exit:
+ return return_value;
+}
+/*[clinic end generated code: output=235d02d1bfa39b2a input=a9049054013a1b77]*/
diff --git a/Modules/_sqlite/clinic/connection.c.h b/Modules/_sqlite/clinic/connection.c.h
index 16ad2ee254eca3..c2a45594ca907b 100644
--- a/Modules/_sqlite/clinic/connection.c.h
+++ b/Modules/_sqlite/clinic/connection.c.h
@@ -145,6 +145,99 @@ pysqlite_connection_cursor(pysqlite_Connection *self, PyObject *const *args, Py_
return return_value;
}
+PyDoc_STRVAR(blobopen__doc__,
+"blobopen($self, table, column, row, /, *, readonly=False, name=\'main\')\n"
+"--\n"
+"\n"
+"Return a blob object. Non-standard.");
+
+#define BLOBOPEN_METHODDEF \
+ {"blobopen", (PyCFunction)(void(*)(void))blobopen, METH_FASTCALL|METH_KEYWORDS, blobopen__doc__},
+
+static PyObject *
+blobopen_impl(pysqlite_Connection *self, const char *table, const char *col,
+ int row, int readonly, const char *name);
+
+static PyObject *
+blobopen(pysqlite_Connection *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+ PyObject *return_value = NULL;
+ static const char * const _keywords[] = {"", "", "", "readonly", "name", NULL};
+ static _PyArg_Parser _parser = {NULL, _keywords, "blobopen", 0};
+ PyObject *argsbuf[5];
+ Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 3;
+ const char *table;
+ const char *col;
+ int row;
+ int readonly = 0;
+ const char *name = "main";
+
+ args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 3, 3, 0, argsbuf);
+ if (!args) {
+ goto exit;
+ }
+ if (!PyUnicode_Check(args[0])) {
+ _PyArg_BadArgument("blobopen", "argument 1", "str", args[0]);
+ goto exit;
+ }
+ Py_ssize_t table_length;
+ table = PyUnicode_AsUTF8AndSize(args[0], &table_length);
+ if (table == NULL) {
+ goto exit;
+ }
+ if (strlen(table) != (size_t)table_length) {
+ PyErr_SetString(PyExc_ValueError, "embedded null character");
+ goto exit;
+ }
+ if (!PyUnicode_Check(args[1])) {
+ _PyArg_BadArgument("blobopen", "argument 2", "str", args[1]);
+ goto exit;
+ }
+ Py_ssize_t col_length;
+ col = PyUnicode_AsUTF8AndSize(args[1], &col_length);
+ if (col == NULL) {
+ goto exit;
+ }
+ if (strlen(col) != (size_t)col_length) {
+ PyErr_SetString(PyExc_ValueError, "embedded null character");
+ goto exit;
+ }
+ row = _PyLong_AsInt(args[2]);
+ if (row == -1 && PyErr_Occurred()) {
+ goto exit;
+ }
+ if (!noptargs) {
+ goto skip_optional_kwonly;
+ }
+ if (args[3]) {
+ readonly = _PyLong_AsInt(args[3]);
+ if (readonly == -1 && PyErr_Occurred()) {
+ goto exit;
+ }
+ if (!--noptargs) {
+ goto skip_optional_kwonly;
+ }
+ }
+ if (!PyUnicode_Check(args[4])) {
+ _PyArg_BadArgument("blobopen", "argument 'name'", "str", args[4]);
+ goto exit;
+ }
+ Py_ssize_t name_length;
+ name = PyUnicode_AsUTF8AndSize(args[4], &name_length);
+ if (name == NULL) {
+ goto exit;
+ }
+ if (strlen(name) != (size_t)name_length) {
+ PyErr_SetString(PyExc_ValueError, "embedded null character");
+ goto exit;
+ }
+skip_optional_kwonly:
+ return_value = blobopen_impl(self, table, col, row, readonly, name);
+
+exit:
+ return return_value;
+}
+
PyDoc_STRVAR(pysqlite_connection_close__doc__,
"close($self, /)\n"
"--\n"
@@ -836,4 +929,4 @@ getlimit(pysqlite_Connection *self, PyObject *arg)
#ifndef PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF
#define PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF
#endif /* !defined(PYSQLITE_CONNECTION_LOAD_EXTENSION_METHODDEF) */
-/*[clinic end generated code: output=c2faf6563397091b input=a9049054013a1b77]*/
+/*[clinic end generated code: output=959de9d109b85d3f input=a9049054013a1b77]*/
diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c
index 02f4ac46b7c356..bf6b5c78d0b1b8 100644
--- a/Modules/_sqlite/connection.c
+++ b/Modules/_sqlite/connection.c
@@ -26,6 +26,7 @@
#include "connection.h"
#include "statement.h"
#include "cursor.h"
+#include "blob.h"
#include "prepare_protocol.h"
#include "util.h"
@@ -231,10 +232,17 @@ pysqlite_connection_init_impl(pysqlite_Connection *self,
return -1;
}
- // Create list of weak references to cursors.
+ /* Create lists of weak references to cursors and blobs */
PyObject *cursors = PyList_New(0);
if (cursors == NULL) {
- Py_DECREF(statement_cache);
+ Py_XDECREF(statement_cache);
+ return -1;
+ }
+
+ PyObject *blobs = PyList_New(0);
+ if (blobs == NULL) {
+ Py_XDECREF(statement_cache);
+ Py_XDECREF(cursors);
return -1;
}
@@ -247,6 +255,7 @@ pysqlite_connection_init_impl(pysqlite_Connection *self,
self->thread_ident = PyThread_get_thread_ident();
self->statement_cache = statement_cache;
self->cursors = cursors;
+ self->blobs = blobs;
self->created_cursors = 0;
self->row_factory = Py_NewRef(Py_None);
self->text_factory = Py_NewRef(&PyUnicode_Type);
@@ -288,6 +297,7 @@ connection_traverse(pysqlite_Connection *self, visitproc visit, void *arg)
Py_VISIT(Py_TYPE(self));
Py_VISIT(self->statement_cache);
Py_VISIT(self->cursors);
+ Py_VISIT(self->blobs);
Py_VISIT(self->row_factory);
Py_VISIT(self->text_factory);
VISIT_CALLBACK_CONTEXT(self->trace_ctx);
@@ -311,6 +321,7 @@ connection_clear(pysqlite_Connection *self)
{
Py_CLEAR(self->statement_cache);
Py_CLEAR(self->cursors);
+ Py_CLEAR(self->blobs);
Py_CLEAR(self->row_factory);
Py_CLEAR(self->text_factory);
clear_callback_context(self->trace_ctx);
@@ -426,6 +437,71 @@ pysqlite_connection_cursor_impl(pysqlite_Connection *self, PyObject *factory)
return cursor;
}
+/*[clinic input]
+_sqlite3.Connection.blobopen as blobopen
+
+ table: str
+ column as col: str
+ row: int
+ /
+ *
+ readonly: bool(accept={int}) = False
+ name: str = "main"
+
+Return a blob object. Non-standard.
+[clinic start generated code]*/
+
+static PyObject *
+blobopen_impl(pysqlite_Connection *self, const char *table, const char *col,
+ int row, int readonly, const char *name)
+/*[clinic end generated code: output=0c8e2e58516d0b5c input=1eec15d2b87bf09e]*/
+{
+ if (!pysqlite_check_thread(self) || !pysqlite_check_connection(self)) {
+ return NULL;
+ }
+
+ int rc;
+ sqlite3_blob *blob;
+
+ Py_BEGIN_ALLOW_THREADS
+ rc = sqlite3_blob_open(self->db, name, table, col, row, !readonly, &blob);
+ Py_END_ALLOW_THREADS
+
+ if (rc != SQLITE_OK) {
+ _pysqlite_seterror(self->state, self->db);
+ return NULL;
+ }
+
+ pysqlite_Blob *obj = PyObject_GC_New(pysqlite_Blob, self->state->BlobType);
+ if (obj == NULL) {
+ goto error;
+ }
+
+ obj->connection = (pysqlite_Connection *)Py_NewRef(self);
+ obj->blob = blob;
+ obj->offset = 0;
+ obj->in_weakreflist = NULL;
+
+ PyObject_GC_Track(obj);
+
+ // Add our blob to connection blobs list
+ PyObject *weakref = PyWeakref_NewRef((PyObject *)obj, NULL);
+ if (weakref == NULL) {
+ goto error;
+ }
+ rc = PyList_Append(self->blobs, weakref);
+ Py_DECREF(weakref);
+ if (rc < 0) {
+ goto error;
+ }
+
+ return (PyObject *)obj;
+
+error:
+ Py_XDECREF(obj);
+ return NULL;
+}
+
/*[clinic input]
_sqlite3.Connection.close as pysqlite_connection_close
@@ -448,6 +524,7 @@ pysqlite_connection_close_impl(pysqlite_Connection *self)
return NULL;
}
+ pysqlite_close_all_blobs(self);
Py_CLEAR(self->statement_cache);
connection_close(self);
@@ -1969,6 +2046,7 @@ static PyMethodDef connection_methods[] = {
PYSQLITE_CONNECTION_SET_TRACE_CALLBACK_METHODDEF
SETLIMIT_METHODDEF
GETLIMIT_METHODDEF
+ BLOBOPEN_METHODDEF
{NULL, NULL}
};
diff --git a/Modules/_sqlite/connection.h b/Modules/_sqlite/connection.h
index 84f1f095cb3867..2b946ff3c7369b 100644
--- a/Modules/_sqlite/connection.h
+++ b/Modules/_sqlite/connection.h
@@ -63,8 +63,9 @@ typedef struct
PyObject *statement_cache;
- /* Lists of weak references to statements and cursors used within this connection */
- PyObject* cursors;
+ /* Lists of weak references to cursors and blobs used within this connection */
+ PyObject *cursors;
+ PyObject *blobs;
/* Counters for how many cursors were created in the connection. May be
* reset to 0 at certain intervals */
diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c
index 3b9f79799b5c59..0eb64bfb12d64a 100644
--- a/Modules/_sqlite/module.c
+++ b/Modules/_sqlite/module.c
@@ -27,6 +27,7 @@
#include "prepare_protocol.h"
#include "microprotocols.h"
#include "row.h"
+#include "blob.h"
#if SQLITE_VERSION_NUMBER < 3007015
#error "SQLite 3.7.15 or higher required"
@@ -580,6 +581,7 @@ module_traverse(PyObject *module, visitproc visit, void *arg)
Py_VISIT(state->Warning);
// Types
+ Py_VISIT(state->BlobType);
Py_VISIT(state->ConnectionType);
Py_VISIT(state->CursorType);
Py_VISIT(state->PrepareProtocolType);
@@ -612,6 +614,7 @@ module_clear(PyObject *module)
Py_CLEAR(state->Warning);
// Types
+ Py_CLEAR(state->BlobType);
Py_CLEAR(state->ConnectionType);
Py_CLEAR(state->CursorType);
Py_CLEAR(state->PrepareProtocolType);
@@ -666,7 +669,8 @@ module_exec(PyObject *module)
(pysqlite_cursor_setup_types(module) < 0) ||
(pysqlite_connection_setup_types(module) < 0) ||
(pysqlite_statement_setup_types(module) < 0) ||
- (pysqlite_prepare_protocol_setup_types(module) < 0)
+ (pysqlite_prepare_protocol_setup_types(module) < 0) ||
+ (pysqlite_blob_setup_types(module) < 0)
) {
goto error;
}
diff --git a/Modules/_sqlite/module.h b/Modules/_sqlite/module.h
index 1d319f1ed5541e..3362cb2e2c16d6 100644
--- a/Modules/_sqlite/module.h
+++ b/Modules/_sqlite/module.h
@@ -53,6 +53,7 @@ typedef struct {
int BaseTypeAdapted;
int enable_callback_tracebacks;
+ PyTypeObject *BlobType;
PyTypeObject *ConnectionType;
PyTypeObject *CursorType;
PyTypeObject *PrepareProtocolType;
diff --git a/PCbuild/_sqlite3.vcxproj b/PCbuild/_sqlite3.vcxproj
index e268c473f4c985..d4b11c3440b4cb 100644
--- a/PCbuild/_sqlite3.vcxproj
+++ b/PCbuild/_sqlite3.vcxproj
@@ -105,6 +105,7 @@
+
@@ -115,6 +116,7 @@
+
diff --git a/PCbuild/_sqlite3.vcxproj.filters b/PCbuild/_sqlite3.vcxproj.filters
index 79fc17b53fb508..f4a265eba7dd80 100644
--- a/PCbuild/_sqlite3.vcxproj.filters
+++ b/PCbuild/_sqlite3.vcxproj.filters
@@ -36,6 +36,9 @@
Header Files
+
+ Header Files
+
@@ -62,6 +65,9 @@
Source Files
+
+ Source Files
+
diff --git a/setup.py b/setup.py
index e30674f31cdb85..30157a68d5ea88 100644
--- a/setup.py
+++ b/setup.py
@@ -1324,6 +1324,7 @@ def detect_dbm_gdbm(self):
def detect_sqlite(self):
sources = [
+ "_sqlite/blob.c",
"_sqlite/connection.c",
"_sqlite/cursor.c",
"_sqlite/microprotocols.c",