
I want to share a tool that was born in the development of a single web project and really helps me not to get lost in the sea of tables, stored procedures, indexes and other database inhabitants.
The project itself is written in Django, PostgreSQL is used as a backend. At the very beginning of the work, it was decided to at least partially abandon the use of Django ORM in favor of “raw” SQL and stored procedures. In other words, almost all business logic is moved to the database level. I can say right away that I can prepare ORM, but in this case it was necessary to perform multi-stage calculations related to a multitude of samples, and this is better done on the database server and not drag intermediate data into the application.
')
Faced with the need to maintain the database structure manually, without Django Migrations, I found out that manually writing incremental SQL patches is possible, but it is difficult to keep track of the dependencies of database objects. For example, when a function that is used somewhere else, you add another argument, a simple CREATE OR REPLACE is not enough - you need to first DROP, and then CREATE. In this case, you must first remove the functions dependent on it, and then create it again (and if someone else depends on these functions, then you need to recreate them).
Under the cut a brief description of the possibilities in the form of a tutorial. Meet - Sqlibrist.
I must say that my problem has already been learned to be solved. For example,
Sqitch has been
around for a relatively long time. It allows you to describe the database structure in a declarative form in SQL. Each table, view, or function is stored in a separate file, and a simple DSL describes the dependencies. The utility is written in Perl, and I, who are not familiar with Perl development and the ecosystem of its packages, had to really try to compile this utility. Perhaps due to the long history of development, Sqitch has a lot of dependencies, as for such a simple program. I also did not like the confusing description of dependencies and the work with versions of the structure. I admit that I just did not want to adapt and understand the tool, which seemed uncomfortable to me.
When creating Sqlibrist, I was inspired by Sqitch, Django Migrations, and some VCS. He also wanted it to be simple and straightforward to use. Objects of the database structure are stored in separate files. Each contains a SQL statement for creating and (optionally) deleting this object. Dependencies between objects are described explicitly in the form of directives on the built-in DSL (in it, by the way, only three keywords: REQ, UP, DOWN). Like the version control system, Sqlibrist stores snapshots of the database structure and SQL patch for updating to it from the previous snapshot.
Sqlibrist's intelligence is limited, it does not parse SQL and does not generate ALTER TABLE - this is your work. It only tracks changes in files and creates patches with your instructions, and also keeps records of applied migrations.
It all sounds like something abstract, let's move on to practice.
Installation
My primary OS is Linux both on the server and on the desktop, so the installation instructions are for it only. Maybe someone will help me with Windows and Mac.
First header files:
Ubuntu
$ sudo apt-get install python-pip python-dev libyaml-dev $ sudo apt-get install libmysqlclient-dev
Fedora / CentOS
$ sudo dnf install python-devel python-pip libyaml-devel $ sudo dnf install postgresql-devel
$ sudo dnf install mariadb-devel
or
$ sudo dnf install mysql++-devel
Sqlibrist is written in Python and has two dependencies: PyYAML and one of psycopg2 and mysql-python.
It is installed using pip either in virtualenv or in the system libraries:
$ pip install sqlibrist
or
$ sudo pip install sqlibrist
After installation, the
sqlibrist command becomes available.
Online Store Database
Let's play with Sqlibrist on the example of a primitive online store.
$ mkdir shop_schema $ cd shop_schema $ sqlibrist init Creating directories... Done.
The
init team created the directory structure of our project:
shop_schema sqlibrist.yaml migrations schema constraints functions indexes tables triggers types views
In
sqlibrist.yaml project configuration for connecting to the database:
--- default: engine: pg user: <username> name: <database_name> password: <password>
To verify that the settings are correct:
$ sqlibrist test_connection Connection OK
Next, we initialize the table where Sqlibrist will store information about the migrations applied. This part is identical to Django Migrations / South.
$ sqlibrist initdb Creating db... Creating schema and migrations log table... Done.
By the way, in the terminology of Sqlibrist, migration is a snapshot of the base structure and patches for applying this migration or reverting to the previous one.
Next, create the file
shop_schema / schema / tables / user.sql :
The first line
--UP means that the following SQL statements create a database object. This is enough to create a table.
Similarly, create two more files:
shop_schema / schema / tables / product.sql :
shop_schema / schema / tables / order.sql :
Note the line
--REQ tables / user . It means that the current object depends on the object in the
tables / user.sql file (the extension is not written to the REQ). This ensures that when the patch is generated, the user table will be created before the
order table. All
--REQ should go at the beginning of the file.
Another file:
shop_schema / schema / tables / order_product.sql :
Create the first migration:
$ sqlibrist makemigration -n 'initial' Creating: tables/user tables/product tables/order tables/order_product Creating new migration 0001-initial
Migration files are created in
shop_schema / migrations / 0001-initial :
up.sql down.sql schema.json
Up.sql contains a patch for applying migration,
down.sql in this case is empty, and
schema.json has a snapshot of the current database structure.
Before applying the patch, you can (and this is desirable) familiarize yourself with the text of the patch and make sure that it is doing what it needs. If it does not suit you, delete the entire directory
0001-initial and re-create the migration. You can edit
up.sql and
down.sql if you know what you are doing, but do not touch
schema.json .
Now apply our first migration:
$ sqlibrist migrate Applying migration 0001-initial... done
Three tables are created. Now we need a view that displays the user's orders with the order amounts:
shop_schema / schema / views / user_orders.sql :
Following the
--DOWN directive,
there are instructions for removing
user_orders when it is being re-created.
The general rule: we update data containing objects, for example, tables manually, so their descriptions do not contain
--DOWN , and functions, types, indexes can be safely removed and created, so this can be entrusted to automation.
We also need a function that returns
user_orders for a given user:
Create and apply the following migration:
$ sqlibrist makemigration -n 'user_orders view and function' Creating: views/user_orders functions/get_user_orders Creating new migration 0002-user_orders view and function $ sqlibrist migrate Applying migration 0002-user_orders view and function... done
Thus, we have 4 tables, one view and one function.
Suppose we need to add another field to the
user_orders view. Here are some problems that may arise:
- we can delete and recreate a new user_orders view, but the database will not allow this, because the get_user_orders function depends on this view;
- you can cheat and get out CREATE OR REPLACE VIEW user_orders ..., but the type of the field of the form and the type of the result of the function will be different. And in this case the database will not allow us to do this without re-creating the function.
Sqlibrist is designed to solve such problems. Add the
SUM field
(op.quantity) as order_total to the
user_orders view:
You can see what has changed:
$ sqlibrist -V diff Changed items: views/user_orders --- +++ @@ -2,7 +2,8 @@ u.id as user_id, o.id as order_id, o.date, - SUM(p.price*op.quantity) AS total + SUM(p.price*op.quantity) AS total, + SUM(op.quantity) as total_quantity FROM "user" u INNER JOIN "order" o ON u.id=o.user_id
Create a migration:
$ sqlibrist makemigration Updating: dropping: functions/get_user_orders views/user_orders creating: views/user_orders functions/get_user_orders Creating new migration 0003-auto
You see that the dependent object is first removed — the
get_user_orders function, then the view itself. Next, the view is created with a new structure, then the function is restored. Such a scheme will work for dependencies of arbitrary depth (but not circular dependencies — Sqlibrist will ask you to fix it).
Apply this migration:
$ sqlibrist migrate Applying migration 0003-auto... done
Finally, let's make a change to the table. Since files with table definitions do not contain
--DROP , we will work with our hands:
- Change the CREATE TABLE statement;
- We will generate a new migration using the same makemigration command;
- Add the required ALTER TABLE to up.sql.
Add a new
“type” text field to the
product table:
shop_schema / schema / tables / product.sql :
This is item 1. Now create the migration:
$ sqlibrist makemigration -n 'new product field' Updating: dropping: functions/get_user_orders views/user_orders creating: views/user_orders functions/get_user_orders Creating new migration 0004-new product field
Note that although we have changed the definition of the product table,
tables / product is not present in the migration log, BUT all objects dependent on it are re-created. This is point 2.
Now point 3: open
shop_schema / migrations / 0004-new product field / up.sql in the editor and find line 12 with the text
- ==== Add your instruction here ==== . This is the logical middle of the migration. At this point, all dependent objects are deleted and we can insert our ALTER TABLE.
Insert the following:
ALTER TABLE product ADD COLUMN "type" TEXT;
Our
up.sql will look like this:
You can apply this patch:
$ sqlibrist migrate Applying migration 0004-new product field... done
At this point, we leave alone our online store.
Sqlibrist is also able to integrate into the Django project, I use it in this context.
Project site -
here , bug reports are welcome.