Skip to content

Commit bb9cc12

Browse files
committed
[feat] add support for the blob interface
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
1 parent a55df58 commit bb9cc12

File tree

10 files changed

+812
-5
lines changed

10 files changed

+812
-5
lines changed

doc/examples/blob.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from pysqlcipher import dbapi2 as sqlite3
2+
con = sqlite3.connect(":memory:")
3+
# creating the table
4+
con.execute("create table test(id integer primary key, blob_col blob)")
5+
con.execute("insert into test(blob_col) values (zeroblob(10))")
6+
# opening blob handle
7+
blob = con.blob("test", "blob_col", 1, 1)
8+
blob.write("a" * 5)
9+
blob.write("b" * 5)
10+
blob.seek(0)
11+
print blob.read() # will print "aaaaabbbbb"
12+
blob.close()
13+

doc/examples/blob_with.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from pysqlcipher import dbapi2 as sqlite3
2+
con = sqlite3.connect(":memory:")
3+
# creating the table
4+
con.execute("create table test(id integer primary key, blob_col blob)")
5+
con.execute("insert into test(blob_col) values (zeroblob(10))")
6+
# opening blob handle
7+
with con.blob("test", "blob_col", 1, 1) as blob:
8+
blob.write("a" * 5)
9+
blob.write("b" * 5)
10+
blob.seek(0)
11+
print blob.read() # will print "aaaaabbbbb"
12+

doc/sphinx/sqlcipher.rst

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,13 @@ Connection Objects
236236
:class:`sqlite3.Cursor`.
237237

238238

239+
.. method:: Connection.blob(table, column, row, flags=0, dbname="main")
240+
241+
On success a :class:`Blob` handle to the blob located in row 'row',
242+
column 'column', table 'table' in database 'dbname' will be returned.
243+
The flags represent the blob mode. 0 for read-only otherwise read-write.
244+
245+
239246
.. method:: Connection.commit()
240247

241248
This method commits the current transaction. If you don't call this method,
@@ -631,6 +638,60 @@ Now we plug :class:`Row` in::
631638
35.14
632639

633640

641+
.. _sqlite3-blob-objects:
642+
643+
Blob Objects
644+
--------------
645+
646+
.. class:: Blob
647+
648+
A :class:`Blob` instance has the following attributes and methods:
649+
650+
A SQLite database blob has the following attributes and methods:
651+
652+
.. method:: Blob.close()
653+
654+
Close the blob now (rather than whenever __del__ is called).
655+
656+
The blob will be unusable from this point forward; an Error (or subclass)
657+
exception will be raised if any operation is attempted with the blob.
658+
659+
.. method:: Blob.length()
660+
661+
Return the blob size.
662+
663+
.. method:: Blob.read([length])
664+
665+
Read lnegth bytes of data from the blob at the current offset position. If the
666+
end of the blob is reached we will return the data up to end of file. When
667+
length is not specified or negative we will read up to end of blob.
668+
669+
.. method:: Blob.write(data)
670+
671+
Write data to the blob at the current offset. This function cannot changed blob
672+
length. If data write will result in writing to more then blob current size an
673+
error will be raised.
674+
675+
.. method:: Blob.tell()
676+
677+
Return the current offset of the blob.
678+
679+
.. method:: Blob.seek(offset, [whence])
680+
681+
Set the blob offset. The whence argument is optional and defaults to os.SEEK_SET
682+
or 0 (absolute blob positioning); other values are os.SEEK_CUR or 1 (seek
683+
relative to the current position) and os.SEEK_END or 2 (seek relative to the blob’s end).
684+
685+
686+
:class:`Blob` example:
687+
688+
.. literalinclude:: ../includes/sqlite3/blob.py
689+
690+
A :class:`Blob` can also be used with context manager:
691+
692+
.. literalinclude:: ../includes/sqlite3/blob_with.py
693+
694+
634695
.. _sqlite3-types:
635696

636697
SQLite and Python types

lib/test/dbapi.py

Lines changed: 263 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,155 @@ class Foo: pass
465465
except TypeError:
466466
pass
467467

