From 2e14aeaace3debc722f380d31eaf07e1f823b211 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ege=20Emir=20=C3=96zkan?=
Date: Fri, 14 Aug 2020 11:45:39 +0300
Subject: [PATCH] Add basic migrations
---
datalite/__init__.py | 2 +-
datalite/commons.py | 54 ++++++-
datalite/datalite_decorator.py | 43 +-----
datalite/fetch.py | 20 +--
datalite/migrations.py | 160 +++++++++++++++++++++
docs/_build/doctrees/datalite.doctree | Bin 70231 -> 77321 bytes
docs/_build/doctrees/environment.pickle | Bin 15617 -> 15317 bytes
docs/_build/html/_sources/datalite.rst.txt | 6 +
docs/_build/html/datalite.html | 55 ++++---
docs/_build/html/genindex.html | 24 +++-
docs/_build/html/index.html | 1 +
docs/_build/html/objects.inv | 4 +-
docs/_build/html/py-modindex.html | 5 +
docs/_build/html/searchindex.js | 2 +-
docs/conf.py | 2 +-
docs/datalite.rst | 6 +
docs/modules.rst | 7 -
setup.py | 2 +-
test/main_tests.py | 33 +++++
19 files changed, 339 insertions(+), 87 deletions(-)
create mode 100644 datalite/migrations.py
delete mode 100644 docs/modules.rst
diff --git a/datalite/__init__.py b/datalite/__init__.py
index c6ab2f5..a45fd71 100644
--- a/datalite/__init__.py
+++ b/datalite/__init__.py
@@ -1,3 +1,3 @@
-__all__ = ['commons', 'datalite_decorator', 'fetch', 'datalite']
+__all__ = ['commons', 'datalite_decorator', 'fetch', 'migrations', 'datalite']
from .datalite_decorator import datalite
diff --git a/datalite/commons.py b/datalite/commons.py
index bc04e43..2876db5 100644
--- a/datalite/commons.py
+++ b/datalite/commons.py
@@ -1,4 +1,6 @@
-from typing import Any, Optional, Dict
+from dataclasses import Field
+from typing import Any, Optional, Dict, List
+import sqlite3 as sql
def _convert_type(type_: Optional[type], type_overload: Dict[Optional[type], str]) -> str:
@@ -33,4 +35,52 @@ def _convert_sql_format(value: Any) -> str:
elif isinstance(value, bytes):
return '"' + str(value).replace("b'", "")[:-1] + '"'
else:
- return str(value)
\ No newline at end of file
+ return str(value)
+
+
+def _get_table_cols(cur: sql.Cursor, table_name: str) -> List[str]:
+ """
+ Get the column data of a table.
+
+ :param cur: Cursor in database.
+ :param table_name: Name of the table.
+ :return: the information about columns.
+ """
+ cur.execute(f"PRAGMA table_info({table_name});")
+ return [row_info[1] for row_info in cur.fetchall()][1:]
+
+
+def _get_default(default_object: object, type_overload: Dict[Optional[type], str]) -> str:
+ """
+ Check if the field's default object is filled,
+ if filled return the string to be put in the,
+ database.
+ :param default_object: The default field of the field.
+ :param type_overload: Type overload table.
+ :return: The string to be put on the table statement,
+ empty string if no string is necessary.
+ """
+ if type(default_object) in type_overload:
+ return f' DEFAULT {_convert_sql_format(default_object)}'
+ return ""
+
+
+def _create_table(class_: type, cursor: sql.Cursor, type_overload: Dict[Optional[type], str]) -> None:
+ """
+ Create the table for a specific dataclass given
+ :param class_: A dataclass.
+ :param cursor: Current cursor instance.
+ :param type_overload: Overload the Python -> SQLDatatype table
+ with a custom table, this is that custom table.
+ :return: None.
+ """
+ fields: List[Field] = [class_.__dataclass_fields__[key] for
+ key in class_.__dataclass_fields__.keys()]
+ fields.sort(key=lambda field: field.name) # Since dictionaries *may* be unsorted.
+ sql_fields = ', '.join(f"{field.name} {_convert_type(field.type, type_overload)}"
+ f"{_get_default(field.default, type_overload)}" for field in fields)
+ 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"}
diff --git a/datalite/datalite_decorator.py b/datalite/datalite_decorator.py
index b3d5bad..b9c69c1 100644
--- a/datalite/datalite_decorator.py
+++ b/datalite/datalite_decorator.py
@@ -6,40 +6,7 @@ a class bound to a sqlite3 database.
from typing import Dict, Optional, List, Callable
from dataclasses import Field, asdict
import sqlite3 as sql
-from .commons import _convert_sql_format, _convert_type
-
-
-def _get_default(default_object: object, type_overload: Dict[Optional[type], str]) -> str:
- """
- Check if the field's default object is filled,
- if filled return the string to be put in the,
- database.
- :param default_object: The default field of the field.
- :param type_overload: Type overload table.
- :return: The string to be put on the table statement,
- empty string if no string is necessary.
- """
- if type(default_object) in type_overload:
- return f' DEFAULT {_convert_sql_format(default_object)}'
- return ""
-
-
-def _create_table(class_: type, cursor: sql.Cursor, type_overload: Dict[Optional[type], str]) -> None:
- """
- Create the table for a specific dataclass given
- :param class_: A dataclass.
- :param cursor: Current cursor instance.
- :param type_overload: Overload the Python -> SQLDatatype table
- with a custom table, this is that custom table.
- :return: None.
- """
- fields: List[Field] = [class_.__dataclass_fields__[key] for
- key in class_.__dataclass_fields__.keys()]
- fields.sort(key=lambda field: field.name) # Since dictionaries *may* be unsorted.
- sql_fields = ', '.join(f"{field.name} {_convert_type(field.type, type_overload)}"
- f"{_get_default(field.default, type_overload)}" for field in fields)
- sql_fields = "obj_id INTEGER PRIMARY KEY AUTOINCREMENT, " + sql_fields
- cursor.execute(f"CREATE TABLE IF NOT EXISTS {class_.__name__.lower()} ({sql_fields});")
+from .commons import _convert_sql_format, _convert_type, _create_table, type_table
def _create_entry(self) -> None:
@@ -105,14 +72,14 @@ def datalite(db_path: str, type_overload: Optional[Dict[Optional[type], str]] =
:return: The new dataclass.
"""
def decorator(dataclass_: type, *args_i, **kwargs_i):
- type_table: Dict[Optional[type], str] = {None: "NULL", int: "INTEGER", float: "REAL",
- str: "TEXT", bytes: "BLOB"}
+ types_table = type_table.copy()
if type_overload is not None:
- type_table.update(type_overload)
+ types_table.update(type_overload)
with sql.connect(db_path) as con:
cur: sql.Cursor = con.cursor()
- _create_table(dataclass_, cur, type_table)
+ _create_table(dataclass_, cur, types_table)
setattr(dataclass_, 'db_path', db_path) # We add the path of the database to class itself.
+ setattr(dataclass_, 'types_table', types_table) # We add the type table for migration.
dataclass_.create_entry = _create_entry
dataclass_.remove_entry = _remove_entry
dataclass_.update_entry = _update_entry
diff --git a/datalite/fetch.py b/datalite/fetch.py
index 1b9d164..c39f100 100644
--- a/datalite/fetch.py
+++ b/datalite/fetch.py
@@ -1,6 +1,6 @@
import sqlite3 as sql
from typing import List, Tuple, Any
-from .commons import _convert_sql_format
+from .commons import _convert_sql_format, _get_table_cols
def _insert_pagination(query: str, page: int, element_count: int) -> str:
@@ -35,18 +35,6 @@ def is_fetchable(class_: type, obj_id: int) -> bool:
return bool(cur.fetchall())
-def _get_table_cols(cur: sql.Cursor, table_name: str) -> List[str]:
- """
- Get the column data of a table.
-
- :param cur: Cursor in database.
- :param table_name: Name of the table.
- :return: the information about columns.
- """
- cur.execute(f"PRAGMA table_info({table_name});")
- return [row_info[1] for row_info in cur.fetchall()][1:]
-
-
def fetch_equals(class_: type, field: str, value: Any, ) -> Any:
"""
Fetch a class_ type variable from its bound db.
@@ -112,7 +100,7 @@ def fetch_if(class_: type, condition: str, page: int = 0, element_count: int = 1
:param page: Which page to retrieve, default all. (0 means closed).
:param element_count: Element count in each page.
:return: A tuple of records that fit the given condition
- of given type class_.
+ of given type class_.
"""
table_name = class_.__name__.lower()
with sql.connect(getattr(class_, 'db_path')) as con:
@@ -146,7 +134,7 @@ def fetch_range(class_: type, range_: range) -> tuple:
:param class_: Class of the records.
:param range_: Range of the object ids.
:return: A tuple of class_ type objects whose values
- come from the class_' bound database.
+ come from the class_' bound database.
"""
return tuple(fetch_from(class_, obj_id) for obj_id in range_ if is_fetchable(class_, obj_id))
@@ -159,7 +147,7 @@ def fetch_all(class_: type, page: int = 0, element_count: int = 10) -> tuple:
:param page: Which page to retrieve, default all. (0 means closed).
:param element_count: Element count in each page.
:return: All the records of type class_ in
- the bound database as a tuple.
+ the bound database as a tuple.
"""
try:
db_path = getattr(class_, 'db_path')
diff --git a/datalite/migrations.py b/datalite/migrations.py
new file mode 100644
index 0000000..c766985
--- /dev/null
+++ b/datalite/migrations.py
@@ -0,0 +1,160 @@
+"""
+Migrations module deals with migrating data when the object
+definitions change. This functions deal with Schema Migrations.
+"""
+from dataclasses import Field
+from os.path import exists
+from typing import Dict, Tuple, List
+import sqlite3 as sql
+
+from .commons import _create_table, _get_table_cols
+
+
+def _get_db_table(class_: type) -> Tuple[str, str]:
+ """
+ Check if the class is a datalite class, the database exists
+ and the table exists. Return database and table names.
+
+ :param class_: A datalite class.
+ :return: A tuple of database and table names.
+ """
+ database_name: str = getattr(class_, 'db_path', None)
+ if not database_name:
+ raise TypeError(f"{class_.__name__} is not a datalite class.")
+ table_name: str = class_.__name__.lower()
+ if not exists(database_name):
+ raise FileNotFoundError(f"{database_name} does not exist")
+ with sql.connect(database_name) as con:
+ cur: sql.Cursor = con.cursor()
+ cur.execute(f"SELECT count(*) FROM sqlite_master "
+ f"WHERE type='table' AND name='{table_name}';")
+ count: int = int(cur.fetchone()[0])
+ if not count:
+ raise FileExistsError(f"Table, {table_name}, already exists.")
+ return database_name, table_name
+
+
+def _get_table_column_names(database_name: str, table_name: str) -> Tuple[str]:
+ """
+ Get the column names of table.
+
+ :param database_name: Name of the database the table
+ resides in.
+ :param table_name: Name of the table.
+ :return: A tuple holding the column names of the table.
+ """
+ with sql.connect(database_name) as con:
+ cur: sql.Cursor = con.cursor()
+ cols: List[str] = _get_table_cols(cur, table_name)
+ return tuple(cols)
+
+
+def _copy_records(database_name: str, table_name: str):
+ """
+ Copy all records from a table.
+
+ :param database_name: Name of the database.
+ :param table_name: Name of the table.
+ :return: A generator holding dataclass asdict representations.
+ """
+ with sql.connect(database_name) as con:
+ cur: sql.Cursor = con.cursor()
+ cur.execute(f'SELECT * FROM {table_name};')
+ values = cur.fetchall()
+ keys = _get_table_cols(cur, table_name)
+ keys.insert(0, 'obj_id')
+ records = (dict(zip(keys, value)) for value in values)
+ return records
+
+
+def _drop_table(database_name: str, table_name: str) -> None:
+ """
+ Drop a table.
+
+ :param database_name: Name of the database.
+ :param table_name: Name of the table to be dropped.
+ :return: None.
+ """
+ with sql.connect(database_name) as con:
+ cur: sql.Cursor = con.cursor()
+ cur.execute(f'DROP TABLE {table_name};')
+ con.commit()
+
+
+def _modify_records(data, col_to_del: Tuple[str], col_to_add: Tuple[str],
+ flow: Dict[str, str]) -> Tuple[Dict[str, str]]:
+ """
+ Modify the asdict records in accordance
+ with schema migration rules provided.
+
+ :param data: Data kept as asdict in tuple.
+ :param col_to_del: Column names to delete.
+ :param col_to_add: Column names to add.
+ :param flow: A dictionary that explain
+ if the data from a deleted column
+ will be transferred to a column
+ to be added.
+ :return: The modified data records.
+ """
+ records = []
+ for record in data:
+ record_mod = {}
+ for key in record.keys():
+ if key in col_to_del and key in flow:
+ record_mod[flow[key]] = record[key]
+ else:
+ record_mod[key] = record[key]
+ for key_to_add in col_to_add:
+ if key_to_add not in record_mod:
+ record_mod[key_to_add] = None
+ records.append(record_mod)
+ return records
+
+
+def _migrate_records(class_: type, database_name: str, data,
+ col_to_del: Tuple[str], col_to_add: Tuple[str], flow: Dict[str, str]) -> None:
+ """
+ Migrate the records into the modified table.
+
+ :param class_: Class of the entries.
+ :param database_name: Name of the database.
+ :param data: Data, asdict tuple.
+ :param col_to_del: Columns to be deleted.
+ :param col_to_add: Columns to be added.
+ :param flow: Flow dictionary stating where
+ column data will be transferred.
+ :return: None.
+ """
+ with sql.connect(database_name) as con:
+ cur: sql.Cursor = con.cursor()
+ _create_table(class_, cur, getattr(class_, 'types_table'))
+ con.commit()
+ new_records = _modify_records(data, col_to_del, col_to_add, flow)
+ for record in new_records:
+ del record['obj_id']
+ class_(**record).create_entry()
+
+
+def basic_migrate(class_: type, column_transfer: dict = None) -> None:
+ """
+ Given a class, compare its previous table,
+ delete the fields that no longer exist,
+ create new columns for new fields. If the
+ column_flow parameter is given, migrate elements
+ from previous column to the new ones.
+
+ :param class_: Datalite class to migrate.
+ :param column_transfer: A dictionary showing which
+ columns will be copied to new ones.
+ :return: None.
+ """
+ database_name, table_name = _get_db_table(class_)
+ table_column_names: Tuple[str] = _get_table_column_names(database_name, table_name)
+ values = class_.__dataclass_fields__.values()
+ data_fields: Tuple[Field] = tuple(field for field in values)
+ data_field_names: Tuple[str] = tuple(field.name for field in data_fields)
+ columns_to_be_deleted: Tuple[str] = tuple(column for column in table_column_names if column not in data_field_names)
+ columns_to_be_added: Tuple[str] = tuple(column for column in data_field_names if column not in table_column_names)
+ records = _copy_records(database_name, table_name)
+ _drop_table(database_name, table_name)
+ _migrate_records(class_, database_name, records, columns_to_be_deleted, columns_to_be_added, column_transfer)
diff --git a/docs/_build/doctrees/datalite.doctree b/docs/_build/doctrees/datalite.doctree
index e83131f57311cd1541765b6adea3869a33ba6e97..c5e0eea6fb4bb6a403d8e8303827bae3b6159405 100644
GIT binary patch
literal 77321
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
Given a class, compare its previous table,
+delete the fields that no longer exist,
+create new columns for new fields. If the
+column_flow parameter is given, migrate elements
+from previous column to the new ones.
+
+
Parameters
+
+
class – Datalite class to migrate.
+
column_transfer – A dictionary showing which
+columns will be copied to new ones.
diff --git a/docs/_build/html/objects.inv b/docs/_build/html/objects.inv
index dc92aca..91427c5 100644
--- a/docs/_build/html/objects.inv
+++ b/docs/_build/html/objects.inv
@@ -2,4 +2,6 @@
# Project: Datalite
# Version:
# The remainder of this file is compressed using zlib.
-xڝN0w?!XSڙcu/ncGxC~6윎;Q^ձ(ԷRRu7tLp{fwerXƖo\Ƶ*Z^ڰ=Y8s_"Ua'hTzH:X z gא|@=J%VzYc@ $f_`26
\ No newline at end of file
+xڝ=N0bݚi%
+hbOڎ z8 y<%F*R!YY.h
ٷRSm';Mp{af9Z+&갍3HUo<چ-ը
+X`WF5-,*J,?B;.X$l(k*+mD4V4=2[l'g~J|08;C6ʆv=8c;~Qg kdatalite.fetch