It is advisable to place the database implementation in a separate Django app within the project. Let’s assume the name of the project is tracking, and the ImmuDB integration resides in the immudb_backend folder. In the main application’s settings.py file, we can specify which Django app communicates with the database. To do this, the DATABASES variable should be configured with the following dictionary value:
DATABASES = { "default": { "ENGINE": "immudb_backend", "USER": environ.get("DATABASE_USER", "immudb"), "PASSWORD": environ.get("DATABASE_PASSWORD", "immudb"), "NAME": environ.get("DATABASE_NAME", "defaultdb"), "HOST": environ.get("DATABASE_HOST", "immudb"), "PORT": "3322", } }
This class defines the highest-level functions required for database communication. It is where we override methods such as execute, commit, and get_new_connection. We also specify our own subclassed components here, such as client_class and features_class.
For instance, since ImmuDB interprets LIKE expressions as regular expressions by default, an example entry in the operators map could be 'regex': 'LIKE %s'. Similarly, the data_types map can be overridden to define how Django ORM field types are translated at the database level, for example, mapping 'DateTimeField': 'TIMESTAMP' to treat Django DateTimeFields as SQL TIMESTAMPTs. However, this mapping does not occur automatically, we must define how to convert a Django-provided date value into a TIMESTAMP format that ImmuDB can understand.
The cursor’s key method is execute, which also needs to be invoked in the DatabaseWrapper's own execute method. This function handles, among other things, date conversions, specific queries such as SELECT 1 or GROUP BY 1, converting UPDATE statements into UPSERTs in ImmuDB’s expected format, and rewriting SET expressions.
field_name_mapping = { 'password': 'user_pass', # PASSWORD is a reserved keyword in ImmuDB }
We then apply this custom cursor in the DatabaseWrapper class as follows:
def create_cursor(self, name=None): return CursorWrapper(self.connection)
sql_create_table = ( "CREATE TABLE IF NOT EXISTS %(table)s" "(%(definition)s," "PRIMARY KEY (%(primary_key)s))" )
Let’s say that during development a field was set as nullable, but later we decide it would be better not to allow NULL values in that column. ImmuDB does not support such modifications directly, so we first need to create a duplicate of the table with the updated field set to non-nullable, then copy over all values to the new table, and finally delete the original one.
Another challenge is how to handle previously NULL values when copying data to the new table. Django offers a built-in solution: during migration, it prompts the developer to provide a default fallback value for such cases. If you don’t want to specify a default either, the safest approach may be to manually delete all records from the table before applying the changes.
Several other files require smaller-scale adjustments. Among these, one of the more important is features.py, where you can explicitly define ORM- and SQL-specific flags, such as:
supports_left_outer_join = False supports_right_outer_join = False supports_full_outer_join = False supports_native_bulk_update = False
In operations.py, among other things, you can override the previously mentioned bulk update feature and implement it manually using a loop, since ImmuDB’s SQL implementation does not support this operation natively.
For introspection, you only need to define the following:
class DatabaseIntrospection(BaseDatabaseIntrospection): """Database introspection for ImmuDB.""" def get_table_list(self, cursor): """Return a list of table and view names in the current database.""" cursor.execute("SELECT * FROM TABLES()") return [TableInfo(row[0], 't') for row in cursor.fetchall()]
Only client.py surpasses it in terms of simplicity:
class DatabaseClient(BaseDatabaseClient): """Database client for ImmuDB.""" executable_name = 'immudb'