Skip to content

Commit

Permalink
[feat] add support for the blob interface
Browse files Browse the repository at this point in the history
Pysqlcipher support for the sqlite blob interface:
https://sqlite.org/c3ref/blob_open.html

Copying the code from the PR in pysqlite:
ghaering/pysqlite#93
  • Loading branch information
meskio committed Feb 2, 2017
1 parent a55df58 commit bb9cc12
Show file tree
Hide file tree
Showing 10 changed files with 812 additions and 5 deletions.
13 changes: 13 additions & 0 deletions doc/examples/blob.py
Original file line number Diff line number Diff line change
@@ -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()

12 changes: 12 additions & 0 deletions doc/examples/blob_with.py
Original file line number Diff line number Diff line change
@@ -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"

61 changes: 61 additions & 0 deletions doc/sphinx/sqlcipher.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
264 changes: 263 additions & 1 deletion lib/test/dbapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,155 @@ class Foo: pass
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:")
Expand Down Expand Up @@ -763,6 +912,20 @@ def CheckClosedCurExecute(self):
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()
Expand Down Expand Up @@ -859,16 +1022,115 @@ def CheckClosed(self):
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()
Expand Down

0 comments on commit bb9cc12

Please sign in to comment.