Add unique constraint, None option

This commit is contained in:
Ege Emir Özkan
2020-08-22 01:00:30 +03:00
parent 6f58590475
commit 768453ab39
10 changed files with 179 additions and 12 deletions

View File

@@ -1,3 +1,12 @@
__all__ = ['commons', 'datalite_decorator', 'fetch', 'migrations', 'datalite']
__all__ = ['commons', 'datalite_decorator', 'fetch', 'migrations', 'datalite', 'constraints']
from dataclasses import dataclass
from .datalite_decorator import datalite
@dataclass
class Student:
id_: int
name: str

View File

@@ -1,8 +1,14 @@
from dataclasses import Field
from typing import Any, Optional, Dict, List
from .constraints import Unique
import sqlite3 as sql
type_table: Dict[Optional[type], str] = {None: "NULL", int: "INTEGER", float: "REAL",
str: "TEXT", bytes: "BLOB"}
type_table.update({Unique[key]: f"{value} NOT NULL UNIQUE" for key, value in type_table.items()})
def _convert_type(type_: Optional[type], type_overload: Dict[Optional[type], str]) -> str:
"""
Given a Python type, return the str name of its
@@ -30,7 +36,9 @@ def _convert_sql_format(value: Any) -> str:
>>> _convert_sql_format("John Smith")
'"John Smith"'
"""
if isinstance(value, str):
if value is None:
return "NULL"
elif isinstance(value, str):
return f'"{value}"'
elif isinstance(value, bytes):
return '"' + str(value).replace("b'", "")[:-1] + '"'
@@ -82,5 +90,5 @@ def _create_table(class_: type, cursor: sql.Cursor, type_overload: Dict[Optional
sql_fields = "obj_id INTEGER PRIMARY KEY AUTOINCREMENT, " + sql_fields
cursor.execute(f"CREATE TABLE IF NOT EXISTS {class_.__name__.lower()} ({sql_fields});")
type_table: Dict[Optional[type], str] = {None: "NULL", int: "INTEGER", float: "REAL",
str: "TEXT", bytes: "BLOB"}

View File

@@ -0,0 +1,26 @@
"""
datalite.constraints module introduces constraint
types that can be used to hint field variables,
that can be used to signal datalite decorator
constraints in the database.
"""
from typing import TypeVar, Union, Tuple
T = TypeVar('T')
class ConstraintFailedError(Exception):
"""
This exception is raised when a Constraint fails.
"""
pass
"""
Dataclass fields hinted with this type signals
datalite that the bound column of this
field in the table is NOT NULL and UNIQUE.
"""
Unique = Union[Tuple[T], T]

View File

@@ -2,10 +2,12 @@
Defines the Datalite decorator that can be used to convert a dataclass to
a class bound to a sqlite3 database.
"""
from sqlite3.dbapi2 import IntegrityError
from typing import Dict, Optional, List, Callable
from dataclasses import Field, asdict
import sqlite3 as sql
from constraints import ConstraintFailedError
from .commons import _convert_sql_format, _convert_type, _create_table, type_table
@@ -22,11 +24,14 @@ def _create_entry(self) -> None:
table_name: str = self.__class__.__name__.lower()
kv_pairs = [item for item in asdict(self).items()]
kv_pairs.sort(key=lambda item: item[0]) # Sort by the name of the fields.
cur.execute(f"INSERT INTO {table_name}("
f"{', '.join(item[0] for item in kv_pairs)})"
f" VALUES ({', '.join(_convert_sql_format(item[1]) for item in kv_pairs)});")
self.__setattr__("obj_id", cur.lastrowid)
con.commit()
try:
cur.execute(f"INSERT INTO {table_name}("
f"{', '.join(item[0] for item in kv_pairs)})"
f" VALUES ({', '.join(_convert_sql_format(item[1]) for item in kv_pairs)});")
self.__setattr__("obj_id", cur.lastrowid)
con.commit()
except IntegrityError:
raise ConstraintFailedError("A constraint has failed.")
def _update_entry(self) -> None:

View File

@@ -0,0 +1,67 @@
Constraints
================
One of the most useful features provided by SQLite is the concept of
*constraints*. Constraints signal the SQL engine that the values hold in a
specific column **MUST** abide by specific constraints, these might be
* Values of this column cannot be ``NULL``. (``NOT NULL``)
* Values of this column cannot be repeated. (``UNIQUE``)
* Values of this column must fulfill a condition. (``CHECK``)
* Values of this column can be used to identify a record. (``PRIMARY``)
* Values of this column has a default value. (``DEFAULT``)
Some of these constraints are already implemented in datalite. With all of the set,
is planned to be implemented in the future.
Default Values
---------------
Columns can be given default values. This is done the same way you would give a
datafield a default value.
.. code-block:: python
@datalite("db.db")
@dataclass
class Student:
id_: int
name: str = "Albert Einstein"
Therefore, from now on, any ``Student`` object, whose name is not specified, will
have the default name ``"Albert Einstein"`` and if ``.create_entry()`` method is
called on them, the newly inserted record will, by default, have this value in its
corresponding column.
Unique Values
--------------
Declaring a field unique is done by a special ``TypeVar`` called ``Unique``
this uniqueness check is done in the database level, this introduces has some pros,
but also some cons.
Pushing the uniqueness check to the database level introduces a better ability to
handle concurrency for applications with large traffic, however, this check only
runs when an item is registered, which means no problem will raise in
the object creation *even if* another object of the same type with the same value
hold in the unique field exists, no error will raise. However, if another *record*
with the same value in the unique field is recorded in the bound database, upon
the invocation of the ``.create_entry()`` will raise the ``ConstraintFailedError``
exception.
Uniqueness constraint is declared thusly:
.. code-block:: python
@datalite("db.db")
@dataclass
class Student:
id_: Unique[int]
name: str = "Albert Einstein"
Hinting a field with the ``Unique[T]`` type variable will introduce two rules:
#. The values in the column of the field in the table that represents the dataclass ``Student`` in the bound database ``db.db`` cannot be NULL, thus the corresponding field cannot be assigned ``None``.
#. These same values **must** be unique for each and every record.
Failure of any of these two rules will result in a ``ConstraintFailedError`` exception.

View File

@@ -6,6 +6,14 @@ datalite Module
.. autodecorator:: datalite.datalite
datalite.constraints module
----------------------------------
.. automodule:: datalite.constraints
:members:
:undoc-members:
:show-inheritence:
datalite.fetch module
---------------------

View File

@@ -19,6 +19,7 @@ Documentation
installation
decorator
constraints
fetch
migration
datalite

View File

@@ -4,3 +4,16 @@ Schema Migrations
Datalite provides a module, ``datalite.migrations`` that handles schema migrations. When a class
definition is modified, ``datalite.migrations.basic_migration`` can be called to automatically
transfer records to a table fitting the new definitions.
Let us say we have made changes to the fields of a dataclass called ``Student`` and now,
we want these changes to be made to the database. More specifically, we had a field called
``studentt_id`` and realised this was a typo, we want it to be named into ``student_id``,
and we want the values that was previously hold in this column to be persistent despite the
name change. We can achieve this easily by:
.. code-block:: python
datalite.basic_migration(Student, {'studentt_id': 'student_id'})
This will make all the changes, if we had not provided the second argument,
the values would be lost.

View File

@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
setuptools.setup(
name="datalite", # Replace with your own username
version="0.5.6",
version="0.5.7",
author="Ege Ozkan",
author_email="egeemirozkan24@gmail.com",
description="A small package that binds dataclasses to an sqlite database",

View File

@@ -1,5 +1,9 @@
import unittest
from typing import Final
from constraints import ConstraintFailedError
from datalite import datalite
from datalite.constraints import Unique
from datalite.fetch import fetch_if, fetch_all, fetch_range, fetch_from, fetch_equals, fetch_where
from sqlite3 import connect
from dataclasses import dataclass, asdict
@@ -40,10 +44,16 @@ class Migrate1:
@datalite(db_path='test.db')
@dataclass
class Migrate2:
cardinal: int
cardinal: Unique[int] = 1
str_: str = "default"
@datalite(db_path='test.db')
@dataclass
class ConstraintedClass:
unique_str: Unique[str]
def getValFromDB(obj_id = 1):
with connect('test.db') as db:
cur = db.cursor()
@@ -163,5 +173,25 @@ class DatabaseMigration(unittest.TestCase):
_drop_table('test.db', 'migrate1')
def helperFunc():
obj = ConstraintedClass("This string is supposed to be unique.")
obj.create_entry()
class DatabaseConstraints(unittest.TestCase):
def setUp(self) -> None:
self.obj = ConstraintedClass("This string is supposed to be unique.")
self.obj.create_entry()
def testUniquness(self):
self.assertRaises(ConstraintFailedError, helperFunc)
def testNullness(self):
self.assertRaises(ConstraintFailedError, lambda : ConstraintedClass(None).create_entry())
def tearDown(self) -> None:
self.obj.remove_entry()
if __name__ == '__main__':
unittest.main()