Versioning

Fork/mirror of https://gitlab.com/depesz/Versioning
git clone https://ccx.te2000.cz/git/Versioning
Log | Files | Refs | README | LICENSE

commit 03ab222a9c2020975a3ec85bd98196978e4024b8
Author: Hubert depesz Lubaczewski <depesz@depesz.com>
Date:   Sun, 22 Aug 2010 15:34:12 +0200

first commit

Diffstat:
ACHANGES | 2++
ALICENSE | 34++++++++++++++++++++++++++++++++++
AREADME | 18++++++++++++++++++
Ainstall.versioning.sql | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
At/00-load.sql.inc | 2++
At/01-objects.sql | 23+++++++++++++++++++++++
At/02-functionality.sql | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Auninstall.versioning.sql | 6++++++
8 files changed, 237 insertions(+), 0 deletions(-)

diff --git a/CHANGES b/CHANGES @@ -0,0 +1,2 @@ +2010-08-22 : +- Initial release to github.com : http://github.com/depesz/Versioning diff --git a/LICENSE b/LICENSE @@ -0,0 +1,34 @@ +LICENSE AND COPYRIGHT + +Copyright (C) 2010 Hubert depesz Lubaczewski + +This program is distributed under the (Revised) BSD License: +L<http://www.opensource.org/licenses/bsd-license.php> + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +* Neither the name of Hubert depesz Lubaczewski's Organization +nor the names of its contributors may be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README b/README @@ -0,0 +1,18 @@ +Versioning + +This project strives to provide simple way to manage changes to +database. + +Instead of making changes on development server, then finding +differences between production and development, deciding which ones +should be installed on production, and finding a way to install them - +you start with writing diffs themselves! + +More detailed description is (so far) in here: +http://www.depesz.com/index.php/2010/08/22/versioning/ + +To install versioning simply run install.versioning.sql in your database +(all of them: production, stage, test, devel, ...). + +If you'd like to suggest new functionality or ask anything - please use +contact form on http://www.depesz.com/ diff --git a/install.versioning.sql b/install.versioning.sql @@ -0,0 +1,93 @@ +BEGIN; + +-- This file adds versioning support to database it will be loaded to. +-- It requires that PL/pgSQL is already loaded - will raise exception otherwise. +-- All versioning "stuff" (tables, functions) is in "_v" schema. + +-- All functions are defined as 'RETURNS SETOF INT4' to be able to make them to RETURN literaly nothing (0 rows). +-- >> RETURNS VOID<< IS similar, but it still outputs "empty line" in psql when calling. +CREATE SCHEMA _v; +COMMENT ON SCHEMA _v IS 'Schema for versioning data and functionality.'; + +CREATE TABLE _v.patches ( + patch_name TEXT PRIMARY KEY, + applied_tsz TIMESTAMPTZ NOT NULL DEFAULT now(), + applied_by TEXT NOT NULL, + requires TEXT[], + conflicts TEXT[] +); +COMMENT ON TABLE _v.patches IS 'Contains information about what patches are currently applied on database.'; +COMMENT ON COLUMN _v.patches.patch_name IS 'Name of patch, has to be unique for every patch.'; +COMMENT ON COLUMN _v.patches.applied_tsz IS 'When the patch was applied.'; +COMMENT ON COLUMN _v.patches.applied_by IS 'Who applied this patch (PostgreSQL username)'; +COMMENT ON COLUMN _v.patches.requires IS 'List of patches that are required for given patch.'; +COMMENT ON COLUMN _v.patches.conflicts IS 'List of patches that conflict with given patch.'; + +CREATE OR REPLACE FUNCTION _v.register_patch( IN in_patch_name TEXT, IN in_requirements TEXT[], in_conflicts TEXT[], OUT versioning INT4 ) RETURNS setof INT4 AS $$ +DECLARE + t_text TEXT; + t_text_a TEXT[]; + i INT4; +BEGIN + -- Thanks to this we know only one patch will be applied at a time + LOCK TABLE _v.patches IN EXCLUSIVE MODE; + + SELECT patch_name INTO t_text FROM _v.patches WHERE patch_name = in_patch_name; + IF FOUND THEN + RAISE EXCEPTION 'Patch % is already applied!', in_patch_name; + END IF; + + t_text_a := ARRAY( SELECT patch_name FROM _v.patches WHERE patch_name = any( in_conflicts ) ); + IF array_upper( t_text_a, 1 ) IS NOT NULL THEN + RAISE EXCEPTION 'Versioning patches conflict. Conflicting patche(s) installed: %.', array_to_string( t_text_a, ', ' ); + END IF; + + IF array_upper( in_requirements, 1 ) IS NOT NULL THEN + t_text_a := '{}'; + FOR i IN array_lower( in_requirements, 1 ) .. array_upper( in_requirements, 1 ) LOOP + SELECT patch_name INTO t_text FROM _v.patches WHERE patch_name = in_requirements[i]; + IF NOT FOUND THEN + t_text_a := t_text_a || t_text; + END IF; + END LOOP; + IF array_upper( t_text_a, 1 ) IS NOT NULL THEN + RAISE EXCEPTION 'Missing prerequisite(s): %.', array_to_string( t_text_a, ', ' ); + END IF; + END IF; + + INSERT INTO _v.patches (patch_name, applied_tsz, applied_by, requires, conflicts ) VALUES ( in_patch_name, now(), current_user, coalesce( in_requirements, '{}' ), coalesce( in_conflicts, '{}' ) ); + RETURN; +END; +$$ language plpgsql; +COMMENT ON FUNCTION _v.register_patch( TEXT, TEXT[], TEXT[] ) IS 'Function to register patches in database. Raises exception if there are conflicts, prerequisites are not installed or the migration has already been installed.'; + +CREATE OR REPLACE FUNCTION _v.register_patch( TEXT ) RETURNS setof INT4 AS $$ + SELECT _v.register_patch( $1, NULL, NULL ); +$$ language sql; +COMMENT ON FUNCTION _v.register_patch( TEXT ) IS 'Wrapper to allow registration of patches without requirements and conflicts.'; + +CREATE OR REPLACE FUNCTION _v.unregister_patch( IN in_patch_name TEXT, OUT versioning INT4 ) RETURNS setof INT4 AS $$ +DECLARE + i INT4; + t_text_a TEXT[]; +BEGIN + -- Thanks to this we know only one patch will be applied at a time + LOCK TABLE _v.patches IN EXCLUSIVE MODE; + + t_text_a := ARRAY( SELECT patch_name FROM _v.patches WHERE in_patch_name = ANY( requires ) ); + IF array_upper( t_text_a, 1 ) IS NOT NULL THEN + RAISE EXCEPTION 'Cannot uninstall %, as it is required by: %.', in_patch_name, array_to_string( t_text_a, ', ' ); + END IF; + + DELETE FROM _v.patches WHERE patch_name = in_patch_name; + GET DIAGNOSTICS i = ROW_COUNT; + IF i < 1 THEN + RAISE EXCEPTION 'Patch % is not installed, so it can''t be uninstalled!', in_patch_name; + END IF; + + RETURN; +END; +$$ language plpgsql; +COMMENT ON FUNCTION _v.unregister_patch( TEXT ) IS 'Function to unregister patches in database. Dies if the patch is not registered, or if unregistering it would break dependencies.'; + +COMMIT; diff --git a/t/00-load.sql.inc b/t/00-load.sql.inc @@ -0,0 +1,2 @@ +-- load pgtap - change next line to point to correct path for your system! +\i /home/pgdba/work/share/postgresql/contrib/pgtap.sql diff --git a/t/01-objects.sql b/t/01-objects.sql @@ -0,0 +1,23 @@ +BEGIN; + -- load pgtap - change next line to point to correct path for your system! + \i t/00-load.sql.inc + + SELECT plan(11); + + SELECT has_schema( '_v', 'There should be schema _v for versioning to work.' ); + SELECT has_table( '_v', 'patches', 'There should be _v.patches table for versioning to work.' ); + + SELECT has_column( '_v', 'patches', 'patch_name', '_v.patches should have patch_name column.' ); + SELECT has_column( '_v', 'patches', 'applied_tsz', '_v.patches should have applied_tsz column.' ); + SELECT has_column( '_v', 'patches', 'applied_by', '_v.patches should have applied_by column.' ); + SELECT has_column( '_v', 'patches', 'requires', '_v.patches should have requires column.' ); + SELECT has_column( '_v', 'patches', 'conflicts', '_v.patches should have conflicts column.' ); + + SELECT has_function( '_v', 'register_patch', ARRAY[ 'text', 'text[]', 'text[]' ], 'register_patch(text, text[], text[]) should exist to be able to register patches' ); + SELECT has_function( '_v', 'register_patch', ARRAY[ 'text' ], 'register_patch(text) should exist to be able to register patches (in simpler way)' ); + SELECT has_function( '_v', 'unregister_patch', ARRAY[ 'text' ], 'unregister_patch(text) should exist to be able to unregister patches that are no longer needed' ); + + SELECT is( ( SELECT count(*) FROM _v.patches ), 0::bigint, 'When running tests _v.patches table should be empty to prevent bad interactions between patches and tests.' ); + +ROLLBACK; + diff --git a/t/02-functionality.sql b/t/02-functionality.sql @@ -0,0 +1,59 @@ +BEGIN; + -- load pgtap - change next line to point to correct path for your system! + \i t/00-load.sql.inc + + SELECT plan(11); + + SELECT is( ( SELECT count(*) FROM _v.patches ), 0::bigint, 'When running tests _v.patches table should be empty to prevent bad interactions between patches and tests.' ); + + SELECT lives_ok( + $$SELECT _v.register_patch( 'first_patch' )$$, + 'Installation of patch without dependencies AND conflicts.' + ); + + SELECT results_eq( + 'SELECT patch_name, applied_tsz, applied_by, requires, conflicts FROM _v.patches', + $$SELECT 'first_patch'::text as patch_name, now() as applied_tsz, current_user::TEXT as applied_by, '{}'::TEXT[] as requires, '{}'::TEXT[] as conflicts$$, + 'Sanity check if patch is correctly saved.' + ); + + SELECT lives_ok( + $$SELECT _v.register_patch( 'second_patch', ARRAY[ 'first_patch' ], NULL )$$, + 'Installation of patch with dependencies.' + ); + SELECT lives_ok( + $$SELECT _v.register_patch( 'third_patch', ARRAY[ 'first_patch', 'second_patch' ], ARRAY[ 'bad_patch' ] )$$, + 'Installation of patch with dependencies and conflict.' + ); + SELECT throws_matching( + $$SELECT _v.register_patch( 'fourth_patch', ARRAY[ 'bad_patch' ], ARRAY[ 'another' ] )$$, + 'Missing prerequisite', + 'Installation of patch without meeting its requirements.' + ); + SELECT throws_matching( + $$SELECT _v.register_patch( 'fifth_patch', NULL, ARRAY[ 'first_patch' ] )$$, + 'Versioning patches conflict.', + 'Installation of patch with conflicting patch installed' + ); + SELECT throws_matching( + $$SELECT _v.register_patch( 'first_patch' )$$, + 'already applied', + 'Installation of patch that is already installed.' + ); + SELECT throws_matching( + $$SELECT _v.unregister_patch( 'first_patch' )$$, + 'it is required', + 'De-installation of patch that is required BY something ELSE.' + ); + SELECT throws_matching( + $$SELECT _v.unregister_patch( 'bad_patch' )$$, + 'is not installed', + 'De-installation of patch that is not installed' + ); + SELECT lives_ok( + $$SELECT _v.unregister_patch( 'third_patch' )$$, + 'De-Installation of patch.' + ); + +ROLLBACK; + diff --git a/uninstall.versioning.sql b/uninstall.versioning.sql @@ -0,0 +1,6 @@ +BEGIN; + +-- This file removes versioning support from database. +DROP SCHEMA _v CASCADE; + +COMMIT;