Add unique constraint, None option
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -6,6 +6,14 @@ datalite Module
|
||||
|
||||
.. autodecorator:: datalite.datalite
|
||||
|
||||
datalite.constraints module
|
||||
----------------------------------
|
||||
|
||||
.. automodule:: datalite.constraints
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritence:
|
||||
|
||||
datalite.fetch module
|
||||
---------------------
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ Documentation
|
||||
|
||||
installation
|
||||
decorator
|
||||
constraints
|
||||
fetch
|
||||
migration
|
||||
datalite
|
||||
|
||||
@@ -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.
|
||||
2
setup.py
2
setup.py
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user