{ "info": { "author": "William Bruschi", "author_email": "william.bruschi@gmail.com", "bugtrack_url": null, "classifiers": [ "Operating System :: OS Independent", "Programming Language :: Python :: 3" ], "description": "pghops is a command line PostgreSQL schema migration utility written\nin Python. It aims to be the simplest database migration utility for\nPostgreSQL.\n\n1. [Features](#features)\n2. [Demo](#demo)\n3. [Usage Overview](#usage-overview)\n4. [Installation](#installation)\n5. [Best Practices](#best-practices)\n6. [Options](#options)\n7. [Managing Indexes](#managing-indexes)\n8. [Unit Testing](#unit-testing)\n9. [FAQ](#faq)\n10. [Miscellaneous](#miscellaneous)\n11. [License](#license)\n\n## Features\n\n* **Simple version file syntax:** pghops version files are yaml files\n with keys representing directories and values of one or more sql\n file names.\n* **Executes scripts with psql:** pghops uses psql to execute all sql,\n leveraging the extensive functionality of the PostgreSQL client. Use\n any psql command in your scripts.\n* **Unit testing framework:** pghops comes equipped with its own unit\n testing framework. No more excuses for skipping sql unit tests!\n* **All or nothing migrations:** Wrap your entire migration in a\n single transaction or each migration script in its own transaction.\n* **All sql commands saved to version table** pghops saves all sql\n executed during migrations to its version table. Make the auditors\n happy!\n\n## Demo\n\nThe below terminal session shows how to create a database named `mydb`\nthat contains two tables: `account` and `account_email`. We will\ncreate a simple test to ensure you cannot insert null emails into the\n`account_email` table. Then we will create a database function for\ncreating accounts, along with another unit test.\n\n```\n[mycluster]$ # Create a directory named after the database, along with a script to create the database.\n[mycluster]$ mkdir mydb\n[mycluster]$ echo \"create database mydb;\" > mydb/create_database.sql\n[mycluster]$ # Create a directory to hold our table definitions.\n[mycluster]$ mkdir -p mydb/schemas/public/tables\n[mycluster]$ # Create SQL files containing our table definitions.\n[mycluster]$ cat - > mydb/schemas/public/tables/account.sql < create table if not exists public.account (\n> account_id bigserial primary key\n> );\n> EOF\n[mycluster]$ cat - > mydb/schemas/public/tables/account_email.sql < create table if not exists public.account_email (\n> account_email_id bigserial primary key\n> , account_id bigint not null references account\n> , email text not null\n> );\n> EOF\n[mycluster]$ # Create our first migration file\n[mycluster]$ mkdir mydb/versions\n[mycluster]$ cat - > mydb/versions/0001.0001.0001.init.yml < public/tables:\n> - account\n> - account_email\n> EOF\n[mycluster]$ # Create our first unit test to ensure we cannot insert NULLs into account_email.email\n[mycluster]$ mkdir mydb/tests\n[mycluster]$ cat - > mydb/tests/01_account_email_test.sql < insert into account values (default);\n> insert into account_email (account_id, email) values ((select max(account_id) from account), null);\n> EOF\n[mycluster]$ # Generated the 'expected' file and review it.\n[mycluster]$ pghops_test generate\n2019-02-23 14:18:42.452426: Looping through tests in /tmp/mycluster/mydb/tests\n2019-02-23 14:18:42.458661: Stopping Postgres pghops-postgresql.\n2019-02-23 14:18:42.476721: Starting Postgres pghops-postgresql postgres.\n2019-02-23 14:18:45.632209: Done starting postgres pghops-postgresql.\n2019-02-23 14:18:45.673046: Migrating cluster /tmp/mycluster.\n2019-02-23 14:18:45.673440: Migrating database mydb\n2019-02-23 14:18:45.674398: Database mydb does not exist. Creating it with /tmp/mycluster/mydb/create_database.sql.\ncreate database mydb;\nCREATE DATABASE\n\n...\n\n...\n\n2019-02-23 14:18:46.106990: Done migrating database mydb\n2019-02-23 14:18:46.107123: Done all migrations.\n2019-02-23 14:18:46.132066: Generated 01_account_email_test.sql expected file.\n2019-02-23 14:18:46.132145: Stopping Postgres pghops-postgresql.\n2019-02-23 14:18:48.449079: Done generating expected files!\n[mycluster]$ # Review the expected file.\n[mycluster]$ cat mydb/tests/01_account_email_expected.txt\ninsert into account values (default);\nINSERT 0 1\ninsert into account_email (account_id, email) values ((select max(account_id) from account), null);\nERROR: null value in column \"email\" violates not-null constraint\nDETAIL: Failing row contains (1, 1, null).\n[mycluster]$ # Looks good! We received the error as expected. As a sanity check, run the tests and they should succeeded\n[mycluster]$ pghops_test run\n\n...\n\n...\n\n2019-02-23 14:22:31.269604: All tests passed!\n[mycluster]$ # Lets run our first migration against a real db!\n[mycluster]$ pghops\n2019-02-23 14:23:47.225273: Migrating cluster /tmp/mycluster.\n2019-02-23 14:23:47.225539: Migrating database mydb\n2019-02-23 14:23:47.226114: Database mydb does not exist. Creating it with /tmp/mycluster/mydb/create_database.sql.\nCREATE DATABASE\n\n\nBEGIN\nCREATE SCHEMA\nCREATE TABLE\nCREATE TABLE\nCREATE INDEX\nINSERT 0 1\nCREATE TABLE\nCREATE TABLE\nINSERT 0 1\nCOMMIT\n\n\n2019-02-23 14:23:47.827395: Done migrating database mydb\n2019-02-23 14:23:47.827536: Done all migrations.\n[mycluster]$ # Check the version table if you wish\n[mycluster]$ psql --dbname=mydb --command=\"select major, minor, patch, label, file_name from pghops.version;\"\n major | minor | patch | label | file_name\n-------+-------+-------+-------------+---------------------------------\n 0000 | 0000 | 0000 | pghops-init | 0000.0000.0000.pghops-init.yaml\n 0001 | 0001 | 0001 | init | 0001.0001.0001.init.yml\n(2 rows)\n\n[mycluster]$ # Create a function that creates accounts.\n[mycluster]$ mkdir mydb/schemas/public/functions\n[mycluster]$ cat - > mydb/schemas/public/functions/create_account.sql < (\n> p_email text\n> )\n> returns bigint\n> language plpgsql\n> as \\$\\$\n> declare l_account_id bigint;\n> begin\n>\n> insert into account (account_id) values (default) returning account_id into l_account_id;\n>\n> insert into account_email (account_id, email) values (l_account_id, p_email);\n>\n> return l_account_id;\n>\n> end\\$\\$;\n> EOF\n[mycluster]$ # Next create our second migration file.\n[mycluster]$ cat - > mydb/versions/0001.0002.0001.create_account.yml < public/functions: create_account\n> EOF\n[mycluster]$ # Create our second test\n[mycluster]$ echo \"select create_account('x@example.com');\" > mydb/tests/02_create_account_test.sql\n[mycluster]$ pghops_test generate\n2019-02-23 14:35:44.742060: Looping through tests in /tmp/mycluster/mydb/tests\n2019-02-23 14:35:44.748353: Stopping Postgres pghops-postgresql.\n2019-02-23 14:35:44.764216: Starting Postgres pghops-postgresql postgres.\n2019-02-23 14:35:47.726734: Done starting postgres pghops-postgresql.\n2019-02-23 14:35:47.767687: Migrating cluster /tmp/mycluster.\n2019-02-23 14:35:47.768116: Migrating database mydb\n2019-02-23 14:35:47.769223: Database mydb does not exist. Creating it with /tmp/mycluster/mydb/create_database.sql.\n...\n\n...\n\n2019-02-23 14:35:48.230893: Done migrating database mydb\n2019-02-23 14:35:48.230981: Done all migrations.\n2019-02-23 14:35:48.251236: Generated 01_account_email_test.sql expected file.\n2019-02-23 14:35:48.269521: Generated 02_create_account_test.sql expected file.\n2019-02-23 14:35:48.269596: Stopping Postgres pghops-postgresql.\n2019-02-23 14:35:50.672626: Done generating expected files!\n[mycluster]$ cat mydb/tests/02_create_account_expected.txt\nselect create_account('x@example.com');\n create_account\n----------------\n 2\n(1 row)\n[mycluster]$ # Our new db function works! Lets run a migartion to update our db\n[mycluster]$ pghops\n2019-02-23 14:37:13.523780: Migrating cluster /tmp/mycluster.\n2019-02-23 14:37:13.524050: Migrating database mydb\nBEGIN\nCREATE FUNCTION\nINSERT 0 1\nCOMMIT\n\n\n2019-02-23 14:37:13.572099: Done migrating database mydb\n2019-02-23 14:37:13.572224: Done all migrations.\n[mycluster]$ psql --dbname=mydb --command=\"select major, minor, patch, label, file_name from pghops.version;\"\n major | minor | patch | label | file_name\n-------+-------+-------+----------------+-----------------------------------\n 0000 | 0000 | 0000 | pghops-init | 0000.0000.0000.pghops-init.yaml\n 0001 | 0001 | 0001 | init | 0001.0001.0001.init.yml\n 0001 | 0002 | 0001 | create_account | 0001.0002.0001.create_account.yml\n(3 rows)\n\n[mycluster]$\n\n\n```\n\n## Usage Overview\n\nWhen you install PostgreSQL you initialize a storage area on disk\ncalled a [database\ncluster](https://www.postgresql.org/docs/current/creating-cluster.html),\nwhich is a collection of databases managed by a single instance of\nPostgreSQL. pghops expects you to place all files associated to\nbuilding and defining your cluster in a single directory, referred to\nhenceforth as the `cluster_directory`. Each sub-directory in\n`cluster_directory` should be the name of a database within your\ncluster (if not, you can add a file named `databases` that contains\nthe list of database directories).\n\nFor example, say your `cluster_directory` is /tmp/pghops/main and you\nhave two databases - dba and dbb. Your directory structure would look\nlike:\n```\n\u2514\u2500\u2500 main\n \u251c\u2500\u2500 dba\n \u2514\u2500\u2500 dbb\n```\n\npghops requires each database directory to have a sub-directory named\n`versions` which contain, you guessed it, all of you database\nmigration files. Each migration file must follow the following\nversioning convention:\n\n`...