parent
2ddcbf6d7a
commit
0968f66e3f
@ -0,0 +1,48 @@
|
|||||||
|
## Playhouse
|
||||||
|
|
||||||
|
The `playhouse` namespace contains numerous extensions to Peewee. These include vendor-specific database extensions, high-level abstractions to simplify working with databases, and tools for low-level database operations and introspection.
|
||||||
|
|
||||||
|
### Vendor extensions
|
||||||
|
|
||||||
|
* [SQLite extensions](http://docs.peewee-orm.com/en/latest/peewee/sqlite_ext.html)
|
||||||
|
* Full-text search (FTS3/4/5)
|
||||||
|
* BM25 ranking algorithm implemented as SQLite C extension, backported to FTS4
|
||||||
|
* Virtual tables and C extensions
|
||||||
|
* Closure tables
|
||||||
|
* JSON extension support
|
||||||
|
* LSM1 (key/value database) support
|
||||||
|
* BLOB API
|
||||||
|
* Online backup API
|
||||||
|
* [APSW extensions](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#apsw): use Peewee with the powerful [APSW](https://github.com/rogerbinns/apsw) SQLite driver.
|
||||||
|
* [SQLCipher](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#sqlcipher-ext): encrypted SQLite databases.
|
||||||
|
* [SqliteQ](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#sqliteq): dedicated writer thread for multi-threaded SQLite applications. [More info here](http://charlesleifer.com/blog/multi-threaded-sqlite-without-the-operationalerrors/).
|
||||||
|
* [Postgresql extensions](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#postgres-ext)
|
||||||
|
* JSON and JSONB
|
||||||
|
* HStore
|
||||||
|
* Arrays
|
||||||
|
* Server-side cursors
|
||||||
|
* Full-text search
|
||||||
|
* [MySQL extensions](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#mysql-ext)
|
||||||
|
|
||||||
|
### High-level libraries
|
||||||
|
|
||||||
|
* [Extra fields](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#extra-fields)
|
||||||
|
* Compressed field
|
||||||
|
* PickleField
|
||||||
|
* [Shortcuts / helpers](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#shortcuts)
|
||||||
|
* Model-to-dict serializer
|
||||||
|
* Dict-to-model deserializer
|
||||||
|
* [Hybrid attributes](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#hybrid)
|
||||||
|
* [Signals](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#signals): pre/post-save, pre/post-delete, pre-init.
|
||||||
|
* [Dataset](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#dataset): high-level API for working with databases popuarlized by the [project of the same name](https://dataset.readthedocs.io/).
|
||||||
|
* [Key/Value Store](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#kv): key/value store using SQLite. Supports *smart indexing*, for *Pandas*-style queries.
|
||||||
|
|
||||||
|
### Database management and framework support
|
||||||
|
|
||||||
|
* [pwiz](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#pwiz): generate model code from a pre-existing database.
|
||||||
|
* [Schema migrations](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#migrate): modify your schema using high-level APIs. Even supports dropping or renaming columns in SQLite.
|
||||||
|
* [Connection pool](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#pool): simple connection pooling.
|
||||||
|
* [Reflection](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#reflection): low-level, cross-platform database introspection
|
||||||
|
* [Database URLs](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#db-url): use URLs to connect to database
|
||||||
|
* [Test utils](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#test-utils): helpers for unit-testing Peewee applications.
|
||||||
|
* [Flask utils](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#flask-utils): paginated object lists, database connection management, and more.
|
@ -0,0 +1,73 @@
|
|||||||
|
/* cache.h - definitions for the LRU cache
|
||||||
|
*
|
||||||
|
* Copyright (C) 2004-2015 Gerhard Häring <gh@ghaering.de>
|
||||||
|
*
|
||||||
|
* This file is part of pysqlite.
|
||||||
|
*
|
||||||
|
* This software is provided 'as-is', without any express or implied
|
||||||
|
* warranty. In no event will the authors be held liable for any damages
|
||||||
|
* arising from the use of this software.
|
||||||
|
*
|
||||||
|
* Permission is granted to anyone to use this software for any purpose,
|
||||||
|
* including commercial applications, and to alter it and redistribute it
|
||||||
|
* freely, subject to the following restrictions:
|
||||||
|
*
|
||||||
|
* 1. The origin of this software must not be misrepresented; you must not
|
||||||
|
* claim that you wrote the original software. If you use this software
|
||||||
|
* in a product, an acknowledgment in the product documentation would be
|
||||||
|
* appreciated but is not required.
|
||||||
|
* 2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
* misrepresented as being the original software.
|
||||||
|
* 3. This notice may not be removed or altered from any source distribution.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef PYSQLITE_CACHE_H
|
||||||
|
#define PYSQLITE_CACHE_H
|
||||||
|
#include "Python.h"
|
||||||
|
|
||||||
|
/* The LRU cache is implemented as a combination of a doubly-linked with a
|
||||||
|
* dictionary. The list items are of type 'Node' and the dictionary has the
|
||||||
|
* nodes as values. */
|
||||||
|
|
||||||
|
typedef struct _pysqlite_Node
|
||||||
|
{
|
||||||
|
PyObject_HEAD
|
||||||
|
PyObject* key;
|
||||||
|
PyObject* data;
|
||||||
|
long count;
|
||||||
|
struct _pysqlite_Node* prev;
|
||||||
|
struct _pysqlite_Node* next;
|
||||||
|
} pysqlite_Node;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
PyObject_HEAD
|
||||||
|
int size;
|
||||||
|
|
||||||
|
/* a dictionary mapping keys to Node entries */
|
||||||
|
PyObject* mapping;
|
||||||
|
|
||||||
|
/* the factory callable */
|
||||||
|
PyObject* factory;
|
||||||
|
|
||||||
|
pysqlite_Node* first;
|
||||||
|
pysqlite_Node* last;
|
||||||
|
|
||||||
|
/* if set, decrement the factory function when the Cache is deallocated.
|
||||||
|
* this is almost always desirable, but not in the pysqlite context */
|
||||||
|
int decref_factory;
|
||||||
|
} pysqlite_Cache;
|
||||||
|
|
||||||
|
extern PyTypeObject pysqlite_NodeType;
|
||||||
|
extern PyTypeObject pysqlite_CacheType;
|
||||||
|
|
||||||
|
int pysqlite_node_init(pysqlite_Node* self, PyObject* args, PyObject* kwargs);
|
||||||
|
void pysqlite_node_dealloc(pysqlite_Node* self);
|
||||||
|
|
||||||
|
int pysqlite_cache_init(pysqlite_Cache* self, PyObject* args, PyObject* kwargs);
|
||||||
|
void pysqlite_cache_dealloc(pysqlite_Cache* self);
|
||||||
|
PyObject* pysqlite_cache_get(pysqlite_Cache* self, PyObject* args);
|
||||||
|
|
||||||
|
int pysqlite_cache_setup_types(void);
|
||||||
|
|
||||||
|
#endif
|
@ -0,0 +1,129 @@
|
|||||||
|
/* connection.h - definitions for the connection type
|
||||||
|
*
|
||||||
|
* Copyright (C) 2004-2015 Gerhard Häring <gh@ghaering.de>
|
||||||
|
*
|
||||||
|
* This file is part of pysqlite.
|
||||||
|
*
|
||||||
|
* This software is provided 'as-is', without any express or implied
|
||||||
|
* warranty. In no event will the authors be held liable for any damages
|
||||||
|
* arising from the use of this software.
|
||||||
|
*
|
||||||
|
* Permission is granted to anyone to use this software for any purpose,
|
||||||
|
* including commercial applications, and to alter it and redistribute it
|
||||||
|
* freely, subject to the following restrictions:
|
||||||
|
*
|
||||||
|
* 1. The origin of this software must not be misrepresented; you must not
|
||||||
|
* claim that you wrote the original software. If you use this software
|
||||||
|
* in a product, an acknowledgment in the product documentation would be
|
||||||
|
* appreciated but is not required.
|
||||||
|
* 2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
* misrepresented as being the original software.
|
||||||
|
* 3. This notice may not be removed or altered from any source distribution.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef PYSQLITE_CONNECTION_H
|
||||||
|
#define PYSQLITE_CONNECTION_H
|
||||||
|
#include "Python.h"
|
||||||
|
#include "pythread.h"
|
||||||
|
#include "structmember.h"
|
||||||
|
|
||||||
|
#include "cache.h"
|
||||||
|
#include "module.h"
|
||||||
|
|
||||||
|
#include "sqlite3.h"
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
PyObject_HEAD
|
||||||
|
sqlite3* db;
|
||||||
|
|
||||||
|
/* the type detection mode. Only 0, PARSE_DECLTYPES, PARSE_COLNAMES or a
|
||||||
|
* bitwise combination thereof makes sense */
|
||||||
|
int detect_types;
|
||||||
|
|
||||||
|
/* the timeout value in seconds for database locks */
|
||||||
|
double timeout;
|
||||||
|
|
||||||
|
/* for internal use in the timeout handler: when did the timeout handler
|
||||||
|
* first get called with count=0? */
|
||||||
|
double timeout_started;
|
||||||
|
|
||||||
|
/* None for autocommit, otherwise a PyString with the isolation level */
|
||||||
|
PyObject* isolation_level;
|
||||||
|
|
||||||
|
/* NULL for autocommit, otherwise a string with the BEGIN statement; will be
|
||||||
|
* freed in connection destructor */
|
||||||
|
char* begin_statement;
|
||||||
|
|
||||||
|
/* 1 if a check should be performed for each API call if the connection is
|
||||||
|
* used from the same thread it was created in */
|
||||||
|
int check_same_thread;
|
||||||
|
|
||||||
|
int initialized;
|
||||||
|
|
||||||
|
/* thread identification of the thread the connection was created in */
|
||||||
|
long thread_ident;
|
||||||
|
|
||||||
|
pysqlite_Cache* statement_cache;
|
||||||
|
|
||||||
|
/* Lists of weak references to statements and cursors used within this connection */
|
||||||
|
PyObject* statements;
|
||||||
|
PyObject* cursors;
|
||||||
|
|
||||||
|
/* Counters for how many statements/cursors were created in the connection. May be
|
||||||
|
* reset to 0 at certain intervals */
|
||||||
|
int created_statements;
|
||||||
|
int created_cursors;
|
||||||
|
|
||||||
|
PyObject* row_factory;
|
||||||
|
|
||||||
|
/* Determines how bytestrings from SQLite are converted to Python objects:
|
||||||
|
* - PyUnicode_Type: Python Unicode objects are constructed from UTF-8 bytestrings
|
||||||
|
* - OptimizedUnicode: Like before, but for ASCII data, only PyStrings are created.
|
||||||
|
* - PyString_Type: PyStrings are created as-is.
|
||||||
|
* - Any custom callable: Any object returned from the callable called with the bytestring
|
||||||
|
* as single parameter.
|
||||||
|
*/
|
||||||
|
PyObject* text_factory;
|
||||||
|
|
||||||
|
/* remember references to functions/classes used in
|
||||||
|
* create_function/create/aggregate, use these as dictionary keys, so we
|
||||||
|
* can keep the total system refcount constant by clearing that dictionary
|
||||||
|
* in connection_dealloc */
|
||||||
|
PyObject* function_pinboard;
|
||||||
|
|
||||||
|
/* a dictionary of registered collation name => collation callable mappings */
|
||||||
|
PyObject* collations;
|
||||||
|
|
||||||
|
/* Exception objects */
|
||||||
|
PyObject* Warning;
|
||||||
|
PyObject* Error;
|
||||||
|
PyObject* InterfaceError;
|
||||||
|
PyObject* DatabaseError;
|
||||||
|
PyObject* DataError;
|
||||||
|
PyObject* OperationalError;
|
||||||
|
PyObject* IntegrityError;
|
||||||
|
PyObject* InternalError;
|
||||||
|
PyObject* ProgrammingError;
|
||||||
|
PyObject* NotSupportedError;
|
||||||
|
} pysqlite_Connection;
|
||||||
|
|
||||||
|
extern PyTypeObject pysqlite_ConnectionType;
|
||||||
|
|
||||||
|
PyObject* pysqlite_connection_alloc(PyTypeObject* type, int aware);
|
||||||
|
void pysqlite_connection_dealloc(pysqlite_Connection* self);
|
||||||
|
PyObject* pysqlite_connection_cursor(pysqlite_Connection* self, PyObject* args, PyObject* kwargs);
|
||||||
|
PyObject* pysqlite_connection_close(pysqlite_Connection* self, PyObject* args);
|
||||||
|
PyObject* _pysqlite_connection_begin(pysqlite_Connection* self);
|
||||||
|
PyObject* pysqlite_connection_commit(pysqlite_Connection* self, PyObject* args);
|
||||||
|
PyObject* pysqlite_connection_rollback(pysqlite_Connection* self, PyObject* args);
|
||||||
|
PyObject* pysqlite_connection_new(PyTypeObject* type, PyObject* args, PyObject* kw);
|
||||||
|
int pysqlite_connection_init(pysqlite_Connection* self, PyObject* args, PyObject* kwargs);
|
||||||
|
|
||||||
|
int pysqlite_connection_register_cursor(pysqlite_Connection* connection, PyObject* cursor);
|
||||||
|
int pysqlite_check_thread(pysqlite_Connection* self);
|
||||||
|
int pysqlite_check_connection(pysqlite_Connection* con);
|
||||||
|
|
||||||
|
int pysqlite_connection_setup_types(void);
|
||||||
|
|
||||||
|
#endif
|
@ -0,0 +1,58 @@
|
|||||||
|
/* module.h - definitions for the module
|
||||||
|
*
|
||||||
|
* Copyright (C) 2004-2015 Gerhard Häring <gh@ghaering.de>
|
||||||
|
*
|
||||||
|
* This file is part of pysqlite.
|
||||||
|
*
|
||||||
|
* This software is provided 'as-is', without any express or implied
|
||||||
|
* warranty. In no event will the authors be held liable for any damages
|
||||||
|
* arising from the use of this software.
|
||||||
|
*
|
||||||
|
* Permission is granted to anyone to use this software for any purpose,
|
||||||
|
* including commercial applications, and to alter it and redistribute it
|
||||||
|
* freely, subject to the following restrictions:
|
||||||
|
*
|
||||||
|
* 1. The origin of this software must not be misrepresented; you must not
|
||||||
|
* claim that you wrote the original software. If you use this software
|
||||||
|
* in a product, an acknowledgment in the product documentation would be
|
||||||
|
* appreciated but is not required.
|
||||||
|
* 2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
* misrepresented as being the original software.
|
||||||
|
* 3. This notice may not be removed or altered from any source distribution.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef PYSQLITE_MODULE_H
|
||||||
|
#define PYSQLITE_MODULE_H
|
||||||
|
#include "Python.h"
|
||||||
|
|
||||||
|
#define PYSQLITE_VERSION "2.8.2"
|
||||||
|
|
||||||
|
extern PyObject* pysqlite_Error;
|
||||||
|
extern PyObject* pysqlite_Warning;
|
||||||
|
extern PyObject* pysqlite_InterfaceError;
|
||||||
|
extern PyObject* pysqlite_DatabaseError;
|
||||||
|
extern PyObject* pysqlite_InternalError;
|
||||||
|
extern PyObject* pysqlite_OperationalError;
|
||||||
|
extern PyObject* pysqlite_ProgrammingError;
|
||||||
|
extern PyObject* pysqlite_IntegrityError;
|
||||||
|
extern PyObject* pysqlite_DataError;
|
||||||
|
extern PyObject* pysqlite_NotSupportedError;
|
||||||
|
|
||||||
|
extern PyObject* pysqlite_OptimizedUnicode;
|
||||||
|
|
||||||
|
/* the functions time.time() and time.sleep() */
|
||||||
|
extern PyObject* time_time;
|
||||||
|
extern PyObject* time_sleep;
|
||||||
|
|
||||||
|
/* A dictionary, mapping colum types (INTEGER, VARCHAR, etc.) to converter
|
||||||
|
* functions, that convert the SQL value to the appropriate Python value.
|
||||||
|
* The key is uppercase.
|
||||||
|
*/
|
||||||
|
extern PyObject* converters;
|
||||||
|
|
||||||
|
extern int _enable_callback_tracebacks;
|
||||||
|
extern int pysqlite_BaseTypeAdapted;
|
||||||
|
|
||||||
|
#define PARSE_DECLTYPES 1
|
||||||
|
#define PARSE_COLNAMES 2
|
||||||
|
#endif
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,137 @@
|
|||||||
|
import sys
|
||||||
|
from difflib import SequenceMatcher
|
||||||
|
from random import randint
|
||||||
|
|
||||||
|
|
||||||
|
IS_PY3K = sys.version_info[0] == 3
|
||||||
|
|
||||||
|
# String UDF.
|
||||||
|
def damerau_levenshtein_dist(s1, s2):
|
||||||
|
cdef:
|
||||||
|
int i, j, del_cost, add_cost, sub_cost
|
||||||
|
int s1_len = len(s1), s2_len = len(s2)
|
||||||
|
list one_ago, two_ago, current_row
|
||||||
|
list zeroes = [0] * (s2_len + 1)
|
||||||
|
|
||||||
|
if IS_PY3K:
|
||||||
|
current_row = list(range(1, s2_len + 2))
|
||||||
|
else:
|
||||||
|
current_row = range(1, s2_len + 2)
|
||||||
|
|
||||||
|
current_row[-1] = 0
|
||||||
|
one_ago = None
|
||||||
|
|
||||||
|
for i in range(s1_len):
|
||||||
|
two_ago = one_ago
|
||||||
|
one_ago = current_row
|
||||||
|
current_row = list(zeroes)
|
||||||
|
current_row[-1] = i + 1
|
||||||
|
for j in range(s2_len):
|
||||||
|
del_cost = one_ago[j] + 1
|
||||||
|
add_cost = current_row[j - 1] + 1
|
||||||
|
sub_cost = one_ago[j - 1] + (s1[i] != s2[j])
|
||||||
|
current_row[j] = min(del_cost, add_cost, sub_cost)
|
||||||
|
|
||||||
|
# Handle transpositions.
|
||||||
|
if (i > 0 and j > 0 and s1[i] == s2[j - 1]
|
||||||
|
and s1[i-1] == s2[j] and s1[i] != s2[j]):
|
||||||
|
current_row[j] = min(current_row[j], two_ago[j - 2] + 1)
|
||||||
|
|
||||||
|
return current_row[s2_len - 1]
|
||||||
|
|
||||||
|
# String UDF.
|
||||||
|
def levenshtein_dist(a, b):
|
||||||
|
cdef:
|
||||||
|
int add, delete, change
|
||||||
|
int i, j
|
||||||
|
int n = len(a), m = len(b)
|
||||||
|
list current, previous
|
||||||
|
list zeroes
|
||||||
|
|
||||||
|
if n > m:
|
||||||
|
a, b = b, a
|
||||||
|
n, m = m, n
|
||||||
|
|
||||||
|
zeroes = [0] * (m + 1)
|
||||||
|
|
||||||
|
if IS_PY3K:
|
||||||
|
current = list(range(n + 1))
|
||||||
|
else:
|
||||||
|
current = range(n + 1)
|
||||||
|
|
||||||
|
for i in range(1, m + 1):
|
||||||
|
previous = current
|
||||||
|
current = list(zeroes)
|
||||||
|
current[0] = i
|
||||||
|
|
||||||
|
for j in range(1, n + 1):
|
||||||
|
add = previous[j] + 1
|
||||||
|
delete = current[j - 1] + 1
|
||||||
|
change = previous[j - 1]
|
||||||
|
if a[j - 1] != b[i - 1]:
|
||||||
|
change +=1
|
||||||
|
current[j] = min(add, delete, change)
|
||||||
|
|
||||||
|
return current[n]
|
||||||
|
|
||||||
|
# String UDF.
|
||||||
|
def str_dist(a, b):
|
||||||
|
cdef:
|
||||||
|
int t = 0
|
||||||
|
|
||||||
|
for i in SequenceMatcher(None, a, b).get_opcodes():
|
||||||
|
if i[0] == 'equal':
|
||||||
|
continue
|
||||||
|
t = t + max(i[4] - i[3], i[2] - i[1])
|
||||||
|
return t
|
||||||
|
|
||||||
|
# Math Aggregate.
|
||||||
|
cdef class median(object):
|
||||||
|
cdef:
|
||||||
|
int ct
|
||||||
|
list items
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.ct = 0
|
||||||
|
self.items = []
|
||||||
|
|
||||||
|
cdef selectKth(self, int k, int s=0, int e=-1):
|
||||||
|
cdef:
|
||||||
|
int idx
|
||||||
|
if e < 0:
|
||||||
|
e = len(self.items)
|
||||||
|
idx = randint(s, e-1)
|
||||||
|
idx = self.partition_k(idx, s, e)
|
||||||
|
if idx > k:
|
||||||
|
return self.selectKth(k, s, idx)
|
||||||
|
elif idx < k:
|
||||||
|
return self.selectKth(k, idx + 1, e)
|
||||||
|
else:
|
||||||
|
return self.items[idx]
|
||||||
|
|
||||||
|
cdef int partition_k(self, int pi, int s, int e):
|
||||||
|
cdef:
|
||||||
|
int i, x
|
||||||
|
|
||||||
|
val = self.items[pi]
|
||||||
|
# Swap pivot w/last item.
|
||||||
|
self.items[e - 1], self.items[pi] = self.items[pi], self.items[e - 1]
|
||||||
|
x = s
|
||||||
|
for i in range(s, e):
|
||||||
|
if self.items[i] < val:
|
||||||
|
self.items[i], self.items[x] = self.items[x], self.items[i]
|
||||||
|
x += 1
|
||||||
|
self.items[x], self.items[e-1] = self.items[e-1], self.items[x]
|
||||||
|
return x
|
||||||
|
|
||||||
|
def step(self, item):
|
||||||
|
self.items.append(item)
|
||||||
|
self.ct += 1
|
||||||
|
|
||||||
|
def finalize(self):
|
||||||
|
if self.ct == 0:
|
||||||
|
return None
|
||||||
|
elif self.ct < 3:
|
||||||
|
return self.items[0]
|
||||||
|
else:
|
||||||
|
return self.selectKth(self.ct / 2)
|
@ -0,0 +1,123 @@
|
|||||||
|
from peewee import *
|
||||||
|
from playhouse.sqlite_ext import JSONField
|
||||||
|
|
||||||
|
|
||||||
|
class BaseChangeLog(Model):
|
||||||
|
timestamp = DateTimeField(constraints=[SQL('DEFAULT CURRENT_TIMESTAMP')])
|
||||||
|
action = TextField()
|
||||||
|
table = TextField()
|
||||||
|
primary_key = IntegerField()
|
||||||
|
changes = JSONField()
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeLog(object):
|
||||||
|
# Model class that will serve as the base for the changelog. This model
|
||||||
|
# will be subclassed and mapped to your application database.
|
||||||
|
base_model = BaseChangeLog
|
||||||
|
|
||||||
|
# Template for the triggers that handle updating the changelog table.
|
||||||
|
# table: table name
|
||||||
|
# action: insert / update / delete
|
||||||
|
# new_old: NEW or OLD (OLD is for DELETE)
|
||||||
|
# primary_key: table primary key column name
|
||||||
|
# column_array: output of build_column_array()
|
||||||
|
# change_table: changelog table name
|
||||||
|
template = """CREATE TRIGGER IF NOT EXISTS %(table)s_changes_%(action)s
|
||||||
|
AFTER %(action)s ON %(table)s
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO %(change_table)s
|
||||||
|
("action", "table", "primary_key", "changes")
|
||||||
|
SELECT
|
||||||
|
'%(action)s', '%(table)s', %(new_old)s."%(primary_key)s", "changes"
|
||||||
|
FROM (
|
||||||
|
SELECT json_group_object(
|
||||||
|
col,
|
||||||
|
json_array("oldval", "newval")) AS "changes"
|
||||||
|
FROM (
|
||||||
|
SELECT json_extract(value, '$[0]') as "col",
|
||||||
|
json_extract(value, '$[1]') as "oldval",
|
||||||
|
json_extract(value, '$[2]') as "newval"
|
||||||
|
FROM json_each(json_array(%(column_array)s))
|
||||||
|
WHERE "oldval" IS NOT "newval"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
END;"""
|
||||||
|
|
||||||
|
drop_template = 'DROP TRIGGER IF EXISTS %(table)s_changes_%(action)s'
|
||||||
|
|
||||||
|
_actions = ('INSERT', 'UPDATE', 'DELETE')
|
||||||
|
|
||||||
|
def __init__(self, db, table_name='changelog'):
|
||||||
|
self.db = db
|
||||||
|
self.table_name = table_name
|
||||||
|
|
||||||
|
def _build_column_array(self, model, use_old, use_new, skip_fields=None):
|
||||||
|
# Builds a list of SQL expressions for each field we are tracking. This
|
||||||
|
# is used as the data source for change tracking in our trigger.
|
||||||
|
col_array = []
|
||||||
|
for field in model._meta.sorted_fields:
|
||||||
|
if field.primary_key:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if skip_fields is not None and field.name in skip_fields:
|
||||||
|
continue
|
||||||
|
|
||||||
|
column = field.column_name
|
||||||
|
new = 'NULL' if not use_new else 'NEW."%s"' % column
|
||||||
|
old = 'NULL' if not use_old else 'OLD."%s"' % column
|
||||||
|
|
||||||
|
if isinstance(field, JSONField):
|
||||||
|
# Ensure that values are cast to JSON so that the serialization
|
||||||
|
# is preserved when calculating the old / new.
|
||||||
|
if use_old: old = 'json(%s)' % old
|
||||||
|
if use_new: new = 'json(%s)' % new
|
||||||
|
|
||||||
|
col_array.append("json_array('%s', %s, %s)" % (column, old, new))
|
||||||
|
|
||||||
|
return ', '.join(col_array)
|
||||||
|
|
||||||
|
def trigger_sql(self, model, action, skip_fields=None):
|
||||||
|
assert action in self._actions
|
||||||
|
use_old = action != 'INSERT'
|
||||||
|
use_new = action != 'DELETE'
|
||||||
|
cols = self._build_column_array(model, use_old, use_new, skip_fields)
|
||||||
|
return self.template % {
|
||||||
|
'table': model._meta.table_name,
|
||||||
|
'action': action,
|
||||||
|
'new_old': 'NEW' if action != 'DELETE' else 'OLD',
|
||||||
|
'primary_key': model._meta.primary_key.column_name,
|
||||||
|
'column_array': cols,
|
||||||
|
'change_table': self.table_name}
|
||||||
|
|
||||||
|
def drop_trigger_sql(self, model, action):
|
||||||
|
assert action in self._actions
|
||||||
|
return self.drop_template % {
|
||||||
|
'table': model._meta.table_name,
|
||||||
|
'action': action}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model(self):
|
||||||
|
if not hasattr(self, '_changelog_model'):
|
||||||
|
class ChangeLog(self.base_model):
|
||||||
|
class Meta:
|
||||||
|
database = self.db
|
||||||
|
table_name = self.table_name
|
||||||
|
self._changelog_model = ChangeLog
|
||||||
|
|
||||||
|
return self._changelog_model
|
||||||
|
|
||||||
|
def install(self, model, skip_fields=None, drop=True, insert=True,
|
||||||
|
update=True, delete=True, create_table=True):
|
||||||
|
ChangeLog = self.model
|
||||||
|
if create_table:
|
||||||
|
ChangeLog.create_table()
|
||||||
|
|
||||||
|
actions = list(zip((insert, update, delete), self._actions))
|
||||||
|
if drop:
|
||||||
|
for _, action in actions:
|
||||||
|
self.db.execute_sql(self.drop_trigger_sql(model, action))
|
||||||
|
|
||||||
|
for enabled, action in actions:
|
||||||
|
if enabled:
|
||||||
|
sql = self.trigger_sql(model, action, skip_fields)
|
||||||
|
self.db.execute_sql(sql)
|
Loading…
Reference in new issue