diff -r 5873cfb42ebe -r d3c3c05ae645 Doc/includes/sqlite3/blob.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Doc/includes/sqlite3/blob.py Wed Feb 03 23:28:50 2016 +0200 @@ -0,0 +1,14 @@ +import sqlite3 + +con = sqlite3.connect(":memory:") +# creating the table +con.execute("create table test(id integer primary key, blob_col blob)") +con.execute("insert into test(blob_col) values (zeroblob(10))") +# opening blob handle +blob = con.blob("test", "blob_col", 1, 1) +blob.write(b"a" * 5) +blob.write(b"b" * 5) +blob.seek(0) +print blob.read() # will print b"aaaaabbbbb" +blob.close() + diff -r 5873cfb42ebe -r d3c3c05ae645 Doc/includes/sqlite3/blob_with.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Doc/includes/sqlite3/blob_with.py Wed Feb 03 23:28:50 2016 +0200 @@ -0,0 +1,13 @@ +import sqlite3 + +con = sqlite3.connect(":memory:") +# creating the table +con.execute("create table test(id integer primary key, blob_col blob)") +con.execute("insert into test(blob_col) values (zeroblob(10))") +# opening blob handle +with con.blob("test", "blob_col", 1, 1) as blob: + blob.write(b"a" * 5) + blob.write(b"b" * 5) + blob.seek(0) + print blob.read() # will print b"aaaaabbbbb" + diff -r 5873cfb42ebe -r d3c3c05ae645 Doc/library/sqlite3.rst --- a/Doc/library/sqlite3.rst Tue Feb 02 17:04:56 2016 -0600 +++ b/Doc/library/sqlite3.rst Wed Feb 03 23:28:50 2016 +0200 @@ -282,6 +282,14 @@ supplied, this must be a custom cursor class that extends :class:`sqlite3.Cursor`. + .. method:: blob(table, column, row, flags=0, dbname="main") + + On success a :class:`Blob` handle to the blob located in row *row*, + column *column*, table *table* in database *dbname* will be returned. + The flags represent the blob mode. 0 for read-only otherwise read-write. + + .. versionadded:: 3.6 + .. method:: commit() This method commits the current transaction. If you don't call this method, @@ -693,6 +701,61 @@ 35.14 +.. _sqlite3-blob-objects: + +Blob Objects +------------ + +.. versionadded:: 3.6 + +.. class:: Blob + +A :class:`Blob` instance has the following attributes and methods: + + A SQLite database blob has the following attributes and methods: + +.. method:: Blob.close() + + Close the blob now (rather than whenever __del__ is called). + + The blob will be unusable from this point forward; an Error (or subclass) + exception will be raised if any operation is attempted with the blob. + +.. method:: Blob.length() + + Return the blob size. + +.. method:: Blob.read([length]) + + Read lnegth 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 + length is not specified or negative we will read up to end of blob. + +.. method:: Blob.write(data) + + Write data to the blob at the current offset. This function cannot changed blob + length. If data write will result in writing to more then blob current size an + error will be raised. + +.. method:: Blob.tell() + + Return the current offset of the blob. + +.. method:: Blob.seek(offset, [whence]) + + Set the blob offset. The whence argument is optional and defaults to os.SEEK_SET + or 0 (absolute blob positioning); other values are os.SEEK_CUR or 1 (seek + relative to the current position) and os.SEEK_END or 2 (seek relative to the blob’s end). + +:class:`Blob` example: + + .. literalinclude:: ../includes/sqlite3/blob.py + +A :class:`Blob` can also be used with context manager: + + .. literalinclude:: ../includes/sqlite3/blob_with.py + + .. _sqlite3-types: SQLite and Python types diff -r 5873cfb42ebe -r d3c3c05ae645 Doc/whatsnew/3.6.rst --- a/Doc/whatsnew/3.6.rst Tue Feb 02 17:04:56 2016 -0600 +++ b/Doc/whatsnew/3.6.rst Wed Feb 03 23:28:50 2016 +0200 @@ -125,6 +125,15 @@ an instance were excluded. (Contributed by Martin Panter in :issue:`25590`.) +sqlite3 +----------- + + +The :class:`sqlite3.Connection` now has the :meth:`sqlite3.Connection.blob` method. The +:class:`sqlite3.Blob` allows incremental I/O operations to blobs. +(Contributed by Aviv Palivoda in :issue:`24905`) + + telnetlib --------- diff -r 5873cfb42ebe -r d3c3c05ae645 Lib/sqlite3/test/dbapi.py --- a/Lib/sqlite3/test/dbapi.py Tue Feb 02 17:04:56 2016 -0600 +++ b/Lib/sqlite3/test/dbapi.py Wed Feb 03 23:28:50 2016 +0200 @@ -511,6 +511,155 @@ except TypeError: pass + +class BlobTests(unittest.TestCase): + def setUp(self): + self.cx = sqlite.connect(":memory:") + self.cx.execute("create table test(id integer primary key, blob_col blob)") + self.blob_data = b"a" * 100 + self.cx.execute("insert into test(blob_col) values (?)", (self.blob_data, )) + self.blob = self.cx.blob("test", "blob_col", 1, 1) + self.second_data = b"b" * 100 + + def tearDown(self): + self.blob.close() + self.cx.close() + + def CheckLength(self): + self.assertEqual(self.blob.length(), 100) + + def CheckTell(self): + self.assertEqual(self.blob.tell(), 0) + + def CheckSeekFromBlobStart(self): + self.blob.seek(10) + self.assertEqual(self.blob.tell(), 10) + self.blob.seek(10, 0) + self.assertEqual(self.blob.tell(), 10) + + def CheckSeekFromCurrentPosition(self): + self.blob.seek(10, 1) + self.blob.seek(10, 1) + self.assertEqual(self.blob.tell(), 20) + + def CheckSeekFromBlobEnd(self): + self.blob.seek(-10, 2) + self.assertEqual(self.blob.tell(), 90) + + def CheckBlobSeekOverBlobSize(self): + try: + self.blob.seek(1000) + self.fail("should have raised a ValueError") + except ValueError: + pass + except Exception: + self.fail("should have raised a ValueError") + + def CheckBlobSeekUnderBlobSize(self): + try: + self.blob.seek(-10) + self.fail("should have raised a ValueError") + except ValueError: + pass + except Exception: + self.fail("should have raised a ValueError") + + def CheckBlobRead(self): + self.assertEqual(self.blob.read(), self.blob_data) + + def CheckBlobReadSize(self): + self.assertEqual(len(self.blob.read(10)), 10) + + def CheckBlobReadAdvanceOffset(self): + self.blob.read(10) + self.assertEqual(self.blob.tell(), 10) + + def CheckBlobReadStartAtOffset(self): + self.blob.seek(10) + self.blob.write(self.second_data[:10]) + self.blob.seek(10) + self.assertEqual(self.blob.read(10), self.second_data[:10]) + + def CheckBlobWrite(self): + self.blob.write(self.second_data) + self.assertEqual(self.cx.execute("select blob_col from test").fetchone()[0], self.second_data) + + def CheckBlobWriteAtOffset(self): + self.blob.seek(50) + self.blob.write(self.second_data[:50]) + self.assertEqual(self.cx.execute("select blob_col from test").fetchone()[0], + self.blob_data[:50] + self.second_data[:50]) + + def CheckBlobWriteAdvanceOffset(self): + self.blob.write(self.second_data[:50]) + self.assertEqual(self.blob.tell(), 50) + + def CheckBlobWriteMoreThenBlobSize(self): + try: + self.blob.write(b"a" * 1000) + self.fail("should have raised a sqlite.OperationalError") + except sqlite.OperationalError: + pass + except Exception: + self.fail("should have raised a sqlite.OperationalError") + + def CheckBlobReadAfterRowChange(self): + self.cx.execute("UPDATE test SET blob_col='aaaa' where id=1") + try: + self.blob.read() + self.fail("should have raised a sqlite.OperationalError") + except sqlite.OperationalError: + pass + except Exception: + self.fail("should have raised a sqlite.OperationalError") + + def CheckBlobWriteAfterRowChange(self): + self.cx.execute("UPDATE test SET blob_col='aaaa' where id=1") + try: + self.blob.write(b"aaa") + self.fail("should have raised a sqlite.OperationalError") + except sqlite.OperationalError: + pass + except Exception: + self.fail("should have raised a sqlite.OperationalError") + + def CheckBlobOpenWithBadDb(self): + try: + self.cx.blob("test", "blob_col", 1, 1, dbname="notexisintg") + self.fail("should have raised a sqlite.OperationalError") + except sqlite.OperationalError: + pass + except Exception: + self.fail("should have raised a sqlite.OperationalError") + + def CheckBlobOpenWithBadTable(self): + try: + self.cx.blob("notexisintg", "blob_col", 1, 1) + self.fail("should have raised a sqlite.OperationalError") + except sqlite.OperationalError: + pass + except Exception: + self.fail("should have raised a sqlite.OperationalError") + + def CheckBlobOpenWithBadColumn(self): + try: + self.cx.blob("test", "notexisting", 1, 1) + self.fail("should have raised a sqlite.OperationalError") + except sqlite.OperationalError: + pass + except Exception: + self.fail("should have raised a sqlite.OperationalError") + + def CheckBlobOpenWithBadRow(self): + try: + self.cx.blob("test", "blob_col", 2, 1) + self.fail("should have raised a sqlite.OperationalError") + except sqlite.OperationalError: + pass + except Exception: + self.fail("should have raised a sqlite.OperationalError") + + @unittest.skipUnless(threading, 'This test requires threading.') class ThreadTests(unittest.TestCase): def setUp(self): @@ -796,6 +945,20 @@ except: self.fail("Should have raised a ProgrammingError") + def CheckClosedBlobRead(self): + con = sqlite.connect(":memory:") + con.execute("create table test(id integer primary key, blob_col blob)") + con.execute("insert into test(blob_col) values (zeroblob(100))") + blob = con.blob("test", "blob_col", 1) + con.close() + try: + blob.read() + self.fail("Should have raised a ProgrammingError") + except sqlite.ProgrammingError: + pass + except: + self.fail("Should have raised a ProgrammingError") + def CheckClosedCreateFunction(self): con = sqlite.connect(":memory:") con.close() @@ -892,6 +1055,99 @@ except: self.fail("Should have raised a ProgrammingError: " + method_name) + +class ClosedBlobTests(unittest.TestCase): + def setUp(self): + self.cx = sqlite.connect(":memory:") + self.cx.execute("create table test(id integer primary key, blob_col blob)") + self.cx.execute("insert into test(blob_col) values (zeroblob(100))") + + def tearDown(self): + self.cx.close() + + def CheckClosedRead(self): + self.blob = self.cx.blob("test", "blob_col", 1) + self.blob.close() + try: + self.blob.read() + self.fail("Should have raised a ProgrammingError") + except sqlite.ProgrammingError: + pass + except Exception: + self.fail("Should have raised a ProgrammingError") + + def CheckClosedWrite(self): + self.blob = self.cx.blob("test", "blob_col", 1) + self.blob.close() + try: + self.blob.write(b"aaaaaaaaa") + self.fail("Should have raised a ProgrammingError") + except sqlite.ProgrammingError: + pass + except Exception: + self.fail("Should have raised a ProgrammingError") + + def CheckClosedSeek(self): + self.blob = self.cx.blob("test", "blob_col", 1) + self.blob.close() + try: + self.blob.seek(10) + self.fail("Should have raised a ProgrammingError") + except sqlite.ProgrammingError: + pass + except Exception: + self.fail("Should have raised a ProgrammingError") + + def CheckClosedTell(self): + self.blob = self.cx.blob("test", "blob_col", 1) + self.blob.close() + try: + self.blob.tell() + self.fail("Should have raised a ProgrammingError") + except sqlite.ProgrammingError: + pass + except Exception: + self.fail("Should have raised a ProgrammingError") + + def CheckClosedClose(self): + self.blob = self.cx.blob("test", "blob_col", 1) + self.blob.close() + try: + self.blob.close() + self.fail("Should have raised a ProgrammingError") + except sqlite.ProgrammingError: + pass + except Exception: + self.fail("Should have raised a ProgrammingError") + + +class BlobContextManagerTests(unittest.TestCase): + def setUp(self): + self.cx = sqlite.connect(":memory:") + self.cx.execute("create table test(id integer primary key, blob_col blob)") + self.cx.execute("insert into test(blob_col) values (zeroblob(100))") + + def tearDown(self): + self.cx.close() + + def CheckContextExecute(self): + data = b"a" * 100 + with self.cx.blob("test", "blob_col", 1, 1) as blob: + blob.write(data) + self.assertEqual(self.cx.execute("select blob_col from test").fetchone()[0], data) + + def CheckContextCloseBlob(self): + with self.cx.blob("test", "blob_col", 1) as blob: + blob.seek(10) + try: + blob.close() + self.fail("Should have raised a ProgrammingError") + except sqlite.ProgrammingError: + pass + except Exception: + self.fail("Should have raised a ProgrammingError") + + def suite(): module_suite = unittest.makeSuite(ModuleTests, "Check") connection_suite = unittest.makeSuite(ConnectionTests, "Check") @@ -901,7 +1157,13 @@ ext_suite = unittest.makeSuite(ExtensionTests, "Check") closed_con_suite = unittest.makeSuite(ClosedConTests, "Check") closed_cur_suite = unittest.makeSuite(ClosedCurTests, "Check") - return unittest.TestSuite((module_suite, connection_suite, cursor_suite, thread_suite, constructor_suite, ext_suite, closed_con_suite, closed_cur_suite)) + blob_suite = unittest.makeSuite(BlobTests, "Check") + closed_blob_suite = unittest.makeSuite(ClosedBlobTests, "Check") + blob_context_manager_suite = unittest.makeSuite(BlobContextManagerTests, "Check") + return unittest.TestSuite((module_suite, connection_suite, cursor_suite, thread_suite, constructor_suite, + ext_suite, closed_con_suite, closed_cur_suite, blob_suite, closed_blob_suite, + blob_context_manager_suite)) + def test(): runner = unittest.TextTestRunner() diff -r 5873cfb42ebe -r d3c3c05ae645 Modules/_sqlite/blob.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Modules/_sqlite/blob.c Wed Feb 03 23:28:50 2016 +0200 @@ -0,0 +1,346 @@ +#include "blob.h" +#include "util.h" + + +int pysqlite_blob_init(pysqlite_Blob *self, pysqlite_Connection* connection, sqlite3_blob *blob) +{ + Py_INCREF(connection); + self->connection = connection; + self->offset=0; + self->blob = blob; + self->in_weakreflist = NULL; + + if (!pysqlite_check_thread(self->connection)){ + return -1; + } + return 0; +} + +static void remove_blob_from_connection_blob_list(pysqlite_Blob* self) +{ + Py_ssize_t i; + + for(i=0;iconnection->blobs);i++) + { + if(PyWeakref_GetObject(PyList_GET_ITEM(self->connection->blobs, i))==(PyObject *)self) + { + PyList_SetSlice(self->connection->blobs, i, i+1, NULL); + break; + } + } +} + + + +static void pysqlite_blob_dealloc(pysqlite_Blob* self) +{ + /* close the blob */ + if (self->blob) { + Py_BEGIN_ALLOW_THREADS + sqlite3_blob_close(self->blob); + Py_END_ALLOW_THREADS + } + + // remove from connection weaklist + remove_blob_from_connection_blob_list(self); + if (self->in_weakreflist != NULL) { + PyObject_ClearWeakRefs((PyObject*)self); + } + + Py_XDECREF(self->connection); + + self->blob = NULL; + + Py_TYPE(self)->tp_free((PyObject*)self); +} + + +/* + * Checks if a blob object is usable (i. e. not closed). + * + * 0 => error; 1 => ok + */ +int pysqlite_check_blob(pysqlite_Blob* blob) +{ + + if (!blob->blob) { + PyErr_SetString(pysqlite_ProgrammingError, "Cannot operate on a closed blob."); + return 0; + } else if (!pysqlite_check_connection(blob->connection) || !pysqlite_check_thread(blob->connection)) { + return 0; + } else { + return 1; + } +} + + +PyObject* pysqlite_blob_close(pysqlite_Blob *self){ + if (!pysqlite_check_blob(self)){ + return NULL; + } + /* close the blob */ + if (self->blob) { + Py_BEGIN_ALLOW_THREADS + sqlite3_blob_close(self->blob); + Py_END_ALLOW_THREADS + } + + self->blob = NULL; + + // remove from connection weaklist + remove_blob_from_connection_blob_list(self); + if (self->in_weakreflist != NULL) { + PyObject_ClearWeakRefs((PyObject*)self); + } + + Py_RETURN_NONE; +}; + + +PyObject* pysqlite_blob_length(pysqlite_Blob *self){ + int blob_length; + if (!pysqlite_check_blob(self)){ + return NULL; + } + Py_BEGIN_ALLOW_THREADS + blob_length = sqlite3_blob_bytes(self->blob); + Py_END_ALLOW_THREADS + + return PyLong_FromLong(blob_length); +}; + + +PyObject* pysqlite_blob_read(pysqlite_Blob *self, PyObject *args){ + int read_length = -1; + int blob_length = 0; + PyObject* buffer; + char* raw_buffer; + int rc; + + if (!PyArg_ParseTuple(args, "|i", &read_length)) { + return NULL; + } + + if (!pysqlite_check_blob(self)){ + return NULL; + } + + + //TODO: make this multithreaded and safe! + Py_BEGIN_ALLOW_THREADS + blob_length = sqlite3_blob_bytes(self->blob); + Py_END_ALLOW_THREADS + + if (read_length < 0) { + // same as file read. + read_length = blob_length; + } + + // making sure we don't read more then blob size + if (self->offset + read_length > blob_length){ + read_length = blob_length - self->offset; + } + + buffer = PyBytes_FromStringAndSize(NULL, read_length); + if (!buffer) { + return NULL; + } + raw_buffer = PyBytes_AS_STRING(buffer); + + Py_BEGIN_ALLOW_THREADS + rc = sqlite3_blob_read(self->blob, raw_buffer, read_length, self->offset); + Py_END_ALLOW_THREADS + + if (rc != SQLITE_OK){ + Py_DECREF(buffer); + // For some reasone after modifieng blob the error is not set on the connection db. + if (rc == SQLITE_ABORT){ + PyErr_SetString(pysqlite_OperationalError, "Cannot operate on modified blob"); + } else { + _pysqlite_seterror(self->connection->db, NULL); + } + return NULL; + } + + // update offset. + self->offset += read_length; + + return buffer; +}; + + +PyObject* pysqlite_blob_write(pysqlite_Blob *self, PyObject *data){ + Py_ssize_t data_size; + char *data_buffer; + int rc; + + if (PyBytes_AsStringAndSize(data, &data_buffer, &data_size)){ + return NULL; + } + + if (!pysqlite_check_blob(self)){ + return NULL; + } + + //TODO: throw better error on data bigger then blob. + + Py_BEGIN_ALLOW_THREADS + rc = sqlite3_blob_write(self->blob, data_buffer, data_size, self->offset); + Py_END_ALLOW_THREADS + if (rc != SQLITE_OK) { + // For some reasone after modifieng blob the error is not set on the connection db. + if (rc == SQLITE_ABORT){ + PyErr_SetString(pysqlite_OperationalError, "Cannot operate on modified blob"); + } else { + _pysqlite_seterror(self->connection->db, NULL); + } + return NULL; + } + + self->offset += (int)data_size; + Py_RETURN_NONE; +} + + +PyObject* pysqlite_blob_seek(pysqlite_Blob *self, PyObject *args){ + int blob_length, offset, from_what=0; + + if(!PyArg_ParseTuple(args, "i|i", &offset, &from_what)){ + return NULL; + } + + + if (!pysqlite_check_blob(self)){ + return NULL; + } + + Py_BEGIN_ALLOW_THREADS + blob_length = sqlite3_blob_bytes(self->blob); + Py_END_ALLOW_THREADS + + switch(from_what){ + case 0: //realtive to blob begin + break; + case 1: //realtive to current position + offset = self->offset + offset; + break; + case 2: //realtive to blob end + offset = blob_length + offset; + break; + default: + return PyErr_Format(PyExc_ValueError, "from_what should be 0, 1 or 2"); + } + + if (offset < 0 || offset > blob_length){ + return PyErr_Format(PyExc_ValueError, "offset out of blob range"); + } + + self->offset = offset; + Py_RETURN_NONE; +}; + + +PyObject* pysqlite_blob_tell(pysqlite_Blob *self){ + if (!pysqlite_check_blob(self)){ + return NULL; + } + + return PyLong_FromLong(self->offset); +} + + +PyObject* pysqlite_blob_enter(pysqlite_Blob *self){ + if (!pysqlite_check_blob(self)){ + return NULL; + } + + Py_INCREF(self); + return (PyObject *)self; +} + + +PyObject* pysqlite_blob_exit(pysqlite_Blob *self, PyObject *args){ + PyObject *res; + if (!pysqlite_check_blob(self)){ + return NULL; + } + + res = pysqlite_blob_close(self); + Py_XDECREF(res); + if (!res) { + return NULL; + } + + Py_RETURN_FALSE; +} + + +static PyMethodDef blob_methods[] = { + {"length", (PyCFunction)pysqlite_blob_length, METH_NOARGS, + PyDoc_STR("return blob length")}, + {"read", (PyCFunction)pysqlite_blob_read, METH_VARARGS, + PyDoc_STR("read data from blob")}, + {"write", (PyCFunction)pysqlite_blob_write, METH_O, + PyDoc_STR("write data to blob")}, + {"close", (PyCFunction)pysqlite_blob_close, METH_NOARGS, + PyDoc_STR("close blob")}, + {"seek", (PyCFunction)pysqlite_blob_seek, METH_VARARGS, + PyDoc_STR("change blob current offset")}, + {"tell", (PyCFunction)pysqlite_blob_tell, METH_NOARGS, + PyDoc_STR("return blob current offset")}, + {"__enter__", (PyCFunction)pysqlite_blob_enter, METH_NOARGS, + PyDoc_STR("blob context manager enter")}, + {"__exit__", (PyCFunction)pysqlite_blob_exit, METH_VARARGS, + PyDoc_STR("blob context manager exit")}, + {NULL, NULL} +}; + + +PyTypeObject pysqlite_BlobType = { + PyVarObject_HEAD_INIT(NULL, 0) + MODULE_NAME ".Blob", /* tp_name */ + sizeof(pysqlite_Blob), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)pysqlite_blob_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + 0, /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + offsetof(pysqlite_Blob, in_weakreflist), /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + blob_methods, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)pysqlite_blob_init, /* tp_init */ + 0, /* tp_alloc */ + 0, /* tp_new */ + 0 /* tp_free */ +}; + +extern int pysqlite_blob_setup_types(void) +{ + pysqlite_BlobType.tp_new = PyType_GenericNew; + return PyType_Ready(&pysqlite_BlobType); +} + diff -r 5873cfb42ebe -r d3c3c05ae645 Modules/_sqlite/blob.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Modules/_sqlite/blob.h Wed Feb 03 23:28:50 2016 +0200 @@ -0,0 +1,24 @@ +#ifndef PYSQLITE_BLOB_H +#define PYSQLITE_BLOB_H +#include "Python.h" +#include "sqlite3.h" +#include "connection.h" + +typedef struct +{ + PyObject_HEAD + pysqlite_Connection* connection; + sqlite3_blob *blob; + int offset; + + PyObject* in_weakreflist; /* List of weak references */ +} pysqlite_Blob; + +extern PyTypeObject pysqlite_BlobType; + +int pysqlite_blob_init(pysqlite_Blob* self, pysqlite_Connection* connection, sqlite3_blob *blob); +PyObject* pysqlite_blob_close(pysqlite_Blob *self); + +int pysqlite_blob_setup_types(void); + +#endif diff -r 5873cfb42ebe -r d3c3c05ae645 Modules/_sqlite/connection.c --- a/Modules/_sqlite/connection.c Tue Feb 02 17:04:56 2016 -0600 +++ b/Modules/_sqlite/connection.c Wed Feb 03 23:28:50 2016 +0200 @@ -27,6 +27,7 @@ #include "connection.h" #include "statement.h" #include "cursor.h" +#include "blob.h" #include "prepare_protocol.h" #include "util.h" @@ -92,6 +93,7 @@ self->statement_cache = NULL; self->statements = NULL; self->cursors = NULL; + self->blobs = NULL; Py_INCREF(Py_None); self->row_factory = Py_None; @@ -142,10 +144,11 @@ self->created_statements = 0; self->created_cursors = 0; - /* Create lists of weak references to statements/cursors */ + /* Create lists of weak references to statements/cursors/blobs */ self->statements = PyList_New(0); self->cursors = PyList_New(0); - if (!self->statements || !self->cursors) { + self->blobs = PyList_New(0); + if (!self->statements || !self->cursors || !self->blobs) { return -1; } @@ -264,6 +267,7 @@ Py_XDECREF(self->collations); Py_XDECREF(self->statements); Py_XDECREF(self->cursors); + Py_XDECREF(self->blobs); Py_TYPE(self)->tp_free((PyObject*)self); } @@ -325,6 +329,80 @@ return cursor; } +PyObject* pysqlite_connection_blob(pysqlite_Connection* self, PyObject* args, PyObject* kwargs) +{ + static char *kwlist[] = {"table", "column", "row", "flags", "dbname", NULL, NULL}; + int rc; + const char *dbname = "main", *table, *column; + sqlite3_int64 row; + int flags = 0; + sqlite3_blob* blob; + pysqlite_Blob *pyblob=0; + PyObject *weakref; + + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ssL|is", kwlist, + &table, &column, &row, &flags, &dbname)) { + return NULL; + } + + Py_BEGIN_ALLOW_THREADS + rc = sqlite3_blob_open(self->db, dbname, table, column, row, flags, &blob); + Py_END_ALLOW_THREADS + + if (rc != SQLITE_OK) { + _pysqlite_seterror(self->db, NULL); + return NULL; + } + + pyblob = PyObject_New(pysqlite_Blob, &pysqlite_BlobType); + if (!pyblob){ + goto error; + } + + rc = pysqlite_blob_init(pyblob, self, blob); + if (rc) { + Py_CLEAR(pyblob); + goto error; + } + + // Add our blob to connection blobs list + weakref=PyWeakref_NewRef((PyObject*)pyblob, NULL); + if (!weakref){ + Py_CLEAR(pyblob); + goto error; + } + if (PyList_Append(self->blobs, weakref) != 0) { + Py_CLEAR(pyblob); + Py_CLEAR(weakref); + goto error; + } + Py_DECREF(weakref); + + return (PyObject*)pyblob; + +error: + Py_BEGIN_ALLOW_THREADS + sqlite3_blob_close(blob); + Py_END_ALLOW_THREADS + return NULL; +} + +void pysqlite_close_all_blobs(pysqlite_Connection* self) +{ + int i; + PyObject* weakref; + PyObject* blob; + + for (i = 0; i < PyList_Size(self->blobs); i++) { + weakref = PyList_GetItem(self->blobs, i); + blob = PyWeakref_GetObject(weakref); + if (blob != Py_None) { + pysqlite_blob_close((pysqlite_Blob*)blob); + } + } +} + PyObject* pysqlite_connection_close(pysqlite_Connection* self, PyObject* args) { int rc; @@ -335,6 +413,8 @@ pysqlite_do_all_statements(self, ACTION_FINALIZE, 1); + pysqlite_close_all_blobs(self); + if (self->db) { Py_BEGIN_ALLOW_THREADS rc = sqlite3_close(self->db); @@ -1634,6 +1714,8 @@ static PyMethodDef connection_methods[] = { {"cursor", (PyCFunction)pysqlite_connection_cursor, METH_VARARGS|METH_KEYWORDS, PyDoc_STR("Return a cursor for the connection.")}, + {"blob", (PyCFunction)pysqlite_connection_blob, METH_VARARGS|METH_KEYWORDS, + PyDoc_STR("return a blob object")}, {"close", (PyCFunction)pysqlite_connection_close, METH_NOARGS, PyDoc_STR("Closes the connection.")}, {"commit", (PyCFunction)pysqlite_connection_commit, METH_NOARGS, diff -r 5873cfb42ebe -r d3c3c05ae645 Modules/_sqlite/connection.h --- a/Modules/_sqlite/connection.h Tue Feb 02 17:04:56 2016 -0600 +++ b/Modules/_sqlite/connection.h Wed Feb 03 23:28:50 2016 +0200 @@ -70,9 +70,10 @@ pysqlite_Cache* statement_cache; - /* Lists of weak references to statements and cursors used within this connection */ + /* Lists of weak references to statements, blobs and cursors used within this connection */ PyObject* statements; PyObject* cursors; + PyObject* blobs; /* Counters for how many statements/cursors were created in the connection. May be * reset to 0 at certain intervals */ diff -r 5873cfb42ebe -r d3c3c05ae645 Modules/_sqlite/module.c --- a/Modules/_sqlite/module.c Tue Feb 02 17:04:56 2016 -0600 +++ b/Modules/_sqlite/module.c Wed Feb 03 23:28:50 2016 +0200 @@ -28,6 +28,7 @@ #include "prepare_protocol.h" #include "microprotocols.h" #include "row.h" +#include "blob.h" #if SQLITE_VERSION_NUMBER >= 3003003 #define HAVE_SHARED_CACHE @@ -336,7 +337,8 @@ (pysqlite_connection_setup_types() < 0) || (pysqlite_cache_setup_types() < 0) || (pysqlite_statement_setup_types() < 0) || - (pysqlite_prepare_protocol_setup_types() < 0) + (pysqlite_prepare_protocol_setup_types() < 0) || + (pysqlite_blob_setup_types() < 0) ) { Py_XDECREF(module); return NULL; diff -r 5873cfb42ebe -r d3c3c05ae645 setup.py --- a/setup.py Tue Feb 02 17:04:56 2016 -0600 +++ b/setup.py Wed Feb 03 23:28:50 2016 +0200 @@ -1145,7 +1145,8 @@ '_sqlite/prepare_protocol.c', '_sqlite/row.c', '_sqlite/statement.c', - '_sqlite/util.c', ] + '_sqlite/util.c', + '_sqlite/blob.c' ] sqlite_defines = [] if host_platform != "win32":