468+
469+
class BlobTests(unittest.TestCase):
470+
def setUp(self):
471+
self.cx = sqlite.connect(":memory:")
472+
self.cx.execute("create table test(id integer primary key, blob_col blob)")
473+
self.blob_data = "a" * 100
474+
self.cx.execute("insert into test(blob_col) values (?)", (self.blob_data, ))
475+
self.blob = self.cx.blob("test", "blob_col", 1, 1)
476+
self.second_data = "b" * 100
477+
478+
def tearDown(self):
479+
self.blob.close()
480+
self.cx.close()
481+
482+
def CheckLength(self):
483+
self.assertEqual(self.blob.length(), 100)
484+
485+
def CheckTell(self):
486+
self.assertEqual(self.blob.tell(), 0)
487+
488+
def CheckSeekFromBlobStart(self):
489+
self.blob.seek(10)
490+
self.assertEqual(self.blob.tell(), 10)
491+
self.blob.seek(10, 0)
492+
self.assertEqual(self.blob.tell(), 10)
493+
494+
def CheckSeekFromCurrentPosition(self):
495+
self.blob.seek(10,1)
496+
self.blob.seek(10,1)
497+
self.assertEqual(self.blob.tell(), 20)
498+
499+
def CheckSeekFromBlobEnd(self):
500+
self.blob.seek(-10,2)
501+
self.assertEqual(self.blob.tell(), 90)
502+
503+
def CheckBlobSeekOverBlobSize(self):
504+
try:
505+
self.blob.seek(1000)
506+
self.fail("should have raised a ValueError")
507+
except ValueError:
508+
pass
509+
except Exception:
510+
self.fail("should have raised a ValueError")
511+
512+
def CheckBlobSeekUnderBlobSize(self):
513+
try:
514+
self.blob.seek(-10)
515+
self.fail("should have raised a ValueError")
516+
except ValueError:
517+
pass
518+
except Exception:
519+
self.fail("should have raised a ValueError")
520+
521+
def CheckBlobRead(self):
522+
self.assertEqual(self.blob.read(), self.blob_data)
523+
524+
def CheckBlobReadSize(self):
525+
self.assertEqual(len(self.blob.read(10)), 10)
526+
527+
def CheckBlobReadAdvanceOffset(self):
528+
self.blob.read(10)
529+
self.assertEqual(self.blob.tell(), 10)
530+
531+
def CheckBlobReadStartAtOffset(self):
532+
self.blob.seek(10)
533+
self.blob.write(self.second_data[:10])
534+
self.blob.seek(10)
535+
self.assertEqual(self.blob.read(10), self.second_data[:10])
536+
537+
def CheckBlobWrite(self):
538+
self.blob.write(self.second_data)
539+
self.assertEqual(str(self.cx.execute("select blob_col from test").fetchone()[0]), self.second_data)
540+
541+
def CheckBlobWriteAtOffset(self):
542+
self.blob.seek(50)
543+
self.blob.write(self.second_data[:50])
544+
self.assertEqual(str(self.cx.execute("select blob_col from test").fetchone()[0]),
545+
self.blob_data[:50] + self.second_data[:50])
546+
547+
def CheckBlobWriteAdvanceOffset(self):
548+
self.blob.write(self.second_data[:50])
549+
self.assertEqual(self.blob.tell(), 50)
550+
551+
def CheckBlobWriteMoreThenBlobSize(self):
552+
try:
553+
self.blob.write("a" * 1000)
554+
self.fail("should have raised a sqlite.OperationalError")
555+
except sqlite.OperationalError:
556+
pass
557+
except Exception:
558+
self.fail("should have raised a sqlite.OperationalError")
559+
560+
def CheckBlobReadAfterRowChange(self):
561+
self.cx.execute("UPDATE test SET blob_col='aaaa' where id=1")
562+
try:
563+
self.blob.read()
564+
self.fail("should have raised a sqlite.OperationalError")
565+
except sqlite.OperationalError:
566+
pass
567+
except Exception:
568+
self.fail("should have raised a sqlite.OperationalError")
569+
570+
def CheckBlobWriteAfterRowChange(self):
571+
self.cx.execute("UPDATE test SET blob_col='aaaa' where id=1")
572+
try:
573+
self.blob.write("aaa")
574+
self.fail("should have raised a sqlite.OperationalError")
575+
except sqlite.OperationalError:
576+
pass
577+
except Exception:
578+
self.fail("should have raised a sqlite.OperationalError")
579+
580+
def CheckBlobOpenWithBadDb(self):
581+
try:
582+
self.cx.blob("test", "blob_col", 1, 1, dbname="notexisintg")
583+
self.fail("should have raised a sqlite.OperationalError")
584+
except sqlite.OperationalError:
585+
pass
586+
except Exception:
587+
self.fail("should have raised a sqlite.OperationalError")
588+
589+
def CheckBlobOpenWithBadTable(self):
590+
try:
591+
self.cx.blob("notexisintg", "blob_col", 1, 1)
592+
self.fail("should have raised a sqlite.OperationalError")
593+
except sqlite.OperationalError:
594+
pass
595+
except Exception:
596+
self.fail("should have raised a sqlite.OperationalError")
597+
598+
def CheckBlobOpenWithBadColumn(self):
599+
try:
600+
self.cx.blob("test", "notexisting", 1, 1)
601+
self.fail("should have raised a sqlite.OperationalError")
602+
except sqlite.OperationalError:
603+
pass
604+
except Exception:
605+
self.fail("should have raised a sqlite.OperationalError")
606+
607+
def CheckBlobOpenWithBadRow(self):
608+
try:
609+
self.cx.blob("test", "blob_col", 2, 1)
610+
self.fail("should have raised a sqlite.OperationalError")
611+
except sqlite.OperationalError:
612+
pass
613+
except Exception:
614+
self.fail("should have raised a sqlite.OperationalError")
615+
616+
468617
class ThreadTests(unittest.TestCase):
469618
def setUp(self):
470619
self.con = sqlite.connect(":memory:")
@@ -763,6 +912,20 @@ def CheckClosedCurExecute(self):
763912
except:
764913
self.fail("Should have raised a ProgrammingError")
765914

