diff options
-rw-r--r-- | doc/examples/blob.py | 13 | ||||
-rw-r--r-- | doc/examples/blob_with.py | 12 | ||||
-rw-r--r-- | doc/sphinx/sqlcipher.rst | 61 | ||||
-rw-r--r-- | lib/test/dbapi.py | 264 | ||||
-rw-r--r-- | setup.py | 2 | ||||
-rw-r--r-- | src/blob.c | 346 | ||||
-rw-r--r-- | src/blob.h | 24 | ||||
-rw-r--r-- | src/connection.c | 90 | ||||
-rw-r--r-- | src/connection.h | 3 | ||||
-rw-r--r-- | src/module.c | 2 |
10 files changed, 812 insertions, 5 deletions
diff --git a/doc/examples/blob.py b/doc/examples/blob.py new file mode 100644 index 0000000..2d63b9c --- /dev/null +++ b/doc/examples/blob.py @@ -0,0 +1,13 @@ +from pysqlcipher import dbapi2 as 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("a" * 5) +blob.write("b" * 5) +blob.seek(0) +print blob.read() # will print "aaaaabbbbb" +blob.close() + diff --git a/doc/examples/blob_with.py b/doc/examples/blob_with.py new file mode 100644 index 0000000..fff9037 --- /dev/null +++ b/doc/examples/blob_with.py @@ -0,0 +1,12 @@ +from pysqlcipher import dbapi2 as 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("a" * 5) + blob.write("b" * 5) + blob.seek(0) + print blob.read() # will print "aaaaabbbbb" + diff --git a/doc/sphinx/sqlcipher.rst b/doc/sphinx/sqlcipher.rst index 7785aeb..2332528 100644 --- a/doc/sphinx/sqlcipher.rst +++ b/doc/sphinx/sqlcipher.rst @@ -236,6 +236,13 @@ Connection Objects :class:`sqlite3.Cursor`. +.. method:: Connection.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. + + .. method:: Connection.commit() This method commits the current transaction. If you don't call this method, @@ -631,6 +638,60 @@ Now we plug :class:`Row` in:: 35.14 +.. _sqlite3-blob-objects: + +Blob Objects +-------------- + +.. 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 --git a/lib/test/dbapi.py b/lib/test/dbapi.py index 05dfd4b..71e5e72 100644 --- a/lib/test/dbapi.py +++ b/lib/test/dbapi.py @@ -465,6 +465,155 @@ class CursorTests(unittest.TestCase): 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 = "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" * 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(str(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(str(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("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("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") + + class ThreadTests(unittest.TestCase): def setUp(self): self.con = sqlite.connect(":memory:") @@ -763,6 +912,20 @@ class ClosedConTests(unittest.TestCase): 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() @@ -859,16 +1022,115 @@ class ClosedCurTests(unittest.TestCase): 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("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 = "a" * 100 + with self.cx.blob("test", "blob_col", 1, 1) as blob: + blob.write("a" * 100) + self.assertEqual(str(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") cursor_suite = unittest.makeSuite(CursorTests, "Check") + blob_suite = unittest.makeSuite(BlobTests, "Check") thread_suite = unittest.makeSuite(ThreadTests, "Check") constructor_suite = unittest.makeSuite(ConstructorTests, "Check") 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)) + closed_blob_suite = unittest.makeSuite(ClosedBlobTests, "Check") + blob_context_manager_suite = unittest.makeSuite(BlobContextManagerTests, "Check") + return unittest.TestSuite((module_suite, connection_suite, cursor_suite, blob_suite, thread_suite, + constructor_suite, ext_suite, closed_con_suite, closed_cur_suite, closed_blob_suite, + blob_context_manager_suite, context_suite)) def test(): runner = unittest.TextTestRunner() @@ -52,7 +52,7 @@ PATCH_VERSION = None sources = ["src/module.c", "src/connection.c", "src/cursor.c", "src/cache.c", "src/microprotocols.c", "src/prepare_protocol.c", "src/statement.c", - "src/util.c", "src/row.c"] + "src/util.c", "src/row.c", "src/blob.c"] if PYSQLITE_EXPERIMENTAL: sources.append("src/backup.c") diff --git a/src/blob.c b/src/blob.c new file mode 100644 index 0000000..b5ef5b2 --- /dev/null +++ b/src/blob.c @@ -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;i<PyList_GET_SIZE(self->connection->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; + + self->ob_type->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 PyInt_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 PyInt_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|Py_TPFLAGS_HAVE_WEAKREFS, /* 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 --git a/src/blob.h b/src/blob.h new file mode 100644 index 0000000..faaef64 --- /dev/null +++ b/src/blob.h @@ -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; + unsigned 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 --git a/src/connection.c b/src/connection.c index 7e4e96c..115d15a 100644 --- a/src/connection.c +++ b/src/connection.c @@ -30,6 +30,8 @@ #include "util.h" #include "sqlitecompat.h" +#include "blob.h" + #ifdef PYSQLITE_EXPERIMENTAL #include "backup.h" #endif @@ -91,6 +93,7 @@ int pysqlite_connection_init(pysqlite_Connection* self, PyObject* args, PyObject self->statement_cache = NULL; self->statements = NULL; self->cursors = NULL; + self->blobs = NULL; Py_INCREF(Py_None); self->row_factory = Py_None; @@ -167,10 +170,12 @@ int pysqlite_connection_init(pysqlite_Connection* self, PyObject* args, PyObject 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; } @@ -293,6 +298,7 @@ void pysqlite_connection_dealloc(pysqlite_Connection* self) Py_XDECREF(self->collations); Py_XDECREF(self->statements); Py_XDECREF(self->cursors); + Py_XDECREF(self->blobs); self->ob_type->tp_free((PyObject*)self); } @@ -390,6 +396,82 @@ PyObject* pysqlite_connection_backup(pysqlite_Connection* self, PyObject* args, } #endif +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) { PyObject* ret; @@ -401,6 +483,8 @@ PyObject* pysqlite_connection_close(pysqlite_Connection* self, PyObject* args) pysqlite_do_all_statements(self, ACTION_FINALIZE, 1); + pysqlite_close_all_blobs(self); + if (self->db) { if (self->apsw_connection) { ret = PyObject_CallMethod(self->apsw_connection, "close", ""); @@ -1597,6 +1681,8 @@ static PyMethodDef connection_methods[] = { {"backup", (PyCFunction)pysqlite_connection_backup, METH_VARARGS|METH_KEYWORDS, PyDoc_STR("Backup database.")}, #endif + {"blob", (PyCFunction)pysqlite_connection_blob, METH_VARARGS|METH_KEYWORDS, + PyDoc_STR("return a blob object")}, {"cursor", (PyCFunction)pysqlite_connection_cursor, METH_VARARGS|METH_KEYWORDS, PyDoc_STR("Return a cursor for the connection.")}, {"close", (PyCFunction)pysqlite_connection_close, METH_NOARGS, diff --git a/src/connection.h b/src/connection.h index 3fc5a70..a110ea2 100644 --- a/src/connection.h +++ b/src/connection.h @@ -70,9 +70,10 @@ typedef struct 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 --git a/src/module.c b/src/module.c index dbc015c..7a26498 100644 --- a/src/module.c +++ b/src/module.c @@ -28,6 +28,7 @@ #include "prepare_protocol.h" #include "microprotocols.h" #include "row.h" +#include "blob.h" #ifdef PYSQLITE_EXPERIMENTAL #include "backup.h" @@ -321,6 +322,7 @@ PyMODINIT_FUNC init_sqlite(void) #ifdef PYSQLITE_EXPERIMENTAL (pysqlite_backup_setup_types() < 0) || #endif + (pysqlite_blob_setup_types() < 0) || (pysqlite_prepare_protocol_setup_types() < 0) ) { return; |