├── .gitignore ├── LICENSE ├── README.md ├── db.schema ├── create_schema.sql ├── migration_20150409.sql ├── migration_20150504.sql ├── migration_20150908.sql └── migration_20160610.sql ├── examples └── databases.json ├── htdocs ├── css │ └── common.css ├── index.php └── js │ ├── common.js │ └── mustache.js └── lib ├── composer.json ├── composer.lock ├── config └── .gitkeep ├── controllers └── RepositoryController.php ├── models ├── DBRepository.php ├── Database.php ├── DatabaseObject.php ├── Diff.php ├── IForwardable.php ├── Query.php ├── Seed.php ├── Sequence.php ├── StoredFunction.php ├── Table.php ├── Trigger.php ├── Type.php ├── User.php └── View.php └── views ├── define.haml ├── describe.haml ├── download_definitions.haml ├── error.haml ├── header.haml ├── index.haml ├── login.haml └── view_diff.haml /.gitignore: -------------------------------------------------------------------------------- 1 | /lib/vendor/* 2 | /lib/config/* 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Denis Milovanov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PostgresqlDeployerGUI 2 | ===================== 3 | 4 | ### Demo 5 | 6 | [http://pg.denismilovanov.net](http://pg.denismilovanov.net) 7 | E-mail `guest`, password `guest`. 8 | 9 | ### Intro 10 | 11 | PostgresqlDeployerGUI provides you web interface that simplifies deployment of PostgreSQL schema. 12 | Single database only, you can not deploy schema on two or more databases with sync commit. 13 | 14 | Generally speaking there are several approaches to schema deployment. 15 | 16 | Popular approach is to use migrations. You take your favorite migration tool built in your framework, 17 | write 'up' code, write 'down' code, and you are done. 18 | Advantages are obvious: one after you can deploy schema changes on any machine (local, staging, production) very easily. 19 | 20 | But I see 2 problems here: 21 | 22 | 1) migrations like `ALTER TABLE ADD COLUMN NOT NULL DEFAULT` on huge tables have to be performed manually in any case 23 | (fast `ALTER TABLE ADD COLUMN NULL` first, then `ALTER TABLE ALTER COLUMN SET DEFAULT`, then batch updates of null values), 24 | 25 | 2) stored functions and types up and down migrations lead to great code overhead (to add one line in function you have to double 26 | its source code, to change function signature - to drop its previous version, to change type signature - to redeploy its depending functions). 27 | 28 | That is why I don't believe in completely automatic migrations. They are suiteable only for small projects. 29 | 30 | Git (where I offer to store schema) is already migration-fashioned system. You may say `git checkout ` and 31 | get schema state at any time in the past to rollback if it is needed, or say `git pull` and see the difference between states 32 | (if you organize database objects storage). 33 | 34 | PostgreSQLDeployerGUI works with 8 database objects types: 35 | * tables, 36 | * seeds (data of system dictionaries, mappings, settings, etc), 37 | * types (PostgreSQL user types), 38 | * functions (PostgreSQL stored procedures), 39 | * sequencies, 40 | * triggers, 41 | * views (limited support, actually you may only download definitions), 42 | * arbitrary queries. 43 | 44 | Tables DDL's (`CREATE TABLE`, `CREATE INDEX`, `ALTER TABLE`) are committed into git and can be 45 | deployed automatically if 2 conditions are satisfied: 46 | * there is no significant `deletions` lines in commit, 47 | * there are no cyclic table references in `additions` lines among commits. 48 | 49 | If commit includes `deletions` it means that you have to apply corresponding changes by hands: system is not smart enough 50 | to produce 'revert' statements (`ALTER TABLE RENAME COLUMN`, `ALTER TABLE DROP COLUMN`, `DROP INDEX`, etc). 51 | 52 | If commits include cyclic references system also requires you to do manual deployment even if statements can be ordered the way avoiding cycles. 53 | 54 | Other cases are marked as 'Can be forwarded #N', it means that statements can be deployed in automatic mode. 55 | 56 | Seeds are deployed automatically via `DELETE - INSERT`. 57 | 58 | Types are deployed automatically by dropping old version of type with all dependent functions. 59 | Interface will show you dependencies, they will be included into deployment list. 60 | 61 | Functions are deployed automatically by dropping old version if signature or return type were changed. 62 | Then `CREATE OR REPLACE FUNCTION` is called. 63 | 64 | All changes are deployed in single transaction. You may exclude any object from list. 65 | 66 | ### Installation 67 | 68 | Clone repository: 69 | 70 | git clone https://github.com/denismilovanov/PostgresqlDeployerGUI.git 71 | 72 | Setup dependencies via composer: 73 | 74 | cd lib && composer update 75 | 76 | Setup your web server: 77 | 78 | http://silex.sensiolabs.org/doc/web_servers.html 79 | 80 | Route all queries to `htdocs/index.php`. 81 | 82 | Or use built-in php server: 83 | 84 | php -S 0.0.0.0:8000 -t htdocs/ 85 | 86 | Perform on your database(s) (psql): 87 | 88 | \i db.schema/create_schema.sql 89 | 90 | Add new user (psql will show how). 91 | 92 | Create databases config file: 93 | 94 | nano lib/config/databases.json 95 | 96 | Example: 97 | 98 | { 99 | "databases": { 100 | "db1": { 101 | "name": "DB 1", 102 | "credentials": { 103 | "host": "localhost", 104 | "port": "5432", 105 | "user_name": "user1", 106 | "user_password": "pass1", 107 | "db_name": "db1" 108 | }, 109 | "git_root": "/home/user/work/project/db1_git_root/" 110 | 111 | ,"schemas_path": "dir1/dir2/schemas/" #optinal, 'schemas/' by default 112 | 113 | ,"settings": { 114 | #optional settings overloading global 115 | #see settings.json 116 | } 117 | }, 118 | "db2": { 119 | "name": "DB 2", 120 | "credentials": { 121 | "host": "localhost", 122 | "port": "5432", 123 | "user_name": "user2", 124 | "user_password": "pass2", 125 | "db_name": "db2" 126 | }, 127 | "git_root": "/home/user/work/project/db2_git_root/" 128 | } 129 | } 130 | } 131 | 132 | All `git-root's` directories should be opened for write to user your server (FPM-workers for example) is running at, 'cause interface will 133 | perform write-commands such as `git checkout`. 134 | 135 | Create settings file: 136 | 137 | nano lib/config/settings.json 138 | 139 | Example: 140 | 141 | { 142 | "settings": { 143 | "not_in_git": { 144 | "active": true, 145 | "exclude_regexp": "(public\\.not_under_git_table)|(main\\.table_number_\\d+)" 146 | }, 147 | "reload_and_apply": { 148 | "active": true, 149 | "ignore_manual": false 150 | }, 151 | "plpgsql_check": { 152 | "active": false, 153 | "exclude_regexp": "tricky_functions_schema\\.tricky_", 154 | "targets": "all" 155 | }, 156 | "paths": { 157 | "pg_bin": "/usr/lib/postgresql/%v/bin/" 158 | }, 159 | "commits_list": { 160 | "limit": 10 161 | }, 162 | "interface": { 163 | "sticky_control_buttons": false 164 | } 165 | } 166 | } 167 | 168 | Available settings: 169 | 170 | - not_in_git - this option tells if non-git database objects are shown (they will be marked as `NOT IN GIT`), 171 | - reload_and_apply - show 'Reload and apply' button (makes sense for development purposes only, not in production), 172 | - plpgsql_check - this option runs checking of all stored functions after deployment but before final commit (checking is performed by [plpgsql_check extension](https://github.com/okbob/plpgsql_check.git) 173 | - active - use plpgsql_check or not. boolean, default false 174 | - exclude_regexp - functions, that match this regexp, will be not checked 175 | - targets - specify check functions: "all" for check all existing functions (default) or "only_selected" for check only functions for deploy 176 | - paths 177 | - pg_bin - path to psql and pg_dump executables (%v will be replaced to MAJOR.MINOR version of current database you work at), 178 | - commits_list 179 | - limit - max amount of commits to show, 180 | - interface - some interface features. 181 | 182 | You may omit any of these options. 183 | 184 | ### Repository 185 | 186 | Your schema repository should have structure like this: 187 | 188 | repository_root/ 189 | schemas/ 190 | schema1/ 191 | functions/ 192 | function_name1.sql 193 | function_name2.sql 194 | ... 195 | seeds/ 196 | seed_table_name1.sql 197 | seed_table_name2.sql 198 | ... 199 | tables/ 200 | table_name1.sql 201 | table_name2.sql 202 | ... 203 | types/ 204 | type_name1.sql 205 | type_name2.sql 206 | ... 207 | sequences/ 208 | sequence_name1.sql 209 | sequence_name2.sql 210 | ... 211 | triggers/ 212 | this_schema_table_name1.trigger_name1.sql 213 | this_schema_table_name2.trigger_name2.sql 214 | ... 215 | queries_before/ 216 | 01_query.sql 217 | 02_query.sql 218 | ... 219 | queries_after/ 220 | 01_query.sql 221 | 02_query.sql 222 | ... 223 | schema2/ 224 | functions/ 225 | seeds/ 226 | tables/ 227 | types/ 228 | sequences/ 229 | triggers/ 230 | queries_before/ 231 | queries_after/ 232 | ... 233 | other_directories/ 234 | ... 235 | 236 | ### Seeds 237 | 238 | Expected structure of seed file `seed_table_name.sql`: 239 | 240 | BEGIN; 241 | 242 | DELETE FROM seed_table_name; 243 | 244 | INSERT INTO seed_table_name (...) 245 | VALUES (...); 246 | 247 | COMMIT; 248 | 249 | Please, do not use `TRUNCATE`, it is not MVCC safe. 250 | 251 | If you use foreign key referenced to seed table you should make it `DERERRABLE INITIALLY DEFERRED`, in other case `DELETE FROM` will throw an error. 252 | 253 | This structure allows you to deploy seed in manual mode with single transaction via simple copying the code. 254 | 255 | ### Stored functions (procedures) 256 | 257 | Expected structure of function file `function_name.sql` (usual pg `CREATE OR REPLACE FUNCTION`): 258 | 259 | CREATE OR REPLACE FUNCTION function_name( 260 | 261 | ) 262 | RETURNS ret_value_type AS 263 | $BODY$ 264 | DECLARE 265 | BEGIN 266 | 267 | END 268 | $BODY$ 269 | LANGUAGE plpgsql; 270 | 271 | 272 | These cases related to functions currently are not supported: 273 | 274 | 1. Functions for functional indexes and handling triggers events are forbidden to be dropped automatically. 275 | 2. Functions overloading. It is not allowed to have more than one functions with the same or different names in one file. 276 | 277 | When you use custom type name you should also provide schema name (even if schema is in `search_path`): 278 | 279 | DECLARE 280 | i_count integer; 281 | r_element queues.t_queue_element; 282 | 283 | ### Types 284 | 285 | Expected structure of type file `type_name.sql` (usual pg `CREATE TYPE`): 286 | 287 | CREATE TYPE type_name AS ( 288 | 289 | ); 290 | 291 | ### Sequences 292 | 293 | Expected structure of sequence file `sequnces_name.sql`: 294 | 295 | CREATE SEQUENCE sequence_name []; 296 | 297 | ### Triggers 298 | 299 | Trigger file name should consist of table name and trigger_name (`table_name.trigger_name.sql`) and contain something like this: 300 | 301 | CREATE TRIGGER trigger_name 302 | AFTER INSERT ON current_schema_name.table_name 303 | FOR EACH ROW 304 | EXECUTE PROCEDURE arbitrary_schema.trigger_procedure(); 305 | 306 | Trigger will be dropped and deployed again as soon as file content is changed (exactly like `seed` objects). 307 | 308 | ### Notes 309 | 310 | 1. Do not forget that `CREATE / DROP INDEX CONCURRENTLY` cannot be performed within a transaction block. 311 | 312 | -------------------------------------------------------------------------------- /db.schema/create_schema.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | ---------------------------------------------------------------------------- 4 | -- create schema to store deployer objects 5 | 6 | CREATE OR REPLACE FUNCTION __temp1() RETURNS varchar AS 7 | $BODY$ 8 | BEGIN 9 | 10 | BEGIN 11 | CREATE SCHEMA postgresql_deployer; 12 | RETURN 'schema postgresql_deployer created.'; 13 | EXCEPTION WHEN others THEN NULL; END; 14 | 15 | RETURN 'schema postgresql_deployer already exists.'; 16 | 17 | END 18 | $BODY$ LANGUAGE plpgsql VOLATILE; 19 | 20 | SELECT __temp1(); 21 | 22 | DROP FUNCTION __temp1(); 23 | 24 | ---------------------------------------------------------------------------- 25 | -- types of migrated objects ... 26 | 27 | CREATE TABLE IF NOT EXISTS postgresql_deployer.migrations_objects ( 28 | id integer PRIMARY KEY, 29 | index varchar(32) NOT NULL, 30 | rank integer NOT NULL, 31 | params json NOT NULL 32 | ); 33 | 34 | DELETE FROM postgresql_deployer.migrations_objects; 35 | 36 | -- are: 37 | INSERT INTO postgresql_deployer.migrations_objects 38 | VALUES 39 | (1, 'tables', 3, '{"is_forwardable":true}'), 40 | (2, 'seeds', 4, '{"is_forwardable":false}'), 41 | (3, 'types', 5, '{"is_forwardable":false}'), 42 | (4, 'functions', 6, '{"is_forwardable":false}'), 43 | (5, 'sequences', 2, '{"is_forwardable":true}'), 44 | (6, 'queries_before', 1, '{"is_forwardable":true}'), 45 | (7, 'queries_after', 100, '{"is_forwardable":true}'), 46 | (8, 'triggers', 7, '{"is_forwardable":false}'), 47 | (9, 'views', 8, '{"is_forwardable":false}'); 48 | 49 | ---------------------------------------------------------------------------- 50 | -- actual information about objects deployed 51 | 52 | CREATE TABLE IF NOT EXISTS postgresql_deployer.migrations ( 53 | schema_name varchar(255) NOT NULL, 54 | type_id integer NOT NULL REFERENCES postgresql_deployer.migrations_objects (id) 55 | DEFERRABLE INITIALLY DEFERRED, 56 | object_name varchar(255) NOT NULL, 57 | hash varchar(32) NOT NULL, 58 | content text NOT NULL, 59 | CONSTRAINT migrations_pkey PRIMARY KEY (schema_name, type_id, object_name) 60 | ); 61 | 62 | ---------------------------------------------------------------------------- 63 | -- drops all functions within schema with given name 64 | 65 | CREATE OR REPLACE FUNCTION postgresql_deployer.drop_all_functions_by_name( 66 | f_function_name varchar, 67 | s_schema_name varchar 68 | ) 69 | RETURNS void AS 70 | $BODY$ 71 | DECLARE 72 | s_sql varchar; 73 | BEGIN 74 | 75 | FOR s_sql IN SELECT 'DROP FUNCTION ' || n.nspname || '.' || p.proname 76 | || '(' || pg_catalog.pg_get_function_identity_arguments(p.oid) || ');' 77 | 78 | FROM pg_catalog.pg_proc AS p 79 | 80 | LEFT JOIN pg_catalog.pg_namespace n ON 81 | n.oid = p.pronamespace 82 | 83 | WHERE p.proname = f_function_name AND 84 | n.nspname = s_schema_name 85 | 86 | LOOP 87 | 88 | IF s_sql IS NOT NULL THEN 89 | EXECUTE s_sql; 90 | END IF; 91 | 92 | END LOOP; 93 | 94 | END 95 | $BODY$ 96 | LANGUAGE plpgsql VOLATILE; 97 | 98 | ---------------------------------------------------------------------------- 99 | -- abstract database object 100 | 101 | DROP TYPE IF EXISTS postgresql_deployer.database_object CASCADE; 102 | 103 | CREATE TYPE postgresql_deployer.database_object AS ( 104 | database_name varchar, 105 | schema_name varchar, 106 | object_index varchar, 107 | object_name varchar, 108 | additional_sql text 109 | ); 110 | 111 | ---------------------------------------------------------------------------- 112 | -- drops type with all dependencies 113 | 114 | CREATE OR REPLACE FUNCTION postgresql_deployer.drop_type_with_dependent_functions( 115 | s_database_name varchar, 116 | s_schema_name varchar, 117 | s_type_name varchar 118 | ) 119 | RETURNS SETOF postgresql_deployer.database_object AS 120 | $BODY$ 121 | DECLARE 122 | r_func postgresql_deployer.database_object; 123 | BEGIN 124 | 125 | FOR r_func IN SELECT * 126 | FROM postgresql_deployer.get_type_dependent_functions(s_database_name, s_schema_name, s_type_name) 127 | 128 | LOOP 129 | 130 | IF r_func.additional_sql IS NOT NULL THEN 131 | r_func.additional_sql := 'DROP FUNCTION ' || r_func.additional_sql; 132 | EXECUTE r_func.additional_sql; 133 | RETURN NEXT r_func; 134 | END IF; 135 | 136 | END LOOP; 137 | 138 | -- drop type, all dependencies were dropped above 139 | EXECUTE 'DROP TYPE IF EXISTS ' || s_schema_name || '.' || s_type_name || ' ;'; 140 | 141 | END 142 | $BODY$ 143 | LANGUAGE plpgsql VOLATILE; 144 | 145 | ---------------------------------------------------------------------------- 146 | -- get types dependencies 147 | 148 | CREATE OR REPLACE FUNCTION postgresql_deployer.get_type_dependent_functions( 149 | s_database_name varchar, 150 | s_schema_name varchar, 151 | s_type_name varchar 152 | ) 153 | RETURNS SETOF postgresql_deployer.database_object AS 154 | $BODY$ 155 | DECLARE 156 | r_func postgresql_deployer.database_object; 157 | i_type_id integer; 158 | i_type_array_id integer; 159 | BEGIN 160 | 161 | -- oid of type 162 | SELECT reltype INTO i_type_id 163 | FROM pg_class 164 | WHERE relname = s_type_name AND 165 | relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = s_schema_name); 166 | 167 | -- oid of type[] 168 | SELECT typarray INTO i_type_array_id 169 | FROM pg_type 170 | WHERE typname = s_type_name AND 171 | typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = s_schema_name); 172 | 173 | -- to prevent NULL in array, it breaks &&, see below 174 | i_type_id := coalesce(i_type_id, 0); 175 | i_type_array_id := coalesce(i_type_array_id, 0); 176 | 177 | -- search for functions 178 | FOR r_func IN SELECT s_database_name, 179 | n.nspname::varchar AS schema_name, 180 | 'functions' AS object_type, 181 | p.proname::varchar AS function_name, 182 | n.nspname || '.' || p.proname 183 | || '(' || pg_catalog.pg_get_function_identity_arguments(p.oid) || ');' AS additional_sql 184 | 185 | FROM pg_catalog.pg_proc AS p 186 | 187 | LEFT JOIN pg_catalog.pg_namespace n ON 188 | n.oid = p.pronamespace 189 | 190 | WHERE prorettype IN (i_type_id, i_type_array_id) OR 191 | proargtypes::int[] && array[i_type_id, i_type_array_id] OR 192 | prosrc ~ ('DECLARE.+?' || s_schema_name || '\.' || s_type_name || '(;|\[).+?BEGIN') 193 | 194 | LOOP 195 | 196 | IF r_func.additional_sql IS NOT NULL THEN 197 | RETURN NEXT r_func; 198 | END IF; 199 | 200 | END LOOP; 201 | 202 | END 203 | $BODY$ 204 | LANGUAGE plpgsql VOLATILE; 205 | 206 | ---------------------------------------------------------------------------- 207 | -- get tables dependencies 208 | 209 | CREATE OR REPLACE FUNCTION postgresql_deployer.get_table_dependent_functions( 210 | s_database_name varchar, 211 | s_schema_name varchar, 212 | s_table_name varchar 213 | ) 214 | RETURNS SETOF postgresql_deployer.database_object AS 215 | $BODY$ 216 | DECLARE 217 | r_func postgresql_deployer.database_object; 218 | i_table_id integer; 219 | i_table_array_id integer; 220 | BEGIN 221 | 222 | -- oid of table 223 | SELECT reltype INTO i_table_id 224 | FROM pg_class 225 | WHERE relname = s_table_name AND 226 | relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = s_schema_name); 227 | 228 | -- oid of table[] 229 | SELECT typarray INTO i_table_array_id 230 | FROM pg_type 231 | WHERE typname = s_table_name AND 232 | typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = s_schema_name); 233 | 234 | -- to prevent NULL in array, it breaks &&, see below 235 | i_table_id := coalesce(i_table_id, 0); 236 | i_table_array_id := coalesce(i_table_array_id, 0); 237 | 238 | -- search for functions 239 | FOR r_func IN SELECT s_database_name, 240 | n.nspname::varchar AS schema_name, 241 | 'functions' AS object_table, 242 | p.proname::varchar AS function_name, 243 | n.nspname || '.' || p.proname 244 | || '(' || pg_catalog.pg_get_function_identity_arguments(p.oid) || ');' AS additional_sql 245 | 246 | FROM pg_catalog.pg_proc AS p 247 | 248 | LEFT JOIN pg_catalog.pg_namespace n ON 249 | n.oid = p.pronamespace 250 | 251 | WHERE prorettype IN (i_table_id, i_table_array_id) OR 252 | proargtypes::int[] && array[i_table_id, i_table_array_id] OR 253 | prosrc ~ ('DECLARE.+?' || s_schema_name || '\.' || s_table_name || '(;|\[).+?BEGIN') 254 | 255 | LOOP 256 | 257 | IF r_func.additional_sql IS NOT NULL THEN 258 | RETURN NEXT r_func; 259 | END IF; 260 | 261 | END LOOP; 262 | 263 | END 264 | $BODY$ 265 | LANGUAGE plpgsql VOLATILE; 266 | 267 | ---------------------------------------------------------------------------- 268 | -- updates or inserts migration record 269 | 270 | CREATE OR REPLACE FUNCTION postgresql_deployer.upsert_migration( 271 | i_user_id bigint, 272 | s_commit_hash varchar, 273 | s_schema_name varchar, 274 | i_type_id integer, 275 | s_object_name varchar, 276 | s_hash varchar, 277 | t_content text 278 | ) 279 | RETURNS void AS 280 | $BODY$ 281 | DECLARE 282 | 283 | BEGIN 284 | 285 | -- log 286 | INSERT INTO postgresql_deployer.migration_log 287 | (user_id, schema_name, type_id, object_name, commit_hash) 288 | VALUES ( 289 | i_user_id, 290 | s_schema_name, 291 | i_type_id, 292 | s_object_name, 293 | s_commit_hash 294 | ); 295 | 296 | -- upsert 297 | BEGIN 298 | 299 | INSERT INTO postgresql_deployer.migrations 300 | SELECT s_schema_name, 301 | i_type_id, 302 | s_object_name, 303 | s_hash, 304 | t_content; 305 | 306 | EXCEPTION WHEN unique_violation THEN 307 | 308 | UPDATE postgresql_deployer.migrations 309 | SET hash = s_hash, 310 | content = t_content 311 | WHERE schema_name = s_schema_name AND 312 | type_id = i_type_id AND 313 | object_name = s_object_name; 314 | 315 | END; 316 | 317 | END 318 | $BODY$ 319 | LANGUAGE plpgsql VOLATILE; 320 | 321 | ---------------------------------------------------------------------------- 322 | -- table of users 323 | 324 | CREATE TABLE IF NOT EXISTS postgresql_deployer.users ( 325 | id bigserial PRIMARY KEY, 326 | name varchar(255) NOT NULL UNIQUE, 327 | email varchar(255) NOT NULL UNIQUE, 328 | password_enc varchar(64), 329 | salt varchar(32), 330 | cookie varchar(32) UNIQUE, 331 | last_seen_at timestamp without time zone NULL 332 | ); 333 | 334 | ---------------------------------------------------------------------------- 335 | -- new user addition 336 | 337 | CREATE OR REPLACE FUNCTION postgresql_deployer.add_user( 338 | s_name varchar, 339 | s_email varchar, 340 | s_password varchar 341 | ) 342 | RETURNS bigint AS 343 | $BODY$ 344 | DECLARE 345 | s_salt varchar; 346 | i_user_id bigint; 347 | BEGIN 348 | 349 | s_salt := md5(now()::varchar || s_email); 350 | 351 | BEGIN 352 | 353 | INSERT INTO postgresql_deployer.users 354 | (name, email, password_enc, salt) 355 | VALUES 356 | (s_name, s_email, md5(s_password || s_salt), s_salt) 357 | RETURNING id INTO i_user_id; 358 | 359 | EXCEPTION WHEN unique_violation THEN 360 | RETURN 0; 361 | END; 362 | 363 | RETURN i_user_id; 364 | 365 | END 366 | $BODY$ 367 | LANGUAGE plpgsql VOLATILE; 368 | 369 | ---------------------------------------------------------------------------- 370 | -- login 371 | 372 | CREATE OR REPLACE FUNCTION postgresql_deployer.login_user( 373 | s_email varchar, 374 | s_password_given varchar 375 | ) 376 | RETURNS postgresql_deployer.users AS 377 | $BODY$ 378 | DECLARE 379 | r_record postgresql_deployer.users; 380 | BEGIN 381 | 382 | UPDATE postgresql_deployer.users 383 | SET cookie = md5(now()::varchar || password_enc), 384 | last_seen_at = now() 385 | WHERE email = s_email AND 386 | md5(s_password_given || salt) = password_enc 387 | RETURNING * INTO r_record; 388 | 389 | RETURN r_record; 390 | 391 | END 392 | $BODY$ 393 | LANGUAGE plpgsql VOLATILE; 394 | 395 | ---------------------------------------------------------------------------- 396 | -- authorize 397 | 398 | CREATE OR REPLACE FUNCTION postgresql_deployer.authorize_user( 399 | s_cookie varchar 400 | ) 401 | RETURNS postgresql_deployer.users AS 402 | $BODY$ 403 | DECLARE 404 | r_record postgresql_deployer.users; 405 | BEGIN 406 | 407 | SELECT * INTO r_record 408 | FROM postgresql_deployer.users 409 | WHERE cookie = s_cookie AND 410 | last_seen_at >= now() - interval '1 day'; 411 | 412 | RETURN r_record; 413 | 414 | END 415 | $BODY$ 416 | LANGUAGE plpgsql STABLE; 417 | 418 | 419 | ---------------------------------------------------------------------------- 420 | -- log 421 | 422 | CREATE TABLE IF NOT EXISTS postgresql_deployer.migration_log ( 423 | id bigserial PRIMARY KEY, 424 | user_id bigint REFERENCES postgresql_deployer.users (id), 425 | schema_name varchar(255) NOT NULL, 426 | type_id integer NOT NULL REFERENCES postgresql_deployer.migrations_objects (id) 427 | DEFERRABLE INITIALLY DEFERRED, 428 | object_name varchar(255) NOT NULL, 429 | commit_hash varchar(40) NOT NULL, 430 | deployed_at timestamp without time zone NOT NULL DEFAULT now() 431 | ); 432 | 433 | ---------------------------------------------------------------------------- 434 | -- create test function for plpgsql_check 435 | 436 | CREATE OR REPLACE FUNCTION postgresql_deployer.test_plpgsql_check_function() RETURNS void AS 437 | $BODY$ 438 | BEGIN 439 | 440 | SELECT * FROM postgresql_deployer.non_existing; 441 | 442 | END 443 | $BODY$ LANGUAGE plpgsql STABLE; 444 | 445 | ---------------------------------------------------------------------------- 446 | -- test for plpgsql_check 447 | 448 | CREATE OR REPLACE FUNCTION postgresql_deployer.test_plpgsql_check_extension() RETURNS text AS 449 | $BODY$ 450 | DECLARE 451 | o_oid oid; 452 | BEGIN 453 | 454 | SELECT oid INTO o_oid 455 | FROM pg_proc 456 | WHERE proname = 'test_plpgsql_check_function'; 457 | 458 | IF NOT FOUND THEN 459 | RETURN 'There is no postgresql_deployer.test_plpgsql_check_function function'; 460 | END IF; 461 | 462 | -- perform function check 463 | BEGIN 464 | PERFORM plpgsql_check_function(o_oid); 465 | EXCEPTION WHEN others THEN 466 | -- extension fails 467 | RETURN 'plpgsql_check_function is NOT ready'; 468 | END; 469 | 470 | RETURN 'plpgsql_check_function is ready'; 471 | 472 | END 473 | $BODY$ LANGUAGE plpgsql VOLATILE; 474 | 475 | ---------------------------------------------------------------------------- 476 | -- that's all 477 | 478 | SELECT 'type "COMMIT".'; 479 | SELECT 'then "SELECT postgresql_deployer.add_user(name, email, pass);" to create user.'; 480 | SELECT postgresql_deployer.test_plpgsql_check_extension(); 481 | 482 | -------------------------------------------------------------------------------- /db.schema/migration_20150409.sql: -------------------------------------------------------------------------------- 1 | ---------------------------------------------------------------------------- 2 | -- this script contains: 3 | -- 1) new function postgresql_deployer.get_table_dependent_functions 4 | -- 2) bugfix in postgresql_deployer.get_type_dependent_functions function 5 | -- (see conditions in WHERE statement) 6 | 7 | 8 | BEGIN; 9 | 10 | ---------------------------------------------------------------------------- 11 | -- get types dependencies 12 | 13 | CREATE OR REPLACE FUNCTION postgresql_deployer.get_type_dependent_functions( 14 | s_database_name varchar, 15 | s_schema_name varchar, 16 | s_type_name varchar 17 | ) 18 | RETURNS SETOF postgresql_deployer.database_object AS 19 | $BODY$ 20 | DECLARE 21 | r_func postgresql_deployer.database_object; 22 | i_type_id integer; 23 | i_type_array_id integer; 24 | BEGIN 25 | 26 | -- oid of type 27 | SELECT reltype INTO i_type_id 28 | FROM pg_class 29 | WHERE relname = s_type_name AND 30 | relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = s_schema_name); 31 | 32 | -- oid of type[] 33 | SELECT typarray INTO i_type_array_id 34 | FROM pg_type 35 | WHERE typname = s_type_name AND 36 | typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = s_schema_name); 37 | 38 | -- search for functions 39 | FOR r_func IN SELECT s_database_name, 40 | n.nspname::varchar AS schema_name, 41 | 'functions' AS object_type, 42 | p.proname::varchar AS function_name, 43 | n.nspname || '.' || p.proname 44 | || '(' || pg_catalog.pg_get_function_identity_arguments(p.oid) || ');' AS additional_sql 45 | 46 | FROM pg_catalog.pg_proc AS p 47 | 48 | LEFT JOIN pg_catalog.pg_namespace n ON 49 | n.oid = p.pronamespace 50 | 51 | WHERE -- returns type, array of type, setof of type (see also pg_proc.proretset) 52 | prorettype IN (i_type_id, i_type_array_id) OR 53 | -- uses type and type[] in arguments 54 | proargtypes::int[] && array[i_type_id, i_type_array_id] OR 55 | -- constains type and type[] in DECLARE section 56 | prosrc ~ ('DECLARE.+?' || s_schema_name || '\.' || s_type_name || '(;|\[).+?BEGIN') 57 | 58 | LOOP 59 | 60 | IF r_func.additional_sql IS NOT NULL THEN 61 | RETURN NEXT r_func; 62 | END IF; 63 | 64 | END LOOP; 65 | 66 | END 67 | $BODY$ 68 | LANGUAGE plpgsql VOLATILE; 69 | 70 | ---------------------------------------------------------------------------- 71 | -- get tables dependencies 72 | 73 | CREATE OR REPLACE FUNCTION postgresql_deployer.get_table_dependent_functions( 74 | s_database_name varchar, 75 | s_schema_name varchar, 76 | s_table_name varchar 77 | ) 78 | RETURNS SETOF postgresql_deployer.database_object AS 79 | $BODY$ 80 | DECLARE 81 | r_func postgresql_deployer.database_object; 82 | i_table_id integer; 83 | i_table_array_id integer; 84 | BEGIN 85 | 86 | -- oid of table row type 87 | SELECT reltype INTO i_table_id 88 | FROM pg_class 89 | WHERE relname = s_table_name AND 90 | relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = s_schema_name); 91 | 92 | -- oid of table row type[] 93 | SELECT typarray INTO i_table_array_id 94 | FROM pg_type 95 | WHERE typname = s_table_name AND 96 | typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = s_schema_name); 97 | 98 | -- search for functions 99 | FOR r_func IN SELECT s_database_name, 100 | n.nspname::varchar AS schema_name, 101 | 'functions' AS object_table, 102 | p.proname::varchar AS function_name, 103 | n.nspname || '.' || p.proname 104 | || '(' || pg_catalog.pg_get_function_identity_arguments(p.oid) || ');' AS additional_sql 105 | 106 | FROM pg_catalog.pg_proc AS p 107 | 108 | LEFT JOIN pg_catalog.pg_namespace n ON 109 | n.oid = p.pronamespace 110 | 111 | WHERE -- returns table row, array of table row, setof of table rows (see also pg_proc.proretset) 112 | prorettype IN (i_table_id, i_table_array_id) OR 113 | -- uses table row and table row[] in arguments 114 | proargtypes::int[] && array[i_table_id, i_table_array_id] OR 115 | -- constains table row type and table row type[] in DECLARE section 116 | prosrc ~ ('DECLARE.+?' || s_schema_name || '\.' || s_table_name || '(;|\[).+?BEGIN') 117 | 118 | LOOP 119 | 120 | IF r_func.additional_sql IS NOT NULL THEN 121 | RETURN NEXT r_func; 122 | END IF; 123 | 124 | END LOOP; 125 | 126 | END 127 | $BODY$ 128 | LANGUAGE plpgsql VOLATILE; 129 | 130 | COMMIT; 131 | 132 | -------------------------------------------------------------------------------- /db.schema/migration_20150504.sql: -------------------------------------------------------------------------------- 1 | ---------------------------------------------------------------------------- 2 | -- this script contains: 3 | -- 1) triggers support 4 | 5 | 6 | BEGIN; 7 | 8 | DELETE FROM postgresql_deployer.migrations_objects 9 | WHERE id = 8; 10 | 11 | INSERT INTO postgresql_deployer.migrations_objects 12 | SELECT 8, 'triggers', 7, '{"is_forwardable":false}'; 13 | 14 | COMMIT; 15 | 16 | -------------------------------------------------------------------------------- /db.schema/migration_20150908.sql: -------------------------------------------------------------------------------- 1 | ---------------------------------------------------------------------------- 2 | -- this script contains: 3 | -- 1) fixes in get_*_dependent_functions(...) procedures, they throw exception 4 | -- "ERROR: array must not contain nulls" under some rare circumstances 5 | 6 | 7 | BEGIN; 8 | 9 | 10 | CREATE OR REPLACE FUNCTION postgresql_deployer.get_type_dependent_functions( 11 | s_database_name varchar, 12 | s_schema_name varchar, 13 | s_type_name varchar 14 | ) 15 | RETURNS SETOF postgresql_deployer.database_object AS 16 | $BODY$ 17 | DECLARE 18 | r_func postgresql_deployer.database_object; 19 | i_type_id integer; 20 | i_type_array_id integer; 21 | BEGIN 22 | 23 | -- oid of type 24 | SELECT reltype INTO i_type_id 25 | FROM pg_class 26 | WHERE relname = s_type_name AND 27 | relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = s_schema_name); 28 | 29 | -- oid of type[] 30 | SELECT typarray INTO i_type_array_id 31 | FROM pg_type 32 | WHERE typname = s_type_name AND 33 | typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = s_schema_name); 34 | 35 | -- to prevent NULL in array, it breaks &&, see below 36 | i_type_id := coalesce(i_type_id, 0); 37 | i_type_array_id := coalesce(i_type_array_id, 0); 38 | 39 | -- search for functions 40 | FOR r_func IN SELECT s_database_name, 41 | n.nspname::varchar AS schema_name, 42 | 'functions' AS object_type, 43 | p.proname::varchar AS function_name, 44 | n.nspname || '.' || p.proname 45 | || '(' || pg_catalog.pg_get_function_identity_arguments(p.oid) || ');' AS additional_sql 46 | 47 | FROM pg_catalog.pg_proc AS p 48 | 49 | LEFT JOIN pg_catalog.pg_namespace n ON 50 | n.oid = p.pronamespace 51 | 52 | WHERE prorettype IN (i_type_id, i_type_array_id) OR 53 | proargtypes::int[] && array[i_type_id, i_type_array_id] OR 54 | prosrc ~ ('DECLARE.+?' || s_schema_name || '\.' || s_type_name || '(;|\[).+?BEGIN') 55 | 56 | LOOP 57 | 58 | IF r_func.additional_sql IS NOT NULL THEN 59 | RETURN NEXT r_func; 60 | END IF; 61 | 62 | END LOOP; 63 | 64 | END 65 | $BODY$ 66 | LANGUAGE plpgsql VOLATILE; 67 | 68 | 69 | 70 | CREATE OR REPLACE FUNCTION postgresql_deployer.get_table_dependent_functions( 71 | s_database_name varchar, 72 | s_schema_name varchar, 73 | s_table_name varchar 74 | ) 75 | RETURNS SETOF postgresql_deployer.database_object AS 76 | $BODY$ 77 | DECLARE 78 | r_func postgresql_deployer.database_object; 79 | i_table_id integer; 80 | i_table_array_id integer; 81 | BEGIN 82 | 83 | -- oid of table 84 | SELECT reltype INTO i_table_id 85 | FROM pg_class 86 | WHERE relname = s_table_name AND 87 | relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = s_schema_name); 88 | 89 | -- oid of table[] 90 | SELECT typarray INTO i_table_array_id 91 | FROM pg_type 92 | WHERE typname = s_table_name AND 93 | typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = s_schema_name); 94 | 95 | -- to prevent NULL in array, it breaks &&, see below 96 | i_table_id := coalesce(i_table_id, 0); 97 | i_table_array_id := coalesce(i_table_array_id, 0); 98 | 99 | -- search for functions 100 | FOR r_func IN SELECT s_database_name, 101 | n.nspname::varchar AS schema_name, 102 | 'functions' AS object_table, 103 | p.proname::varchar AS function_name, 104 | n.nspname || '.' || p.proname 105 | || '(' || pg_catalog.pg_get_function_identity_arguments(p.oid) || ');' AS additional_sql 106 | 107 | FROM pg_catalog.pg_proc AS p 108 | 109 | LEFT JOIN pg_catalog.pg_namespace n ON 110 | n.oid = p.pronamespace 111 | 112 | WHERE prorettype IN (i_table_id, i_table_array_id) OR 113 | proargtypes::int[] && array[i_table_id, i_table_array_id] OR 114 | prosrc ~ ('DECLARE.+?' || s_schema_name || '\.' || s_table_name || '(;|\[).+?BEGIN') 115 | 116 | LOOP 117 | 118 | IF r_func.additional_sql IS NOT NULL THEN 119 | RETURN NEXT r_func; 120 | END IF; 121 | 122 | END LOOP; 123 | 124 | END 125 | $BODY$ 126 | LANGUAGE plpgsql VOLATILE; 127 | 128 | 129 | COMMIT; 130 | 131 | -------------------------------------------------------------------------------- /db.schema/migration_20160610.sql: -------------------------------------------------------------------------------- 1 | ---------------------------------------------------------------------------- 2 | -- this script contains: 3 | -- 1) views support 4 | 5 | 6 | BEGIN; 7 | 8 | DELETE FROM postgresql_deployer.migrations_objects 9 | WHERE id = 9; 10 | 11 | INSERT INTO postgresql_deployer.migrations_objects 12 | SELECT 9, 'views', 7, '{"is_forwardable":false}'; 13 | 14 | COMMIT; 15 | 16 | -------------------------------------------------------------------------------- /examples/databases.json: -------------------------------------------------------------------------------- 1 | { 2 | "databases": { 3 | "db1": { 4 | "index": "db1", 5 | "name": "DB 1", 6 | "credentials": { 7 | "host": "localhost", 8 | "port": "5432", 9 | "user_name": "user1", 10 | "user_password": "pass1", 11 | "db_name": "db1" 12 | }, 13 | "git_root": "/home/user/work/project/db1_git_root/" 14 | }, 15 | "db2": { 16 | "index": "db2", 17 | "name": "DB 2", 18 | "credentials": { 19 | "host": "localhost", 20 | "port": "5432", 21 | "user_name": "user2", 22 | "user_password": "pass2", 23 | "db_name": "db2" 24 | }, 25 | "git_root": "/home/user/work/project/db2_git_root/" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /htdocs/css/common.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 20px; 3 | background-color: rgba(211, 211, 211, 0.26); 4 | } 5 | 6 | ::-webkit-scrollbar { 7 | width: 3em; 8 | height: 2em; 9 | } 10 | 11 | ::-webkit-scrollbar-button { 12 | background: #ccc; 13 | } 14 | 15 | ::-webkit-scrollbar-track-piece { 16 | background: #eee; 17 | } 18 | 19 | ::-webkit-scrollbar-thumb { 20 | background: #ddd; 21 | }​ 22 | 23 | .dummy { 24 | 25 | } 26 | 27 | .pointer { 28 | cursor: pointer; 29 | } 30 | 31 | .navbar-brand { 32 | font-size: 22px; 33 | } 34 | 35 | .color-red { 36 | color: red; 37 | } 38 | 39 | .container { 40 | margin-left: 0px; 41 | } 42 | 43 | .commits td { 44 | padding: 3px; 45 | vertical-align: middle; 46 | border-bottom: 1px dashed rgba(192, 192, 192, 0.24); 47 | } 48 | 49 | .commit { 50 | } 51 | 52 | .commit-active { 53 | font-weight: bold; 54 | } 55 | 56 | table { 57 | table-layout: fixed; 58 | } 59 | 60 | .pre { 61 | font-family: Courier; 62 | white-space: pre; 63 | } 64 | 65 | ins, del{ 66 | text-decoration: none; 67 | } 68 | 69 | ins { 70 | color: green; 71 | } 72 | 73 | del { 74 | color: red; 75 | } 76 | 77 | tt { 78 | color: blue; 79 | font-weight: bold; 80 | } 81 | 82 | .item { 83 | } 84 | 85 | .item-name { 86 | width: 30%; 87 | border-right: 1px silver dotted; 88 | font-size: 110%; 89 | word-wrap: break-word; 90 | } 91 | 92 | .item-dependent-name { 93 | border-right: 1px silver dotted; 94 | padding-left: 100px; 95 | } 96 | 97 | .item-view-diff { 98 | width: 20%; 99 | border-right: 1px silver dotted; 100 | cursor: pointer; 101 | } 102 | 103 | .item-view-dependencies { 104 | width: 30%; 105 | border-right: 1px silver dotted; 106 | } 107 | 108 | .item-apply { 109 | width: 20%; 110 | cursor: pointer; 111 | } 112 | 113 | .table-diff { 114 | table-layout: fixed; 115 | } 116 | 117 | .semi-link { 118 | border-bottom: 1px dashed silver; 119 | cursor: pointer; 120 | } 121 | 122 | .solid-link { 123 | border-bottom: 1px solid silver; 124 | cursor: pointer; 125 | } 126 | 127 | .label { 128 | font-size: 100%; 129 | display: inline-block; 130 | margin-bottom: 2px; 131 | } 132 | 133 | .branch { 134 | cursor: pointer; 135 | } 136 | 137 | .message { 138 | font-family: Courier; 139 | white-space: pre; 140 | } 141 | 142 | .message-success { 143 | color: green; 144 | } 145 | 146 | .message-alert { 147 | color: red; 148 | } 149 | 150 | .resolved-branch { 151 | display: inline; 152 | padding: .2em .6em .3em; 153 | font-size: 100%; 154 | line-height: 1; 155 | color: #fff; 156 | text-align: center; 157 | white-space: nowrap; 158 | vertical-align: baseline; 159 | border-radius: .25em; 160 | background-color: rgba(102, 0, 255, 0.46); 161 | margin-right: 10px; 162 | } 163 | 164 | .hint { 165 | display: inline-block; 166 | } 167 | 168 | .label-for-checkbox { 169 | font-weight: normal; 170 | cursor: pointer; 171 | border-bottom: 1px dashed silver; 172 | } 173 | 174 | .td-checkbox { 175 | vertical-align: middle; 176 | padding-left: 2px; 177 | padding-right: 10px; 178 | } 179 | 180 | #message-wrapper { 181 | z-index: 100000; 182 | position: fixed; 183 | top: 70px; 184 | left: 50%; 185 | font-size: 18px; 186 | display: none; 187 | } 188 | 189 | #message { 190 | z-index: 100001; 191 | position: relative; 192 | margin-left: -300px; 193 | width: 600px; 194 | padding: 20px; 195 | overflow: hidden; 196 | } 197 | 198 | .affix { 199 | width: inherit; 200 | top: 5px; 201 | } 202 | -------------------------------------------------------------------------------- /htdocs/index.php: -------------------------------------------------------------------------------- 1 | register(new Silex\Provider\TwigServiceProvider(), array( 44 | 'twig.path' => '../lib/views', 45 | )); 46 | 47 | $app->register(new SilexMtHaml\MtHamlServiceProvider()); 48 | 49 | $app['twig']->addGlobal('VERSION', VERSION); 50 | 51 | $app->get('/', 'RepositoryController::redirectToDatabase'); 52 | 53 | $app->get('/error/{error_type}/', 'RepositoryController::error'); 54 | 55 | $app->get('/{database_name}/', 'RepositoryController::index')->before('RepositoryController::useDatabase'); 56 | 57 | $app->match('/{database_name}/login/', 'RepositoryController::login')->before('RepositoryController::useDatabase'); 58 | $app->match('/{database_name}/logout/', 'RepositoryController::logout')->before('RepositoryController::useDatabase'); 59 | 60 | $app->get('/{database_name}/get_commits/', 'RepositoryController::getCommits')->before('RepositoryController::useDatabase'); 61 | 62 | $app->get('/{database_name}/{hash}/checkout/', 'RepositoryController::checkout') 63 | ->assert('hash', '[a-zA-Z\d_\/\.\#]+') 64 | ->before('RepositoryController::useDatabase'); 65 | 66 | $app->get('/{database_name}/{schema_name}/{object_index}/{file_name}/view_diff/', 'RepositoryController::viewDiff')->before('RepositoryController::useDatabase'); 67 | $app->get('/{database_name}/{schema_name}/{object_index}/{file_name}/define/', 'RepositoryController::define')->before('RepositoryController::useDatabase'); 68 | $app->get('/{database_name}/{schema_name}/{object_index}/{file_name}/describe/', 'RepositoryController::describe')->before('RepositoryController::useDatabase'); 69 | $app->post('/{database_name}/{schema_name}/{object_index}/{file_name}/drop/', 'RepositoryController::drop')->before('RepositoryController::useDatabase'); 70 | 71 | $app->post('{database_name}/apply/', 'RepositoryController::apply')->before('RepositoryController::useDatabase'); 72 | 73 | $app->get('{database_name}/tools/download_definitions/', 'RepositoryController::downloadDefinitions')->before('RepositoryController::useDatabase'); 74 | 75 | $app->run(); 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /htdocs/js/common.js: -------------------------------------------------------------------------------- 1 | Messager = { 2 | 3 | flash: function(message) { 4 | $('#message').text(message); 5 | $('#message-wrapper').show(); 6 | }, 7 | 8 | hideFlash: function() { 9 | $('#message-wrapper').hide(); 10 | $('#message').text(''); 11 | }, 12 | 13 | alert: function (status, message) { 14 | message = message || ''; 15 | 16 | if (status == 1) { 17 | message = '' + message + '
'; 18 | } else { 19 | message = '' + message + '
'; 20 | } 21 | $("#messages-panel-anchor").before(message); 22 | $('#messages-panel').scrollTop($('#messages-panel')[0].scrollHeight); 23 | } 24 | 25 | } 26 | 27 | Git = { 28 | 29 | database_name: '', 30 | schemas_states: {}, 31 | schemas_objects_states: {}, 32 | last_hash: '', 33 | diff_template: '', 34 | commits_template: '', 35 | 36 | request: function(method, url, data, success) { 37 | Messager.flash('Pending...'); 38 | $.ajax({ 39 | url: url, 40 | dataType: "json", 41 | type: method, 42 | data: data 43 | }).done(function(data) { 44 | Messager.hideFlash(); 45 | success(data); 46 | }).fail(function() { 47 | Messager.hideFlash(); 48 | Messager.alert(0, 'Unable to make request'); 49 | }); 50 | }, 51 | 52 | onApplyCheck: function(e) { 53 | // when apply is checked/is not checked also check/uncheck forward (if exists) 54 | var forward_id = $(e).attr('id').replace(/^apply/, 'forward'); 55 | var forward = $("#" + forward_id); 56 | if ( ! forward.length) { 57 | return; 58 | } 59 | forward.prop('checked', $(e).prop('checked')); 60 | }, 61 | 62 | onForwardCheck: function(e) { 63 | // when forward is checked also check apply 64 | var apply_id = $(e).attr('id').replace(/^forward/, 'apply'); 65 | var apply = $("#" + apply_id); 66 | if ($(e).prop('checked')) { 67 | apply.prop('checked', true); 68 | } 69 | }, 70 | 71 | checkout: function (hash, show_alert, f) { 72 | f = f || function() {}; 73 | Git.request( 74 | 'GET', 75 | '/' + Git.database_name + '/' + hash + '/checkout/', 76 | { }, 77 | function (data) { 78 | if (data.status == 1) { 79 | $(".commit").removeClass("commit-active"); 80 | Git.clearDiff(); 81 | data.commit_hash = hash; 82 | // 83 | $("#diff").html(Mustache.render(Git.diff_template, data)); 84 | // listen to apply checkboxes 85 | $(".apply.apply-main").change(function() { 86 | Git.onApplyCheck(this); 87 | }); 88 | // listen to forward checkboxes 89 | $(".apply.forward").change(function() { 90 | Git.onForwardCheck(this); 91 | }); 92 | // 93 | Git.last_hash = hash; 94 | // 95 | Git.getCommits(function() { 96 | if (show_alert) { 97 | // show current hash or branch 98 | Messager.alert(1, 'Checked out to ' + hash); 99 | 100 | var forwardable_types = ['queries_before', 'sequences', 'tables', 'queries_after']; 101 | 102 | for (var forwardable_type_key in forwardable_types) { 103 | var forwardable_type = forwardable_types[forwardable_type_key]; 104 | 105 | // 106 | var cbf = data['stat']['can_be_forwarded'][forwardable_type]; 107 | if (cbf.length) { 108 | Messager.alert(1, 'Can be forwarded ' + forwardable_type + ': ' + cbf.join(' -> ')); 109 | } 110 | // 111 | var cnbf = data['stat']['cannot_be_forwarded'][forwardable_type]; 112 | if (cnbf.length) { 113 | Messager.alert(1, 'Cannot be forwarded ' + forwardable_type + ': ' + cnbf.join(', ')); 114 | } 115 | } 116 | } 117 | f(); 118 | }); 119 | } else { 120 | Messager.alert(0, data.message); 121 | } 122 | } 123 | ); 124 | }, 125 | 126 | clearDiff: function() { 127 | $("#diff").children().remove(); 128 | }, 129 | 130 | clearCommits: function() { 131 | $("#commits").children().remove(); 132 | }, 133 | 134 | toggleSchema: function(schema) { 135 | if (Git.schemas_states[schema] == undefined) { 136 | Git.schemas_states[schema] = false; 137 | } 138 | Git.schemas_states[schema] = ! Git.schemas_states[schema]; 139 | $(".s-" + schema).prop('checked', Git.schemas_states[schema]); 140 | }, 141 | 142 | toggleSchemaObject: function(schema, object_index) { 143 | if (Git.schemas_objects_states[schema] == undefined) { 144 | Git.schemas_objects_states[schema] = {}; 145 | } 146 | if (Git.schemas_objects_states[schema][object_index] == undefined) { 147 | Git.schemas_objects_states[schema][object_index] = false; 148 | } 149 | Git.schemas_objects_states[schema][object_index] = ! Git.schemas_objects_states[schema][object_index]; 150 | $(".s-" + schema).filter(".o-" + object_index).prop('checked', Git.schemas_objects_states[schema][object_index]); 151 | }, 152 | 153 | toggleSchemaObjectTable: function(schema, object_index) { 154 | $("#row_" + schema + "_" + object_index).slideToggle(); 155 | }, 156 | 157 | apply: function(imitate) { 158 | imitate = imitate || false; 159 | 160 | var objects = []; 161 | 162 | // foreach chosen object 163 | $(".apply-main").filter(":checked").each(function(n, e) { 164 | var forward_order = 0; 165 | // do we have to use forward? let's see at special checkbox: 166 | var ff = $("input[name='" + e.name + "/forward_order']"); 167 | var forwarded = false; 168 | if (ff.length) { 169 | // checkbox exists (for tables only) 170 | forwarded = $(ff.get(0)).prop('checked'); 171 | // order is in value 172 | forward_order = $(ff.get(0)).attr('value'); 173 | } 174 | // gather information 175 | objects.push({ 176 | object_name: e.name, 177 | forwarded: forwarded ? 1 : 0, 178 | forward_order: forward_order 179 | }); 180 | }); 181 | 182 | // 183 | if (objects.length == 0) { 184 | Messager.alert(1, 'Nothing to ' + (! imitate ? 'apply' : 'imitate')); 185 | return; 186 | } 187 | 188 | Git.request( 189 | 'POST', 190 | '/' + Git.database_name + '/apply/', 191 | { 192 | objects: objects, 193 | imitate: imitate ? 1 : 0 194 | }, 195 | function (data) { 196 | Messager.alert(data.status, data.status == 1 ? (! imitate ? 'Applied' : 'Imitated') : data.message); 197 | if (data.status == 1) { 198 | Git.checkout(Git.last_hash, false); 199 | } 200 | } 201 | ); 202 | }, 203 | 204 | drop: function(schema_name, object_index, object_name) { 205 | if (! confirm('Are you sure to drop ' + schema_name + '.' + object_name + '?')) { 206 | return; 207 | } 208 | 209 | Git.request( 210 | 'POST', 211 | '/' + Git.database_name + '/' + schema_name + '/' + object_index + '/' + object_name + '/drop/' , 212 | { 213 | 214 | }, 215 | function (data) { 216 | Messager.alert(data.status, data.status == 1 ? 'Dropped' : data.message); 217 | if (data.status == 1) { 218 | Git.checkout(Git.last_hash, false); 219 | } 220 | } 221 | ); 222 | }, 223 | 224 | reloadAndApply: function(ignore_manual) { 225 | ignore_manual = ignore_manual || false; 226 | 227 | Git.checkout(Git.last_hash, false, function() { 228 | // check all checkboxes (they are not checked by default) 229 | // if we need to ignore manual row filter them out 230 | (ignore_manual ? $(".apply").filter('[manual!=true]') : $(".apply")).prop('checked', true); 231 | // 232 | Git.apply(); 233 | }) 234 | }, 235 | 236 | switchToBranch: function (branch) { 237 | Git.checkout(branch, true); 238 | }, 239 | 240 | imitate: function() { 241 | if (confirm('It will fill postgresql_deployer.migrations table without actual deploying objects. Are you sure?')) { 242 | Git.apply(true); 243 | } 244 | }, 245 | 246 | getCommits: function(f) { 247 | f = f || function() {}; 248 | Git.request( 249 | 'GET', 250 | '/' + Git.database_name + '/get_commits/', 251 | { }, 252 | function (data) { 253 | if (data.status == 1) { 254 | Git.clearCommits(); 255 | $("#commits").html(Mustache.render(Git.commits_template, data)); 256 | f(); 257 | } else { 258 | Messager.alert(0, data['message']); 259 | } 260 | } 261 | ); 262 | }, 263 | 264 | changeDatabase: function() { 265 | Git.last_hash = ''; 266 | Git.schemas_states = []; 267 | Git.schemas_objects_states = []; 268 | } 269 | 270 | } 271 | 272 | -------------------------------------------------------------------------------- /htdocs/js/mustache.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * mustache.js - Logic-less {{mustache}} templates with JavaScript 3 | * http://github.com/janl/mustache.js 4 | */ 5 | 6 | /*global define: false*/ 7 | 8 | (function (global, factory) { 9 | if (typeof exports === "object" && exports) { 10 | factory(exports); // CommonJS 11 | } else if (typeof define === "function" && define.amd) { 12 | define(['exports'], factory); // AMD 13 | } else { 14 | factory(global.Mustache = {}); //