915+
def CheckClosedBlobRead(self):
916+
con = sqlite.connect(":memory:")
917+
con.execute("create table test(id integer primary key, blob_col blob)")
918+
con.execute("insert into test(blob_col) values (zeroblob(100))")
919+
blob = con.blob("test", "blob_col", 1)
920+
con.close()
921+
try:
922+
blob.read()
923+
self.fail("Should have raised a ProgrammingError")
924+
except sqlite.ProgrammingError:
925+
pass
926+
except:
927+
self.fail("Should have raised a ProgrammingError")
928+
766929
def CheckClosedCreateFunction(self):
767930
con = sqlite.connect(":memory:")
768931
con.close()
@@ -859,16 +1022,115 @@ def CheckClosed(self):
8591022
except:
8601023
self.fail("Should have raised a ProgrammingError: " + method_name)
8611024

1025+
1026+
class ClosedBlobTests(unittest.TestCase):
1027+
def setUp(self):
1028+
self.cx = sqlite.connect(":memory:")
1029+
self.cx.execute("create table test(id integer primary key, blob_col blob)")
1030+
self.cx.execute("insert into test(blob_col) values (zeroblob(100))")
1031+
1032+
def tearDown(self):
1033+
self.cx.close()
1034+
1035+
def CheckClosedRead(self):
1036+
self.blob = self.cx.blob("test", "blob_col", 1)
1037+
self.blob.close()
1038+
try:
1039+
self.blob.read()
1040+
self.fail("Should have raised a ProgrammingError")
1041+
except sqlite.ProgrammingError:
1042+
pass
1043+
except Exception:
1044+
self.fail("Should have raised a ProgrammingError")
1045+
1046+
def CheckClosedWrite(self):
1047+
self.blob = self.cx.blob("test", "blob_col", 1)
1048+
self.blob.close()
1049+
try:
1050+
self.blob.write("aaaaaaaaa")
1051+
self.fail("Should have raised a ProgrammingError")
1052+
except sqlite.ProgrammingError:
1053+
pass
1054+
except Exception:
1055+
self.fail("Should have raised a ProgrammingError")
1056+
1057+
def CheckClosedSeek(self):
1058+
self.blob = self.cx.blob("test", "blob_col", 1)
1059+
self.blob.close()
1060+
try:
1061+
self.blob.seek(10)
1062+
self.fail("Should have raised a ProgrammingError")
1063+
except sqlite.ProgrammingError:
1064+
pass
1065+
except Exception:
1066+
self.fail("Should have raised a ProgrammingError")
1067+
1068+
def CheckClosedTell(self):
1069+
self.blob = self.cx.blob("test", "blob_col", 1)
1070+
self.blob.close()
1071+
try:
1072+
self.blob.tell()
1073+
self.fail("Should have raised a ProgrammingError")
1074+
except sqlite.ProgrammingError:
1075+
pass
1076+
except Exception:
1077+
self.fail("Should have raised a ProgrammingError")
1078+
1079+
def CheckClosedClose(self):
1080+
self.blob = self.cx.blob("test", "blob_col", 1)
1081+
self.blob.close()
1082+
try:
1083+
self.blob.close()
1084+
self.fail("Should have raised a ProgrammingError")
1085+
except sqlite.ProgrammingError:
1086+
pass
1087+
except Exception:
1088+
self.fail("Should have raised a ProgrammingError")
1089+
1090+
1091+
class BlobContextManagerTests(unittest.TestCase):
1092+
def setUp(self):
1093+
self.cx = sqlite.connect(":memory:")
1094+
self.cx.execute("create table test(id integer primary key, blob_col blob)")
1095+
self.cx.execute("insert into test(blob_col) values (zeroblob(100))")
1096+
1097+
def tearDown(self):
1098+
self.cx.close()
1099+
1100+
def CheckContextExecute(self):
1101+
data = "a" * 100
1102+
with self.cx.blob("test", "blob_col", 1, 1) as blob:
1103+
blob.write("a" * 100)
1104+
self.assertEqual(str(self.cx.execute("select blob_col from test").fetchone()[0]), data)
1105+
1106+
def CheckContextCloseBlob(self):
1107+
with self.cx.blob("test", "blob_col", 1) as blob:
1108+
blob.seek(10)
1109+
try:
1110+
blob.close()
1111+
self.fail("Should have raised a ProgrammingError")
1112+
except sqlite.ProgrammingError:
1113+
pass
1114+
except Exception:
1115+
self.fail("Should have raised a ProgrammingError")
1116+
1117+
1118+
8621119
def suite():
8631120
module_suite = unittest.makeSuite(ModuleTests, "Check")
8641121
connection_suite = unittest.makeSuite(ConnectionTests, "Check")
8651122
cursor_suite = unittest.makeSuite(CursorTests, "Check")
1123+
blob_suite = unittest.makeSuite(BlobTests, "Check")
8661124
thread_suite = unittest.makeSuite(ThreadTests, "Check")
8671125
constructor_suite = unittest.makeSuite(ConstructorTests, "Check")
8681126
ext_suite = unittest.makeSuite(ExtensionTests, "Check")
8691127
closed_con_suite = unittest.makeSuite(ClosedConTests, "Check")
8701128
closed_cur_suite = unittest.makeSuite(ClosedCurTests, "Check")
871-
return unittest.TestSuite((module_suite, connection_suite, cursor_suite, thread_suite, constructor_suite, ext_suite, closed_con_suite, closed_cur_suite))
1129+
closed_blob_suite = unittest.makeSuite(ClosedBlobTests, "Check")
1130+
blob_context_manager_suite = unittest.makeSuite(BlobContextManagerTests, "Check")
1131+
return unittest.TestSuite((module_suite, connection_suite, cursor_suite, blob_suite, thread_suite,
1132+
constructor_suite, ext_suite, closed_con_suite, closed_cur_suite, closed_blob_suite,
1133+
blob_context_manager_suite, context_suite))
8721134

8731135
def test():
8741136
runner = unittest.TextTestRunner()

0 commit comments

Comments
 (0)